When the JWT token of a user is expired, a JWTAuthenticationFailureResponse is shown. Is there a way to not do that? And just stop authentication and let the user continue anonymous?
Currently, there is no way to do this by subscribing to the JWTExpiredEvent event. Since the event has a default JWTAuthenticationFailureResponse response that can only be overwritten (not set to null).
I'm willing to create a PR for this use case, but first want to discuss the best approach. Any thoughts?
I was using a cookie to store the JWT. I think I already solved this issue, because I had to make sure the cookie expiry time was the same as the JWT expiry time. Then, the scenario above will almost never occur.
Hello @ruudk,
What you are asking is legit to me. Though, the need is quite specific, the expected behavior stays to throw exceptions if bad credentials are given (which is the case) and don't throw them if no credentials are given, so I'm not sure that it should be solved in the bundle.
A way to solve that could be to inject the Symfony\Bundle\SecurityBundle\Security\FirewallMap (security.firewall_map service) in the authenticator, catch all AuthenticationException in getCredentials() and rethrow them only if the firewall doesn't allow anonymous.
Here is what I end up with:
namespace AppBundle\Security;
use Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\TokenExtractorInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Guard\JWTTokenAuthenticator;
use Symfony\Bundle\SecurityBundle\Security\FirewallMap;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
final class JWTAuthenticator extends JWTTokenAuthenticator
{
private $firewallMap;
public function __construct(
JWTTokenManagerInterface $jwtManager,
EventDispatcherInterface $dispatcher,
TokenExtractorInterface $tokenExtractor,
FirewallMap $firewallMap
) {
parent::__construct($jwtManager, $dispatcher, $tokenExtractor);
$this->firewallMap = $firewallMap;
}
public function getCredentials(Request $request)
{
try {
return parent::getCredentials($request);
} catch (AuthenticationException $e) {
$firewall = $this->firewallMap->getFirewallConfig($request);
if ($firewall->allowsAnonymous()) {
return;
}
throw $e;
}
}
}
services:
app.jwt_authenticator:
parent: lexik_jwt_authentication.security.guard.jwt_token_authenticator
class: AppBundle\Security\JWTAuthenticator
arguments: ['@security.firewall.map']
Note that this requires symfony 3.2+
It works! Awesome. Thank you :)
Would be nice to add as a doc :)
Would be nice to add as a doc :)
Indeed 馃憤 Please let this open, I'll do it asap
Hello @chalasr,
I have a problem with your solution, my Symfony version is 3.3.6, but it keeps return 401 error when the token is expired but when I send valid one it's working fine and I can get the user information
could you help me, please?
@abdallahmaali the best would be to create a sample app and share it here so I can give it a look.
Hello, @chalasr,
I have a problem with symfony 4 using this trick. I have setup the service exactly as you proposed and then set it up as guard on the firewall:
app_api:
pattern: ^/
stateless: true
anonymous: true
guard:
authenticators:
- app.jwt_authenticator
On the expired token case, when getCredentials() returns null I get an exception from symfony's GuardAuthenticationListener:
The return value of "App\Security\JWTAuthenticator::getCredentials()" must not be null. Return false from "App\Security\JWTAuthenticator::supports()" instead.
I guess I should make supports() to return false in the case of the expired token. What would be a correct implementation for this?
Thank you very much for your help!
You have to implement the supports() method in a way that it checks if the token is expired and return false when it is. Unfortunately you have to actually decode the token which makes it being called twice.
I'm wondering if there's another solution to bypass the expired token.
Hi there,
I'm running into this problem too. I'm running Symfony v4 (and API-Platform) and I am storing the JWT token as a HTTP Only cookie.
I am refreshing that cookie with an event subscriber onAuthenticatedResponse and this all works fine unless the token expires between accesses.
When this happens I need to delete the cookie on the client browser before the client can log back in.
Now I have an api logout endpoint on my backend that deletes the cookie, but although its route is set under Access_Control as
- { path: ^/api_logout, roles: IS_AUTHENTICATED_ANONYMOUSLY }
it only works when the user has a current session.
Now, I tried the above fix by creating a JWTAuthenticator, but Symfony 4 complains that:
Attribute "autowire" on service "app.jwt_authenticator" cannot be inherited from "_defaults" when a "parent" is set. Move your child definitions to a separate file or define this attribute explicitly in /srv/api/config/services.yaml (which is loaded in resource "/srv/api/config/services.yaml").<
Any ideas?
@SDenman
app.jwt_authenticator:
parent: lexik_jwt_authentication.security.guard.jwt_token_authenticator
class: App\Security\JWTAuthenticator
arguments: ['@security.firewall.map']
autowire: true
autoconfigure: false
This seems to be the only way for now:
<?php
namespace App\Security\Guard;
use Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\TokenExtractorInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Guard\JWTTokenAuthenticator;
use Symfony\Bundle\SecurityBundle\Security\FirewallMap;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class JWTAuthenticator extends JWTTokenAuthenticator
{
private $firewallMap;
public function __construct(
JWTTokenManagerInterface $jwtManager,
EventDispatcherInterface $dispatcher,
TokenExtractorInterface $tokenExtractor,
FirewallMap $firewallMap
)
{
parent::__construct($jwtManager, $dispatcher, $tokenExtractor);
$this->firewallMap = $firewallMap;
}
public function supports(Request $request)
{
if($this->getTokenExtractor()->extract($request)===false) {
return false;
}
try {
parent::getCredentials($request);
return true;
} catch (AuthenticationException $e) {
$firewall = $this->firewallMap->getFirewallConfig($request);
if ($firewall->allowsAnonymous()) {
return false;
}
throw $e;
}
}
}
app.jwt_token_authenticator:
class: App\Security\Guard\JWTAuthenticator
parent: lexik_jwt_authentication.security.guard.jwt_token_authenticator
arguments: ['@security.firewall.map']
autowire: true
autoconfigure: false
main:
stateless: true
anonymous: true
guard:
authenticators:
- app.jwt_token_authenticator
For me, using Symfony 4.4, the above example did not work, it was complaining about the bind property not being inherited from _defaults.
Another similar way that worked for me was to use a decorator:
<?php
declare(strict_types=1);
namespace App\Security;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Guard\JWTTokenAuthenticator;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\TokenExtractorInterface;
use Symfony\Bundle\SecurityBundle\Security\FirewallMap;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
class JWTAuthenticator extends JWTTokenAuthenticator
{
/** @var FirewallMap */
private $firewallMap;
/** @var JWTTokenAuthenticator */
private $decorated;
/**
* @param JWTTokenAuthenticator $decorated
* @param JWTTokenManagerInterface $jwtManager
* @param EventDispatcherInterface $dispatcher
* @param TokenExtractorInterface $tokenExtractor
*/
public function __construct(
JWTTokenAuthenticator $decorated,
JWTTokenManagerInterface $jwtManager,
EventDispatcherInterface $dispatcher,
TokenExtractorInterface $tokenExtractor
) {
$this->decorated = $decorated;
parent::__construct($jwtManager, $dispatcher, $tokenExtractor);
}
/**
* @param FirewallMap $firewallMap
*/
public function setFirewallMap(FirewallMap $firewallMap): void
{
$this->firewallMap = $firewallMap;
}
/**
* @param Request $request
*
* @return bool
*/
public function supports(Request $request): bool
{
try {
return $this->decorated->supports($request) && $this->decorated->getCredentials($request);
} catch (AuthenticationException $e) {
if ($this->firewallMap->getFirewallConfig($request)->allowsAnonymous()) {
return false;
}
throw $e;
}
}
}
And the services.yaml definition:
App\Security\JWTAuthenticator:
decorates: lexik_jwt_authentication.security.guard.jwt_token_authenticator
calls:
- ['setFirewallMap', ['@security.firewall.map']]
Maybe it helps somebody.
Hi
None of the above solution works for me on symfony 5.2.
I Always get an allowsAnonymous = false
even if I have this in the security.yml
access_control:
- { path: ^/, roles: PUBLIC_ACCESS }
I'm still having this exception generated if the cookie is expired...
Any clue?
Thanks
A slightly different implementation than @deepmikoto, to avoid PHPStan complaining about Right side of && is always true.
Also, it does not re-throw the exception, so that the onAuthenticationFailure method is invoked, and returns the nice JSON response.
public function supports(Request $request): bool
{
if (!$this->decorated->supports($request)) {
return false;
}
try {
$credentials = $this->decorated->getCredentials($request);
} catch (ExpiredTokenException $e) {
if ($this->firewallMap->getFirewallConfig($request)->allowsAnonymous()) {
return false;
}
}
return true;
}
Thanks everyone for sharing your up-to-date solutions. I will try to make this behavior easier to implement in a next version.
For anyone finding this later, I added another approach if the above do not work for your situation: https://github.com/lexik/LexikJWTAuthenticationBundle/issues/862
Most helpful comment
Hello @ruudk,
What you are asking is legit to me. Though, the need is quite specific, the expected behavior stays to throw exceptions if bad credentials are given (which is the case) and don't throw them if no credentials are given, so I'm not sure that it should be solved in the bundle.
A way to solve that could be to inject the
Symfony\Bundle\SecurityBundle\Security\FirewallMap(security.firewall_mapservice) in the authenticator, catch allAuthenticationExceptioningetCredentials()and rethrow them only if the firewall doesn't allow anonymous.Here is what I end up with:
Note that this requires symfony 3.2+