Skip to content

Commit

Permalink
Introduce JWTExpiredEvent
Browse files Browse the repository at this point in the history
    * Refactor encoder exceptions
    * Define encode/decode exceptions message from Encoder, Remove auto-renew behavior
    * Code quality tweeaks
  • Loading branch information
chalasr committed Sep 2, 2016
1 parent 73ebbac commit 8680a1c
Show file tree
Hide file tree
Showing 19 changed files with 175 additions and 32 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ For a diff between two versions https://github.com/lexik/LexikJWTAuthenticationB

## [2.0](https://github.com/lexik/LexikJWTAuthenticationBundle/tree/2.0)

* feature [\#230](https://github.com/lexik/LexikJWTAuthenticationBundle/pull/230) Introduce JWTExpiredEvent ([chalasr](https://github.com/chalasr))

* feature [\#184](https://github.com/lexik/LexikJWTAuthenticationBundle/pull/184) [Security] Deprecate current system in favor of a JWTTokenAuthenticator (Guard) ([chalasr](https://github.com/chalasr))

* feature [\#218](https://github.com/lexik/LexikJWTAuthenticationBundle/pull/218) Add more flexibility in token extractors configuration ([chalasr](https://github.com/chalasr))
Expand Down
14 changes: 8 additions & 6 deletions Encoder/DefaultEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ public function encode(array $payload)
try {
$jws = $this->jwsProvider->create($payload);
} catch (InvalidArgumentException $e) {
throw new JWTEncodeFailureException('An error occurred while trying to encode the JWT token.', $e);
throw new JWTEncodeFailureException(JWTEncodeFailureException::INVALID_CONFIG, 'An error occured while trying to encode the JWT token. Please verify your configuration (private key/passphrase)', $e);
}

if (!$jws->isSigned()) {
throw new JWTEncodeFailureException('Unable to create a signed JWT from the given configuration.');
throw new JWTEncodeFailureException(JWTEncodeFailureException::UNSIGNED_TOKEN, 'Unable to create a signed JWT from the given configuration.');
}

return $jws->getToken();
Expand All @@ -53,17 +53,19 @@ public function decode($token)
try {
$jws = $this->jwsProvider->load($token);
} catch (InvalidArgumentException $e) {
throw new JWTDecodeFailureException('Invalid JWT Token', $e);
throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid JWT Token', $e);
}

$payload = $jws->getPayload();

if ($jws->isExpired()) {
throw new JWTDecodeFailureException('Expired JWT Token');
throw new JWTDecodeFailureException(JWTDecodeFailureException::EXPIRED_TOKEN, 'Expired JWT Token');
}

if (!$jws->isVerified()) {
throw new JWTDecodeFailureException('Unable to verify the given JWT through the given configuration. If the "lexik_jwt_authentication.encoder" encryption options have been changed since your last authentication, please renew the token. If the problem persists, verify that the configured keys/passphrase are valid.');
throw new JWTDecodeFailureException(JWTDecodeFailureException::UNVERIFIED_TOKEN, 'Unable to verify the given JWT through the given configuration. If the "lexik_jwt_authentication.encoder" encryption options have been changed since your last authentication, please renew the token. If the problem persists, verify that the configured keys/passphrase are valid.');
}

return $jws->getPayload();
return $payload;
}
}
6 changes: 4 additions & 2 deletions Encoder/JWTEncoderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ interface JWTEncoderInterface
*
* @return string the encoded token string
*
* @throws JWTEncodeFailureException If an error occurred during the creation of the token (invalid configuration...)
* @throws JWTEncodeFailureException If an error occurred while trying to create
* the token (invalid crypto key, invalid payload...)
*/
public function encode(array $data);

Expand All @@ -26,7 +27,8 @@ public function encode(array $data);
*
* @return array
*
* @throws JWTDecodeFailureException If an error occurred during the loading of the token (invalid signature, expired token...)
* @throws JWTDecodeFailureException If an error occurred while trying to load the token
* (invalid signature, invalid crypto key, expired token...)
*/
public function decode($token);
}
12 changes: 12 additions & 0 deletions Event/JWTExpiredEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace Lexik\Bundle\JWTAuthenticationBundle\Event;

