Skip to content

Commit

Permalink
Fixes markitosgv#287 - Add LogoutEventListener
Browse files Browse the repository at this point in the history
This commit introcudes a `LogoutEventListener` which invalidates the
given `refresh_token` and unsets the cookie, if enabled.

If there is no `refresh_token`, an error is returned but the cookie is
still unset. The same happens if the supplied `refresh_token` is invalid.

Because the `LogoutEventListener` always sets a response, it would
inhibit normal logout behavior and therefore should only run on a
specifically configured firewall.

Therefore a new configuration option is introduced, called
`logout_firewall`, which contains the name of the firewall that
triggers the logout event we want to hook into (default: `api`).
  • Loading branch information
Jayfrown committed Feb 5, 2022
1 parent 1167006 commit 7faacfb
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 0 deletions.
3 changes: 3 additions & 0 deletions DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ public function getConfigTreeBuilder(): TreeBuilder
->scalarNode('remove_token_from_body')->defaultTrue()->end()
->end()
->end()
->scalarNode('logout_firewall')
->defaultValue('api')
->info('Name of the firewall that triggers the logout event to hook into (default: api)')
->end();

return $treeBuilder;
Expand Down
4 changes: 4 additions & 0 deletions DependencyInjection/GesdinetJWTRefreshTokenExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ public function load(array $configs, ContainerBuilder $container): void
$container->setParameter('gesdinet_jwt_refresh_token.token_parameter_name', $config['token_parameter_name']);
$container->setParameter('gesdinet_jwt_refresh_token.doctrine_mappings', $config['doctrine_mappings']);
$container->setParameter('gesdinet_jwt_refresh_token.cookie', $config['cookie'] ?? []);
$container->setParameter('gesdinet_jwt_refresh_token.logout_firewall_context', sprintf(
'security.firewall.map.context.%s',
$config['logout_firewall']
));

$refreshTokenClass = RefreshTokenEntity::class;
$objectManager = 'doctrine.orm.entity_manager';
Expand Down
126 changes: 126 additions & 0 deletions EventListener/LogoutEventListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php

/*
* This file is part of the GesdinetJWTRefreshTokenBundle package.
*
* (c) Gesdinet <http://www.gesdinet.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Gesdinet\JWTRefreshTokenBundle\EventListener;

use Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenManagerInterface;
use Gesdinet\JWTRefreshTokenBundle\Request\Extractor\ExtractorInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Security\Http\Event\LogoutEvent;

class LogoutEventListener
{
/**
* @var RefreshTokenManagerInterface
*/
private $refreshTokenManager;

/**
* @var ExtractorInterface
*/
private $refreshTokenExtractor;

/**
* @var string
*/
private $tokenParameterName;

/**
* @var array
*/
private $cookieSettings;

/**
* @var string
*/
private $logout_firewall_context;

public function __construct(
RefreshTokenManagerInterface $refreshTokenManager,
ExtractorInterface $refreshTokenExtractor,
string $tokenParameterName,
array $cookieSettings,
string $logout_firewall_context,
) {
$this->refreshTokenManager = $refreshTokenManager;
$this->refreshTokenExtractor = $refreshTokenExtractor;
$this->tokenParameterName = $tokenParameterName;
$this->cookieSettings = array_merge([
'enabled' => false,
'same_site' => 'lax',
'path' => '/',
'domain' => null,
'http_only' => true,
'secure' => true,
'remove_token_from_body' => true,
], $cookieSettings);
$this->logout_firewall_context = $logout_firewall_context;
}

