RefreshToken not working as expecting in ktor - android

Hey I am working in Ktor in KMM. I tried to use refreshToken as per suggestion for authentication in my application.
HttpClient.kt
package com.example.kotlinmultiplatformsharedmodule
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.auth.*
import io.ktor.client.plugins.auth.providers.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
import java.util.concurrent.TimeUnit
actual fun httpClient(config: HttpClientConfig<*>.() -> Unit) = HttpClient(OkHttp) {
config(this)
install(Logging) {
logger = Logger.SIMPLE
level = LogLevel.BODY
}
install(ContentNegotiation) {
json(Json {
prettyPrint = true
ignoreUnknownKeys = true
explicitNulls = false
})
}
engine {
config {
retryOnConnectionFailure(true)
connectTimeout(30, TimeUnit.SECONDS)
readTimeout(40, TimeUnit.SECONDS)
}
}
defaultRequest {
header("Client-Version", Platform().versionCode)
}
install(Auth) {
bearer {
loadTokens {
BearerTokens(tokenProvider.accessToken, "")
}
refreshTokens {
val response =
client.post("https://vivek-modi.com/api/v1/session/refresh") {
contentType(ContentType.Application.Json)
setBody(KtorSessionCommand(tokenProvider.refreshToken))
}
if (response.status == HttpStatusCode.Unauthorized) {
println("application will logout")
null
} else {
println("application in else part")
val ktorLoginResponse = response.body<KtorLoginResponse>()
ktorLoginResponse.ktorAccessToken?.let { ktorAccessToken ->
ktorAccessToken.accessToken?.let { accessToken ->
ktorAccessToken.refreshToken?.let { refreshToken ->
BearerTokens(accessToken, refreshToken)
}
}
}
}
}
}
}
}
build.gradle.kts
plugins {
kotlin("multiplatform")
kotlin("native.cocoapods")
id("com.android.library")
id("kotlinx-serialization")
}
version = "1.0"
kotlin {
android()
iosX64()
iosArm64()
iosSimulatorArm64()
cocoapods {
summary = "Some description for the Shared Module"
homepage = "Link to the Shared Module homepage"
ios.deploymentTarget = "14.1"
framework {
baseName = "kotlinmultiplatformsharedmodule"
}
}
sourceSets {
val ktorVersion = "2.0.0"
val commonMain by getting {
dependencies {
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-logging:$ktorVersion")
implementation("io.ktor:ktor-server-default-headers:$ktorVersion")
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
implementation("io.ktor:ktor-client-auth:$ktorVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.3.2")
implementation("io.insert-koin:koin-core:3.2.0-beta-1")
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
val androidMain by getting {
dependencies {
implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
implementation("io.ktor:ktor-client-logging-jvm:$ktorVersion")
}
}
val androidTest by getting
val iosX64Main by getting
val iosArm64Main by getting
val iosSimulatorArm64Main by getting
val iosMain by creating {
dependsOn(commonMain)
iosX64Main.dependsOn(this)
iosArm64Main.dependsOn(this)
iosSimulatorArm64Main.dependsOn(this)
dependencies {
implementation("io.ktor:ktor-client-darwin:$ktorVersion")
implementation("io.ktor:ktor-client-logging-native:$ktorVersion")
}
}
val iosX64Test by getting
val iosArm64Test by getting
val iosSimulatorArm64Test by getting
val iosTest by creating {
dependsOn(commonTest)
iosX64Test.dependsOn(this)
iosArm64Test.dependsOn(this)
iosSimulatorArm64Test.dependsOn(this)
}
}
}
android {
compileSdk = 32
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
defaultConfig {
minSdk = 21
targetSdk = 32
}
}
Expected Output
Scenario 1
when my main api return 401, I need to call https://vivek-modi.com/api/v1/session/refresh if this return different then 401 status, then I need to call my main api again.
Scenario 2
when my main api return 401, I need to call https://vivek-modi.com/api/v1/session/refresh if this return 401, I need to logout my application.
Actual Ouput
I am calling api in button click.
So when I click 1st time on button, it called my main api, which return 401. So refreshToken call https://vivek-modi.com/api/v1/session/refresh which return 401. I used println to print message in console but it not printing the message.
when I click 2nd time or more time on button. It call only my main api. It not calling my refresh api.
Can someone guide me please. How can I achieve my expected output.

The problem is that a client (specifically the Auth plugin) tries to infinitely refresh a token. Here is the comment from the corresponding issue:
When refresh token request fails with 401 it tries to refresh token again, resulting in an infinite cycle. Since token refreshing is a users' code, users need to mark such requests, so we can have a special case.
To solve it you need to call markAsRefreshTokenRequest() inside the request builder:
val response =
client.post("https://vivek-modi.com/api/v1/session/refresh") {
markAsRefreshTokenRequest()
contentType(ContentType.Application.Json)
setBody(KtorSessionCommand(tokenProvider.refreshToken))
}

Related

Android compose ktor failed with error Lio/ktor/utils/io/NativeUtilsJvmKt

i am trying to setup ktor replacing retrofit on a compose simple android app but i am getting this error:
java.lang.NoClassDefFoundError: Failed resolution of: Lio/ktor/utils/io/NativeUtilsJvmKt;
at io.ktor.client.features.HttpSend.<init>(HttpSend.kt:49)
at io.ktor.client.features.HttpSend.<init>(HttpSend.kt:41)
at io.ktor.client.features.HttpSend$Feature.prepare(HttpSend.kt:75)
at io.ktor.client.features.HttpSend$Feature.prepare(HttpSend.kt:72)
at io.ktor.client.HttpClientConfig$install$3.invoke(HttpClientConfig.kt:77)
at io.ktor.client.HttpClientConfig$install$3.invoke(HttpClientConfig.kt:74)
at io.ktor.client.HttpClientConfig.install(HttpClientConfig.kt:97)
at io.ktor.client.HttpClient.<init>(HttpClient.kt:172)
at io.ktor.client.HttpClient.<init>(HttpClient.kt:81)
at io.ktor.client.HttpClientKt.HttpClient(HttpClient.kt:43)
My service is like this:
interface ApiService {
suspend fun getProducts(): List<ResponseModel>
companion object {
private val json = kotlinx.serialization.json.Json {
ignoreUnknownKeys = true
isLenient = true
encodeDefaults = false
}
fun create(): ApiService {
return ApiServiceImpl(
client = HttpClient(Android) {
// Logging
install(Logging) {
level = LogLevel.BODY
}
// JSON
install(JsonFeature) {
serializer = KotlinxSerializer(json)
//or serializer = KotlinxSerializer()
}
// Timeout
install(HttpTimeout) {
requestTimeoutMillis = 15000L
connectTimeoutMillis = 15000L
socketTimeoutMillis = 15000L
}
}
)
}
}
}
and ApiServiceImpl
class ApiServiceImpl(private val client: HttpClient) : ApiService {
override suspend fun getProducts(): List<ResponseModel> {
return client.get {
url(HttpRoutes.PRODUCTS)
}
}
}
On my main activity I just want to make the api call and just display an attribute of the first item of the list(just for verifying that the call was successful).
class MainActivity : ComponentActivity() {
private val service = ColorBarApiService.create()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
// A surface container using the 'background' color from the theme
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
val products = produceState(
initialValue = emptyList<ResponseModel>(),
producer = {
value = service.getProducts()
}
)
// just to see if api call is success
Text(text = "First product has description ${products.value[0].description}!")
}
}
}
}
}
Finally on app build.gradle I am using these:
// ktor
implementation "io.ktor:ktor-client-core:$ktor_version"
// HTTP engine: The HTTP client used to perform network requests.
implementation "io.ktor:ktor-client-android:$ktor_version"
// The serialization engine used to convert objects to and from JSON.
implementation "io.ktor:ktor-client-serialization:$ktor_version"
// Logging
implementation "io.ktor:ktor-client-logging:$ktor_version"
implementation "io.ktor:ktor-utils-jvm:$ktor_version"
implementation "io.ktor:ktor-client-okhttp:$ktor_version"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_version"
with values
ext {
compose_ui_version = '1.2.1'
koin_version= "3.2.2"
koin_android_version= "3.2.3"
koin_android_compose_version= "3.2.2"
koin_ktor= "3.2.2"
ktor_version = '1.6.4'
serialization_version = '1.4.1'
}