/**
* JWTExpiredEvent.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class JWTExpiredEvent extends AuthenticationFailureEvent implements JWTFailureEventInterface
{
}
7 changes: 7 additions & 0 deletions Events.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,11 @@ final class Events
* Hook into this event to set a custom response.
*/
const JWT_NOT_FOUND = 'lexik_jwt_authentication.on_jwt_not_found';

/**
* Dispatched when the token is expired.
* The expired token's payload can be retrieved by hooking into this event, so you can set a different
* response.
*/
const JWT_EXPIRED = 'lexik_jwt_authentication.on_jwt_expired';
}
23 changes: 23 additions & 0 deletions Exception/ExpiredTokenException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Lexik\Bundle\JWTAuthenticationBundle\Exception;

use Lexik\Bundle\JWTAuthenticationBundle\Security\Guard\JWTTokenAuthenticator;
use Symfony\Component\Security\Core\Exception\AuthenticationException;

/**
* Exception that should be thrown from a {@link JWTTokenAuthenticator} implementation during
* an authentication process.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class ExpiredTokenException extends AuthenticationException
{
/**
* {@inheritdoc}
*/
public function getMessageKey()
{
return 'Expired JWT Token';
}
}
5 changes: 4 additions & 1 deletion Exception/JWTDecodeFailureException.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
namespace Lexik\Bundle\JWTAuthenticationBundle\Exception;

/**
* Base class for exceptions thrown during JWTEncoderInterface::decode().
* JWTDecodeFailureException is thrown if an error occurs in the token decoding process.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class JWTDecodeFailureException extends JWTFailureException
{
const INVALID_TOKEN = 'invalid_token';
const UNVERIFIED_TOKEN = 'unverified_token';
const EXPIRED_TOKEN = 'expired_token';
}
4 changes: 3 additions & 1 deletion Exception/JWTEncodeFailureException.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
namespace Lexik\Bundle\JWTAuthenticationBundle\Exception;

/**
* Base class for exceptions thrown during JWTEncoderInterface::encode().
* JWTEncodeFailureException is thrown if an error occurs in the token encoding process.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class JWTEncodeFailureException extends JWTFailureException
{
const INVALID_CONFIG = 'invalid_config';
const UNSIGNED_TOKEN = 'unsigned_token';
}
18 changes: 17 additions & 1 deletion Exception/JWTFailureException.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,27 @@
class JWTFailureException extends \Exception
{
/**
* @var string
*/
private $reason;

/**
* @param string $reason
* @param string $message
* @param \Exception|null $previous
*/
public function __construct($message, \Exception $previous = null)
public function __construct($reason, $message, \Exception $previous = null)
{
$this->reason = $reason;

parent::__construct($message, 0, $previous);
}

/**
* @return string
*/
public function getReason()
{
return $this->reason;
}
}
40 changes: 35 additions & 5 deletions Resources/doc/2-data-customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,6 @@ public function onJWTInvalid(JWTInvalidEvent $event)
}
```

__Note:__ This feature is not available if the `throw_exceptions` firewall option is set to `true`.

#### Events::JWT_NOT_FOUND - customize the response on token not found

By default, if no token is found in a request, the authentication listener will either call the entry point that returns a unauthorized (401) json response, or (if the firewall allows anonymous requests), just let the request continue.
Expand Down Expand Up @@ -326,6 +324,38 @@ public function onJWTNotFound(JWTNotFoundEvent $event)
}
```

__Protip:__ You might want to use the same method for customizing the response on both `JWT_INVALID` and `JWT_NOT_FOUND` events.
For that, use the `Event\JWTFailureEventInterface` interface to typehint the event argument of your listener's method, rather than
a specific event class (i.e. `JWTNotFoundEvent` or `JWTInvalidEvent`).
#### Events::JWT_EXPIRED - customize the response message on expired token

By default, if the token provided in the request is expired, the authentication listener will call the entry point returning an unauthorized (401) json response.
Thanks to this event, you can set a custom response or simply change the response message.

``` yaml
# services.yml
services:
acme_api.event.jwt_expired_listener:
class: Acme\Bundle\ApiBundle\EventListener\JWTExpiredListener
tags:
- { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_expired, method: onJWTExpired }
```
Example 8: customize the response in case of expired token
``` php
// Acme\Bundle\ApiBundle\EventListener\JWTExpiredListener.php

use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTExpiredEvent;

/**
* @param JWTExpiredEvent $event
*/
public function onJWTExpired(JWTExpiredEvent $event)
{
/** @var \Lexik\Bundle\JWTAuthenticationBundle\Response\JWTAuthenticationFailureResponse */
$response = $event->getResponse();

$response->setMessage('Your token is expired, please renew it.');
}
```

__Protip:__ You might want to use the same method for customizing the response on both `JWT_INVALID`, `JWT_NOT_FOUND` and/or `JWT_EXPIRED` events.
For that, use the `Event\JWTFailureEventInterface` interface to type-hint the event argument of your listener's method, rather the class corresponding to one of these specific events.
20 changes: 18 additions & 2 deletions Security/Guard/JWTTokenAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
namespace Lexik\Bundle\JWTAuthenticationBundle\Security\Guard;

use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTAuthenticatedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTExpiredEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTInvalidEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTNotFoundEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Events;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\ExpiredTokenException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTAuthenticationException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Response\JWTAuthenticationFailureResponse;
Expand Down Expand Up @@ -86,6 +88,10 @@ public function getCredentials(Request $request)

$preAuthToken->setPayload($payload);
} catch (JWTDecodeFailureException $e) {
if (JWTDecodeFailureException::EXPIRED_TOKEN === $e->getReason()) {
throw new ExpiredTokenException();
}

throw JWTAuthenticationException::invalidToken($e);
}

