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 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.
- JWT stored in cookie
- JWT includes user ID
- JWT signed and then encrypted
expclaim set to 30 minutes in the future
- JWT cookie set to expire 7 days in the future
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.
- 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
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.
- 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
- 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.