Why tor get this error Parent job is Completed

I'm using ktor for the android client but I have an error.
When I run the app for the first time everything is fine and there is no issue, but when I click on the device back button and close the app, and open it again, the app is crashed and I get this error about the ktor:
Parent job is Completed
this is my ktor configure the module:
#InstallIn(SingletonComponent::class)
#Module
object NetworkModule {
private const val TIME_OUT = 60_000
#Singleton
#Provides
fun provideKtor(): HttpClient = HttpClient(Android) {
install(HttpCache)
defaultRequest {
contentType(ContentType.Application.Json)
accept(ContentType.Application.Json)
}
install(ContentNegotiation) {
json(json = Json {
prettyPrint = true
ignoreUnknownKeys = true
isLenient = true
encodeDefaults = false
})
}
install(HttpTimeout) {
connectTimeoutMillis = TIME_OUT.toLong()
socketTimeoutMillis = TIME_OUT.toLong()
requestTimeoutMillis = TIME_OUT.toLong()
}
install(ResponseObserver) {
onResponse { response ->
Log.d("HttpClientLogger - HTTP status", "${response.status.value}")
Log.d("HttpClientLogger - Response:", response.toString())
}
}
install(Logging) {
logger = object : Logger {
override fun log(message: String) {
Log.v("Logger Ktor =>", message)
}
}
level = LogLevel.NONE
}
}
}
Note: I use ktor version "2.0.2".
const val ktor_client_core = "io.ktor:ktor-client-core:$ktor_version"
const val ktor_client_cio = "io.ktor:ktor-client-cio:$ktor_version"
const val ktor_serialization_json = "io.ktor:ktor-serialization-kotlinx-json:$ktor_version"
const val ktor_serialization = "io.ktor:ktor-client-serialization:$ktor_version"
const val ktor_android = "io.ktor:ktor-client-android:$ktor_version"
const val ktor_negotiation = "io.ktor:ktor-client-content-negotiation:$ktor_version"
const val ktor_okhttp = "io.ktor:ktor-client-okhttp:$ktor_version"
const val ktor_logging = "io.ktor:ktor-client-logging:$ktor_version"
How can i fix it?
I found the reason: This is related to Hilt Di (NetworkModule). I have to use an object instead of hilt module for now
The problem is that you cannot use the same instance of the HttpClient.
companion object {
val client get() = HttpClient(Android) { }
}

