Understanding the tymon/jwt-auth refresh token mechanism. When & why JWT_TTL, JWT_REFRESH_TTL.
If you’re familiar with jwt and you want to implement jwt with Laravel/Lumen, you may know a few packages. But after knowing about the tymon/jwt-auth
I didn’t look for any other packages. For the last two production applications, I have been using this package. I use this package because of its extensibility. Moreover, it does all sorts of validation out of the box.
I had to go through the codebase several times to get the most out of it. I had implemented the following using the package.
- Had implemented a cache on the subject.
- Had implemented passwordless user authentication.
- Had implemented a storage-less authentication system for a gateway.
After deploying the application in the production, I get to find an error in the log. It was shooting Tymon\JWTAuth\Exceptions\TokenExpiredException
. I had to spend a good portion of time to check what is going on actually. After a thorough investigation, I found that it’s actually causing an error due to the TTL
values. The user’s token had a validity of 2 months but the refresh token had a validity of 14 days. 😐
There are two types of JWT_*TTL
.
JWT_TTL
— which defines the expiry for the token. It’s being added to the token. After the expiry, the token will no longer work.JWT_REFRESH_TTL
— which denotes the amount of time slot the token has within which it should be refreshed.
How the authentication works and the TTL has an effect?
Upon setting up the jwt-auth successfully, when calling the auth()
’s check()
or user()
methods, it checks in the following order.
- Sets the request for token parsing.
- Tries to parse the token.
- If the token is parsed successfully, tries to initialize the token.
- Sets the token here.
- Validates the token if manipulated or not. [If invalid then throws an exception otherwise gets the token back].
- Then checks if the token can be decoded with the algorithm and sign.
- If everything was okay (no exception was thrown), then it tries to parse the JWT token to internal
Jwt/Auth/Payload
class. - Then the Payload class validates each claim of the token.
- Then the above phase checks all the required claims one by one using the
validatePayload
method in the claims. - Check the available claims here.
So, what is wrong here?
So, when you’re trying to make a request with an invalid non-jwt token or not token with that, it’ll be blocked in the 3rd–5th step of the list. And when your token is a valid JWT token, then in the final steps, when checking one by one, then
iat
— Issued At should be before the current timestamp.nbf
— Not Before should be before the current timestamp. Know about NBF.exp
— Expiration should be a future timestamp.
If any of these timestamps are not found as expected or unavailable, then the token becomes invalid. So, when requesting with JWT token, these values must be available and should be exactly as said above.
Where is the JWT_REFRESH_TTL?
According to their documentation, the auth()->refresh()
should be behind the auth
middleware. That means whenever you need to refresh a token, you should pass a valid JWT token which will be parsed and should have valid iat
nbf
and exp
values. And, if you call the auth()->refresh()
method, it’ll set refresh flow and tries to generate a new token. Like before, it now tries to validate the payload with validateRefresh
method instead of validatePayload
. Each claim implements the method. So, when refreshing a token, the iat
uses the refresh ttl value. So, when refreshing a token, your token generation time (iat
) + refresh_ttl (in seconds) should be in the future. If it’s in the past, it’ll then throw TokenExpiredException
. But if it’s in the future, you’ll get a new token.
BUT THE CATCH!!!
Whenever generating a new token, calling the *->refresh()
then the newly issued token still has the previous iat
value. So, at a certain point, your endpoint to refresh a token will raise a TokenExpiredException
. Even if you set your JWT_TTL
lower than the JWT_REFRESH_TTL
, and as your token’s iat
remains constant, the iat + refresh_ttl
value will always be in the past after a certain period of time.
So, what to do then?
Whenever you need to refresh a token, generate a new token for the user like
$token = auth()->fromUser(auth()->user());
And, if you want to invalid the current token, then you can use
auth()->invalidate(); // auth()->invalidate(true);
So, you’ll get a new token with a new iat
and you’ll invalidate the currently available token with the invalidate
method before the exp
value. And return the newly generated token as a response.
That’s all. Happy coding. ❤