Alex
Alex

Reputation: 1260

I can't get the user from JWT

I have a project with Symfony 5.1 using Lexik JWT v2.8.0, gesdinet jwt refresh token v0.9.1 and my own entity user. I can log in with JWT and get the token, save it in a HttpOnly cookie and use it with the protected APIs successfully.

My web app has some API Rest but it has also pages to browse. So my intention is do it all from API, but also login in the web browser when user obtains a token.

But as api login is stateless, after the success login the web profiler still shows logged in as anon. and I can't get te user from the token. I made a service to get the user from the token, to call it in the controller and send some logged in user data to the front end.

I've got a service from this question. The service I have implemented to get the user is:

use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use UserBundle\Domain\Entity\User;

class UserService
{
    private TokenStorageInterface $tokenStorage;
    private JWTTokenManagerInterface $jwtManager;

    public function __construct( TokenStorageInterface $storage, JWTTokenManagerInterface $jwtManager )
    {
        $this->tokenStorage = $storage;
        $this->jwtManager = $jwtManager;
    }

    public function getCurrentUser() : ?User
    {
        $decodedJwtToken = $this->jwtManager->decode($this->tokenStorage->getToken());
        if ($decodedJwtToken instanceof TokenInterface) {

            $user = $decodedJwtToken->getUser();
            return $user;

        } else {
            return null;
        }
    }
}

And it is declared as a service:

get.token.user.service:
    class: UserBundle\Domain\Service\UserService
    arguments: [ '@security.token_storage' ]

But all I get from $this->tokenStorage->getToken() is a web token with "anon." user, so it is not a JWT and jwtManager can't decode it.

UserService.php on line 25:
Symfony\Component\Security\Core\Authentication\Token\AnonymousToken {#16 ▼
  -secret: "RXVaVx3"
  -user: "anon."
  -roleNames: []
  -authenticated: true
  -attributes: []
}

I also tried to get the jwt from the cookie in the controller and send it to the service as an argument, but I get an error from decode that I'm passing a string and it expects a TokenInterface object, so it did not worked neither.

How can I get the user from the token in the service? is there a best practice to login in the web through api than get the user from the jwt and send it to the render?

Edit: add the code to use lexik jwt and to save token in cookie:

# /config/packages/lexik_jwt_authentication.yaml
lexik_jwt_authentication:
    secret_key: '%env(resolve:JWT_SECRET_KEY)%'
    public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
    pass_phrase: '%env(JWT_PASSPHRASE)%'
    token_ttl: 3600
    user_identity_field: email

token_extractors:
    cookie:
        enabled: true
        name: BEARER

the security file code

# /config/packages/security.yaml
security:
    encoders:
        UserBundle\Domain\Entity\User:
            algorithm: auto

    providers:
        app_user_provider:
            entity:
                class: UserBundle\Domain\Entity\User
                property: email
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        login:
            pattern: ^/api/login
            stateless: true
            anonymous: true
            json_login:
                provider: app_user_provider
                check_path: /api/login_check
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure
        refresh:
            pattern:  ^/api/token/refresh
            stateless: true
            anonymous: true
        register:
            pattern: ^/api/register
            stateless: true
            anonymous: true
        api:
            pattern: ^/api
            stateless: true
            anonymous: true
            provider: app_user_provider
            guard:
                authenticators:
                    - lexik_jwt_authentication.jwt_token_authenticator
        main:
            anonymous: true
            lazy: true
            provider: app_user_provider

    access_control:
        - { path: ^/api/user, roles: IS_AUTHENTICATED_ANONYMOUSLY, methods: [GET] }
        - { path: ^/api/linkpage, roles: IS_AUTHENTICATED_ANONYMOUSLY, methods: [GET] }
        - { path: ^/api/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/api/token/refresh, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/api/register, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/api, roles: IS_AUTHENTICATED_FULLY }

Listener to save jwt in a cookie and avoid the token to be in the response:

use Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationSuccessEvent;
use Symfony\Component\HttpFoundation\Cookie;

class AuthenticationSuccessListener
{
    private bool $secure = false;
    private int $tokenTtl;

    public function __construct(int $tokenTtl)
    {
        $this->tokenTtl = $tokenTtl;
    }

    public function onAuthenticationSuccess( AuthenticationSuccessEvent $event)
    {
        $response = $event->getResponse();
        $data = $event->getData();

        $token = $data['token'];
        unset($data['token']);  // remove token from response. It works.
        unset($data['refresh_token']); // remove token from refresh token, even though I still get this token in the response
        $event->setData($data);

        $response->headers->setCookie(
            new Cookie(
                'BEARER',
                $token,
                (new \DateTime())->add(new \DateInterval('PT'. $this->tokenTtl . 'S')),
                '/',
                null,
                $this->secure
            )
        );
    }
}


#services.yaml
app.listener.authenticationsuccesslistener:
    class: UserBundle\Application\Listeners\AuthenticationSuccessListener
    arguments: ['%lexik_jwt_authentication.token_ttl%']
    tags:
      - { name: kernel.event_listener, event: lexik_jwt_authentication.on_authentication_success, method: onAuthenticationSuccess }

