The recommended config disallow an expired JWT to be refreshed
webda2l opened this issue · 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
You have two options:
- 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 - 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
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
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.
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: ~