Skip to content

Commit

Permalink
Introduce JWTExpiredEvent
Browse files Browse the repository at this point in the history
  • Loading branch information
chalasr committed Aug 27, 2016
1 parent 34d8901 commit 058f66f
Show file tree
Hide file tree
Showing 17 changed files with 244 additions and 19 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
7 changes: 5 additions & 2 deletions Encoder/DefaultEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use InvalidArgumentException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTEncodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTExpirationTimeReachedException;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWSProviderInterface;

/**
Expand Down Expand Up @@ -56,14 +57,16 @@ public function decode($token)
throw new JWTDecodeFailureException('Invalid JWT Token', $e);
}

$payload = $jws->getPayload();

if ($jws->isExpired()) {
throw new JWTDecodeFailureException('Expired JWT Token');
throw new JWTExpirationTimeReachedException($payload);
}

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.');
}

return $jws->getPayload();
return $payload;
}
}
8 changes: 6 additions & 2 deletions Encoder/JWTEncoderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTEncodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTExpirationTimeReachedException;

/**
* JWTEncoderInterface.
Expand All @@ -17,7 +18,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 configuration...)
*/
public function encode(array $data);

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

namespace Lexik\Bundle\JWTAuthenticationBundle\Event;

use Lexik\Bundle\JWTAuthenticationBundle\Exception\ExpiredTokenException;
use Symfony\Component\HttpFoundation\Response;

/**
* JWTExpiredEvent.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class JWTExpiredEvent extends AuthenticationFailureEvent implements JWTFailureEventInterface
{
/**
* @param ExpiredTokenException $exception
* @param Response $response
*/
public function __construct(ExpiredTokenException $exception, Response $response)
{
parent::__construct($exception, $response);
}

public function getInvalidPayload()
{
return $this->exception->getPayload();
}
}
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, containing a new token for instance.
*/
const JWT_EXPIRED = 'lexik_jwt_authentication.on_jwt_expired';
}
46 changes: 46 additions & 0 deletions Exception/ExpiredTokenException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?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
{
/**
* @var array
*/
private $payload;

/**
* @param array $payload The invalidated payload
*/
public function __construct(array $payload)
{
parent::__construct();

$this->payload = $payload;
}

/**
* @return array
*/
public function getPayload()
{
return $this->payload;
}

/**
* {@inheritdoc}
*/
public function getMessageKey()
{
return 'Expired JWT Token';
}
}
2 changes: 1 addition & 1 deletion Exception/JWTDecodeFailureException.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
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>
*/
Expand Down
2 changes: 1 addition & 1 deletion Exception/JWTEncodeFailureException.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
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>
*/
Expand Down
34 changes: 34 additions & 0 deletions Exception/JWTExpirationTimeReachedException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace Lexik\Bundle\JWTAuthenticationBundle\Exception;

/**
* JWTExpirationTimeReachedException should be thrown from an encoder when the decoded
* token is expired, catchable during an authentication process.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class JWTExpirationTimeReachedException extends JWTDecodeFailureException
{
/**
* @param array $invalidPayload The payload in which the exp time is reached
*/
public function __construct(array $invalidPayload)
{
$this->invalidPayload = $invalidPayload;

parent::__construct('Expired JWT Token');
}

/**
* Gets the payload of the expired token. This can be useful
* to reuse the user informations, for automatically providing
* a new token for instance.
*
* @return array
*/
public function getInvalidPayload()
{
return $this->invalidPayload;
}
}
36 changes: 34 additions & 2 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,40 @@ public function onJWTNotFound(JWTNotFoundEvent $event)
}
```

#### Events::JWT_EXPIRED - customize the response on expired token

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.
Thanks to this event, you can set a custom response.

``` yaml
# services.yml
services:
acme_api.event.jwt_invalid_listener:
class: Acme\Bundle\ApiBundle\EventListener\JWTInvalidListener
arguments:
- '@lexik_jwt_authentication.jwt_manager'
tags:
- { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_not_found, method: attachNewToken }
```
Example 8: attach a new token to the response in case of expired token
``` php
// Acme\Bundle\ApiBundle\EventListener\JWTAutoRefresher.php

use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTExpiredEvent;

/**
* @param JWTExpiredEvent $event
*/
public function onJWTExpired(JWTNotFoundEvent $event)
{
$response = new JsonResponse($data, 403);

$event->setResponse($response);
}
```

__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`).
20 changes: 18 additions & 2 deletions Security/Guard/JWTTokenAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
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\Exception\JWTExpirationTimeReachedException;
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 @@ -85,6 +88,8 @@ public function getCredentials(Request $request)
}

$preAuthToken->setPayload($payload);
} catch (JWTExpirationTimeReachedException $e) {
throw new ExpiredTokenException($e->getInvalidPayload());
} catch (JWTDecodeFailureException $e) {
throw JWTAuthenticationException::invalidToken($e);
}
Expand Down Expand Up @@ -140,8 +145,19 @@ 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()),
$authException->getPayload()
);
$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
2 changes: 1 addition & 1 deletion Tests/Encoder/DefaultEncoderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ 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\JWTExpirationTimeReachedException
*/
public function testDecodeFromExpiredPayload()
{
Expand Down
8 changes: 5 additions & 3 deletions Tests/Functional/CompleteTokenAuthenticationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public static function setupBeforeClass()
static::bootKernel();
}

public function testAccessSecuredRoute()
public function testAccessSecuredRoute($token = null)
{
static::$client = static::createAuthenticatedClient();
static::$client->request('GET', '/api/secured');
Expand Down 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
Loading

0 comments on commit 058f66f

Please sign in to comment.