LexikJWTAuthenticationBundle: Ignore expired tokens and let user continue unauthenticated

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?

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Comments: 17 (3 by maintainers)

Most upvoted comments

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+

Thanks everyone for sharing your up-to-date solutions. I will try to make this behavior easier to implement in a next version.

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.

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?