Using JWTs with Retrofit

Helder Pinhal
Helder Pinhal
Apr 21 2023
Posted in Engineering & Technology

A comprehensive guide to handling user authentication in Android.

Using JWTs with Retrofit

Authentication is an essential part of any application that requires user access. JSON Web Tokens (JWTs) have become a popular choice for implementing authentication in web and mobile applications. In this blog post, we'll explore how to use JWTs for authentication in Android with Retrofit.

Retrofit is a widely used HTTP client library for Android that simplifies the process of making HTTP requests to APIs. We'll cover the basics of JWTs, how to integrate them with Retrofit, and how to handle token expiration and refreshing. By the end of this post, you'll have a solid understanding of how to use JWTs with Retrofit to implement secure authentication in your Android application.

What is a JSON Web Token

JWTs are an open standard (RFC 7519) that defines a compact and self-contained way to transmit information between parties as a JSON object securely. They are widely used in modern web applications and APIs and are supported by many programming languages and frameworks.

JWTs have several advantages over other authentication and authorization mechanisms. They are stateless, meaning the server does not need to keep track of the user's session or any other state information. This makes JWTs suitable for scaling web applications horizontally and allows them to be used in distributed architectures and microservices. JWTs are also highly customizable, as they can carry arbitrary information in their payload, such as user roles, permissions, and custom claims.

Understanding the contents of a JWT

A JWT is a string that consists of three components: a header, a payload, and a signature.

  1. The header of a JWT is a JSON object that describes the type of token and the algorithm used to sign it. It typically contains two properties: "alg" (algorithm) and "typ" (type). The "alg" property specifies the algorithm used to sign the token, such as "HS256" (HMAC-SHA256) or "RS256" (RSA-SHA256). The "typ" property specifies the type of token, which is usually set to "JWT".

  2. The payload of a JWT is a JSON object that contains the actual data that you want to transmit in the token. This can include user information, permissions, or any other data that you want to associate with the token. The payload can also contain custom claims, which are additional properties that are not defined by the JWT standard, but can be used to add application-specific information. Some common claims that are defined by the JWT standard include "iss" (issuer), "sub" (subject), "exp" (expiration time), and "iat" (issued at time).

  3. The signature of a JWT is used to verify that the token has not been tampered with and was actually issued by the intended party. The signature is created by taking the base64url-encoded header and payload, concatenating them with a period (".") separator, and then encrypting the resulting string using the algorithm specified in the header and a secret key that is known only to the issuer. The resulting signature is then appended to the token as a third component.

Together, the header, payload, and signature form a compact and self-contained token that can be transmitted between parties and verified without the need for additional state information. When a client presents a JWT to a server, the server can verify the token by computing the signature using the same algorithm and secret key, and comparing it to the signature included in the token. If the signatures match, the server can trust the token's authenticity and extract the payload data to make authorization decisions.

Authenticating your requests

When building a mobile app, secure authentication is a top priority. Once the user has successfully logged in and obtained a JWT, they can use that token to access protected resources on the server. In Retrofit, adding the access token to requests is straightforward. We can use an OkHttp Authenticator to add the token to the Authorization header of each request.

// Assuming you store and process the authentication credentials with the following data class.
data class Credentials(
    val accessToken: String,
    val refreshToken: String,
    val expiresAt: Date,
)

class OAuthAuthenticator : Authenticator {
    override fun authenticate(route: Route?, response: Response): Request? {
        if (response.request.header("Authorization") == null) {
            val credentials = getCredentials()

            // NOTE: this can be improved by checking the expiration date locally instead of
            // sending a request to the API which will result in a 401.

            // Adding the access token to the request.
            return response.request.newBuilder()
                .header("Authorization", "Bearer ${credentials.accessToken}")
                .build()
        }

        // Use the authenticated original request.
        return response.request
    }

    private fun getCredentials(): Credentials {
        TODO("Get the user credentials from local storage.")
    }
}

Refreshing expired credentials

One of the challenges of using JSON Web Tokens (JWTs) for authentication is managing token expiration. JWTs have a limited lifespan, and once they expire, clients need to obtain a new token to continue accessing protected resources. This can be particularly challenging in a mobile app since network conditions can be unpredictable.

To solve this problem, we can use refresh tokens to obtain a new access token when the current one expires. A refresh token is a long-lived token that is used to obtain a new access token without requiring the user to log in again. We can improve the authenticator above to automatically refresh the access token when needed.

class OAuthAuthenticator : Authenticator {
    override fun authenticate(route: Route?, response: Response): Request? {
        if (response.request.header("Authorization") == null) {
            val credentials = getCredentials()

            // NOTE: this can be improved by checking the expiration date locally instead of
            // sending a request to the API which will result in a 401.

            // Adding the access token to the request.
            return response.request.newBuilder()
                .header("Authorization", "Bearer ${credentials.accessToken}")
                .build()
        }

        if (response.code == 401) {
            // The access token is expired. Refresh the credentials.
            synchronized(this) {
                // Make sure only one coroutine refreshes the token at a time.
                return runBlocking {
                    try {
                        val newCredentials = refreshCredentials()

                        // Update the access token in your storage.
                        updateCredentials(newCredentials)

                        return@runBlocking response.request.newBuilder()
                            .header("Authorization", "Bearer ${newCredentials.accessToken}")
                            .build()
                    } catch (e: Exception) {
                        // The refresh process failed. Give up on retrying the request.
                        return@runBlocking null
                    }
                }
            }
        }

        // Use the authenticated original request.
        return response.request
    }

    private fun getCredentials(): Credentials {
        TODO("Get the user credentials from local storage.")
    }

    private fun updateCredentials(credentials: Credentials) {
        TODO("Save the updated credentials to local storage.")
    }

    private suspend fun refreshCredentials(): Credentials {
        TODO("Integrate with your OAuth provider to refresh user credentials.")
    }
}

Conclusion

Overall, refreshing JWTs in Retrofit is a crucial step in implementing secure authentication and ensuring that the user remains authenticated. With the right tools and techniques, we can make token expiration a seamless and transparent part of our app's authentication process.

As always, we hope you liked this article, and if you have anything to add, we are available via our Support Channel.

Keep up-to-date with the latest news