Expand Down Expand Up @@ -140,8 +146,18 @@ public function getUser($preAuthToken, UserProviderInterface $userProvider)
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $authException)
{
$event = new JWTInvalidEvent($authException, new JWTAuthenticationFailureResponse($authException->getMessage()));
$this->dispatcher->dispatch(Events::JWT_INVALID, $event);
if ($authException instanceof ExpiredTokenException) {
$event = new JWTExpiredEvent(
$authException,
// After adding other AuthException classes, assign $response
// before the check and use it for both events, using getMessageKey()
new JWTAuthenticationFailureResponse($authException->getMessageKey())
);
$this->dispatcher->dispatch(Events::JWT_EXPIRED, $event);
} else {
$event = new JWTInvalidEvent($authException, new JWTAuthenticationFailureResponse($authException->getMessage()));
$this->dispatcher->dispatch(Events::JWT_INVALID, $event);
}

return $event->getResponse();
}
Expand Down
3 changes: 2 additions & 1 deletion Tests/Encoder/DefaultEncoderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ public function testDecodeFromUnverifiedJWS()
/**
* Tests that calling DefaultEncoder::decode() with an expired payload correctly fails.
*
* @expectedException \Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException
* @expectedException \Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException
* @expectedExceptionMessage Expired JWT Token
*/
public function testDecodeFromExpiredPayload()
{
Expand Down
6 changes: 4 additions & 2 deletions Tests/Functional/CompleteTokenAuthenticationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public function testAccessSecuredRouteWithInvalidToken()
/**
* @group time-sensitive
*/
public function testAccessSecuredRouteWithExpiredToken()
public function testAccessSecuredRouteWithExpiredToken($fail = true)
{
static::bootKernel();

Expand All @@ -64,7 +64,9 @@ public function testAccessSecuredRouteWithExpiredToken()

$response = static::$client->getResponse();

$this->assertFailure($response);
if (true === $fail) {
$this->assertFailure($response);
}

return json_decode($response->getContent(), true);
}
Expand Down
2 changes: 1 addition & 1 deletion Tests/Functional/DefaultTokenAuthenticationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public function testAccessSecuredRouteWithInvalidToken()
/**
* @group time-sensitive
*/
public function testAccessSecuredRouteWithExpiredToken()
public function testAccessSecuredRouteWithExpiredToken($fail = true)
{
$response = parent::testAccessSecuredRouteWithExpiredToken();

Expand Down
7 changes: 4 additions & 3 deletions Tests/Functional/SubscribedTokenAuthenticationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Lexik\Bundle\JWTAuthenticationBundle\Tests\Functional;

use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTDecodedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTExpiredEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTInvalidEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTNotFoundEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Events;
Expand Down Expand Up @@ -75,9 +76,9 @@ public function testAccessSecuredRouteWithInvalidJWTDecodedEvent()
/**
* @group time-sensitive
*/
public function testAccessSecuredRouteWithExpiredToken()
public function testAccessSecuredRouteWithExpiredToken($fail = true)
{
self::$subscriber->setListener(Events::JWT_INVALID, function (JWTInvalidEvent $e) {
self::$subscriber->setListener(Events::JWT_EXPIRED, function (JWTExpiredEvent $e) {
$response = $e->getResponse();

if ($response instanceof JWTAuthenticationFailureResponse) {
Expand All @@ -89,6 +90,6 @@ public function testAccessSecuredRouteWithExpiredToken()

$this->assertSame('Custom JWT Expired Token message', $response['message']);

self::$subscriber->unsetListener(Events::JWT_INVALID);
self::$subscriber->unsetListener(Events::JWT_EXPIRED);
}
}
2 changes: 2 additions & 0 deletions Tests/Functional/Utils/CallableEventSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTDecodedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTEncodedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTExpiredEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTInvalidEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTNotFoundEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Events;
Expand All @@ -22,6 +23,7 @@ class CallableEventSubscriber implements EventSubscriberInterface
Events::JWT_NOT_FOUND => JWTNotFoundEvent::class,
Events::JWT_ENCODED => JWTEncodedEvent::class,
Events::JWT_AUTHENTICATED => JWTAuthenticatedEvent::class,
Events::JWT_EXPIRED => JWTExpiredEvent::class,
];

public static function getSubscribedEvents()
Expand Down
26 changes: 25 additions & 1 deletion Tests/Security/Guard/JWTTokenAuthenticatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTInvalidEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTNotFoundEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Events;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\ExpiredTokenException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTAuthenticationException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Response\JWTAuthenticationFailureResponse;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Authentication\Token\JWTUserToken;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Authentication\Token\PreAuthenticationJWTUserToken;
Expand Down Expand Up @@ -51,7 +53,29 @@ public function testGetCredentialsWithInvalidToken()
))->getCredentials($this->getRequestMock());
}

public function testGetCredentialsWithoutToken()
public function testGetCredentialsWithExpiredToken()
{
$jwtManager = $this->getJWTManagerMock();
$jwtManager
->expects($this->once())
->method('decode')
->with(new PreAuthenticationJWTUserToken('token'))
->will($this->throwException(new JWTDecodeFailureException(JWTDecodeFailureException::EXPIRED_TOKEN, 'Expired JWT Token')));

try {
(new JWTTokenAuthenticator(
$jwtManager,
$this->getEventDispatcherMock(),
$this->getTokenExtractorMock('token')
))->getCredentials($this->getRequestMock());

$this->fail(sprintf('Expected exception of type "%s" to be thrown.', ExpiredTokenException::class));
} catch (ExpiredTokenException $e) {
$this->assertSame('Expired JWT Token', $e->getMessageKey());
}
}

public function testGetCredentialsReturnsNullWithoutToken()
{
$authenticator = new JWTTokenAuthenticator(
$this->getJWTManagerMock(),
Expand Down
Loading

0 comments on commit 8680a1c

Please sign in to comment.