fix(core) : logout API renvoie 204 sans redirection
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.
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Core\Infrastructure\Security;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Http\Event\LogoutEvent;
|
||||
|
||||
/**
|
||||
* Logout d'une API JWT stateless : renvoie « 204 No Content » au lieu de la
|
||||
* redirection 302 par defaut de Symfony.
|
||||
*
|
||||
* Pourquoi : le `DefaultLogoutListener` pose toujours une RedirectResponse (vers
|
||||
* le `target` configure, ou `/` par defaut). Cote navigateur, `fetch` suit cette
|
||||
* 302 ; le Location est resolu en URL absolue a partir du Host de la requete, et
|
||||
* en dev ce Host est l'upstream du proxy Nuxt (« nginx »), non resolvable par le
|
||||
* navigateur => `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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Module\Core\Api;
|
||||
|
||||
/**
|
||||
* Logout de l'API JWT stateless (POST /api/logout).
|
||||
*
|
||||
* Garde-fou de regression : le logout doit renvoyer 204 sans redirection. Une
|
||||
* 302 (comportement Symfony par defaut via `target`) ferait suivre au `fetch`
|
||||
* du front un Location absolu base sur le Host de la requete ; en dev, ce Host
|
||||
* est l'upstream du proxy Nuxt (« nginx »), non resolvable par le navigateur =>
|
||||
* `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.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user