> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wethehivers.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Flow JWT et refresh

> Cycle complet access token / refresh token, blacklist, rotation

# Flow JWT complet

THE HIVE utilise un schéma **access + refresh token** avec blacklist Redis partagée.

## Caractéristiques

| Token   | Durée   | Stockage client                      | Usage                     |
| ------- | ------- | ------------------------------------ | ------------------------- |
| Access  | 15 min  | Memory (JS) / SecureStorage (mobile) | Header `Authorization`    |
| Refresh | 7 jours | HttpOnly cookie / SecureStorage      | Renouveler l'access token |

## Architecture

```mermaid theme={null}
flowchart TB
    subgraph Client
        FE[Frontend Next.js]
    end

    subgraph Backend
        AUTH[Auth Controller]
        JWT_S[JwtService]
        SEC[SecurityFilter]
    end

    subgraph Redis
        BL[Blacklist JWT\n SET bl:jwt:&lt;jti&gt;]
        RL[Rate limit\n INCR rl:*]
    end

    subgraph DB
        USERS[(users / refresh_tokens)]
    end

    FE -->|login| AUTH
    AUTH --> USERS
    AUTH --> JWT_S
    JWT_S -->|signé HS256| FE

    FE -->|API call\n Authorization: Bearer| SEC
    SEC --> JWT_S
    SEC --> BL

    FE -->|refresh| AUTH
    AUTH --> USERS
    AUTH --> JWT_S
```

## Login

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant C as Client
    participant B as Backend
    participant DB as PostgreSQL
    participant R as Redis

    C->>B: POST /v1/api/auth/login\n {email, password}
    B->>DB: SELECT user WHERE email=?
    DB-->>B: hash BCrypt
    B->>B: BCrypt.matches(pw, hash)
    B->>B: Générer jti (UUID)
    B->>B: Signer access JWT (15 min)
    B->>B: Signer refresh JWT (7 j)
    B->>DB: INSERT refresh_tokens(jti, userId, expires_at)
    B-->>C: 200 OK\n {accessToken, refreshToken, user}
```

## Appel API authentifié

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant C as Client
    participant S as SecurityFilter
    participant J as JwtService
    participant R as Redis
    participant H as Handler

    C->>S: GET /v1/api/candidates/profile\n Authorization: Bearer eyJ...
    S->>J: parseAndValidate(token)
    J->>J: Vérifier signature HS256
    J->>J: Vérifier exp, iat, iss
    J-->>S: Claims (sub, jti, role)
    S->>R: EXISTS bl:jwt:<jti>
    R-->>S: 0 (pas blacklist)
    S->>S: Construire Authentication
    S->>H: chain.doFilter(...)
    H-->>C: 200 OK
```

## Refresh

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant C as Client
    participant B as Backend
    participant DB as PostgreSQL
    participant R as Redis

    Note over C: Access token expiré
    C->>B: POST /v1/api/auth/refresh-token\n {refreshToken}
    B->>B: Valider signature + exp refresh
    B->>DB: SELECT refresh_tokens WHERE jti=? AND revoked=false
    DB-->>B: row trouvée
    B->>DB: UPDATE SET revoked=true (rotation)
    B->>B: Générer nouveaux access + refresh
    B->>DB: INSERT nouveau refresh_tokens
    B->>R: SET bl:jwt:&lt;ancien-jti&gt; 604800
    B-->>C: 200 OK\n {accessToken, refreshToken}
```

<Info>
  **Rotation du refresh token** : chaque refresh génère un nouveau refresh token et invalide l'ancien. Un refresh token ne peut être utilisé qu'une fois.
</Info>

## Détection de replay

```mermaid theme={null}
flowchart TD
    REQ[POST /auth/refresh-token] --> V{Refresh valide?}
    V -->|non| R401[401 Unauthorized]
    V -->|oui| DB{DB: jti revoked?}
    DB -->|non| OK[Rotation + 200 OK]
    DB -->|oui| ATK[🚨 Replay attaque\n probable]
    ATK --> NUKE[Révoquer TOUS\n les refresh du user]
    NUKE --> ALERT[Alerte monitoring]
    ALERT --> R401
```

<Warning>
  Si un refresh token déjà révoqué est réutilisé, **tous les refresh tokens du user** sont révoqués en cascade et un incident est loggé.
</Warning>

## Logout

```mermaid theme={null}
sequenceDiagram
    participant C as Client
    participant B as Backend
    participant DB as PostgreSQL
    participant R as Redis

    C->>B: POST /v1/api/auth/logout\n Authorization: Bearer &lt;access&gt;\n {refreshToken}
    B->>R: SET bl:jwt:&lt;access-jti&gt; EX 900
    B->>DB: UPDATE refresh_tokens SET revoked=true\n WHERE jti=?
    B-->>C: 204 No Content
```

## Structure du JWT

```json theme={null}
{
  "header": {
    "alg": "HS256",
    "typ": "JWT"
  },
  "payload": {
    "sub": "42",
    "jti": "e4f1a3b0-1234-5678-9abc-def012345678",
    "role": "CANDIDAT",
    "email": "jean@example.com",
    "iat": 1713454200,
    "exp": 1713455100,
    "iss": "thehive"
  }
}
```

## Claims

| Claim   | Type   | Description                                 |
| ------- | ------ | ------------------------------------------- |
| `sub`   | string | ID utilisateur                              |
| `jti`   | string | ID unique du token (blacklist key)          |
| `role`  | string | CANDIDAT / RECRUTEUR / ADMIN / SUPER\_ADMIN |
| `email` | string | Email utilisateur                           |
| `iat`   | int    | Issued at (epoch)                           |
| `exp`   | int    | Expiration (epoch)                          |
| `iss`   | string | `thehive`                                   |

## Gestion côté client (JS)

```javascript theme={null}
class TokenManager {
  constructor() {
    this.access = null;
    this.refresh = localStorage.getItem('refresh');
    this.refreshing = null;
  }

  async getValidAccess() {
    if (this.access && !this.isExpired(this.access)) return this.access;
    if (!this.refreshing) this.refreshing = this.doRefresh();
    return this.refreshing;
  }

  async doRefresh() {
    const res = await fetch('/v1/api/auth/refresh-token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ refreshToken: this.refresh }),
    });
    if (!res.ok) { this.logout(); throw new Error('Refresh failed'); }
    const { accessToken, refreshToken } = await res.json();
    this.access = accessToken;
    this.refresh = refreshToken;
    localStorage.setItem('refresh', refreshToken);
    this.refreshing = null;
    return accessToken;
  }

  isExpired(token) {
    const { exp } = JSON.parse(atob(token.split('.')[1]));
    return Date.now() >= (exp - 30) * 1000;
  }
}
```

<Warning>
  Ne pas stocker l'access token en `localStorage` → vulnérable XSS. Preferer mémoire + refresh en cookie HttpOnly.
</Warning>

## Clock skew

Le backend tolère **30 secondes** d'écart entre l'horloge client et l'horloge serveur. Au-delà, le token est considéré invalide.

## Horizon des sessions

| Scenario                             | Durée max session                                    |
| ------------------------------------ | ---------------------------------------------------- |
| Utilisateur actif (refresh régulier) | 7 jours glissants                                    |
| Utilisateur inactif                  | 7 jours depuis dernier refresh                       |
| Logout explicite                     | Immédiat                                             |
| Changement password                  | Immédiat (tous refresh révoqués)                     |
| Admin suspend user                   | Immédiat (tous refresh révoqués + access blacklisté) |
