Skip to content

Commit 4d86995

Browse files
committed
KTOR-6569 Add option for Bearer auth to not cache client token
1 parent 8381289 commit 4d86995

File tree

3 files changed

+290
-17
lines changed

3 files changed

+290
-17
lines changed

ktor-client/ktor-client-plugins/ktor-client-auth/api/ktor-client-auth.api

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,18 +68,20 @@ public final class io/ktor/client/plugins/auth/providers/BasicAuthProviderKt {
6868

6969
public final class io/ktor/client/plugins/auth/providers/BearerAuthConfig {
7070
public fun <init> ()V
71+
public final fun getCacheTokens ()Z
7172
public final fun getRealm ()Ljava/lang/String;
7273
public final fun loadTokens (Lkotlin/jvm/functions/Function1;)V
7374
public final fun refreshTokens (Lkotlin/jvm/functions/Function2;)V
7475
public final fun sendWithoutRequest (Lkotlin/jvm/functions/Function1;)V
76+
public final fun setCacheTokens (Z)V
7577
public final fun setRealm (Ljava/lang/String;)V
7678
}
7779

7880
public final class io/ktor/client/plugins/auth/providers/BearerAuthProvider : io/ktor/client/plugins/auth/AuthProvider {
79-
public fun <init> (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Ljava/lang/String;)V
80-
public synthetic fun <init> (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
81+
public fun <init> (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Z)V
82+
public synthetic fun <init> (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
8183
public fun addRequestHeaders (Lio/ktor/client/request/HttpRequestBuilder;Lio/ktor/http/auth/HttpAuthHeader;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
82-
public final fun clearToken ()V
84+
public final fun clearToken (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
8385
public fun getSendWithoutRequest ()Z
8486
public fun isApplicable (Lio/ktor/http/auth/HttpAuthHeader;)Z
8587
public fun refreshToken (Lio/ktor/client/statement/HttpResponse;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;

ktor-client/ktor-client-plugins/ktor-client-auth/common/src/io/ktor/client/plugins/auth/providers/BearerAuthProvider.kt

Lines changed: 84 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import io.ktor.client.statement.*
1111
import io.ktor.http.*
1212
import io.ktor.http.auth.*
1313
import io.ktor.utils.io.*
14+
import kotlinx.coroutines.sync.Mutex
15+
import kotlinx.coroutines.sync.withLock
1416

1517
/**
1618
* Installs the client's [BearerAuthProvider].
@@ -19,7 +21,7 @@ import io.ktor.utils.io.*
1921
*/
2022
public fun AuthConfig.bearer(block: BearerAuthConfig.() -> Unit) {
2123
with(BearerAuthConfig().apply(block)) {
22-
this@bearer.providers.add(BearerAuthProvider(refreshTokens, loadTokens, sendWithoutRequest, realm))
24+
this@bearer.providers.add(BearerAuthProvider(refreshTokens, loadTokens, sendWithoutRequest, realm, cacheTokens))
2325
}
2426
}
2527

@@ -62,6 +64,13 @@ public class BearerAuthConfig {
6264

6365
public var realm: String? = null
6466

67+
/**
68+
* Controls whether to cache tokens between requests.
69+
* When set to false, the provider will call [loadTokens] for each request.
70+
* Default value is true.
71+
*/
72+
public var cacheTokens: Boolean = true
73+
6574
/**
6675
* Configures a callback that refreshes a token when the 401 status code is received.
6776
*
@@ -97,23 +106,34 @@ public class BearerAuthConfig {
97106
* As an example, these tokens can be used as a part of OAuth flow to authorize users of your application
98107
* by using external providers, such as Google, Facebook, Twitter, and so on.
99108
*
109+
* You can control whether tokens are cached between requests with the [cacheTokens] parameter:
110+
* - When `true` (default), tokens are cached after the first request and reused.
111+
* - When `false`, [loadTokens] is called for each request, and the token is never cached.
112+
*
100113
* You can learn more from [Bearer authentication](https://ktor.io/docs/bearer-client.html).
101114
*
102115
* [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.client.plugins.auth.providers.BearerAuthProvider)
103116
*/
104117
public class BearerAuthProvider(
105118
private val refreshTokens: suspend RefreshTokensParams.() -> BearerTokens?,
106-
loadTokens: suspend () -> BearerTokens?,
119+
private val loadTokensCallback: suspend () -> BearerTokens?,
107120
private val sendWithoutRequestCallback: (HttpRequestBuilder) -> Boolean = { true },
108-
private val realm: String?
121+
private val realm: String?,
122+
private val cacheTokens: Boolean = true
109123
) : AuthProvider {
110124

111125
@Suppress("OverridingDeprecatedMember")
112126
@Deprecated("Please use sendWithoutRequest function instead", level = DeprecationLevel.ERROR)
113127
override val sendWithoutRequest: Boolean
114128
get() = error("Deprecated")
115129

116-
private val tokensHolder = AuthTokenHolder(loadTokens)
130+
// Only create the tokens holder if caching is enabled
131+
private val tokensHolder = if (cacheTokens) AuthTokenHolder(loadTokensCallback) else null
132+
133+
// When caching is disabled, we still need to store the current refreshed token
134+
// so it can be used in the retry mechanism during the current request cycle
135+
private val currentRefreshTokenMutex = Mutex()
136+
private var currentRefreshedToken: BearerTokens? = null
117137

118138
override fun sendWithoutRequest(request: HttpRequestBuilder): Boolean = sendWithoutRequestCallback(request)
119139

@@ -144,24 +164,65 @@ public class BearerAuthProvider(
144164
* [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.client.plugins.auth.providers.BearerAuthProvider.addRequestHeaders)
145165
*/
146166
override suspend fun addRequestHeaders(request: HttpRequestBuilder, authHeader: HttpAuthHeader?) {
147-
val token = tokensHolder.loadToken() ?: return
167+
// If the request has the circuit breaker attribute, don't add any auth headers
168+
if (request.attributes.contains(AuthCircuitBreaker)) {
169+
LOGGER.trace("Circuit breaker active - no auth header will be added")
170+
return
171+
}
172+
173+
// Get the appropriate token based on caching settings
174+
val token = currentRefreshTokenMutex.withLock {
175+
if (currentRefreshedToken != null) {
176+
// If we have a refreshed token for the current retry cycle, use that
177+
LOGGER.trace("Using refreshed token for request: ${currentRefreshedToken!!.accessToken}")
178+
return@withLock currentRefreshedToken
179+
} else if (cacheTokens) {
180+
// If caching is enabled, use tokensHolder to cache between requests
181+
return@withLock tokensHolder!!.loadToken()
182+
} else {
183+
// If caching is disabled, load a fresh token
184+
val freshToken = loadTokensCallback()
185+
LOGGER.trace("Using fresh token for request: ${freshToken?.accessToken}")
186+
return@withLock freshToken
187+
}
188+
} ?: return
148189

149190
request.headers {
150191
val tokenValue = "Bearer ${token.accessToken}"
151192
if (contains(HttpHeaders.Authorization)) {
152193
remove(HttpHeaders.Authorization)
153194
}
154-
if (request.attributes.contains(AuthCircuitBreaker).not()) {
155-
append(HttpHeaders.Authorization, tokenValue)
156-
}
195+
append(HttpHeaders.Authorization, tokenValue)
157196
}
158197
}
159198

160199
public override suspend fun refreshToken(response: HttpResponse): Boolean {
161-
val newToken = tokensHolder.setToken {
162-
refreshTokens(RefreshTokensParams(response.call.client, response, tokensHolder.loadToken()))
200+
return if (cacheTokens) {
201+
// With caching enabled, use the token holder for persistent caching
202+
val newToken = tokensHolder!!.setToken {
203+
refreshTokens(RefreshTokensParams(response.call.client, response, tokensHolder.loadToken()))
204+
}
205+
newToken != null
206+
} else {
207+
// Thread-safe access to currentRefreshedToken
208+
currentRefreshTokenMutex.withLock {
209+
// Get the current token (used as oldTokens in RefreshTokensParams)
210+
val currentToken = loadTokensCallback()
211+
212+
// Get the new token from the refresh function
213+
val newToken = refreshTokens(RefreshTokensParams(response.call.client, response, currentToken))
214+
215+
// Store the refreshed token for use in the retry process
216+
if (newToken != null) {
217+
LOGGER.trace("Setting refreshed token: ${newToken.accessToken}")
218+
currentRefreshedToken = newToken
219+
true
220+
} else {
221+
LOGGER.trace("No refreshed token returned")
222+
false
223+
}
224+
}
163225
}
164-
return newToken != null
165226
}
166227

167228
/**
@@ -171,13 +232,22 @@ public class BearerAuthProvider(
171232
* - When access or refresh tokens have been updated externally
172233
* - When you want to clear sensitive token data (for example, during logout)
173234
*
174-
* Note: The result of `loadTokens` invocation is cached internally.
235+
* Note: The result of `loadTokens` invocation is cached internally when [cacheTokens] is true.
175236
* Calling this method will force the next authentication attempt to fetch fresh tokens
176237
* through the configured `loadTokens` function.
177238
*
239+
* If [cacheTokens] is false, this method will clear any temporarily stored refresh token.
240+
*
178241
* [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.client.plugins.auth.providers.BearerAuthProvider.clearToken)
179242
*/
180-
public fun clearToken() {
181-
tokensHolder.clearToken()
243+
public suspend fun clearToken() {
244+
if (cacheTokens) {
245+
tokensHolder!!.clearToken()
246+
} else {
247+
// Thread-safe access to clear any temporarily stored refreshed token
248+
currentRefreshTokenMutex.withLock {
249+
currentRefreshedToken = null
250+
}
251+
}
182252
}
183253
}

0 commit comments

Comments
 (0)