Part one of a two-part series.
A REST API is a wonderful way to expose application data to a wide variety of clients. Anything that can speak HTTP can communicate with a REST API, and these days that means an exciting variety of devices. From plain old web browsers to mobile devices to a whole array of IoT applications — there are a lot of good reasons to use a REST API. And if you’ve ever built one, you’re familiar with the predominant means of restricting endpoints to authenticated users — JSON Web Token (JWT) based authentication.
What is a JWT
First, some termes d’art need to be disambiguated. It’s not uncommon to hear somebody refer to a REST API as simply an API. Likewise, we frequently see JWTs conflated with the pattern of JWT-based authentication. JWT on its own is nothing but an open standard (RFC 7519) for transmitting messages via HTTP. JWT as a standard can be used for any message at all. It has one characteristic in particular that makes it a good tool for sending a user’s identity to a backend service. We can trust the integrity of a JWT because JWTs are signed, making them tamper-proof.
A JWT looks like this:
A JWT is a Base64URL encoded string, split into three sections, delimited by periods.
- Section one is the header. This section contains JWT metadata; typically information about the type of token and the algorithm used to sign it. It is encoded JSON.
- Section two is the payload. This is the content of the token and is also encoded JSON.
- Section three is the signature. This is the SHA256 (or some other HMAC) hash of the encoded header, encoded payload, and a secret. This part of the JWT is used to verify the integrity of the message.
You can visit jwt.io and decode the token above to view the claims. A brief note about JWT types. A JWT is one of two types: signed or encrypted, JWS or JWE respectively.
Token-based authentication is probably the most common method for authenticating requests to REST API endpoints. It works like this:
- A user logs in to an application with a username and password, or otherwise proves her identity.
- The server confirms her identity and sends back an access token containing a reference to her identity (e.g. a private key pointing to a unique User instance).
- The client then includes this access token with every request to the server.
- For protected routes, REST API authentication middleware asserts the presence of a valid access token. The server can further use the identity asserted by the validated token to implement more granular permissions, such as acting on resources belonging to that particular user.
A JWT’s characteristics make it a great choice for token-based authentication. We want a lightweight package, since it will be included on every single request to our REST API. It also must be tamper-proof, so that the identity claim cannot be altered in transit or spoofed outright.
One of the greatest advantages of this approach is that it is stateless. It doesn’t require the client or the REST server to maintain sessions. Indeed, no database lookup is required at all to verify the identity of the requesting user. Because the JWT is signed with a secret held by the REST API, we can (and indeed must, for this authentication schema to work) implicitly trust that this user is who they claim to be. Database lookups may occur in certain implementations involving more granular permissions — for example, verifying user.is_active.
An important part of token-based authentication is the expiration of JWTs. The security of this pattern relies heavily on JWTs being short-lived. Remember that access tokens are credentials. If I have an access token that says I am you, as far as the server is concerned I am you. To reduce the threat of an intercepted access token, JWT access tokens are usually set to expire quickly after being issued. Five minutes is a common lifetime for an access token. For that reason, a second token is often issued to an authorized user: the refresh token. This token has a longer life, and must be stored by the authorizing server. When a client’s access token expires, they must either request a new access token using the refresh token, or re-authorize. Using this pattern, the life of the refresh token is effectively the length of time a user can access the application without needing to sign back in.
JWTs are an excellent fit for REST API authentication. But as a relatively new pattern, most implementations contain some large security holes. Part two of this series addresses some of the security concerns around a typical implementation of JWT authentication, as well as one potential solution to those concerns.