Hello guys, I am having trouble with Ktor's Author...
# multiplatform
k
Hello guys, I am having trouble with Ktor's Authorization module using Koin DI in Kotlin / Compose Multiplatform. The scenario is that when the app launches, then saved access and refresh tokens are obtained from the TokenProvider and tokens are set to loadTokens and BearerTokens() methods. This works fine when the app launches the first time. When an error happens and returns a 401 response code, then a new access and refresh token need to be fetched from the refresh API. If the refresh API also returns an error, then null or empty BearerTokens("", "") will be set and redirected to the login page as per implementation done through Koin DI. The issue is that when trying to log in and after successful login new access and refresh tokens are saved in the Room database, then any API that needs an access token will not get it as loadTokens is not called to update new tokens. I figured this might be an issue as Koin makes HttpClient a singleton. Can anyone give me insight into this? Does anyone have the same kind of issues? Please provide me insight into how to fix this issue. Provided HttpClientModule and TokenProvider in thread. Please check it out and help me.
🧵 2
Copy code
val provideHttpClientModule = module {
    fun provideHttpClient(
        appDatabase: AppDatabase,
        tokenProvider: TokenProvider
    ): HttpClient {
        return HttpClient {
            install(Logging) {
                logger = Logger.SIMPLE
                level = LogLevel.ALL
            }
            install(ContentNegotiation) {
                json(
                    json = Json {
                        prettyPrint = true
                        isLenient = true
                        ignoreUnknownKeys = true
                        explicitNulls = false
                    },
                    contentType = ContentType.Application.Json
                )
            }
            install(HttpTimeout) {
                socketTimeoutMillis = 60_000
                requestTimeoutMillis = 60_000
            }
            install(UserAgent) {
                agent = getPlatform().userAgent
            }
            defaultRequest {
                runBlocking {
                    url("baseUrl")

                    contentType(ContentType.Application.Json)

                    val platform = getPlatform()

                    header(HEADER_USER_DEVICE, platform.userDevice + HEADER_USER_DEVICE_KEY)
                    header(HEADER_USER_DEVICE_PLATFORM, platform.devicePlatform)
                    header(HEADER_USER_DEVICE_VERSION, platform.deviceVersion)
                    header(HEADER_USER_DEVICE_BUILD, platform.deviceBuild)
                    header(HEADER_USER_DEVICE_BRAND, platform.deviceBrand)
                    header(HEADER_USER_DEVICE_MODEL, platform.deviceModel)
                    header(HEADER_USER_DEVICE_APP_VERSION, platform.appVersion)
                    header(HEADER_USER_DEVICE_APP_VERSION_CODE, platform.appVersionCode)
                }
            }
            install(Auth) {
                bearer {
                    loadTokens {
                        runBlocking {
                            val accessToken = tokenProvider.getAccessToken()
                            val refreshToken = tokenProvider.getRefreshToken()
                            if (accessToken.isNotEmpty() && refreshToken.isNotEmpty()) {
                                BearerTokens(accessToken, refreshToken)
                            } else {
                                tokenProvider.clearTokens()

                                null
                                // BearerTokens("", "")
                            }
                        }
                    }
                    refreshTokens {
                        runBlocking {
                            val refreshToken = tokenProvider.getRefreshToken()
                            if (refreshToken.isNotEmpty()) {
                                val response: HttpResponse = client.submitForm(
                                    url = ApiEndpoints.API_REFRESH_TOKEN,
                                    formParameters = parameters {
                                        append("refresh_token", refreshToken)
                                    }
                                ) {
                                    markAsRefreshTokenRequest()
                                }
                                if (response.status.isSuccess()) {
                                    val responses: RefreshTokenDTO = response.body()

                                    if (getStatus<Status>(responses.status)) {
                                        tokenProvider.updateTokens(
                                            responses.response?.token!!,
                                            responses.response.refresh_token!!
                                        )

                                        BearerTokens(
                                            responses.response.token,
                                            responses.response.refresh_token
                                        )
                                    } else {
                                        tokenProvider.clearTokens()

                                        null
                                        // BearerTokens("", "")
                                    }
                                } else {
                                    tokenProvider.clearTokens()

                                    null
                                    // BearerTokens("", "")
                                }
                            } else {
                                tokenProvider.clearTokens()

                                null
                                // BearerTokens("", "")
                            }
                        }
                    }
                    sendWithoutRequest { request ->
                        when (request.url.pathSegments.last()) {
                            "check-user" -> false
                            "verify-otp" -> false
                            else -> true
                        }
                    }
                }
            }
        }
    }

    singleOf(::provideHttpClient)
    singleOf(::TokenProvider)
}

class TokenProvider(private val appDatabase: AppDatabase) {
    private var accessToken: String? = null
    private var refreshToken: String? = null
    private val mutex = Mutex()

    suspend fun getAccessToken(): String = mutex.withLock {
        if (accessToken.isNullOrEmpty()) {
            val tokens = appDatabase.apiTokenDao.findAll()
            accessToken = tokens.firstOrNull()?.token?.takeIf { it.isNotEmpty() } ?: ""
        }
        accessToken ?: ""
    }

    suspend fun getRefreshToken(): String = mutex.withLock {
        if (refreshToken.isNullOrEmpty()) {
            val tokens = appDatabase.apiTokenDao.findAll()
            refreshToken = tokens.firstOrNull()?.refresh_token?.takeIf { it.isNotEmpty() } ?: ""
        }
        refreshToken ?: ""
    }

    suspend fun updateTokens(newAccessToken: String, newRefreshToken: String) = mutex.withLock {
        accessToken = newAccessToken
        refreshToken = newRefreshToken

        appDatabase.apiTokenDao.deleteAll() // Clear old tokens if needed
        appDatabase.apiTokenDao.add(ApiTokenDTO(newAccessToken, newRefreshToken)) // Add the new one
    }

    suspend fun clearTokens() = mutex.withLock {
        accessToken = ""
        refreshToken = ""

        appDatabase.apiTokenDao.deleteAll()
    }
}
s
This is not an answer to your issue (i never used Ktor Auth module). However, storing your token(s) in a database is not very safe, in case the phone winds up in the hands of someone else. Consider storing these tokens in the Keystore/Keychain provider
1