JWTs

There’s a lot of talk about whether or not to use JSON Web Tokens and how to use them if you choose to. I’ll go over some of the basics here.

Overview

A JWT (I’ve always said “jot”, but I’ve heard “j-w-t” and “j-watt”) is a bearer token that consists of a signed set of authorization claims. Let’s unpack that. Being a “bearer token” means that the token itself is considered sufficient proof of authentication. A client needs only to present the token and the server will respond. A JWT is made up of a set of claims to particular capabilities (e.g. can read email, can edit other users, can upload pictures). This set of claims is then signed by a trusted key. Because of the guarantees provided by asymmetric key cryptography, we only need the private part of the key to create the signature, not to verify it.

As a result we get some nice capabilities. JWTs are set up to scale. A limited number of authentication servers can issue JWTs; other servers do not need to contact the authentication servers to verify that the JWT is valid. And since the JWT contains authorization claims the other servers will also be able to provide only the capabilities that the specific user needs (or is authorized to have). All this with only one centralized user database query. This concept is called “stateless auth”.

Downsides

Bearer token nature

Unfortunately, since a JWT is a bearer token, it can be sniffed or phished by a third party and used to make malicious requests. To combat this, JWTs are typically issued in two tiers. A shorter-lived token that is sent with every request (the “access” token) and a longer-lived token that is only sent when requesting new access tokens (the “session” token). This way if an access token is stolen it is only valid for the rest of its lifetime (typically 15 minutes or less). If a refresh token is stolen that is a different beast, but since there’s only one request every ~15 minutes, this is much rarer. And since we’re all using HTTPS anyways sniffing should not be a problem (unless your system certificate trust is compromised; but then it’s all over anyways, they’ll just read your credentials).

Logout

We still have the problem of how to handle logouts. We have a few options here. When we log a user out, we’ve got to invalidate their tokens. Since the tokens are bearer tokens, in order to invalidate them we’ve got to log them in a centralized blacklist and check that when they’re used. Since the refresh token is used against the authentication server anyways, we’re already performing a centralized operation; it’s fine to also check a blacklist. However, if we always check a blacklist when the access token is used, we lose the stateless nature of our authentication system. We have a couple options here. We could consider security paramount and use a single, centralized blacklist for all of our tokens. We could sacrifice some of our security by using a distributed eventually-consistent database as our blacklist and accept that there may be some delay before a logout event reaches all our servers. We could sacrifice a bit more of our security and not blacklist access tokens at all, tuning the access token lifetime to balance server load and logout effectiveness (notably not to the user; the user can be instructed to delete their token and they will. This issue is about malicious third parties). They’ve all got pros and cons. Your particular solution will depend on your security, compute, and scaling constraints.

As for me, I don’t tend to use the two-tier architecture. I use short-lived access tokens that are able to refresh themselves. Since they can refresh themselves, I also use a blacklist. There are a couple advantages to this method. Firstly, your blacklist stays small - you can remove any tokens that have expired (and they expire in under 15 minutes). Secondly, your implementation stays simple since you only deal with a single type of token. Thirdly, user sessions automatically die if the app is closed for longer than the access token lifetime (and they don’t if the app is open since the app will renew them in the background). My main constraints here are developer time and server resources, but I also don’t have to scale (yet) or contain any important capabilities or information. As a result I use a simple solution that’s secure enough.

Other concerns

Storage

JWTs are typically shown being stored into localStorage or sessionStorage. In the modern era this shouldn’t be done. Both of these data stores can be read by any JavaScript that was served from the same domain. This means if you built your client-side application using third-party packages that you are implicitly trusting all the code in those packages, and all the code those packages reference, so on and so on. All of those libraries have access to those data stores. This might not be something you’re worried about; we did just talk about picking a solution that’s secure enough. However, you get the solution pretty much for free. Store JWTs in HTTP-only cookies. This way they’re sent to your server on every request via the cookie header and are unreadable via JavaScript. Cookies are vulnerable to cross-site request forgery (enabling a third party to force you to send a particular request; they can’t see your cookies or the response), but this is easy enough to combat. Protect your mutating endpoints (PUT, POST, etc.) by placing an anti-CSRF token into the JWT and into the JavaScript that the page uses. The JavaScript should then send the anti-CSRF token with future requests (the JWT in the Cookie header and the token in the X-Csrf-Token header). This will let your server validate that not only did the request come from a user with a valid JWT, but that the source of the request was the server’s own JavaScript code, which is enough to prevent CSRF.

Unencrypted nature

JWTs are not encrypted. They look opaque, but they’re just encoded signed values. It’s trivial to decode them into a human-readable format (they’re typically JavaScript objects, not binary values). What’s not trivial is to create a signature without the server’s trusted key. It’s important to recognize that all a JWT guarantees us is that the value inside was not changed, not that it was not read.

Old library vulnerabilities

The JWT standard has a field that lets the creator specify what algorithm was used to generate the signature. It is valid to specify ‘None’ in this field. As a result an apparently-valid signature could be generated by anyone for any set of claims. It would be like if you went to a job interview and they first asked you what you’d like to be tested on. You’d say “nothing” and they’d respond “Alright then. Well done! You’ve got the job.” As a result most libraries will only consider JWTs valid if they’re using a specific algorithm (since the validating server is owned by the same people who own the authentication server, this should be known in both places) which neutralizes this issue.

Conclusion

I think JWTs are a good idea. They make it easy to manage user sessions, are easy to implement (with a few gotchas), and scale pretty far. You should always do your research before implementing any kind of user authentication or session management, but the known issues with JWTs are pretty well known and easy to avoid.

Further reading