I'm implementing JWTs in my app and I'd like to make them as secure as possible. I'll lay out everything I'm planning, I'd greatly appreciate any suggestions as to the security of this implementation. This is my site, I have full access to every aspect of it, both front-end and back-end.
This will be an SPA, using an API to access the back-end. I'm using JWTs to save DB calls with each API hit.
JWTs
JWTs are stored in an access_token
cookie. They are first signed then encrypted using jose-jwt. Signing algorithm is HS256, encryption is DIR. They include the user's ID, an expiration exp
claim, and several other custom claims. JWTs expire in 30 minutes, and the JWT cookie expires in 7 days.
(short version):
- JWT stored in cookie
- JWT includes user ID
- JWT signed and then encrypted
- JWT
exp
claim set to 30 minutes in the future - JWT cookie set to expire 7 days in the future
CSRF Protection
JWTs include a cst
claim that stores a randomly-generated CSRF token. The CSRF token is sent in the response body upon login and when a new JWT is issued. The CSRF token is stored in the browser's localStorage. It is sent with every request and validated against the value in the JWT.
(short version):
- JWT includes a randomly-generated CSRF token
- CSRF token sent upon login and stored in localStorage
- CSRF token sent in request header of all requests
- Header CSRF token compared to CSRF token in the JWT
Refreshing Tokens
As the JWTs expire in 30 minutes, it is necessary to refresh them. The JWT includes an rfs
claim that stores a random refresh token. This refresh token is also stored in the DB (a separate table from users to allow for multiple sessions). If the JWT is expired (based on its exp
claim), the DB is checked to ensure the user is still valid (e.g. account not deleted, password not changed, etc.). If the user is valid, the refresh token is verified and a new JWT/CSRF token are generated and passed back in the response. If the user is invalid, the access_token
is sent back with an arbitrary value like 0
, and its expiration is set to the past so the browser will clear it. The CSRF token is passed back empty so it will be cleared from localStorage. All refresh tokens for the user are cleared from the DB.
(short version):
- If JWT expired, check user DB to verify user is still valid
- IF VALID:
- Compare refresh token with DB (assume it matches for the rest of this)
- Generate new refresh token, overwrite previous value in DB
- Reissue JWT with new
exp
date - Pass refresh token back and store in localStorage
- IF INVALID:
- Clear JWT cookie by a) setting to invalid value, b) setting expiration to past
- Tell browser to clear localStorage CSRF token
- Clear all refresh tokens for user from DB
Quick and dirty TL;DR
- Upon login, add a random CSRF token to the JWT.
- Send that same CSRF token back to the client in the response body.
- Store the CSRF token in localStorage.
- Include a refresh token in the JWT.
- Set the JWT cookie to expire after 1 week.
- Set the JWT exp claim to 30 minutes.
- If JWT claim is expired, verify refresh token against DB to ensure user is still valid.
- IF USER VALID:
- Issue updated JWT with new CSRF token and new refresh token.
- Set expiration of JWT cookie to one week in the future. (reissue the cookie, basically)
- Send new CSRF token in body of response, overwrite existing localStorage value.
- IF USER INVALID:
- Return JWT cookie with the same name but no content.
- Set cookie expiration to an arbitrary date in the past.
- Tell browser to clear the localStorage value.