@@ -11,6 +11,9 @@ import io.ktor.client.statement.*
11
11
import io.ktor.http.*
12
12
import io.ktor.http.auth.*
13
13
import io.ktor.utils.io.*
14
+ import kotlinx.coroutines.Job
15
+ import kotlinx.coroutines.sync.Mutex
16
+ import kotlinx.coroutines.sync.withLock
14
17
15
18
/* *
16
19
* Installs the client's [BearerAuthProvider].
@@ -19,7 +22,7 @@ import io.ktor.utils.io.*
19
22
*/
20
23
public fun AuthConfig.bearer (block : BearerAuthConfig .() -> Unit ) {
21
24
with (BearerAuthConfig ().apply (block)) {
22
- this @bearer.providers.add(BearerAuthProvider (refreshTokens, loadTokens, sendWithoutRequest, realm))
25
+ this @bearer.providers.add(BearerAuthProvider (refreshTokens, loadTokens, sendWithoutRequest, realm, cacheTokens ))
23
26
}
24
27
}
25
28
@@ -61,6 +64,13 @@ public class BearerAuthConfig {
61
64
internal var sendWithoutRequest: (HttpRequestBuilder ) -> Boolean = { true }
62
65
63
66
public var realm: String? = null
67
+
68
+ /* *
69
+ * Controls whether to cache tokens between requests.
70
+ * When set to false, the provider will call [loadTokens] for each request.
71
+ * Default value is true.
72
+ */
73
+ public var cacheTokens: Boolean = true
64
74
65
75
/* *
66
76
* Configures a callback that refreshes a token when the 401 status code is received.
@@ -97,23 +107,34 @@ public class BearerAuthConfig {
97
107
* As an example, these tokens can be used as a part of OAuth flow to authorize users of your application
98
108
* by using external providers, such as Google, Facebook, Twitter, and so on.
99
109
*
110
+ * You can control whether tokens are cached between requests with the [cacheTokens] parameter:
111
+ * - When `true` (default), tokens are cached after the first request and reused.
112
+ * - When `false`, [loadTokens] is called for each request, and the token is never cached.
113
+ *
100
114
* You can learn more from [Bearer authentication](https://ktor.io/docs/bearer-client.html).
101
115
*
102
116
* [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.client.plugins.auth.providers.BearerAuthProvider)
103
117
*/
104
118
public class BearerAuthProvider (
105
119
private val refreshTokens : suspend RefreshTokensParams .() -> BearerTokens ? ,
106
- loadTokens : suspend () -> BearerTokens ? ,
120
+ private val loadTokensCallback : suspend () -> BearerTokens ? ,
107
121
private val sendWithoutRequestCallback : (HttpRequestBuilder ) -> Boolean = { true },
108
- private val realm : String?
122
+ private val realm : String? ,
123
+ private val cacheTokens : Boolean = true
109
124
) : AuthProvider {
110
125
111
126
@Suppress(" OverridingDeprecatedMember" )
112
127
@Deprecated(" Please use sendWithoutRequest function instead" , level = DeprecationLevel .ERROR )
113
128
override val sendWithoutRequest: Boolean
114
129
get() = error(" Deprecated" )
115
130
116
- private val tokensHolder = AuthTokenHolder (loadTokens)
131
+ // Only create the tokens holder if caching is enabled
132
+ private val tokensHolder = if (cacheTokens) AuthTokenHolder (loadTokensCallback) else null
133
+
134
+ // When caching is disabled, we still need to store the current refreshed token
135
+ // so it can be used in the retry mechanism during the current request cycle
136
+ private val currentRefreshTokenMutex = Mutex ()
137
+ private var currentRefreshedToken: BearerTokens ? = null
117
138
118
139
override fun sendWithoutRequest (request : HttpRequestBuilder ): Boolean = sendWithoutRequestCallback(request)
119
140
@@ -144,24 +165,65 @@ public class BearerAuthProvider(
144
165
* [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.client.plugins.auth.providers.BearerAuthProvider.addRequestHeaders)
145
166
*/
146
167
override suspend fun addRequestHeaders (request : HttpRequestBuilder , authHeader : HttpAuthHeader ? ) {
147
- val token = tokensHolder.loadToken() ? : return
168
+ // If the request has the circuit breaker attribute, don't add any auth headers
169
+ if (request.attributes.contains(AuthCircuitBreaker )) {
170
+ LOGGER .trace(" Circuit breaker active - no auth header will be added" )
171
+ return
172
+ }
173
+
174
+ // Get the appropriate token based on caching settings
175
+ val token = currentRefreshTokenMutex.withLock {
176
+ if (currentRefreshedToken != null ) {
177
+ // If we have a refreshed token for the current retry cycle, use that
178
+ LOGGER .trace(" Using refreshed token for request: ${currentRefreshedToken!! .accessToken} " )
179
+ return @withLock currentRefreshedToken
180
+ } else if (cacheTokens) {
181
+ // If caching is enabled, use tokensHolder to cache between requests
182
+ return @withLock tokensHolder!! .loadToken()
183
+ } else {
184
+ // If caching is disabled, load a fresh token
185
+ val freshToken = loadTokensCallback()
186
+ LOGGER .trace(" Using fresh token for request: ${freshToken?.accessToken} " )
187
+ return @withLock freshToken
188
+ }
189
+ } ? : return
148
190
149
191
request.headers {
150
192
val tokenValue = " Bearer ${token.accessToken} "
151
193
if (contains(HttpHeaders .Authorization )) {
152
194
remove(HttpHeaders .Authorization )
153
195
}
154
- if (request.attributes.contains(AuthCircuitBreaker ).not ()) {
155
- append(HttpHeaders .Authorization , tokenValue)
156
- }
196
+ append(HttpHeaders .Authorization , tokenValue)
157
197
}
158
198
}
159
199
160
200
public override suspend fun refreshToken (response : HttpResponse ): Boolean {
161
- val newToken = tokensHolder.setToken {
162
- refreshTokens(RefreshTokensParams (response.call.client, response, tokensHolder.loadToken()))
201
+ return if (cacheTokens) {
202
+ // With caching enabled, use the token holder for persistent caching
203
+ val newToken = tokensHolder!! .setToken {
204
+ refreshTokens(RefreshTokensParams (response.call.client, response, tokensHolder.loadToken()))
205
+ }
206
+ newToken != null
207
+ } else {
208
+ // Thread-safe access to currentRefreshedToken
209
+ currentRefreshTokenMutex.withLock {
210
+ // Get the current token (used as oldTokens in RefreshTokensParams)
211
+ val currentToken = loadTokensCallback()
212
+
213
+ // Get the new token from the refresh function
214
+ val newToken = refreshTokens(RefreshTokensParams (response.call.client, response, currentToken))
215
+
216
+ // Store the refreshed token for use in the retry process
217
+ if (newToken != null ) {
218
+ LOGGER .trace(" Setting refreshed token: ${newToken.accessToken} " )
219
+ currentRefreshedToken = newToken
220
+ true
221
+ } else {
222
+ LOGGER .trace(" No refreshed token returned" )
223
+ false
224
+ }
225
+ }
163
226
}
164
- return newToken != null
165
227
}
166
228
167
229
/* *
@@ -171,13 +233,22 @@ public class BearerAuthProvider(
171
233
* - When access or refresh tokens have been updated externally
172
234
* - When you want to clear sensitive token data (for example, during logout)
173
235
*
174
- * Note: The result of `loadTokens` invocation is cached internally.
236
+ * Note: The result of `loadTokens` invocation is cached internally when [cacheTokens] is true .
175
237
* Calling this method will force the next authentication attempt to fetch fresh tokens
176
238
* through the configured `loadTokens` function.
177
239
*
240
+ * If [cacheTokens] is false, this method will clear any temporarily stored refresh token.
241
+ *
178
242
* [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.client.plugins.auth.providers.BearerAuthProvider.clearToken)
179
243
*/
180
- public fun clearToken () {
181
- tokensHolder.clearToken()
244
+ public suspend fun clearToken () {
245
+ if (cacheTokens) {
246
+ tokensHolder!! .clearToken()
247
+ } else {
248
+ // Thread-safe access to clear any temporarily stored refreshed token
249
+ currentRefreshTokenMutex.withLock {
250
+ currentRefreshedToken = null
251
+ }
252
+ }
182
253
}
183
254
}
0 commit comments