Aller au contenu

Authentification OTP + JWT

Documentation de l'authentification par OTP (sans mot de passe) et JWT dans l'API Primatch.


Endpoints

Méthode Route Description Auth
POST /api/v1/auth/request-otp Demander un code OTP (inscription/connexion) Non
POST /api/v1/auth/verify-otp Vérifier le code OTP → JWT Non
POST /api/v1/auth/refresh Renouveler le token JWT Oui
POST /api/v1/auth/logout Déconnexion (invalidation JWT) Oui
GET /api/v1/auth/me Profil utilisateur connecté Oui
POST /api/v1/auth/level-assessment Auto-évaluation du niveau Oui
GET /api/v1/game-levels Liste des niveaux de jeu Non

Flux d'authentification

Inscription

sequenceDiagram
    participant U as Utilisateur
    participant F as Frontend React
    participant A as API Laravel
    participant R as Redis (OTP)
    participant M as Service Email

    U->>F: Remplit nom, prénom, email + accepte CGU
    F->>A: POST /api/v1/auth/request-otp<br/>{email, first_name, last_name, accept_cgu: true}
    A->>R: Stocke OTP (clé: otp:{email}, TTL: 15min)
    A->>M: Envoie email avec code OTP 4 chiffres
    A-->>F: 200 {message: "Code envoyé"}

    M-->>U: Email avec code OTP
    U->>F: Saisit le code 4 chiffres
    F->>A: POST /api/v1/auth/verify-otp<br/>{email, code}
    A->>R: Vérifie le code OTP
    A-->>F: 200 {access_token, is_new_user: true}

    Note over F: Stocke le token en mémoire

    F->>A: GET /api/v1/game-levels
    A-->>F: 200 [niveaux de jeu]

    U->>F: Sélectionne son niveau
    F->>A: POST /api/v1/auth/level-assessment<br/>{game_level_id}
    A-->>F: 200 {user avec niveau}
    F-->>U: Redirige vers accueil

Connexion

sequenceDiagram
    participant U as Utilisateur
    participant F as Frontend React
    participant A as API Laravel
    participant R as Redis (OTP)

    U->>F: Saisit son email
    F->>A: POST /api/v1/auth/request-otp<br/>{email}
    A->>R: Stocke OTP (TTL: 15min)
    A-->>F: 200 {message: "Code envoyé"}

    U->>F: Saisit le code OTP
    F->>A: POST /api/v1/auth/verify-otp<br/>{email, code}
    A-->>F: 200 {access_token, is_new_user: false}
    F-->>U: Redirige vers accueil

OTP — Détails techniques

Paramètre Valeur
Longueur du code 4 chiffres
TTL du code 15 minutes
Tentatives max 5 avant expiration
Cooldown entre envois 60 secondes
Stockage Redis (otp:{email}, otp_cooldown:{email})
Throttle request-otp 5 requêtes/minute
Throttle verify-otp 10 requêtes/minute

Sécurité

  • Non-révélation d'email : l'endpoint request-otp retourne toujours 200 même si l'email n'existe pas (flux login)
  • Inscription avec email existant : retourne 422 (l'utilisateur sait déjà qu'il a un compte)
  • Codes OTP en log uniquement en dev (driver MAIL_MAILER=log)

JWT — Configuration

Paramètre Valeur
Access Token TTL 60 minutes
Refresh Token TTL 10 080 minutes (7 jours)
Guard api (driver jwt)
Algorithme HS256 (par défaut tymon/jwt-auth)
Stockage frontend Mémoire JS (pas localStorage)

Gestion côté React

// services/apiClient.ts — Token en mémoire
let accessToken: string | null = null

export const setAccessToken = (token: string | null) => { accessToken = token }
export const getAccessToken = () => accessToken

// Intercepteur requête : ajoute le Bearer token
apiClient.interceptors.request.use((config) => {
    const token = getAccessToken()
    if (token) config.headers.Authorization = `Bearer ${token}`
    return config
})

// Intercepteur réponse : refresh automatique sur 401
apiClient.interceptors.response.use(
    (res) => res,
    async (error) => {
        if (error.response?.status === 401 && !error.config._retry) {
            error.config._retry = true
            const { access_token } = await refreshToken()
            setAccessToken(access_token)
            return apiClient(error.config)
        }
        return Promise.reject(error)
    }
)

Protéger une route API

// routes/api.php
Route::prefix('v1')->group(function () {
    // Routes publiques
    Route::post('/auth/request-otp', [AuthController::class, 'requestOtp'])
        ->middleware('throttle:5,1');
    Route::post('/auth/verify-otp', [AuthController::class, 'verifyOtp'])
        ->middleware('throttle:10,1');
    Route::get('/game-levels', [GameLevelController::class, 'index']);

    // Routes protégées
    Route::middleware('auth:api')->group(function () {
        Route::post('/auth/refresh', [AuthController::class, 'refresh']);
        Route::post('/auth/logout', [AuthController::class, 'logout']);
        Route::get('/auth/me', [AuthController::class, 'me']);
        Route::post('/auth/level-assessment', [AuthController::class, 'assessLevel']);
    });
});

Réponses d'erreur

Code Signification Cause
401 Unauthorized Token absent, expiré ou invalide
403 Forbidden Token valide mais permissions insuffisantes
422 Unprocessable Entity Validation échouée (email, code OTP, etc.)
429 Too Many Requests Trop de tentatives (throttle)