|
@@ -239,6 +239,18 @@ func (s *AuthService) RefreshSession(ctx context.Context, plainRefreshToken stri
|
|
|
return nil, "", fmt.Errorf("replace old refresh token: %w", err)
|
|
return nil, "", fmt.Errorf("replace old refresh token: %w", err)
|
|
|
}
|
|
}
|
|
|
if !replaced {
|
|
if !replaced {
|
|
|
|
|
+ // Token was already replaced (grace period after lost cookie / fast F5).
|
|
|
|
|
+ // Not theft — browser didn't receive the new cookie. Revoke this specific
|
|
|
|
|
+ // token to prevent further reuse, then issue fresh tokens.
|
|
|
|
|
+ if storedToken.ReplacedAt != nil {
|
|
|
|
|
+ logger.InfoContext(ctx, "grace period token reuse, issuing new tokens",
|
|
|
|
|
+ slog.String("user_id", user.ID),
|
|
|
|
|
+ slog.Time("replaced_at", *storedToken.ReplacedAt))
|
|
|
|
|
+ if revokeErr := s.refreshTokenRepo.Revoke(ctx, storedToken.TokenHash); revokeErr != nil {
|
|
|
|
|
+ logger.ErrorContext(ctx, "failed to revoke grace-period token", log.WithError(revokeErr))
|
|
|
|
|
+ }
|
|
|
|
|
+ return s.generateTokens(ctx, user)
|
|
|
|
|
+ }
|
|
|
logger.WarnContext(ctx, "concurrent token rotation detected", slog.String("user_id", user.ID))
|
|
logger.WarnContext(ctx, "concurrent token rotation detected", slog.String("user_id", user.ID))
|
|
|
return nil, "", ErrTokenReused
|
|
return nil, "", ErrTokenReused
|
|
|
}
|
|
}
|