Authentication Handling with Ktor in Android

Emirhan Emmez
2 min readMay 17, 2023

--

In this tutorial. We will learn how to handle bearer token management with Ktor in Android. I created a server also for testing. Before the implementation please clone this repo and run for testing. After that we will test on Android emulator.

Scenerio:

1. We have a /login endpoint and an authenticated /authHello endpoint.

2. /login endpoint returns bearer and refresh tokens.

3. If bearer token is valid /authHello endpoint returns 200, if it is not it returns 401 and token will be refreshed by /refreshToken endpoint with refresh token. After refresh operation, our HttpClient will resend request with new bearer token automatically.

Implementation:

The implementation will be with DI (Hilt). There will be a LocalService (Preferences DataStore) for saving our bearer and refresh tokens and a RemoteService for requesting endpoints. So let’s start with providing our objects.

LocalService.kt:

class LocalService @Inject constructor(@ApplicationContext private val context: Context) {
private val Context.dataStore by preferencesDataStore(context.packageName)

suspend fun saveBearerToken(bearerToken: String) {
context.dataStore.edit {
it[bearerTokenKey] = bearerToken
}
}

suspend fun saveRefreshToken(refreshToken: String) {
context.dataStore.edit {
it[refreshTokenKey] = refreshToken
}
}

suspend fun getBearerToken(): String? {
return context.dataStore.data.firstOrNull()?.get(bearerTokenKey)
}

suspend fun getRefreshToken(): String? {
return context.dataStore.data.firstOrNull()?.get(refreshTokenKey)
}

private val bearerTokenKey = stringPreferencesKey("bearer_token")
private val refreshTokenKey = stringPreferencesKey("refresh_token")
}

RemoteService.kt:

class RemoteService @Inject constructor(
private val httpClient: HttpClient,
private val localService: LocalService
) {
suspend fun login(user: User): Token =
httpClient.post {
url("login")
setBody(user)
}.body()

suspend fun authHello(): String {
val bearerToken = localService.getBearerToken()
return httpClient.get {
url("authHello")
headers {
append(HttpHeaders.Authorization, "Bearer $bearerToken")
}
}.body()
}
}

OurDiModule.kt:

@Module
@InstallIn(SingletonComponent::class)
object OurDiModule {

@Provides
@Singleton
fun provideLocalService(@ApplicationContext context: Context): LocalService =
LocalService(context)

@Provides
@Singleton
fun provideHttpClient(localService: LocalService): HttpClient =
HttpClient(Android).config {
defaultRequest {
url("http://10.0.2.2:8080/")
contentType(ContentType.Application.Json)
accept(ContentType.Application.Json)
}
install(Logging) {
level = LogLevel.ALL
logger = object : Logger {
override fun log(message: String) {
Log.i("HttpClient", message)
}
}
}
install(ContentNegotiation) {
json()
}
install(Auth) {
bearer {
refreshTokens {
val token = client.get {
markAsRefreshTokenRequest()
url("refreshToken")
parameter("refreshToken", localService.getRefreshToken())
}.body<Token>()
BearerTokens(
accessToken = token.bearerToken,
refreshToken = token.refreshToken
)
}
}
}
}

@Provides
@Singleton
fun provideRemoteService(httpClient: HttpClient, localService: LocalService): RemoteService =
RemoteService(httpClient, localService)
}

In here, main point is Auth plugin. As we understand, refreshTokens block triggers when server returns 401. Firstly token refresh request will be sent after that our client will resend request with new token.

NOTE: We should add markAsRefreshTokenRequest() method to HttpRequestBuilder block for token refresh requests.

Thanks for reading :)
Here is my application’s repo. If you have any question you can reach me via:
https://twitter.com/emirhan_emmez
https://www.linkedin.com/in/emirhanemmez/

--

--