Skip to content

Instantly share code, notes, and snippets.

@soulmachine
Last active November 19, 2024 12:48
Show Gist options
  • Save soulmachine/b368ce7292ddd7f91c15accccc02b8df to your computer and use it in GitHub Desktop.
Save soulmachine/b368ce7292ddd7f91c15accccc02b8df to your computer and use it in GitHub Desktop.
How to deal with JWT expiration?

First of all, please note that token expiration and revoking are two different things.

  1. Expiration only happens for web apps, not for native mobile apps, because native apps never expire.
  2. Revoking only happens when (1) uses click the logout button on the website or native Apps;(2) users reset their passwords; (3) users revoke their tokens explicitly in the administration panel.

1. How to hadle JWT expiration

A JWT token that never expires is dangerous if the token is stolen then someone can always access the user's data.

Quoted from JWT RFC:

The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. The processing of the "exp" claim requires that the current date/time MUST be before the expiration date/time listed in the "exp" claim.

So the answer is obvious, set the expiration date in the exp claim and reject the token on the server side if the date in the exp claim is before the current date.

Quite easy, huh?

The problem is that mobile apps never expire, for example, people can reopen the APP after a month without the need to login again.

  1. For Web Apps: If you set the expiration time to 1 week, do not use the token for 1 week. Use it less than a week and get a new token before the old token expires. For example, make the browser send out a request to exchange for a new token at the sixth day. This is not different than the normal concept of session and cookies.

    Accordingly, on the server side, create a restful API named /token/extend which will return a new token if given a valid token.

    If the user does not use your application for a week, next time he goes to your app, he will have to login again and this is fine and widely accepted.

  2. For native mobile Apps: you can use the same explained above, but that's not how mobile Apps work nowadays, e.g., an user can open the Facebook App after a month not using it and next time when he open the App he doesn't need to login again.

    One solution is to add an audience claim named aud to JWT tokens, for example, use the payload like {"sub": "username", "exp": "2015-11-18T18:25:43.511Z", "aud":"iPhone-App"}. On the server side if the token has an aud field that has the value iPhone-App then ignore the exp claim, so that tokens with iPhone-App never expire. However, you can still revoke this kind of tokens by using the methods described in Section 2.

    Another solution is to use a refresh token that never expires to fetch a new JWT token that does expire. Since the refresh token never expires, what happens if your phone is stolen? Again, refresh tokens are still valid JWT token, you can revoke refresh tokens using the methods described in Section 2.

    Normally to distinguish with different refresh tokens of one user, a good practice is to put the specific device name into the refresh token, for example, {"sub": "username", "exp": "2015-11-18T18:25:43.511Z", "device":"Frank's iPhone"}, so that when a user wants to revoke refresh tokens, he can know this refresh token is being used on his iPhone.

2. How to revoke a JWT token

Sometimes users need to revoke a token, for example, clicking the logout button, or changing the password.

Assume that each user has multiple devices, let's say, a browser, a native iPhone APP, and a native Android APP.

There are three ways:

  1. Changing the secret key.

    This will revoke all tokens of all users, which is not acceptable.

  2. Make each user has his own secret and just change the secret of a specified user.

    Now the RESTful backend is not stateless anymore. Every time a request comes in the server needs to query the database to get the secret of a user.

    To get better performance let's store the (user, secret) pairs in Redis instead of MySQL, use the username as the key and the secret as the value.

    This way will revoke all tokens of one user, much better, but still not good enough.

  3. Store the revoked JWT tokens in Redis.

    Use the token as the key and the value is always a boolean true.

    The token will be stored only for a specific amount of time, which is the time in the exp claim, after the expiration time it will be deleted from Redis.

    This way only revokes just one token at a time, perfect!

    For more details please refer to this blog, Use Redis to revoke Tokens generated from jsonwebtoken

Suggestions are welcomed, please correct me if I'm wrong.

3. How to use JWT tokens securely

First, always use HTTPS to make sure JWT tokens transmission over network is safe. By using HTTPS nobody can sniff users' JWT tokens over network.

Second, make sure JWT tokens are stored securely on users' Android, iOS and browser.

  • For Android, store tokens in KeyStore
  • For iOS, store tokens in KeyChain
  • For browsers, use HttpOnly and Secure cookies. cookie. The HttpOnly flag protects the cookies from being accessed by JavaScript and prevents XSS attack. The Secure flag will only allow cookies to be sent to servers over HTTPS connection.

As long as we make the browsers, user devices and tokens transmission safe, token revocation mechanism is not necessary anymore.We can still keep our RESTful services stateless.

