Späť na blog

JWT Revokovanie Stratégie: Keď Stateless Tokeny Potrebujú Stav

|
| security, jwt, authentication, redis, performance, auth

Myslel som si, ze revokacia JWT je jednoducha, kym neprisiel prvy incident. “Účet používateľa kompromitovaný. Revokuj všetky ich tokeny TERAZ.” Ale JWT sú stateless a immutable. Raz vydané, sú platné do expirácie. Ako revokovať to čo sa revokovať nedá?

Testované na: Go 1.21, Redis 7.2, 10,000 requestov/sekundu autentifikačná záťaž

Problém JWT Revokovania

Prečo JWT Nemôžu Byť Revokované

JWT Dizajn:

Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

┌─────────────────────────────────────────────────────────────┐
│ Header: {"alg": "HS256", "typ": "JWT"}                      │
│ Payload: {                                                   │
│   "sub": "user123",                                         │
│   "exp": 1704844800,  ← Expiruje za 24 hodín                │
│   "iat": 1704758400,                                        │
│   "roles": ["admin"]                                        │
│ }                                                           │
│ Signature: HMACSHA256(header + payload, secret)             │
└─────────────────────────────────────────────────────────────┘

Verifikácia: Skontroluj podpis + skontroluj exp > now
Žiadny databázový lookup potrebný = stateless = rýchle

Ale: Žiadny spôsob ako invalidovať pred exp
Token uniknutý? Platný ďalších 24 hodín.

Stratégia 1: Krátka Expirácia + Refresh Tokeny

Dizajn

Access Token: 15 minút expirácia
Refresh Token: 7 dní expirácia (uložený server-side)

┌─────────────────────────────────────────────────────────────┐
│ Flow:                                                        │
│                                                              │
│ 1. Login → Access Token (15m) + Refresh Token (7d)          │
│ 2. API volania používajú Access Token                        │
│ 3. Access Token expiruje → Použi Refresh Token na nový      │
│ 4. Revokácia: Zmaž Refresh Token z databázy                 │
│                                                              │
│ Max expozícia: 15 minút (kým Access Token expiruje)          │
└─────────────────────────────────────────────────────────────┘

Implementácia

// tokens.go
type TokenService struct {
    accessTTL  time.Duration  // 15 minút
    refreshTTL time.Duration  // 7 dní
    db         *sql.DB
    jwtSecret  []byte
}

func (s *TokenService) RevokeAllUserTokens(userID string) error {
    // Zmaž všetky refresh tokeny = používateľ sa musí prihlásiť
    _, err := s.db.Exec(
        `DELETE FROM refresh_tokens WHERE user_id = $1`,
        userID,
    )
    return err
}

Stratégia 2: Token Denylist (Blocklist)

Dizajn

Pri revokácii: Pridaj token do Redis blocklistu
Pri verifikácii: Skontroluj či token je v blockliste

┌─────────────────────────────────────────────────────────────┐
│ Redis Denylist:                                              │
│                                                              │
│ SET revoked:<jti> 1 EX 86400  (TTL = zostávajúce TTL tokenu)│
│                                                              │
│ Verifikácia:                                                 │
│ 1. Over JWT podpis                                          │
│ 2. Skontroluj exp > now                                     │
│ 3. GET revoked:<jti> → ak existuje, odmietni               │
└─────────────────────────────────────────────────────────────┘

Implementácia

// denylist.go
func (s *DenylistService) VerifyToken(tokenStr string) (*jwt.MapClaims, error) {
    token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
        return s.jwtSecret, nil
    })
    if err != nil {
        return nil, err
    }

    claims := token.Claims.(jwt.MapClaims)
    jti := claims["jti"].(string)

    // Skontroluj denylist
    exists, err := s.redis.Exists(context.Background(), "revoked:"+jti).Result()
    if err != nil {
        return nil, err
    }
    if exists == 1 {
        return nil, fmt.Errorf("token revokovaný")
    }

    return &claims, nil
}

func (s *DenylistService) RevokeToken(tokenStr string) error {
    token, _ := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
        return s.jwtSecret, nil
    })

    claims := token.Claims.(jwt.MapClaims)
    jti := claims["jti"].(string)
    exp := int64(claims["exp"].(float64))

    // TTL = zostávajúca životnosť tokenu
    ttl := time.Until(time.Unix(exp, 0))
    if ttl <= 0 {
        return nil // Už expirovaný
    }

    return s.redis.Set(context.Background(), "revoked:"+jti, "1", ttl).Err()
}

Stratégia 3: Token Verzia / Generácia

Dizajn

Ulož "token version" per používateľ
Zahrň verziu do JWT
Pri revokácii: Inkrementuj verziu

┌─────────────────────────────────────────────────────────────┐
│ Databáza: users.token_version = 5                           │
│                                                              │
│ JWT: {"sub": "user123", "ver": 5, "exp": ...}               │
│                                                              │
│ Verifikácia:                                                 │
│ 1. Over JWT podpis                                          │
│ 2. Načítaj user.token_version z DB/cache                    │
│ 3. Porovnaj JWT.ver == user.token_version                   │
│ 4. Ak sa nerovnajú → token je revokovaný                    │
│                                                              │
│ Revokácia:                                                   │
│ UPDATE users SET token_version = token_version + 1          │
│ → Všetky existujúce tokeny okamžite neplatné                │
└─────────────────────────────────────────────────────────────┘

Implementácia

// version.go
func (s *VersionService) VerifyToken(tokenStr string) (*jwt.MapClaims, error) {
    token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
        return s.jwtSecret, nil
    })
    if err != nil {
        return nil, err
    }

    claims := token.Claims.(jwt.MapClaims)
    userID := claims["sub"].(string)
    tokenVersion := int64(claims["ver"].(float64))

    // Skontroluj aktuálnu verziu
    currentVersion, err := s.getUserVersion(userID)
    if err != nil {
        return nil, err
    }

    if tokenVersion != currentVersion {
        return nil, fmt.Errorf("token revokovaný (nesúlad verzií)")
    }

    return &claims, nil
}

func (s *VersionService) RevokeAllUserTokens(userID string) error {
    // Inkrementuj verziu → všetky tokeny neplatné
    _, err := s.db.Exec(
        `UPDATE users SET token_version = token_version + 1 WHERE id = $1`,
        userID,
    )
    if err != nil {
        return err
    }

    // Invaliduj cache
    return s.redis.Del(context.Background(), "user_version:"+userID).Err()
}

Porovnanie Performance

Benchmark: 10,000 requestov/sekundu, 100k používateľov

Stratégia             │ Verify Latencia│ Úložisko     │ Revoke All
──────────────────────┼────────────────┼──────────────┼─────────────
Krátka expirácia      │ 0.05ms (no DB) │ Refresh: 1MB │ 1 DELETE
Denylist              │ 0.2ms (Redis)  │ ~10KB/deň    │ N SETs
Token verzia          │ 0.3ms (cached) │ 8B/user      │ 1 UPDATE

Odporúčanie:
- Nízka bezpečnosť: Krátka expirácia (15 min)
- Vysoká bezpečnosť: Token verzia (okamžitá revokácia)
- Potrebná individuálna revokácia: Denylist

Checklist

## JWT Revokácia Implementácia

### Výber Stratégie
- [ ] Zhodnoť bezpečnostné požiadavky (okno expozície)
- [ ] Zváž individuálnu vs all-token revokáciu
- [ ] Zhodnoť infraštruktúru (Redis dostupný?)

### Implementácia
- [ ] Pridaj jti (JWT ID) pre denylist stratégiu
- [ ] Pridaj ver (verzia) pre version stratégiu
- [ ] Nastav refresh token úložisko pre krátku expiráciu
- [ ] Implementuj cache vrstvu pre version lookupy

### Monitoring
- [ ] Sleduj revokačné eventy
- [ ] Monitoruj veľkosť denylistu (ak používaš)
- [ ] Alert na vysokú verifikačnú latenciu

Záver

JWT revokácia vyžaduje pridanie stavu niekde:

  1. Krátka expirácia limituje expozíciu ale neodstraňuje ju
  2. Denylist poskytuje okamžitú revokáciu per token
  3. Token verzia revokuje všetky tokeny používateľa okamžite

Vyber podľa tvojich bezpečnostných požiadaviek a infraštruktúry.


Súvisiace články

Súvisiace články

Citujte tento článok

Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.

Michal Drozd. "JWT Revokovanie Stratégie: Keď Stateless Tokeny Potrebujú Stav". https://www.michal-drozd.com/sk/blog/jwt-revokovanie-strategie/ (Publikované 12. októbra 2025).