diff --git a/Event/AuthenticationFailureEvent.php b/Event/AuthenticationFailureEvent.php index 4e0cc9a0..88bcf8ae 100644 --- a/Event/AuthenticationFailureEvent.php +++ b/Event/AuthenticationFailureEvent.php @@ -11,6 +11,7 @@ * AuthenticationFailureEvent * * @author Emmanuel Vella + * @author Robin Chalas */ class AuthenticationFailureEvent extends Event { @@ -36,9 +37,9 @@ class AuthenticationFailureEvent extends Event */ public function __construct(Request $request, AuthenticationException $exception, Response $response) { - $this->request = $request; + $this->request = $request; $this->exception = $exception; - $this->response = $response; + $this->response = $response; } /** @@ -64,4 +65,12 @@ public function getResponse() { return $this->response; } + + /** + * @param Response $response + */ + public function setResponse(Response $response) + { + $this->response = $response; + } } diff --git a/Event/JWTInvalidEvent.php b/Event/JWTInvalidEvent.php new file mode 100644 index 00000000..fc9f6651 --- /dev/null +++ b/Event/JWTInvalidEvent.php @@ -0,0 +1,12 @@ + + */ +class JWTInvalidEvent extends AuthenticationFailureEvent +{ +} diff --git a/Events.php b/Events.php index 641c4e0c..a10f4209 100644 --- a/Events.php +++ b/Events.php @@ -16,7 +16,8 @@ final class Events const AUTHENTICATION_SUCCESS = 'lexik_jwt_authentication.on_authentication_success'; /** - * Dispatched after an authentication failure + * Dispatched after an authentication failure. + * Hook into this event to add a custom error message in the response body. */ const AUTHENTICATION_FAILURE = 'lexik_jwt_authentication.on_authentication_failure'; @@ -43,4 +44,10 @@ final class Events * Hook into this event to perform additional modification to the authenticated token using the payload. */ const JWT_AUTHENTICATED = 'lexik_jwt_authentication.on_jwt_authenticated'; + + /** + * Dispatched after the token has been invalidated by the provider. + * Hook into this event to add a custom error message in the response body. + */ + const JWT_INVALID = 'lexik_jwt_authentication.on_jwt_invalid'; } diff --git a/Resources/config/services.xml b/Resources/config/services.xml index e703a0c5..28d3233d 100644 --- a/Resources/config/services.xml +++ b/Resources/config/services.xml @@ -62,6 +62,9 @@ + + + diff --git a/Resources/doc/2-data-customization.md b/Resources/doc/2-data-customization.md index 9eb457a5..2daf6f96 100644 --- a/Resources/doc/2-data-customization.md +++ b/Resources/doc/2-data-customization.md @@ -178,6 +178,7 @@ public function onAuthenticationSuccessResponse(AuthenticationSuccessEvent $even $event->setData($data); } ``` + #### Events::JWT_ENCODED - get JWT string You may need to get JWT after its creation. @@ -194,4 +195,72 @@ public function onJwtEncoded(JWTEncodedEvent $event) { $token = $event->getJWTString(); } -``` \ No newline at end of file +``` + +#### Events::AUTHENTICATION_FAILURE - customize the failure response + +By default, the response in case of failed authentication is just a json containing a "Bad credentials" message and a 401 status code, but you can set a custom response. + +``` yaml +# services.yml +services: + acme_api.event.authentication_failure_listener: + class: Acme\Bundle\ApiBundle\EventListener\AuthenticationFailureListener + tags: + - { name: kernel.event_listener, event: lexik_jwt_authentication.on_authentication_failure, method: onAuthenticationFailureResponse } +``` + +Example 7: set a custom response on authentication failure + +``` php +// Acme\Bundle\ApiBundle\EventListener\AuthenticationFailureListener.php +/** + * @param AuthenticationFailureEvent $event + */ +public function onAuthenticationFailureResponse(AuthenticationFailureEvent $event) +{ + $data = [ + 'status' => '401 Unauthorized', + 'message' => 'Bad credentials, please verify that your username/password are correctly set', + ]; + + $response = new JsonResponse($data, 401); + + $event->setResponse($response); +} +``` + +#### Events::JWT_INVALID - customize the invalid token response + +By default, if the token is invalid or not set, the response is just a json containing the corresponding error message and a 401 status code, but you can set a custom response. + +``` yaml +# services.yml +services: + acme_api.event.jwt_invalid_listener: + class: Acme\Bundle\ApiBundle\EventListener\JWTInvalidListener + tags: + - { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_invalid, method: onJWTInvalid } +``` + +Example 8: set a custom response message on invalid token + +``` php +// Acme\Bundle\ApiBundle\EventListener\JWTInvalidListener.php +/** + * @param JWTInvalidEvent $event + */ +public function onJWTInvalid(JWTInvalidEvent $event) +{ + $data = [ + 'status' => '403 Forbidden', + 'message' => 'Your token is invalid, please login again to get a new one', + ]; + + $response = new JsonResponse($data, 403); + + $event->setResponse($response); +} +``` + +__Note:__ This feature is not available if the `throw_exceptions` firewall option is set to `true`. diff --git a/Security/Firewall/JWTListener.php b/Security/Firewall/JWTListener.php index 621f0088..fad1656f 100644 --- a/Security/Firewall/JWTListener.php +++ b/Security/Firewall/JWTListener.php @@ -4,20 +4,24 @@ use Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\TokenExtractorInterface; use Lexik\Bundle\JWTAuthenticationBundle\Security\Authentication\Token\JWTUserToken; +use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTInvalidEvent; +use Lexik\Bundle\JWTAuthenticationBundle\Events; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException; use Symfony\Component\Security\Core\SecurityContextInterface; use Symfony\Component\Security\Http\Firewall\ListenerInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * JWTListener * * @author Nicolas Cabot - * @author Robin Chalas + * @author Robin Chalas */ class JWTListener implements ListenerInterface { @@ -31,6 +35,11 @@ class JWTListener implements ListenerInterface */ protected $authenticationManager; + /** + * @var EventDispatcherInterface + */ + protected $dispatcher; + /** * @var array */ @@ -46,8 +55,7 @@ class JWTListener implements ListenerInterface * @param AuthenticationManagerInterface $authenticationManager * @param array $config */ - public function __construct($tokenStorage, AuthenticationManagerInterface $authenticationManager, array $config = [] - ) + public function __construct($tokenStorage, AuthenticationManagerInterface $authenticationManager, array $config = []) { if (!$tokenStorage instanceof TokenStorageInterface && !$tokenStorage instanceof SecurityContextInterface) { throw new \InvalidArgumentException('Argument 1 should be an instance of Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface or Symfony\Component\Security\Core\SecurityContextInterface'); @@ -64,15 +72,15 @@ public function __construct($tokenStorage, AuthenticationManagerInterface $authe */ public function handle(GetResponseEvent $event) { - if (!($requestToken = $this->getRequestToken($event->getRequest()))) { - return; - } - - $token = new JWTUserToken(); - $token->setRawToken($requestToken); + $request = $event->getRequest(); try { + $requestToken = $this->getRequestToken($request); + + $token = new JWTUserToken(); + $token->setRawToken($requestToken); + $authToken = $this->authenticationManager->authenticate($token); $this->tokenStorage->setToken($authToken); @@ -80,21 +88,22 @@ public function handle(GetResponseEvent $event) } catch (AuthenticationException $failed) { - $statusCode = 401; - if ($this->config['throw_exceptions']) { throw $failed; } $data = [ - 'code' => $statusCode, + 'code' => 401, 'message' => $failed->getMessage(), ]; - $response = new JsonResponse($data, $statusCode); + $response = new JsonResponse($data, $data['code']); $response->headers->set('WWW-Authenticate', 'Bearer'); - $event->setResponse($response); + $jwtInvalidEvent = new JWTInvalidEvent($request, $failed, $response); + $this->dispatcher->dispatch(Events::JWT_INVALID, $jwtInvalidEvent); + + $event->setResponse($jwtInvalidEvent->getResponse()); } } @@ -106,6 +115,14 @@ public function addTokenExtractor(TokenExtractorInterface $extractor) $this->tokenExtractors[] = $extractor; } + /** + * @param EventDispatcherInterface $dispatcher + */ + public function setDispatcher(EventDispatcherInterface $dispatcher) + { + $this->dispatcher = $dispatcher; + } + /** * @param Request $request * @@ -120,6 +137,6 @@ protected function getRequestToken(Request $request) } } - return false; + throw new AuthenticationCredentialsNotFoundException('No JWT token found'); } } diff --git a/Tests/Security/Authentication/Firewall/JWTListenerTest.php b/Tests/Security/Authentication/Firewall/JWTListenerTest.php index 9a92e0cb..2fc4d674 100644 --- a/Tests/Security/Authentication/Firewall/JWTListenerTest.php +++ b/Tests/Security/Authentication/Firewall/JWTListenerTest.php @@ -20,11 +20,13 @@ public function testHandle() // no token extractor : should return void $listener = new JWTListener($this->getTokenStorageMock(), $this->getAuthenticationManagerMock()); + $listener->setDispatcher($this->getEventDispatcherMock()); $this->assertNull($listener->handle($this->getEvent())); // one token extractor with no result : should return void $listener = new JWTListener($this->getTokenStorageMock(), $this->getAuthenticationManagerMock()); + $listener->setDispatcher($this->getEventDispatcherMock()); $listener->addTokenExtractor($this->getAuthorizationHeaderTokenExtractorMock(false)); $this->assertNull($listener->handle($this->getEvent())); @@ -34,6 +36,7 @@ public function testHandle() $authenticationManager->expects($this->once())->method('authenticate'); $listener = new JWTListener($this->getTokenStorageMock(), $authenticationManager); + $listener->setDispatcher($this->getEventDispatcherMock()); $listener->addTokenExtractor($this->getAuthorizationHeaderTokenExtractorMock('token')); $listener->handle($this->getEvent()); @@ -50,6 +53,7 @@ public function testHandle() ->will($this->throwException($invalidTokenException)); $listener = new JWTListener($this->getTokenStorageMock(), $authenticationManager); + $listener->setDispatcher($this->getEventDispatcherMock()); $listener->addTokenExtractor($this->getAuthorizationHeaderTokenExtractorMock('token')); $event = $this->getEvent(); @@ -134,4 +138,14 @@ protected function getEvent() return $event; } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject + */ + protected function getEventDispatcherMock() + { + return $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcher') + ->disableOriginalConstructor() + ->getMock(); + } }