I still coded 2 more listeners for the refresh token, but I don't think they are needed.

The rest of the authentication is the default authentication code from lexik jwt bundle.

In the BEARER cookie is stored the jwt with the correct format, something like this:

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1OTgzNTE0ODYsImV4cCI6MTU5ODM1NTA4Niwicm9sZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfVVNFUiJdLCJlbWFpbCI6ImFkbWluQGVtYWlsLmNvbSJ9.p5WYCxEE-VSxp09Eo7CxXoxi6zy1ZcnLJiBe1YGsrk3iFm7T-6JAWbvyb9ZW_-1jtpYcWQlFOjOf7uET4wRHvlvygnPOeZck7tZM8TUlSqMXIllMeVARz8mEUvXVwhDWEk5T7Ibw9c3VgfvyiLUgSb_cmSyK3DwtgPCd9vOYarkag9XKxkxNI9M1OHGL61v1NoQHdkXloC72xdUUMcj5Y8yCJWZFdOp8-Xtfq8ZChAHzwCjXUhC3VnBqZcMT0tAvtilwTGDYDykppNKK1vbNoyOex47wQH_ILEFuX5Eh1p2xfbc0lWm3Ip21z3EQ2M_eOQgZvHR65T3b2dv9g5GPiFp3CNo8AuW8m6rXjWK6NZXJO8qYodxI5cUYYyFooCfVXXU8JXzGQfCZIdOPw-iBGzQEfFuLL50_sAOVcxjklAYCFZYRHwFKWmwl1BwJF4mAw4jnNIAdMmc66Z17ul2Jep9apOO90C1dZzGuVKxWqglc9GZo7-teHt0dMsg0ADrvaOKNJUwDBGJ4lZpWx6_stcl7DkCdc5k1kePGdLa7YXRO3umPwa_FVYVgnT_Z9x7RtfnGioa2TZJCIdbJnuj0L90vkgFBjHqFdVydDaaBu3Y0mKoQ2v3Sf1so4-uwJm8z1vQVZleMQgFibMiyyk3YyDidhPSxxyp4u-4xPNOSDNo

If I am not logged in and don't have jwt or I delete the BEARER cookie I can't access the protected APIs ({"code": 401, "message": "JWT Token not found"}), when I have the jwt assigned I can request APIs correctly.

Upvotes: 0

Views: 4017

Answers (1)

Alex
Alex

Reputation: 1260

The problem was on the security.yaml configuration, specifically in the firewalls.main section, where I had to add the jwt_token_authenticator as the guard. Finally, my security.yaml is:

security:
    encoders:
        UserBundle\Domain\Entity\User:
            algorithm: auto

    providers:
        app_user_provider:
            entity:
                class: UserBundle\Domain\Entity\User
                property: email
        jwt:
            lexik_jwt: ~
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        login:
            pattern: ^/api/login
            stateless: true
            anonymous: true
            json_login:
                provider: app_user_provider
                check_path: /api/login_check
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure
                require_previous_session: false
        refresh:
            pattern:  ^/api/token/refresh
            stateless: true
            anonymous: true
        register:
            pattern: ^/api/register
            stateless: true
            anonymous: true
        api:
            pattern: ^/api
            stateless: true
            anonymous: true
            provider: jwt
            guard:
                authenticators:
                    - lexik_jwt_authentication.jwt_token_authenticator
        main:
            anonymous: true
            lazy: true
            provider: jwt
            guard:
                authenticators:
                    - lexik_jwt_authentication.jwt_token_authenticator
            logout:
                path:   app_logout
                target: /

    access_control:
        - { path: ^/api/login_check, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/api/token/refresh, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/api/register, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/logout, roles: IS_AUTHENTICATED_FULLY }
        - { path: ^/api, roles: IS_AUTHENTICATED_FULLY }

Then I had some problems with the logout, as the logout didn't erase the jwt cookies and the logout kept me logged in. So I made a Listener to the LogoutEvent and made the listener delete the cookies.

use Symfony\Component\Security\Http\Event\LogoutEvent;

class LogoutListener
{

    public function onSymfonyComponentSecurityHttpEventLogoutEvent(LogoutEvent $event)
    {
        $response = $event->getResponse();
        $response->headers->clearCookie('BEARER');
        $response->headers->clearCookie('REFRESH_TOKEN');

        $event->setResponse($response);
    }

}

and declared it as a service:

app.listener.logout:
    class: UserBundle\Application\Listeners\LogoutListener
    tags:
        - name: 'kernel.event_listener'
        event: 'Symfony\Component\Security\Http\Event\LogoutEvent'
        dispatcher: security.event_dispatcher.main

And now the login is working on the web without having to decode the jwt with the service I was using and pass the user to the front. Though the service that decoded the jwt, now is working fine.

I've lost almost a week with this issue, but finally I've found a solution and it's working fine. So I'll post it here to save time if anybody has some similar issue.

Upvotes: 1

Related Questions