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) |