'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