Skip to content

Commit

Permalink
Merge pull request #157 from chalasr/failure_events
Browse files Browse the repository at this point in the history
Allow to set a custom response in case of authentication failure or invalid/not found token
  • Loading branch information
slashfan committed Apr 7, 2016
2 parents c012cac + 3051ad5 commit 49f53fa
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 19 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 <vella.emmanuel@gmail.com>
* @author Robin Chalas <robin.chalas@gmail.com>
*/
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;
}
}
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 <robin.chalas@gmail.com>
*/
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 <n.cabot@lexik.fr>
* @author Robin Chalas <robin.chalas@gmail.com>
* @author Robin Chalas <robin.chalas@gmail.com>
*/
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');

This comment has been minimized.

Copy link
@slepp

slepp Apr 8, 2016

This is a breaking change in the behaviour of using IS_AUTHENTICATE_ANONYMOUSLY in the security configuration. Parts of the API that were open to the public are returning 401 invalid from here as they're not providing tokens to the API.

This comment has been minimized.

Copy link
@slashfan

slashfan Apr 8, 2016

Author Contributor

@chalasr Could you check that ?

This comment has been minimized.

Copy link
@chalasr

chalasr Apr 8, 2016

Collaborator

@slashfan Of course, really sorry for that.
@slepp, just to be sure , you are getting a "No JWT Token found" ? I will add a check for this in the Listener.

This comment has been minimized.

Copy link
@chalasr

chalasr Apr 8, 2016

Collaborator

@slepp fixed in #159

This comment has been minimized.

Copy link
@slepp

slepp Apr 8, 2016

Just to confirm, it was the No JWT found exception being thrown previously.

I tried the changes in #159 and they seem to work as usual with the anonymous authentication. Thanks.

This comment has been minimized.

Copy link
@chalasr

chalasr Apr 8, 2016

Collaborator

@slepp You're welcome, thank you for the report.

}
}
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 49f53fa

Please sign in to comment.