Add JWT refresh route POST /api/auth/refresh
- verifyJwt now returns {userId, expired} instead of just userId
- Expired token with valid signature can be refreshed (new 24h token)
- POST /api/auth/refresh: send expired token in ETicket-JWT header → get new token
- Returns 400 if token is still valid (no need to refresh)
- Returns 401 if signature invalid or user not found
- API doc: refresh endpoint documented with statuses
- Insomnia: both login and refresh auto-store jwt_token via afterResponseScript
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -54,11 +54,14 @@ class ApiAuthController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
public static function verifyJwt(string $token, string $email, string $appSecret): ?int
|
||||
/**
|
||||
* @return array{userId: int|null, expired: bool}
|
||||
*/
|
||||
public static function verifyJwt(string $token, string $email, string $appSecret): array
|
||||
{
|
||||
$parts = explode('.', $token);
|
||||
if (3 !== \count($parts)) {
|
||||
return null;
|
||||
return ['userId' => null, 'expired' => false];
|
||||
}
|
||||
|
||||
[$headerB64, $payloadB64, $signatureB64] = $parts;
|
||||
@@ -68,23 +71,64 @@ class ApiAuthController extends AbstractController
|
||||
);
|
||||
|
||||
if (!hash_equals($expectedSignature, $signatureB64)) {
|
||||
return null;
|
||||
return ['userId' => null, 'expired' => false];
|
||||
}
|
||||
|
||||
$payload = json_decode(self::base64UrlDecode($payloadB64), true);
|
||||
if (!$payload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (($payload['exp'] ?? 0) < time()) {
|
||||
return null;
|
||||
return ['userId' => null, 'expired' => false];
|
||||
}
|
||||
|
||||
if (($payload['email'] ?? '') !== $email) {
|
||||
return null;
|
||||
return ['userId' => null, 'expired' => false];
|
||||
}
|
||||
|
||||
return $payload['userId'] ?? null;
|
||||
if (($payload['exp'] ?? 0) < time()) {
|
||||
return ['userId' => $payload['userId'] ?? null, 'expired' => true];
|
||||
}
|
||||
|
||||
return ['userId' => $payload['userId'] ?? null, 'expired' => false];
|
||||
}
|
||||
|
||||
#[Route('/refresh', name: 'app_api_auth_refresh', methods: ['POST'])]
|
||||
public function refresh(
|
||||
Request $request,
|
||||
EntityManagerInterface $em,
|
||||
#[Autowire('%kernel.secret%')] string $appSecret,
|
||||
): JsonResponse {
|
||||
$email = $request->headers->get('ETicket-Email', '');
|
||||
$jwt = $request->headers->get('ETicket-JWT', '');
|
||||
|
||||
if ('' === $email || '' === $jwt) {
|
||||
return $this->json(['success' => false, 'data' => null, 'error' => 'Headers ETicket-Email et ETicket-JWT requis.'], 401);
|
||||
}
|
||||
|
||||
$result = self::verifyJwt($jwt, $email, $appSecret);
|
||||
|
||||
if (null === $result['userId']) {
|
||||
return $this->json(['success' => false, 'data' => null, 'error' => 'Token invalide.'], 401);
|
||||
}
|
||||
|
||||
if (!$result['expired']) {
|
||||
return $this->json(['success' => false, 'data' => null, 'error' => 'Token encore valide, pas besoin de refresh.'], 400);
|
||||
}
|
||||
|
||||
$user = $em->getRepository(User::class)->find($result['userId']);
|
||||
if (!$user || $user->getEmail() !== $email) {
|
||||
return $this->json(['success' => false, 'data' => null, 'error' => 'Utilisateur introuvable.'], 401);
|
||||
}
|
||||
|
||||
$token = $this->generateJwt($user, $appSecret);
|
||||
$expiresAt = (new \DateTimeImmutable())->modify('+'.self::JWT_TTL.' seconds');
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'token' => $token,
|
||||
'expiresAt' => $expiresAt->format(\DateTimeInterface::ATOM),
|
||||
],
|
||||
'error' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
private function generateJwt(User $user, string $appSecret): string
|
||||
|
||||
@@ -110,7 +110,7 @@ class ApiDocController extends AbstractController
|
||||
];
|
||||
|
||||
if ($isAuth) {
|
||||
$req['afterResponseScript'] = "const res = insomnia.response.json();\nif (res.success && res.data && res.data.token) {\n insomnia.environment.set('jwt_token', res.data.token);\n}";
|
||||
$req['afterResponseScript'] = "const res = insomnia.response.json();\nif (res && res.success && res.data && res.data.token) {\n insomnia.environment.set('jwt_token', res.data.token);\n}";
|
||||
}
|
||||
|
||||
$resources[] = $req;
|
||||
@@ -193,6 +193,24 @@ class ApiDocController extends AbstractController
|
||||
429 => 'Trop de tentatives',
|
||||
],
|
||||
],
|
||||
[
|
||||
'method' => 'POST',
|
||||
'path' => '/api/auth/refresh',
|
||||
'summary' => 'Rafraichir un token expire',
|
||||
'description' => 'Genere un nouveau token JWT a partir d\'un token expire. La signature doit etre valide. Si le token est encore valide, retourne une erreur 400.',
|
||||
'headers' => $this->authHeaders(),
|
||||
'params' => [],
|
||||
'request' => null,
|
||||
'response' => [
|
||||
'success' => ['type' => 'bool', 'example' => true],
|
||||
'data' => ['type' => 'object', 'example' => '{"token": "eyJ...", "expiresAt": "2026-03-25T12:00:00+00:00"}'],
|
||||
],
|
||||
'statuses' => [
|
||||
200 => 'Nouveau token genere',
|
||||
400 => 'Token encore valide',
|
||||
401 => 'Token invalide ou utilisateur introuvable',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user