You are (probably) using JWT wrong
A JSON Web Token (JWT) is a widely used web standard1, including in OAuth 2.0 services on major platforms. It has three parts: a header, a payload, and a signature. The header names the algorithm used to sign the token. The payload holds information such as when the token was issued, when it expires, and what the user may access. The signature is calculated from the base64-encoded header and payload; changing either makes the signature invalid.
In this example, the three base64-encoded parts are separated by periods:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cThe first part decodes to this header:
{
"alg": "HS256",
"typ": "JWT"
}The second part decodes to this payload:
{
"sub": "123", // user id
"name": "John Doe", // user name
"iat": 1600000000 // issued at
}The signature is calculated like this:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), MY_SECRET);JWT libraries handle token generation for you.
Stolen tokens
An attacker with a valid JWT can use the permissions it grants to the victim. Services therefore need to reduce the chance of token theft, even when requests travel over HTTPS.
Access and refresh tokens
JWT authentication can work without the server storing a user's session. The server checks the signature with its secret key and reads the user information from the payload. Without stored session information, the server cannot revoke an issued token. A stolen token can be used until it expires.
Many services limit that window with a short-lived access token and a longer-lived refresh token. The access token identifies the user and grants access. The refresh token gets a new access token when the old one expires, so the user can stay signed in. The client stores the refresh token securely, but it can still be stolen. Some servers block known invalid refresh tokens or replace them after use to reduce that risk.
When requests refresh the same token
HTTP clients such as Retrofit for Android can make several requests at once to reduce loading time. Those requests may all read the same token from local storage. What happens when they do so just as the access token expires?
Clients often have a filter that runs before each outgoing request. The filter can check the token and refresh it if needed. But overlapping requests can each see the expired token before any refresh has finished. The first request starts a refresh, and the others start their own refreshes using the same stored token.
When one refresh finishes, it saves the replacement token. The other requests may still send the old refresh token, unaware that it has been replaced. The server rejects it, and the app can end up asking the user to log in again.
Allowing one refresh at a time
Use a lock, such as a mutex or semaphore, to make requests wait while a refresh is already running. Modern HTTP clients let you intercept incoming and outgoing requests. With OkHttp3 and coroutines, a mutex can be used like this:
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)
}
}