Контекст: В backend/internal/services/auth.go реализована JWT-аутентификация с access (15 мин) и refresh (30 дней) токенами. Реализация включает серверное хранение refresh токенов, ротацию и детект повторного использования. Обновлено после code review 2026-06 — код уже исправлен, заметка отражает текущее состояние.
Система аутентификации использует два типа токенов:
Authorization: Bearer, содержит user_id и rolesequenceDiagram
participant C as Client
participant A as Go API
participant DB as PostgreSQL
C->>A: POST /auth/login { email, password }
A->>DB: SELECT user WHERE email
A->>A: bcrypt.CompareHashAndPassword
A->>DB: INSERT refresh_token (SHA-256 hash)
A-->>C: 200 { access_token, user }
Note over C: Set-Cookie: refresh_token (HttpOnly, Secure, SameSite)
C->>A: GET /auth/me (Authorization: Bearer access_token)
A->>A: JWT validation (signature + expiry)
A-->>C: 200 { user_id, role }
C->>A: POST /auth/refresh (Cookie: refresh_token)
A->>DB: SELECT refresh_token WHERE token_hash = SHA-256(cookie)
A->>DB: DELETE old token (if exists — rotation)
A->>DB: INSERT new refresh_token
A-->>C: 200 { new_access_token }
Note over C: Set-Cookie: new_refresh_token
alt Token Reuse Detected
C->>A: POST /auth/refresh (stale refresh_token)
A->>DB: SELECT — not found (already rotated)
A->>DB: SELECT revoked — found!
A->>DB: DELETE ALL tokens for user_id
A-->>C: 401 token reused, session revoked
end
services/auth.go:152-182)func (s *AuthService) generateTokens(ctx context.Context, user *models.User) (*AuthResult, string, error) {
accessToken, _ := jwt.NewWithClaims(jwt.SigningMethodHS256, TokenClaims{
UserID: user.ID,
Role: user.Role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)),
},
}).SignedString(s.jwtSecret)
refreshToken, _ := generateSecureToken(32) // crypto/rand, base64
// Хеш SHA-256 → сохраняем в БД
s.refreshTokenRepo.Create(ctx, user.ID, refreshToken, expiresAt)
return &AuthResult{User: user, AccessToken: accessToken}, refreshToken, nil
}
services/auth.go:198-249)func (s *AuthService) RefreshSession(ctx context.Context, plainRefreshToken string) (*AuthResult, string, error) {
// 1. Ищем валидный токен в БД
storedToken, _ := s.refreshTokenRepo.GetValid(ctx, plainRefreshToken)
if storedToken == nil {
// 2. Не найден → проверяем, не был ли он отозван
revokedToken, _ := s.refreshTokenRepo.GetRevoked(ctx, plainRefreshToken)
if revokedToken != nil {
// 3. Token reuse! Отзываем ВСЕ сессии пользователя
s.refreshTokenRepo.RevokeAllForUser(ctx, revokedToken.UserID)
return nil, "", ErrTokenReused
}
return nil, "", ErrInvalidToken
}
// 4. Атомарная ротация: удаляем старый, создаём новый
deleted, _ := s.refreshTokenRepo.DeleteIfExists(ctx, storedToken.TokenHash)
if !deleted {
return nil, "", ErrTokenReused // concurrent rotation detected
}
return s.generateTokens(ctx, user)
}
DeleteIfExists проверяет rows_affected — гарантирует, что только одна горутина выиграет race.// handlers/auth.go:153-168
http.Cookie{
Name: "refresh_token",
HttpOnly: true,
Secure: h.isProd,
SameSite: http.SameSiteStrictMode, // CSRF защита
Path: "/",
MaxAge: int((30 * 24 * time.Hour).Seconds()),
}
HttpOnly: true — защита от XSS (JavaScript не прочитает cookie)Secure: true — только по HTTPS (в production)SameSite: Strict — защита от CSRFPath: "/" — доступен на всех путях (включая /auth/refresh)// middleware/auth.go:23-48
func AuthMiddleware(authSvc *services.AuthService) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
// Парсинг "Bearer <token>"
claims, err := authSvc.ValidateAccessToken(token)
if err != nil {
writeAuthError(w, "invalid or expired token")
return
}
// Встраиваем user_id и role в контекст
ctx := context.WithValue(r.Context(), UserIDKey, claims.UserID)
ctx = context.WithValue(ctx, UserRoleKey, claims.Role)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
| Роль | Разрешения |
|---|---|
superadmin |
Полный доступ |
moderator |
Модерация мест, управление пользователями |
landlord |
Создание/редактирование своих студий |
executor |
Создание услуг |
customer |
Создание мест |
RoleMiddleware(roles ...string) проверяет роль из контекста.
| Подход | Плюсы | Минусы |
|---|---|---|
| Текущий: БД + ротация | Полный контроль, отзыв, безопасность | +1 round-trip к БД |
| JWT blacklist в Redis | Быстро, не трогает БД | Нужно хранить до expiry |
| Refresh token как JWT (без БД) | Просто, 0 запросов к БД | Нельзя отозвать — утечка = полный доступ |
Code review PhotoPlaces 2026-06. Актуализация после внедрения refresh token rotation в services/auth.go.
#backend #security #jwt #auth #best-practice