'LexikJWTAuthenticationBundle returning 401 for invalid token on anonymous route
I'm using this LexikJWTAuthenticationBundle with FosUserBundle.
I have this in security.yml :
firewalls:
app:
pattern: ^/api
stateless: true
anonymous: true
lexik_jwt: ~
with the following access_control :
- { path: ^/api/user/action1, roles: IS_AUTHENTICATED_FULLY }
- { path: ^/api/user/action2, roles: IS_AUTHENTICATED_ANONYMOUSLY }
The behaviour I was expecting for /api/user/action2 is having access no matter what is inside the request header. However I'm getting a 401 in the case where the Authorization Bearer is set but not valid (it is ok with valid token or no Authorization Bearer at all).
My use case is I need to check in my controller if the user is logged in but if not, I still want to let that anonymous user access the route.
Solution 1:[1]
You have to create a specific firewall for the route/pattern you want allow for anonymous users :
action2:
pattern: ^/api/user/action2
anonymous: true
lexik_jwt: ~
Then, just move your less-protected access_control just before the fully-protected :
- { path: ^/api/user/action2, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/api/user/action1, roles: IS_AUTHENTICATED_FULLY }
In this way, you are application doesn't care about an Authorization header, and all users can access the resource without JWT.
Update
Change the anonymous route's firewall to :
action2:
pattern: ^/api/user/action2
anonymous: true
lexik_jwt: ~
And make the access_control accepting anonymous And fully authenticated users :
- { path: ^/api/user/action2, roles: [IS_AUTHENTICATED_ANONYMOUSLY, IS_AUTHENTICATED_FULLY] }
- { path: ^/api/user/action1, roles: IS_AUTHENTICATED_FULLY }
Please use the same order and clear your cache correctly.
It's working well in my JWT/FOSUB application, if it doesn't work for you I'll give you a working ready-to-use example.
And the controller :
$currentToken = $this->get('security.token_storage')->getToken();
if (is_object($currentToken->getUser())) {
// Do your logic with the current user
return new JsonResponse(['user' => $currentToken->getUser()->getUsername()]);
} else {
return new JsonResponse(['user' => 'Anonymous']);
}
Hope it works for you.
Solution 2:[2]
I resolved your problem in this way:
api_public:
pattern: ^/api/v1/public
anonymous: true
lexik_jwt:
authorization_header:
enabled: false
prefix: Bearer
query_parameter:
enabled: false
name: bearer
api:
pattern: ^/api
stateless: true
anonymous: true
lexik_jwt:
authorization_header:
enabled: true
prefix: Bearer
query_parameter:
enabled: true
name: bearer
Solution 3:[3]
*** For those landing here in 2022 ***
To allow anonymous access with JWT
You must write your own JWTAuthenticator class -
(Code Source)
// src/Security/JWTAuthenticator.php
namespace App\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;
// use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; // For Symfony 4.4 and above
final class JWTAuthenticator extends JWTTokenAuthenticator
{
private $firewallMap;
public function __construct(
JWTTokenManagerInterface $jwtManager,
EventDispatcherInterface $dispatcher,
TokenExtractorInterface $tokenExtractor,
// TokenStorage $tokenStorage, // For Symfony 4.4 and above
FirewallMap $firewallMap
) {
parent::__construct($jwtManager, $dispatcher, $tokenExtractor);
// For Symfony 4.4 and above, use the next line instead of the above one
// parent::__construct($jwtManager, $dispatcher, $tokenExtractor, $tokenStorage);
$this->firewallMap = $firewallMap;
}
/* For Symfony 3.x and below */
public function getCredentials(Request $request)
{
try {
return parent::getCredentials($request);
} catch (AuthenticationException $e) {
$firewall = $this->firewallMap->getFirewallConfig($request);
// if anonymous is allowed, do not throw error
if ($firewall->allowsAnonymous()) {
return;
}
throw $e;
}
}
/* For Symfony 4.x and above */
public function supports(Request $request) {
try {
return parent::supports($request) && parent::getCredentials($request);
} catch (AuthenticationException $e) {
$firewall = $this->firewallMap->getFirewallConfig($request);
// if anonymous is allowed, skip authenticator
if ($firewall->allowsAnonymous()) {
return false;
}
throw $e;
}
}
}
Register this class as a service by adding the following to your services.yaml file
app.jwt_authenticator:
#autowire: false # uncomment if you had autowire enabled.
autoconfigure: false
public: false
parent: lexik_jwt_authentication.security.guard.jwt_token_authenticator
class: App\Security\JWTAuthenticator
arguments: ['@security.firewall.map']
Then update the firewall in security.yaml to use the newly registered service
api:
pattern: ^/api
stateless: true
guard:
authenticators:
- app.jwt_authenticator
Lastly, here's a complete tutorial to setup Lexik JWT bundle with Symfony.
Solution 4:[4]
I had to add token extractor to config. I was wrong thinking this is enabled by default.
# lexic_jwt_authentication.yaml
token_extractors:
authorization_header:
enabled: true
prefix: Bearer
name: Authorization
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 | Gerardo |
| Solution 3 | |
| Solution 4 | Saulius maladec |
