You are (probably) using JWT wrong

12/01/2023

| 4 min read


JSON Web Token (JWT) is a widely used web standard in various web services1 such as OAuth 2.0 by major platforms. For those unfamiliar, a JWT consists of three parts: the header, payload, and signature. The header specifies the cryptographic algorithm used for the token’s signature. The payload contains claims such as issuance timestamp, expiry date, and user authority. Lastly, the signature is a string generated by the specified algorithm using the base64-encoded header and payload. Consequently, if the token is tampered with, the signature becomes invalid.

An example of JWT looks like this, where each part is encoded with base64 and separated by a period:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

The header is the first part, decoding to:

{
  "alg": "HS256",
  "typ": "JWT"
}

The payload is the second part, decoding to:

{
  "sub": "123", // user id
  "name": "John Doe", // user name
  "iat": 1600000000 // issued at
}

The signature is computed as follows:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  MY_SECRET
)

It’s quite straightforward! Plus, there are JWT libraries available that handle JWT generation details, so you don’t have to worry about them.

Caveats

In practice, if an attacker gains access to a valid JWT, they essentially have the authority of the victim. To mitigate this risk, many implementations aim to reduce the likelihood of JWT theft, even when the token is transmitted over a supposedly secure protocol like HTTPS.

Refresh token

One common method is to give the JWT used for client-server communication (often called the access token) a short expiry. To refresh the expired JWT, a refresh token is used, securely stored on both client and server.

When the access token expires, a refresh request is sent to the server with the refresh token. The server validates the token by comparing it with one stored in the server database.

If a valid, matching refresh token is found in the database, the server replaces the token with a new one and returns it. Otherwise, the user will be prompted to log in again. Following standard security measures such as encrypted storage should make everything straightforward, right?

Race conditions

However, there’s another crucial caveat to consider. The client needs to store the refresh token on the device, and it will be accessed in the future when the access token expires. In many modern HTTP client frameworks such as Retrofit for Android, requests may be made concurrently to reduce perceived loading time.

Ignoring encrypted storage and other factors, what happens if the client makes concurrent requests as the access token is about to expire? In modern HTTP clients, we often encounter filter interfaces for outgoing requests, where we can implement logic to refresh the token when necessary. Note that concurrency deals with multiple tasks being executed at seemingly simultaneous, but different instants.

Consider a scenario where many concurrents requests are made. Each request needs to access the same local storage. When the first request is made, it checks the token and realizes it has expired, triggering a refresh request to the server, ideally through an outgoing request filter. Meanwhile, the subsequent requests will do the same since the token has not been refreshed yet from their point of view.

The issue arises when a finishes refreshing the token first and saves it to the storage. The subsequent requests, unware of the fact that the token has been replaced, sends the refresh request to the server with the old token, resulting in an error and prompting the user to login again. This situation is clearly undesirable in terms of user experience.

Solution

Hence, it is crucial to implement some form of mutex logic, or equivalently some semaphore to lock other concurrent processes when there is already an ongoing refresh request. Fortunately, modern HTTP client frameworks supports interfaces that can intercept in- and outbound requests. In the style of OkHttp3, implementation with mutex using coroutines can be done in the following way:

class AuthenticationInterceptor(private val tokenStorage: TokenStorage) : Interceptor {
    private val mutext = Mutex()

    private fun refreshToken(): Response {
        mutex.withLock {
            // implement refreshing logic
            ...
        }
    }

    override fun intercept(chain: Chain): Response {
        // intercept request to add bearer token
        val accessToken = tokenStorage.getAccessToken()
        val request = chain.request()
        if (accessToken.isValid()) {
            val newRequest = request
                .newBuilder()
                .header("Authorization", "Bearer $accessToken")
                .build()
            val response = chain.proceed(newRequest)
            return if (response.code() == 401) {
                refreshToken()
            } else response
        } else {
            refreshToken()
        }
        return chain.proceed(request)
    }
}