JWTRefreshTokenBundle icon indicating copy to clipboard operation
JWTRefreshTokenBundle copied to clipboard

The recommended config disallow an expired JWT to be refreshed

Open webda2l opened this issue 1 year ago • 4 comments

Hi,

With https://github.com/markitosgv/JWTRefreshTokenBundle#configure-the-authenticator and so:

        api:
            pattern: ^/api
            stateless: true
            entry_point: jwt
            json_login:
                check_path: /api/login # or, if you have defined a route for your login path, the route name you used
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure
            jwt: ~
            refresh_jwt:
                check_path: /api/token/refresh

Because of the entry_point: jwt, the JWTAuthenticationBundle - JWTAuthenticator will throw a ExpiredTokenException before the JWTRefreshTokenBundle - RefreshTokenAuthenticator handle itself the refresh process.

My current solution for now is to decouple the two firewalls with the tokenRefresh one at first.

        api_tokenRefresh:
            pattern: ^/api/token/refresh
            stateless: true
            refresh_jwt:
                check_path: api_refresh_token
            logout:
                path: api_token_invalidate
        api_main:
            pattern: ^/api/
            stateless: true
            provider: entity_provider
            jwt: ~

I don't see another good/simple solution for now (Except maybe a PR on the JWTAuthenticationBundle - JWTAuthenticator to avoid handling the api_refresh_token or sort of :/)

Any thoughts? Thanks

webda2l avatar Jul 27 '22 15:07 webda2l

You have two options:

  1. Sort it so that the authenticators are (in order) json_login, refresh_jwt, jwt; at runtime, the order they're executed in is based on the order they're set on your firewall config and you want the refresh token authenticator to be attempted before the JWT authenticator
  2. Don't pass the JWT when calling the refresh route; the JWT authenticator's supports() method is based on whether the request contains the JWT and the authenticator manager will execute any authenticator that returns true from that method, so calling the refresh route with a JWT and the refresh token is probably a bad idea in general

mbabker avatar Jul 27 '22 16:07 mbabker

You have two options:

1. Sort it so that the authenticators are (in order) `json_login`, `refresh_jwt`, `jwt`; at runtime, the order they're executed in is based on the order they're set on your firewall config and you want the refresh token authenticator to be attempted before the JWT authenticator

2. Don't pass the JWT when calling the refresh route; the JWT authenticator's `supports()` method is based on whether the request contains the JWT and the authenticator manager will execute any authenticator that returns true from that method, so calling the refresh route with a JWT and the refresh token is probably a bad idea in general

About the 2. I use the cookie httpOnly feature of both Lexik & Gesdinet bundles, and so I cannot sending only the refreshToken cookie without the JWT one as well, from what I know :/

I will continue with the 1. otherwise, thanks

webda2l avatar Jul 27 '22 16:07 webda2l

Can't say I'm a fan of the cookie mechanism, but yeah, you'd have to sort the authenticators to make it work since you don't get to control what requests the cookie is included with.

By design, the refresh_jwt authenticator should really only work against a single URL whereas the jwt authenticator is supposed to work for any request to a firewall (as the jwt authenticator replaces stateful sessions in more traditional websites). That all can probably be better documented in general; it's not really a bug or quirk of either bundle, but more of how the authenticator manager in Symfony works in general, so it's one of those cases of setting the configuration to make sure things happen in the right order.

mbabker avatar Jul 27 '22 16:07 mbabker

Was not sure if I should create a new issue or rather respond to this one, since on the one hand this one is rather old but on the other hand it is still open.

I'm running into the same issue as the OP, but the solution of just putting the authenticators in the proposed order does not seem to fix it for me. I keep getting the "Expired JWT Token" error when trying to refresh with an expired token.

Solution 2 would mean hacking in some workaround since I'm automatically adding the authorization header using an interceptor, so I would rather get this to work - if at all possible - using a simple configuration change.

The weird thing is, I know I had this setup working on a previous project, so I'm a bit confused why it suddenly wouldn't.

So I was wondering, if there's anything else I should change? Like the entrypoint? Because it looks like it just jumps into the jwt authenticator (or something).

I originally started with a setup from the Lexik bundle configuration example where there's 2 firewalls: 1 for the login, and another for the api itself.

But since I noticed the OP has merged these two into one and I figured that might be the issue, so I now copied that setup, changing the order:

        api:
            pattern: ^/api
            stateless: true
            entry_point: jwt
            json_login:
                check_path: api_login
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure
            refresh_jwt:
                check_path: api_token_refresh
            logout:
                path: api_token_invalidate
            jwt: ~

my access_control:

    access_control:
        - { path: ^/api/(login|token/refresh|token/invalidate), roles: PUBLIC_ACCESS }
        - { path: ^/api, roles: IS_AUTHENTICATED_FULLY }

corresponding routes:


api_login:
    path: /api/login

api_token_refresh:
    path: /api/token/refresh

api_token_invalidate:
    path: /api/token/invalidate

But like I said, I'm still getting the 'expired token' error.

Any ideas?

EDIT:

I was able to locate the older project and see I had also used the solution that OP mentioned: splitting the refresh token and actual firewalls. This does seem to solve the issue. However, I've been unable to get the invalidate token working with this setup. When trying to invalidate the refresh_token with an expired JWT token I get a '401 Expired', after which the HTTP-interceptor successfully refreshes the token.

I've tried putting the configuration option

logout:
    path: api_token_invaldiate

in both the api_refresh firewall and api firewalls, but it does not make a difference.

For now, I have "fixed" it by not sending the JWT token to the refresh/invalidate endpoints. However, I hate not knowing why the proposed solution of @mbabker would not work, so any suggestions are still welcome :)

The split situation:

        api_login:
            pattern: ^/api/login
            stateless: true
            json_login:
                check_path: api_login
                username_path: username
                password_path: password
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure
        
        api_refresh:
            pattern: ^/api/token/refresh
            stateless: true
            refresh_jwt:
                check_path: api_token_refresh 
            logout:
                path: api_token_invalidate
        api:
            pattern:   ^/api
            stateless: true
            entry_point: jwt
            jwt: ~

antiftw avatar Oct 21 '23 20:10 antiftw