package auth import ( "context" "fmt" "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 } // NewRefreshTokenStore creates a refresh token store backed by Redis. func NewRefreshTokenStore(rdb *redis.Client, cfg *config.JWTConfig) *RefreshTokenStore { return &RefreshTokenStore{rdb: rdb, cfg: cfg} } // 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 { 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) { 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 { 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) }