From b93737391d927bca0caedb04f23a907de526f8d2 Mon Sep 17 00:00:00 2001 From: tristan Date: Mon, 29 Jun 2026 10:25:03 +0200 Subject: [PATCH] fix(core) : logout API renvoie 204 sans redirection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le firewall répondait par une 302 (target /login). Le fetch front suivait le Location absolu (host upstream du proxy « nginx » en dev) → ERR_NAME_NOT_RESOLVED + ~3s de timeout DNS. ApiLogoutSuccessListener rétrograde la réponse en 204 en conservant le Set-Cookie qui efface BEARER. --- config/packages/security.yaml | 7 ++- .../Security/ApiLogoutSuccessListener.php | 55 +++++++++++++++++++ tests/Module/Core/Api/LogoutApiTest.php | 48 ++++++++++++++++ 3 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 src/Module/Core/Infrastructure/Security/ApiLogoutSuccessListener.php create mode 100644 tests/Module/Core/Api/LogoutApiTest.php diff --git a/config/packages/security.yaml b/config/packages/security.yaml index f440a21..0152ffc 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -33,9 +33,14 @@ security: stateless: true provider: app_user_provider jwt: ~ + # API JWT stateless : pas de `target` (redirection 302) — le logout + # renvoie 204 via ApiLogoutSuccessListener. Une redirection generait + # une URL absolue basee sur le Host (en dev : l'upstream proxy + # « nginx », non resolvable par le navigateur => ERR_NAME_NOT_RESOLVED + # + ~3 s de timeout DNS). Le cookie BEARER reste efface par + # delete_cookies. logout: path: /api/logout - target: /login enable_csrf: false delete_cookies: BEARER: diff --git a/src/Module/Core/Infrastructure/Security/ApiLogoutSuccessListener.php b/src/Module/Core/Infrastructure/Security/ApiLogoutSuccessListener.php new file mode 100644 index 0000000..14671f9 --- /dev/null +++ b/src/Module/Core/Infrastructure/Security/ApiLogoutSuccessListener.php @@ -0,0 +1,55 @@ + `ERR_NAME_NOT_RESOLVED` apres ~3 s de timeout DNS avant l'echec + * de la promesse (en prod, c'est un GET parasite de la page cible). Une API + * consommee en fetch ne doit pas rediriger : 204 suffit. + * + * On s'enregistre a une priorite NEGATIVE pour passer APRES les listeners par + * defaut (DefaultLogoutListener priorite 64, CookieClearingLogoutListener + * priorite 0) : la reponse et les Set-Cookie de suppression du BEARER sont alors + * deja en place, on se contente de retrograder la redirection en 204 en + * conservant les en-tetes (donc le cookie BEARER reste efface). + */ +final class ApiLogoutSuccessListener implements EventSubscriberInterface +{ + public static function getSubscribedEvents(): array + { + return [ + LogoutEvent::class => ['onLogout', -255], + ]; + } + + public function onLogout(LogoutEvent $event): void + { + $response = $event->getResponse(); + + // Aucun listener par defaut n'a pose de reponse : on cree directement la 204. + if (null === $response) { + $event->setResponse(new Response(null, Response::HTTP_NO_CONTENT)); + + return; + } + + // Retrograde la redirection (ou toute autre reponse) en 204 sans toucher + // aux en-tetes Set-Cookie deja poses (suppression du BEARER). + $response->setStatusCode(Response::HTTP_NO_CONTENT); + $response->setContent(null); + $response->headers->remove('Location'); + } +} diff --git a/tests/Module/Core/Api/LogoutApiTest.php b/tests/Module/Core/Api/LogoutApiTest.php new file mode 100644 index 0000000..5f3fdff --- /dev/null +++ b/tests/Module/Core/Api/LogoutApiTest.php @@ -0,0 +1,48 @@ + + * `ERR_NAME_NOT_RESOLVED` + ~3 s de timeout DNS. Cf. ApiLogoutSuccessListener. + * + * @internal + */ +final class LogoutApiTest extends AbstractApiTestCase +{ + public function testLogoutReturns204WithoutRedirectAndClearsBearerCookie(): void + { + $client = $this->authenticatedClient('admin', 'admin'); + + $response = $client->request('POST', '/api/logout'); + + self::assertSame(204, $response->getStatusCode(), 'Le logout API doit renvoyer 204 No Content.'); + + $headers = $response->getHeaders(false); + + // Aucune redirection : un fetch ne doit pas avoir de Location a suivre. + self::assertArrayNotHasKey( + 'location', + $headers, + 'Le logout API ne doit pas rediriger (fetch suivrait un Location absolu => ERR_NAME_NOT_RESOLVED).', + ); + + // Le cookie BEARER est efface (Set-Cookie expire / supprime). + $clearsBearer = false; + foreach ($headers['set-cookie'] ?? [] as $cookie) { + if (str_starts_with($cookie, 'BEARER=') + && (str_contains($cookie, 'BEARER=deleted') || str_contains($cookie, 'Max-Age=0')) + ) { + $clearsBearer = true; + } + } + self::assertTrue($clearsBearer, 'Le cookie BEARER doit etre efface au logout.'); + } +}