ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Follow publication

Handling Token Expiration in Ktor: Automatic Token Refresh for API Calls

Dobri Kostadinov
ProAndroidDev
Published in
6 min readFeb 27, 2025

Introduction

Goals of This Guide

The Problem: Expiring Tokens

interface ApiService {
@GET("products")
suspend fun getProducts(): List<Product>
}

Step 1: Creating an Interceptor to Add Authorization Headers

class AuthInterceptor(private val sharedPreferences: SharedPreferences) {
fun intercept(builder: HttpRequestBuilder) {
val accessToken = sharedPreferences.getString("access_token", "") ?: ""
builder.header("Authorization", "Bearer $accessToken")
}
}

How It Works

Step 2: Implementing a Synchronized Token Refresh Mechanism

import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

class TokenAuthenticator(
private val apiService: ApiService,
private val sharedPreferences: SharedPreferences
) {
private val lock = Mutex() // Ensures only one refresh at a time
suspend fun refreshToken(): Boolean {
return lock.withLock {
val currentAccessToken = sharedPreferences.getString("access_token", null)
val refreshToken = sharedPreferences.getString("refresh_token", null) ?: return false
// If another request has refreshed the token, return success
if (currentAccessToken != sharedPreferences.getString("access_token", null)) {
return true
}
return try {
val response = apiService.refreshToken(refreshToken)
sharedPreferences.edit()
.putString("access_token", response.accessToken)
.putString("refresh_token", response.refreshToken)
.apply()
true
} catch (e: Exception) {
false // Refresh failed, logout user
}
}
}
}

How It Works

Step 3: Intercepting Requests and Handling Token Expiry in Ktor

fun provideKtorClient(
authInterceptor: AuthInterceptor,
tokenAuthenticator: TokenAuthenticator,
sharedPreferences: SharedPreferences
)
: HttpClient {
return HttpClient {
install(HttpRequestRetry) {
retryOnServerErrors(maxRetries = 3)
exponentialDelay()
}
install(DefaultRequest) {
authInterceptor.intercept(this)
}
HttpResponseValidator {
handleResponseExceptionWithRequest { exception, request ->
if (exception is ClientRequestException && exception.response.status == HttpStatusCode.Unauthorized) {
val refreshed = tokenAuthenticator.refreshToken()
if (refreshed) {
request.headers["Authorization"] = "Bearer ${sharedPreferences.getString("access_token", "")}"
throw exception // Rethrow to let Ktor retry
} else {
logoutUser() // Handle logout scenario
}
}
}
}
}
}

How It Works

Step 4: Implementing the Refresh Token API in ApiService

interface ApiService {
@POST("auth/refresh")
suspend fun refreshToken(@Body refreshToken: String): TokenResponse
}

data class TokenResponse(
val accessToken: String,
val refreshToken: String
)

Handling Edge Cases

1. Refresh Token Expired?

if (!tokenAuthenticator.refreshToken()) {
logoutUser()
}

2. Multiple Requests Fail at the Same Time?

3. Network Failure During Token Refresh?

Step 6: Setting Up Dependency Injection for Ktor Client

Option 1: Manual Dependency Injection (Basic Approach)

// Create SharedPreferences instance (e.g., in Application class)
val sharedPreferences: SharedPreferences =
context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)

// Create ApiService instance using Ktor Client
val apiService: ApiService = provideRetrofit(sharedPreferences).create(ApiService::class.java)
// Instantiate the dependencies
val authInterceptor = AuthInterceptor(sharedPreferences)
val tokenAuthenticator = TokenAuthenticator(apiService, sharedPreferences)
// Provide the Ktor client instance
val ktorClient = provideKtorClient(authInterceptor, tokenAuthenticator, sharedPreferences)

Option 2: Using Hilt (Recommended for Larger Projects)

@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
return context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
}
}
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideApiService(sharedPreferences: SharedPreferences): ApiService {
return provideRetrofit(sharedPreferences).create(ApiService::class.java)
}
}
@Module
@InstallIn(SingletonComponent::class)
object AuthModule {
@Provides
@Singleton
fun provideAuthInterceptor(sharedPreferences: SharedPreferences): AuthInterceptor {
return AuthInterceptor(sharedPreferences)
}
@Provides
@Singleton
fun provideTokenAuthenticator(
apiService: ApiService,
sharedPreferences: SharedPreferences
)
: TokenAuthenticator {
return TokenAuthenticator(apiService, sharedPreferences)
}
}
@Module
@InstallIn(SingletonComponent::class)
object KtorModule {

@Provides
@Singleton
fun provideKtorClient(
authInterceptor: AuthInterceptor,
tokenAuthenticator: TokenAuthenticator,
sharedPreferences: SharedPreferences
)
: HttpClient {
return HttpClient {
install(HttpRequestRetry) {
retryOnServerErrors(maxRetries = 3)
exponentialDelay()
}
install(DefaultRequest) {
authInterceptor.intercept(this)
}
HttpResponseValidator {
handleResponseExceptionWithRequest { exception, request ->
if (exception is ClientRequestException && exception.response.status == HttpStatusCode.Unauthorized) {
val refreshed = tokenAuthenticator.refreshToken()
if (refreshed) {
request.headers["Authorization"] =
"Bearer ${sharedPreferences.getString("access_token", "")}"
throw exception // Rethrow to let Ktor retry
} else {
logoutUser() // Handle logout scenario
}
}
}
}
}
}
}
@HiltViewModel
class ProductsViewModel @Inject constructor(
private val ktorClient: HttpClient
) : ViewModel() {

suspend fun getProducts(): List<Product> {
return ktorClient.get("https://api.example.com/products").body()
}
}

Why This Step is Important?

Conclusion

Final Thought

Published in ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Written by Dobri Kostadinov

15+ years in native Android dev (Java, Kotlin). Expert in developing beautiful android native apps.

Responses (2)

Write a response