Reference

  1. RFC 7519 - JSON Web Token (JWT)
  2. Use Redis to revoke Tokens generated from jsonwebtoken
  3. I don’t see the point in Revoking or Blacklisting JWT
  4. JSON Web Tokens | Hacker News
  5. Blacklisting JSON Web Token API Keys
  6. Token Based Authentication for Single Page Apps (SPAs)
@ParagKadam101
Copy link

I'm not sure what is meant by:
"The problem is that mobile apps never expire, for example, people can reopen the APP after a month without the need to login again."

When the user launches the Mobile App wouldn't you just connect with server as part of the start up process for the app and check to see if there is a valid/non-expired token on the server for that device. If the token has expired just require that the user login.

What am I missing here?

Cheers.

You do not store the JWT on the server, this token is stored only at the client-side, and JWT has the information like the expiry date within itself.

@bilaalabdelhassan
Copy link

That was helpful!

@Variiuz
Copy link

Variiuz commented Jul 29, 2021

Old gist but maybe someone will find it helpful lol

A simple solution to adapt when you have expiring Tokens already:
There are probably articles out there describing my problem and solution, but I didn't bother finding them tbh and this was surprisingly the first entry when searching something like that. So maybe someone will find it kinda helpful.

Example:
I have a system in place that gives out a JWT which is valid for an hour, after that it's expired etc. we know that stuff by now
My problem/Goal:
Don't want to show a login window every time the token expires and the app didn't renew it (ex: user logs in, does things, closes app, reopens after 2-3 hours = invalid token, boom login again) (and no, the solution "just show the login window" or "make the expire date longer" is not acceptable here, I want 1 Hour nothing more for Tokens and I don't want to be the EA Desktop Application, which requires you to log in every day again, that is frustrating)

Adapting a solution like 2.3 is pretty simple: (assuming we know the token got compromised, which is mostly the fault of the user IMO)
We have a API Endpoint (ex.: POST http://api.com/login?token=expired {'token': 'blablaexpiredtokenhere'}) that accepts the expired(?token=expired ) token and checks it against a database or redis k-v set full of revoked keys. If it's not revoked => give out a valid 1 hour JWT again.

Now, the gist is pretty unclear of how to implement this "revocation", so the idea behind this here is:

  • We don't need to issue tokens that are valid forever or at least pretty long
  • We don't need to change much inside the API itself, just a new Endpoint and a simple redis check
  • We don't need to ask redis for revoked tokens in each and every request/endpoint

Maybe you already thought of this, maybe not, maybe it helped someone out here.

@aangrisani
Copy link

On point 2.3 -

If you delete the token from Redis after the exp, then aud will be able to log in again (even if the JWT was issued with a different password, etc.)

Hi ,

I do not agree with this "On the server side if the token has an aud field that has the value iPhone-App then ignore the exp claim, so that tokens with iPhone-App never expire."

I have try it on a my .NET core application and once the token has expired the serivce reply with 401!

@bernardwiesner
Copy link

@Variiuz
JWT is supposed to be stateless (for scaling reasons). If you store tokens on redis its not anymore stateless.

JWT is only truly stateless if you have a hard expiration date that cant get refreshed, and preferably should be short, ~1h. Like that its truly stateless. But that is not practical for most apps out there, so in reality I guess most implementations of JWT are not really stateless.

@WalterJoel
Copy link

Thank You, So much, very useful information.

@wppaing
Copy link

wppaing commented Aug 17, 2023

On point 2.3 -

If you delete the token from Redis after the exp, then aud will be able to log in again (even if the JWT was issued with a different password, etc.)

We only need to mark a compromised token as revoked before it's expiration. After that, I think it will be catched up at exp check.

@TALBI-svg
Copy link

TALBI-svg commented Oct 30, 2023

i have the error i use audiance and token didn't expire anymore ,how can i fixe this ?
date_default_timezone_set('Africa/Casablanca'); $issued="localhost"; $issued_at=time(); $not_before=$issued_at+10; $expiration=$issued_at+30; $audiance="my_users"; $user_data_arr=array( "id"=>$res['id'], "name"=>$res['name'], "email"=>$res['email'], ); $secret_key="utf127"; $payload_info=array( "issued"=>$issued, "issued_at"=>$issued_at, "not_before"=>$not_before, "expiration"=>$expiration, "audiance"=>$audiance, "user_data"=>$user_data_arr, ); $jwt=JWT::encode($payload_info, $secret_key, 'HS256');

when i remove audiance from token params iget "message": "Signature verification failed"

@TriSBie
Copy link

TriSBie commented Dec 18, 2023

great

@fberrez
Copy link

fberrez commented Jun 5, 2024

great gist! Thank you for sharing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment