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 f779216
Show file tree
Hide file tree
Showing 16 changed files with 211 additions and 17 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;
}
}
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
32 changes: 29 additions & 3 deletions Tests/Functional/SubscribedTokenAuthenticationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
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;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Tests\Stubs\User;
use Lexik\Bundle\JWTAuthenticationBundle\Response\JWTAuthenticationSuccessResponse;

/**
* Tests the overriding authentication response mechanism.
Expand Down Expand Up @@ -66,16 +70,38 @@ 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) {
$e->getResponse()->setMessage('Custom JWT Expired Token message');
});

$response = parent::testAccessSecuredRouteWithExpiredToken();

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

self::$subscriber->unsetListener(Events::JWT_INVALID);
self::$subscriber->unsetListener(Events::JWT_EXPIRED);
}

/**
* @group time-sensitive
*/
public function testRenewTokenOnJWTExpired()
{
static::bootKernel();
$jwtManager = static::$kernel->getContainer()->get('lexik_jwt_authentication.jwt_manager');

self::$subscriber->setListener(Events::JWT_EXPIRED, function (JWTExpiredEvent $e) use ($jwtManager) {
$user = new User($e->getInvalidPayload(), '');
$e->setResponse(new JWTAuthenticationSuccessResponse($jwtManager->create($user)));
});

$response = parent::testAccessSecuredRouteWithExpiredToken(false);

$this->assertArrayHasKey('token', $response);

parent::testAccessSecuredRoute($response['token']);

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
Loading

0 comments on commit f779216

Please sign in to comment.