15

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.
10
  • 1
    It would be helpful to know what threats need to be defended against such that all of this machinery- thought through, to be sure, but nevertheless complex with complex implications- is necessary. One example- keeping CSRF token in localStorage requires machinery to ensure every request to the server pulls it from local storage and includes it in some payload. However, if client side code is going to be providing that abstraction, there is no need to use Cookies at all, the value of which are simply that they are sent automatically by existing browser machinery. Aug 13, 2016 at 19:32
  • 1
    @JonahB The reason I'm not storing the JWT in localStorage is that it can then be pulled by any script on the page. I want to avoid that by making the cookie be HTTP-only. localStorage avoids the need for a CSRF token, but if a user's browser were compromised the attacker would have access to the JWT and therefore the access token.
    – vaindil
    Aug 13, 2016 at 19:36
  • 1
    Is it the intention of this machinery to defend against attackers controlling the user's browser, or attacker scripts running in the same browser context? Aug 14, 2016 at 16:17
  • 1
    @JonahB I want the site to be as secure as possible. I'd like to do whatever I need to.
    – vaindil
    Aug 14, 2016 at 19:44
  • 1
    My points are: a) very important to keep things simple. Throwing pieces in without solid justification just makes for fragility, not security. b) impossible to evaluate specific decisions without understanding the context and threats. Why use JWTs in the first place, over a simple opaque token- what payload is important to transmit? Why encrypt them, when there is nothing to hide? Why keep CSRF token in local storage, when that requires machinery to override normal request submission? If customizing request submission, why use cookies? Aug 14, 2016 at 20:17

3 Answers 3

16

You seem to be mixing up several different opposing technologies, and do not make it clear why you have chosen these technologies and why they control the threats you are trying to secure against.

JWTs are stored in an access_token cookie. They are first signed then encrypted using jose-jwt.

Is there any reason why they are encrypted? Signing is used when you want to verify the integrity of the data - i.e. that it has not been altered by anyone without the key. Encryption is used when you want to protect the confidentiality of the data - i.e. that it cannot be read by anyone without the key. If there is nothing in the token that shouldn't be able to be read by the end-user then there is no reason to encrypt.

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.

This sounds like an implementation of the Double Submit Cookie CSRF control. localStorage is protected by the Same Origin Policy preventing another web session in the user's browser from accessing the token.

Ensure that the CSRF token has at least 128 bits of entropy.

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.

This is where things get mixed up. Your security model should decide on either client-side sessions or server-side sessions. JWTs are usually employed for the former. That is, if you have a token in the client signed by a key only available to the server then your application should be trusting that the presence of this token denotes a valid session should it not have expired and that the signature is valid.

The disadvantage of this method is that it is difficult to revoke tokens. If a user account is compromised, and a user changes their password, any attacker sessions will still be active for 30 minutes because they still have a valid, unexpired and signed token.

The fix for this is to implement server-side sessions instead. For example, you keep track of sessions in a database table which means they can be instantly logged out of by removing the row in the database. The session token can be a 128 bit entropy random string, supplied as a cookie client-side and stored hashed with SHA-256 server-side to mitigate any data leakage from your database. You could always send a plain text expiry date with your cookie, so the client knows that when it needs to refresh the token. e.g. a HttpOnly cookie for the token, and a non-HttpOnly cookie containing the expiry so the client-side JavaScript can read it. HttpOnly cookies can help mitigate the impact of an XSS flaw.

Therefore, if you are tracking sessions server-side there is little advantage of having a signed JWT client-side. Extra code means more attack surface and more chance of vulnerabilities being introduced into the surplus code. Complexity in an application is often inverse to security.

If you are using Double Submit Cookie in combination with a server-side session for session state, then you are wise to use a different cookie for the CSRF token. This will enable you to add the CSRF token as a header using client-side code without risking your session identifier token. Note that if you are setting custom headers and not implementing CORS, then this can mitigate CSRF somewhat. It is recommended to use a token as well though, because technologies like Flash tend to disrupt browser security (Flash means more code running, more code gives more attack surface, more code means more chance of vulnerabilities).

5

This sounds fairly sound to me (the CSRF solution is cute too) until the Refresh Token part.

One of the advantages I see in a JWT-based system is that the access tokens expire regularly. This means that a compromised JWT access token only gives an attacker access for a period of minutes or hours.

If you include the Refresh Token in the JWT, an attacker that compromises the JWT can easily use it to refresh itself, making it have no effective expiration date.

An access token should not be able to grant a new access token. Only a Refresh Token (or full-authentication) should be able to grant a new access token.

3

Aggregating from comments:

  • JWT as a bearer credential in an Single Page App that talks to a server-side API over ajax makes sense.
  • Use of cookies in a Single Page App does not make sense. If all requests to the server API bottleneck through an ajax function, that function can include JWT in a header. If there are no cookies, then there is no CSRF, because requests from attacking frames will not go through the ajax function
  • Use of third party scripts is of course pervasive, but it is nevertheless a big security risk. Third party scripts operate within the same javascript and DOM context and can see the entire DOM. If they are malicious, there are many, many attack options. If possible, eliminate third party scripts or ensure they are verified, using sub-resource integrity or some other means (e.g. monitor them for changes).
  • localStorage follows same origin policy, and can be treated like a client cache for DOM or authorization data. In an SPA, the JWT can be kept in localStorage, because it follows the same security model as the DOM.
  • A refresh token can be kept in the JWT
  • JWT expiration and refresh lifetimes are hard to comment on in the abstract. It's important to find the right balance between security and usability, to not require users to jump through hoops unless there is good reason, and to provide users with the right assurance should they have concerns.

Some other things to do:

  • Enforce https and use related resource protocol headers like x-frame-options, strict transport security, and content security policy
  • Keep some metadata of the particular version of the Single Page App in the JWT, and require a refresh following app update
  • Keep metadata of the particular client in the JWT. Don't allow use of a desktop browser JWT from a mobile browser.
  • Have the ability from the server to revoke all outstanding JWTs and refresh tokens associated with a user. If a user had JWTs and refresh tokens tied to their phone and desktop, and tells you that they lost their phone, you probably want to invalidate their phone and desktop JWTs and refresh tokens.
  • Have strong object/operation level authorization on the server API - meaning, verify the integrity of the JWT, confirm the authorization it packages has not been revoked, and then check that the claims it includes provide the appropriate access to the API, before performing any API operation.
1
  • 8
    "A refresh token can be kept in the JWT" This seems wrong to me. One of the advantages of a JWT is that they expire quickly, reducing the impact of having one compromised. If you have a JWT that can be used to renew itself, then it effectively has no expiration in the hands of an attacker.
    – ProdigySim
    Dec 6, 2017 at 2:07

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .