package auth import ( "context" "fmt" "sync" "time" "github.com/redis/go-redis/v9" "github.com/gochat/gochat/internal/config" ) // Reference: P2E §1.4 — Refresh Token storage in Redis for rotation tracking // Refresh tokens are stored in Redis with TTL matching their JWT expiry. // This enables: token rotation, revocation, and audit trail. // RefreshTokenStore manages refresh token storage in Redis. type RefreshTokenStore struct { rdb *redis.Client cfg *config.JWTConfig mu sync.RWMutex mem map[uint]string } // NewRefreshTokenStore creates a refresh token store backed by Redis. func NewRefreshTokenStore(rdb *redis.Client, cfg *config.JWTConfig) *RefreshTokenStore { return &RefreshTokenStore{rdb: rdb, cfg: cfg, mem: map[uint]string{}} } // Store saves a refresh token in Redis with TTL. // Key pattern: refresh_token:{user_id}:{token_hash} func (s *RefreshTokenStore) Store(ctx context.Context, userID uint, refreshToken string) error { if s.rdb == nil { s.mu.Lock() defer s.mu.Unlock() s.mem[userID] = refreshToken return nil } key := fmt.Sprintf("gochat:refresh_token:%d", userID) ttl := time.Duration(s.cfg.RefreshExpiryHours) * time.Hour return s.rdb.Set(ctx, key, refreshToken, ttl).Err() } // Validate checks if a refresh token exists and matches the stored value. func (s *RefreshTokenStore) Validate(ctx context.Context, userID uint, refreshToken string) (bool, error) { if s.rdb == nil { s.mu.RLock() defer s.mu.RUnlock() stored, ok := s.mem[userID] return ok && stored == refreshToken, nil } key := fmt.Sprintf("gochat:refresh_token:%d", userID) stored, err := s.rdb.Get(ctx, key).Result() if err == redis.Nil { return false, nil // token not found (expired or revoked) } if err != nil { return false, fmt.Errorf("redis error: %w", err) } return stored == refreshToken, nil } // Revoke removes a refresh token from Redis (logout). func (s *RefreshTokenStore) Revoke(ctx context.Context, userID uint) error { if s.rdb == nil { s.mu.Lock() defer s.mu.Unlock() delete(s.mem, userID) return nil } key := fmt.Sprintf("gochat:refresh_token:%d", userID) return s.rdb.Del(ctx, key).Err() } // Rotate replaces an old refresh token with a new one (refresh token rotation). // This ensures each refresh token can only be used once. func (s *RefreshTokenStore) Rotate(ctx context.Context, userID uint, newRefreshToken string) error { return s.Store(ctx, userID, newRefreshToken) }