atomic-refresh-token-race-condition.md 1.6 KB

Race condition на refresh токенах

Контекст: Два параллельных POST /auth/refresh с одним refresh token вызывают ложное срабатывание детекции кражи.

Суть

Без блокировки потоков:

  1. Request A: GetValid → found
  2. Request B: GetValid → found (на том же токене, ещё не удалён)
  3. Request A: Delete → ok
  4. Request A: Create → новый токен T2
  5. Request B: GetRevoked → found (т.к. A уже удалил)
  6. Request B: вызывает RevokeAllForUserвсе сессии пользователя сброшены

Решение

deleted, err := s.refreshTokenRepo.DeleteIfExists(ctx, storedToken.TokenHash)
if !deleted {
    // Токен уже был сротирован — значит, это конкурентный запрос, не кража
    return nil, "", ErrTokenReused
}

Схема

sequenceDiagram
    participant A as Request A
    participant B as Request B
    participant DB as PostgreSQL

    A->>DB: GetValid → found
    B->>DB: GetValid → found
    A->>DB: DeleteIfExists → true (deleted)
    A->>DB: Create (new token)
    B->>DB: DeleteIfExists → false (already deleted)
    Note over B: ErrTokenReused (graceful, без RevokeAll)

Файлы: backend/internal/services/auth.go:236-243, backend/internal/repository/refresh_tokens.go:143-155

Связанные заметки

  • [[decision-jwt-refresh-storage]]
  • [[backend-auth-security]]

#golang #concurrency #auth #race-condition #security