When working with authenticated APIs in Android development, injecting an Authorization
token into HTTP headers can be repetitive and error-prone. A clean and automated approach is to use OkHttp Interceptors combined with a custom annotation in Retrofit. This not only simplifies the process but also keeps your codebase clean and maintainable.
In this article, we’ll explore how to achieve this by leveraging an interceptor and annotations to handle token injection dynamically.
The Idea
The goal is to:
- Use a custom annotation, e.g.,
@InjectAuth
, on Retrofit API methods that require anAuthorization
header. - Automatically inject the auth token as
Authorization
header into the request headers for annotated methods. - Keep the rest of the API calls unaffected.
Here’s how we can accomplish this.
Sequence of Events
Here’s the high-level flow when making an API request:
-
Client invokes a Retrofit method annotated with
@InjectAuth
. - Retrofit delegates the call to the OkHttp Interceptor.
- The interceptor checks for the annotation and appends the token to the header if required.
- The server processes the authenticated request and returns a response.
This interaction is illustrated in the following sequence diagram:
Implementing the Auth Token Interceptor
The core of our implementation is the custom AuthInterceptor
. This interceptor checks for the @InjectAuth
annotation and dynamically appends the auth token.
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class InjectAuth
The interceptor implementation:
class AuthInterceptor(private val tokenProvider: () -> String?) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
if (request.markedForInjection()) {
val token = tokenProvider.invoke()
if (!token.isNullOrEmpty()) {
val newRequest = request.newBuilder()
.addHeader("Authorization", "Bearer $token")
.build()
return chain.proceed(newRequest)
}
}
return chain.proceed(request)
}
/**
* Check if request is annotated with `@InjectAuth` annotation,
* If annotated, then it's marked for `Authorization` injection
*/
private fun Request.markedForInjection(): Boolean = tag<Invocation>()?.method()
?.annotations?.toSet()?.find { it is InjectAuth } != null
}
Setting Up Retrofit
You need to add the AuthInterceptor
to OkHttp’s interceptor chain when building the Retrofit instance:
val tokenProvider: () -> String? = {
/* Logic to fetch token (i.e. get from shared preferences) */
}
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(AuthInterceptor(tokenProvider))
.build()
val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
val apiService = retrofit.create<ApiService>()
Annotating API Methods
Use the @InjectAuth
annotation for any Retrofit API method that requires authentication.
interface ApiService {
@InjectAuth
@GET("protected/endpoint")
suspend fun getProtectedData(): Response<ProtectedData>
@GET("public/endpoint")
suspend fun getPublicData(): Response<PublicData>
}
In this example:
-
getProtectedData
will include theAuthorization
header automatically. -
getPublicData
will remain unaffected.
Key Benefits
- Code Reusability: No need to manually add headers to every request.
-
Scalability: New endpoints requiring authentication can easily adopt this by using
@InjectAuth
annotation. - Centralized Token Management: The interceptor ensures token handling is managed in one place.
Conclusion
By combining Retrofit, OkHttp, and custom annotations, you can build a robust and automated mechanism for handling API authentication. This approach improves code readability, scalability, and maintainability.
Have thoughts or questions? Share them in the comments below!
Top comments (0)