Skip to content

Commit

Permalink
Fixes #287 - Add LogoutEventListener (#302)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jayfrown committed Apr 7, 2022
1 parent 1c1abc8 commit a3d3167
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- Dropped support for MongoDB ODM 1.x
- Dropped support for Symfony 3.4
- Added support for Symfony 6.0
- Added a LogoutEventListener that will invalidate the supplied refresh token and clear the cookie (if configured) when a LogoutEvent is triggered on the configured firewall.

## 1.0.0-beta4

Expand Down
4 changes: 4 additions & 0 deletions DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ 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()
->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
109 changes: 109 additions & 0 deletions EventListener/LogoutEventListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?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
{
private RefreshTokenManagerInterface $refreshTokenManager;
private ExtractorInterface $refreshTokenExtractor;
private string $tokenParameterName;
private array $cookieSettings;
private string $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' => 400,
'message' => 'No refresh_token found.',
],
JsonResponse::HTTP_BAD_REQUEST
)
);

return;
} else {
$refreshToken = $this->refreshTokenManager->get($tokenString);
if (null === $refreshToken) {
$event->setResponse(
new JsonResponse(
[
'code' => 200,
'message' => 'The supplied refresh_token is already invalid.',
],
JsonResponse::HTTP_OK
)
);
} else {
$this->refreshTokenManager->delete($refreshToken);
$event->setResponse(
new JsonResponse(
[
'code' => 200,
'message' => 'The supplied 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 @@ -319,6 +319,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 @@ -331,6 +333,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 and the cookie remains untouched. If the supplied refresh token is (already) invalid, the cookie is unset.

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 @@ -166,4 +167,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 a3d3167

Please sign in to comment.