Exception with Kmm and Ktor

I'm trying to create my first KMM project ( using Ktor to get a simple query ). I've managed to run it in Android, but each time I try in iOS i got an exception when i use Ktor.
This is my gradle
"Shared"
plugins {
kotlin("multiplatform")
kotlin("plugin.serialization")
id("com.android.library")
id("com.squareup.sqldelight")
}
kotlin {
android()
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach {
it.binaries.framework {
baseName = "shared"
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1")
implementation("io.ktor:ktor-client-core:1.6.6")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.3.2")
implementation("io.ktor:ktor-client-serialization:1.6.5")
implementation("com.squareup.sqldelight:runtime:1.5.3")
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
val androidMain by getting {
dependencies {
implementation("io.ktor:ktor-client-android:1.6.5")
implementation("com.squareup.sqldelight:android-driver:1.5.3")
}
}
val androidTest by getting
val iosX64Main by getting
val iosArm64Main by getting
val iosSimulatorArm64Main by getting
val iosMain by creating {
dependsOn(commonMain)
iosX64Main.dependsOn(this)
iosArm64Main.dependsOn(this)
iosSimulatorArm64Main.dependsOn(this)
dependencies {
implementation("io.ktor:ktor-client-ios:1.6.5")
implementation("com.squareup.sqldelight:native-driver:1.5.3")
}
}
val iosX64Test by getting
val iosArm64Test by getting
val iosSimulatorArm64Test by getting
val iosTest by creating {
dependsOn(commonTest)
iosX64Test.dependsOn(this)
iosArm64Test.dependsOn(this)
iosSimulatorArm64Test.dependsOn(this)
}
}
}
android {
compileSdk = 32
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
defaultConfig {
minSdk = 21
targetSdk = 32
}
}
sqldelight {
database("AppDatabase") {
packageName = "com.example.basekmm_003"
}
}
This is the base gradle
buildscript {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21")
classpath("com.android.tools.build:gradle:7.2.0")
classpath("com.squareup.sqldelight:gradle-plugin:1.5.3")
classpath("org.jetbrains.kotlin:kotlin-serialization:1.6.21")
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
tasks.register("clean", Delete::class) {
delete(rootProject.buildDir)
}
The Android Studio version I'm using is: Chipmunk / 2021.2.1
The code for my api is:
class JsonApi() {
suspend fun getLatestMovies(): List<JsonMessage>? =
KtorClient.httpClient.get {
url("https://gitcdn.link/cdn/KaterinaPetrova/greeting/7d47a42fc8d28820387ac7f4aaf36d69e434adc1/greetings.json")
}
}
#Serializable
data class JsonMessage(val string: String?, val lang: String?)
object KtorClient {
private val json = Json {
encodeDefaults = true
ignoreUnknownKeys = true
}
val httpClient = HttpClient {
install(JsonFeature) { serializer = KotlinxSerializer(json) }
install(HttpTimeout) {
socketTimeoutMillis = 30_000
requestTimeoutMillis = 30_000
connectTimeoutMillis = 30_000
}
defaultRequest {
contentType(ContentType.Application.Json)
accept(ContentType.Application.Json)
}
}
}
And the iOS Code is
#main
struct iOSApp: App {
let sdk = MovieSDK(databaseDriverFactory: DatabaseDriverFactory())
var body: some Scene {
WindowGroup {
ContentView(viewModel: .init(sdk: sdk))
}
}
}
struct RocketLaunchRow: View {
var jsonMessage: JsonMessage
var body: some View {
HStack() {
VStack(alignment: .leading, spacing: 10.0) {
Text("Launch name: \(jsonMessage)")
}
Spacer()
}
}
}
struct ContentView: View {
#ObservedObject private(set) var viewModel: ViewModel
enum LoadableLaunches {
case loading
case result([JsonMessage])
case error(String)
}
var body: some View {
NavigationView {
listView()
.navigationBarTitle("SpaceX Launches")
.navigationBarItems(trailing:
Button("Reload") {
self.viewModel.loadLaunches()
}
)
}
}
private func listView() -> AnyView {
switch viewModel.launches {
case .loading:
return AnyView(Text("Loading...").multilineTextAlignment(.center))
case .result(_):
return AnyView(
Text("Succes")
)
case .error(let description):
return AnyView(Text(description).multilineTextAlignment(.center))
}
}
class ViewModel: ObservableObject {
let sdk: MovieSDK
#Published var launches = LoadableLaunches.loading
init(sdk: MovieSDK) {
self.sdk = sdk
self.loadLaunches()
}
func loadLaunches() {
self.launches = .loading
sdk.getLatestMovies(completionHandler: { launches, error in
if let launches = launches {
self.launches = .result(launches)
} else {
self.launches = .error(error?.localizedDescription ?? "error")
}
}
)
}
}
}
The exception I'm getting is:
What I'm doing wrong?, I've tried several approaches with the same result ( event with different urls )

How to use refershToken in ktor

Hey I am working in kotlin multiplatform moblie. I want to ask that if api returns 401 status, I want to call refresh api. I am reading the doc to configure ktor but unable to understand this. Can someone guide me on this. I tried some code in my side, Can some one guide me any proper example how to achieve in my solution.
commonMain
expect fun httpClient(config: HttpClientConfig<*>.() -> Unit = {}): HttpClient
iOSMain
actual class Platform actual constructor() {
actual val versionCode =
platform.Foundation.NSBundle.mainBundle.infoDictionary?.get("CFBundleVersion").toString()
actual val accessToken = ""
}
androidMain
actual fun httpClient(config: HttpClientConfig<*>.() -> Unit) = HttpClient(OkHttp) {
config(this)
install(ContentNegotiation) {
json(Json {
prettyPrint = true
ignoreUnknownKeys = true
explicitNulls = false
})
}
engine {
config {
retryOnConnectionFailure(true)
connectTimeout(30, TimeUnit.SECONDS)
readTimeout(40, TimeUnit.SECONDS)
}
}
defaultRequest {
header("Client-Version", Platform().versionCode)
}
HttpResponseValidator {
validateResponse { response ->
when (response.status.value) {
401 -> {
}
}
}
}
install(Auth) {
bearer {
loadTokens {
BearerTokens(tokenProvider.accessToken, "")
}
}
}
}
Platform.kt
package com.example.kotlinmultiplatformsharedmodule
lateinit var provider: VersionAndroidProvider
lateinit var tokenProvider: AndroidToken
actual class Platform actual constructor() {
actual val versionCode get() = provider.version
actual val accessToken: String
get() = tokenProvider.accessToken
}
interface VersionAndroidProvider {
val version: String
}
interface AndroidToken {
val accessToken: String
}
I need to call api, if api returns 401 status, I need to call refershToken api. After getting new accessToken from refreshToken api, I need to send this to api call.
If refreshToken is giving 401 then I need to infrom my application to logout.
If you use the Bearer provider in Ktor’s Authentication plugin then the refreshTokens lambda will be called when a server returns 401. For more information read the documentation. Here is an incomplete example for your use case:
val client = HttpClient(Apache) {
install(Auth) {
bearer {
loadTokens {
BearerTokens("initial_access_token", "initial_refresh_token")
}
refreshTokens {
val response = client.get("https://example.com/get_token")
if (response.status == HttpStatusCode.Unauthorized) {
// logout
null
} else {
// get token from a response
BearerTokens("new_access_token", "new_refresh_token")
}
}
}
}
}

Unresolved reference trying to build .aar with kotlin mpp

I am trying to get https://github.com/Kotlin/kotlinx.serialization to work with my android library I am building using kotlin mpp. This is my gradle file
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.multiplatform")
kotlin("plugin.serialization") version "1.3.61"
}
group = "org.greeting"
version = 1.0
android {
compileSdkVersion(27)
defaultConfig {
minSdkVersion(15)
}
buildTypes {
getByName("release") {
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
}
dependencies {
// Specify Kotlin/JVM stdlib dependency.
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7")
implementation(kotlin("stdlib", org.jetbrains.kotlin.config.KotlinCompilerVersion.VERSION)) // or "stdlib-jdk8"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0") // JVM dependency
testImplementation("junit:junit:4.12")
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
androidTestImplementation("junit:junit:4.12")
androidTestImplementation("org.jetbrains.kotlin:kotlin-test")
androidTestImplementation("org.jetbrains.kotlin:kotlin-test-junit")
}
kotlin {
android("androidLib")
val buildForDevice = project.findProperty("device") as? Boolean ?: false
val iosTarget = if (buildForDevice) iosArm64("ios") else iosX64("ios")
iosTarget.binaries {
framework {
// Disable bitcode embedding for the simulator build.
if (!buildForDevice) {
embedBitcode("disable")
}
}
}
sourceSets["androidLibMain"].dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0")
}
sourceSets {
commonMain {
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-common")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:0.14.0")
}
}
commonTest {
dependencies {
implementation("org.jetbrains.kotlin:kotlin-test-common")
implementation("org.jetbrains.kotlin:kotlin-test-annotations-common")
}
}
}
}
tasks.register("copyFramework") {
val buildType = project.findProperty("kotlin.build.type") as? String ?: "DEBUG"
dependsOn("link${buildType.toLowerCase().capitalize()}FrameworkIos")
doLast {
val srcFile = (kotlin.targets["ios"] as KotlinNativeTarget).binaries.getFramework(buildType).outputFile
val targetDir = project.property("configuration.build.dir")!!
copy {
from(srcFile.parent)
into(targetDir)
include("greeting.framework/**")
include("greeting.framework.dSYM")
}
}
}
tasks.register("iosTest") {
val device = project.findProperty("iosDevice") as? String ?: "iPhone 8"
dependsOn("linkDebugTestIos")
group = JavaBasePlugin.VERIFICATION_GROUP
description = "Runs tests for target 'ios' on an iOS simulator"
doLast {
val binary = (kotlin.targets["ios"] as KotlinNativeTarget).binaries.getTest("DEBUG").outputFile
exec {
commandLine("xcrun", "simctl", "spawn", "--standalone", device, binary.absolutePath)
}
}
}
When I try to run build i get Unresolved reference: serialization on line import kotlinx.serialization.ImplicitReflectionSerializer; But if compile the androidTests the linking works fine. Strangely i have to do sourceSets["androidLibMain"] and can't include it in the sourceSet{} block not sure if this is related. Any ideas on why the build task is not linking my serialization library? Thanks

Categories

Resources