Skip to content

Commit

Permalink
Add Events::JWT_INVALID and Events::AUTHENTICATION_FAILURE listening …
Browse files Browse the repository at this point in the history
…feature

Allow users to create an AuthenticationFailureListener and set a custom Response to the Events::AUTHENTICATION_FAILURE
Add Events::JWT_INVALID and allow users to listen on it and set a custom Response

Fix services

Replace JWTInvalidEvent|AuthenticationFailureEvent::setMessage() by setResponse(), Replace dispatcher constructor injection in favor of setter injection

Handle case of no token found as JWTInvalid

Update documentation for JWT_INVALID & AUTHENTICATION_FAILURE Events

Fix mistake in doc

remove duplicated code in JWTInvalidEvent by extending AuthenticationFailureEvent
  • Loading branch information
chalasr committed Apr 5, 2016
1 parent c012cac commit 74014aa
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 20 deletions.
13 changes: 11 additions & 2 deletions Event/AuthenticationFailureEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* AuthenticationFailureEvent
*
* @author Emmanuel Vella <[email protected]>
* @author Robin Chalas <[email protected]>
*/
class AuthenticationFailureEvent extends Event
{
Expand All @@ -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;
}

/**
Expand All @@ -64,4 +65,12 @@ public function getResponse()
{
return $this->response;
}

/**
* @param Response $response
*/
public function setResponse(Response $response)
{
$this->response = $response;
}
}
2 changes: 1 addition & 1 deletion Event/JWTAuthenticatedEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;

/**
* JWTAuthenticatedEvent
* JWTCreatedEvent
*/
class JWTAuthenticatedEvent extends Event
{
Expand Down
12 changes: 12 additions & 0 deletions Event/JWTInvalidEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace Lexik\Bundle\JWTAuthenticationBundle\Event;

/**
* JWTInvalidEvent
*
* @author Robin Chalas <[email protected]>
*/
class JWTInvalidEvent extends AuthenticationFailureEvent
{
}
9 changes: 8 additions & 1 deletion Events.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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';
}
3 changes: 3 additions & 0 deletions Resources/config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@
<argument /> <!-- security.token_storage or security.context for Symfony <2.6 -->
<argument type="service" id="security.authentication.manager" />
<argument /> <!-- Options -->
<call method="setDispatcher">
<argument type="service" id="event_dispatcher"/>
</call>
</service>
<!-- Authorization Header Token Extractor -->
<service id="lexik_jwt_authentication.extractor.authorization_header_extractor" class="%lexik_jwt_authentication.extractor.authorization_header_extractor.class%" public="false">
Expand Down
71 changes: 70 additions & 1 deletion Resources/doc/2-data-customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -194,4 +195,72 @@ public function onJwtEncoded(JWTEncodedEvent $event)
{
$token = $event->getJWTString();
}
```
```

#### 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`.
47 changes: 32 additions & 15 deletions Security/Firewall/JWTListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>
* @author Robin Chalas <[email protected]>
* @author Robin Chalas <[email protected]>
*/
class JWTListener implements ListenerInterface
{
Expand All @@ -31,6 +35,11 @@ class JWTListener implements ListenerInterface
*/
protected $authenticationManager;

/**
* @var EventDispatcherInterface
*/
protected $dispatcher;

/**
* @var array
*/
Expand All @@ -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');
Expand All @@ -64,37 +72,38 @@ 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);

return;

} 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());
}
}

Expand All @@ -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
*
Expand All @@ -120,6 +137,6 @@ protected function getRequestToken(Request $request)
}
}

return false;
throw new AuthenticationCredentialsNotFoundException('No JWT token found');
}
}
14 changes: 14 additions & 0 deletions Tests/Security/Authentication/Firewall/JWTListenerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()));

Expand All @@ -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());

Expand All @@ -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();
Expand Down Expand Up @@ -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();
}
}

0 comments on commit 74014aa

Please sign in to comment.