public function onLogout(LogoutEvent $event): void
{
$request = $event->getRequest();
$current_firewall_context = $request->attributes->get('_firewall_context');

if ($current_firewall_context !== $this->logout_firewall_context) {
return;
}

$tokenString = $this->refreshTokenExtractor->getRefreshToken($request, $this->tokenParameterName);
if (null === $tokenString) {
$event->setResponse(
new JsonResponse(
[
'code' => 422,
'message' => 'No refresh_token found.',
],
JsonResponse::HTTP_UNPROCESSABLE_ENTITY
)
);
} else {
$refreshToken = $this->refreshTokenManager->get($tokenString);
if (null === $refreshToken) {
$event->setResponse(
new JsonResponse(
[
'code' => 422,
'message' => 'Invalid refresh_token supplied.',
],
JsonResponse::HTTP_UNPROCESSABLE_ENTITY
)
);
} else {
$this->refreshTokenManager->delete($refreshToken);
$event->setResponse(
new JsonResponse(
[
'code' => 200,
'message' => 'The refresh_token has been invalidated.',
],
JsonResponse::HTTP_OK
)
);
}
}

if ($this->cookieSettings['enabled']) {
$response = $event->getResponse();
$response->headers->clearCookie(
$this->tokenParameterName,
$this->cookieSettings['path'],
$this->cookieSettings['domain'],
$this->cookieSettings['secure'],
$this->cookieSettings['http_only'],
$this->cookieSettings['same_site']
);
}
}
}
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,8 @@ gesdinet_jwt_refresh_token:

By default, the refresh token is returned in the body of a JSON response. You can use the following configuration to set it in a HttpOnly cookie instead. The refresh token is automatically extracted from the cookie during refresh.

To allow users to logout when using cookies, you need to [configure the `LogoutEvent` to trigger on a specific route](#invalidate-refresh-token-on-logout), and call that route during logout.

```yaml
gesdinet_jwt_refresh_token:
cookie:
Expand All @@ -317,6 +319,50 @@ gesdinet_jwt_refresh_token:
remove_token_from_body: true # default value
```

### Invalidate refresh token on logout

This bundle automatically registers an `EventListener` which triggers on `LogoutEvent`s from a specific firewall (default: `api`).

The `LogoutEventListener` automatically invalidates the given refresh token and, if enabled, unsets the cookie.
If no refresh token is supplied, an error is returned but the cookie is still unset. The same happens if the supplied refresh token is invalid.

All you have to do is make sure the `LogoutEvent` triggers on a specific route, and call that route during logout:

```yaml
# in security.yaml
security:
firewalls:
api:
logout:
path: api_token_invalidate
```
```yaml
# in routes.yaml
api_token_invalidate:
path: /api/token/invalidate
```

If you want to configure the `LogoutEvent` to trigger on a different firewall, the name of the firewall has to be configured:

```yaml
# in security.yaml
security:
firewalls:
myfirewall:
logout:
path: api_token_invalidate
```
```yaml
# in routes.yaml
api_token_invalidate:
path: /api/token/invalidate
```
```yaml
# in gesdinet_jwt_refresh_token.yaml
gesdinet_jwt_refresh_token:
logout_firewall: myfirewall
```

### Doctrine Manager Type

By default, the bundle will try to set the appropriate Doctrine object manager for your application using the following logic to define the manager type:
Expand Down
14 changes: 14 additions & 0 deletions Resources/config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use Gesdinet\JWTRefreshTokenBundle\Command\RevokeRefreshTokenCommand;
use Gesdinet\JWTRefreshTokenBundle\Doctrine\RefreshTokenManager;
use Gesdinet\JWTRefreshTokenBundle\EventListener\AttachRefreshTokenOnSuccessListener;
use Gesdinet\JWTRefreshTokenBundle\EventListener\LogoutEventListener;
use Gesdinet\JWTRefreshTokenBundle\Generator\RefreshTokenGenerator;
use Gesdinet\JWTRefreshTokenBundle\Generator\RefreshTokenGeneratorInterface;
use Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenManagerInterface;
Expand Down Expand Up @@ -157,4 +158,17 @@
new Reference('gesdinet.jwtrefreshtoken.refresh_token_manager'),
])
->tag('console.command');

$services->set(LogoutEventListener::class)
->args([
new Reference('gesdinet.jwtrefreshtoken.refresh_token_manager'),
new Reference('gesdinet.jwtrefreshtoken.request.extractor.chain'),
new Parameter('gesdinet_jwt_refresh_token.token_parameter_name'),
new Parameter('gesdinet_jwt_refresh_token.cookie'),
new Parameter('gesdinet_jwt_refresh_token.logout_firewall_context'),
])
->tag('kernel.event_listener', [
'event' => 'Symfony\Component\Security\Http\Event\LogoutEvent',
'method' => 'onLogout',
]);
};

0 comments on commit 7faacfb

Please sign in to comment.