Search code examples
ioskotlin-multiplatformktorkmmktor-client

Refresh auth token in Ktor for iOS Http client


I have a KMM project in which I have been using Ktor for the API calls. I have a requirement in which I need to update my access token with the help of refresh tokens if they are expired. Basically I just need to add an authentication module in my Ktor client. No I have gone through all Ktor documentation and added Auth module in my KMM.

Now when I add auth module in my http client it gets added successfully and whenever I receive UnAuthorized user error from any API it calls my refresh token API. The issue is even though it calls my refresh token API but on success of refresh token it does not call the other API from which I have received UnAuthorized user error.

It works as expected in Android but the only issue is in iOS client.

Expected (Works fine in Android Http client) :-

  • Call any API -> if received any UnAuthorized user error call refresh token API -> onSuccess of refresh token API -> Call the first API again with the updated refresh token.

Issue I am facing :-

  • Call any API -> if received any UnAuthorized user error call refresh token API -> onSuccess of refresh token API -> Does nothing on this step just throws unauthorized user error from the first API.

HttpClient for iOS :-

actual class HttpBaseClient {

    actual val tokenClient = HttpClient {
        defaultRequest {
            host = ApiEndPoints.Base.url
            url {
                protocol = URLProtocol.HTTPS
            }
            contentType(ContentType.Application.Json)
            header(CONNECTION, CLOSE)
        }
        install(JsonFeature) {
            val json = kotlinx.serialization.json.Json {
                ignoreUnknownKeys = true
                coerceInputValues = true
            }
            serializer = KotlinxSerializer(json)
        }
    }

    actual val httpClient: HttpClient = HttpClient {
        defaultRequest {
            host = ApiEndPoints.Base.url
            url {
                protocol = URLProtocol.HTTPS
            }
            contentType(ContentType.Application.Json)
            header(CONNECTION, CLOSE)
        }
        // Validate Response
        expectSuccess = false
        // Install Auth
        install(Auth) {
            lateinit var refreshTokenInfo : LoginResponse
            bearer {
                refreshTokens { unauthorizedResponse: HttpResponse ->
                    NSLog("Unauthorized response received")
                    BaseAPIClass().refreshAuthToken().fold(
                        failed = {
                            // On Failed
                            NSLog("Token Failed"). // No Callback received here
                        },
                        succeeded = { response ->
                            refreshTokenInfo = response
                            NSLog("Token Updated") // No Callback received here even when API is success
                        }
                    )
                    BearerTokens(
                        accessToken = refreshTokenInfo.accessToken ?: "",
                        refreshToken = refreshTokenInfo.refreshToken ?: ""
                    )
                }
            }
        }
        // JSON Deserializer
        install(JsonFeature) {
            val json = kotlinx.serialization.json.Json {
                ignoreUnknownKeys = true
                coerceInputValues = true
            }
            serializer = KotlinxSerializer(json)
        }

Android Client (Pretty much same):-

actual class HttpBaseClient {

    actual val tokenClient = HttpClient {
        defaultRequest {
            host = ApiEndPoints.Base.url
            url {
                protocol = URLProtocol.HTTPS
            }
            contentType(ContentType.Application.Json)
            header(CONNECTION, CLOSE)
        }
        install(JsonFeature) {
            val json = kotlinx.serialization.json.Json {
                ignoreUnknownKeys = true
                coerceInputValues = true
            }
            serializer = KotlinxSerializer(json)
        }
        install(Logging) {
            logger = Logger.DEFAULT
            level = LogLevel.ALL
        }
    }

    actual val httpClient: HttpClient = HttpClient {
        defaultRequest {
            host = ApiEndPoints.Base.url
            url {
                protocol = URLProtocol.HTTPS
            }
            contentType(ContentType.Application.Json)
            header(CONNECTION, CLOSE)
        }
        // Validate Response
        expectSuccess = false
        //Authentication
        install(Auth) {
            lateinit var refreshTokenInfo : LoginResponse
            bearer {
                refreshTokens { unauthorizedResponse: HttpResponse ->
                    BaseAPIClass().refreshAuthToken().fold(
                        failed = {
                            // On Failed
                        },
                        succeeded = { response ->
                            refreshTokenInfo = response
                        }
                    )
                    BearerTokens(
                        accessToken = refreshTokenInfo.accessToken ?: "",
                        refreshToken = refreshTokenInfo.refreshToken ?: ""
                    )
                }
            }
        }

Ktor version :- 1.6.2 (tried 1.6.4 as well after reading this issue but didn't work)


Solution

  • I got this working only the error is at this code :-

    succeeded = { response ->
                            refreshTokenInfo = response
                            NSLog("Token Updated") // No Callback received here even when API is success
                        }
    

    I am not sure why but assigning the response to my lateinit var refreshTokenInfo is causing the main problem here. I removed that and updated my code to

    refreshTokens { unauthorizedResponse: HttpResponse ->
                    BaseAPIClass().refreshAuthToken().fold(
                        failed = {
                            // On Failed
                            return@refreshTokens BearerTokens(
                                accessToken = "",
                                refreshToken = ""
                            )
                        },
                        succeeded = { response ->
                          return@refreshTokens BearerTokens(
                                accessToken = response.accessToken ?: "",
                                refreshToken = response.refreshToken ?: ""
                            )
                        }
                    )
                }
    

    and this works.

    I have also raised the issue here you can go through the details.