Skip to content

Commit 7fa43c8

Browse files
committed
Do not reuse the same refresh token multiple times
- The "add a UAA token to HTTP request headers" flow works like this: - User makes a request using AbstractReactorOperations -> Operator - This adds an "Authorization" header, using Provider#getToken mono ; if a value is cached it uses that, otherwise it uses whatever flow is available to request a token. - If the response is unauthorized, it means the access token is expired, and the Operator calls Provider#invalidate ; and then retries the request, which will trigger another #getToken call. - There was a race condition, when an access_token was cached and multiple request used it concurrently, they would all call AbstractUaaTokenProvider#invalidate, and all reuse the same refresh token. This is an issue when the UAA is configured with non-reusable refresh tokens (revocable + rotating or unique), only the first refresh token request succeeds, and all other refresh token requests fail. - This PR addresses this by ensuring that the cached refresh token is removed from the cache right before being used. Any other call to #invalidate will be a no-op. - This is NOT a perfect fix, and there are some smaller scale race conditions happening. For example, #invalidate calls refreshTokens.remove and accessTokens.put sequentially. It is possible that a concurrent request calls invalidate, finds the refreshTokens cache empty, and then will populate accessTokens through #getToken ; in that case there could be a race condition and two tokens fetched. - Re-architecting the whole token logic is too big of a lift for the project, so we accept that this solution is not perfect - as long as the issues are recoverable. - Fixes #1146 Signed-off-by: Daniel Garnier-Moiroux <[email protected]>
1 parent 6667c56 commit 7fa43c8

File tree

1 file changed

+38
-26
lines changed

1 file changed

+38
-26
lines changed

cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/tokenprovider/AbstractUaaTokenProvider.java

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public abstract class AbstractUaaTokenProvider implements TokenProvider {
8080
private final ConcurrentMap<ConnectionContext, RefreshToken> refreshTokenStreams =
8181
new ConcurrentHashMap<>(1);
8282

83-
private final ConcurrentMap<ConnectionContext, Mono<String>> refreshTokens =
83+
private final ConcurrentMap<ConnectionContext, String> refreshTokens =
8484
new ConcurrentHashMap<>(1);
8585

8686
/**
@@ -116,7 +116,10 @@ public final Mono<String> getToken(ConnectionContext connectionContext) {
116116

117117
@Override
118118
public void invalidate(ConnectionContext connectionContext) {
119-
this.accessTokens.put(connectionContext, token(connectionContext));
119+
String refreshToken = this.refreshTokens.remove(connectionContext);
120+
if (refreshToken != null) {
121+
this.accessTokens.put(connectionContext, token(connectionContext, refreshToken));
122+
}
120123
}
121124

122125
/**
@@ -133,6 +136,30 @@ public void invalidate(ConnectionContext connectionContext) {
133136
*/
134137
abstract void tokenRequestTransformer(HttpClientRequest request, HttpClientForm form);
135138

139+
private Mono<String> token(ConnectionContext connectionContext) {
140+
Mono<String> token =
141+
primaryToken(connectionContext)
142+
.doOnSubscribe(s -> LOGGER.debug("Negotiating using token provider"));
143+
144+
return cacheResult(connectionContext, token);
145+
}
146+
147+
private Mono<String> token(ConnectionContext connectionContext, String refreshToken) {
148+
Mono<String> token =
149+
refreshToken(connectionContext, refreshToken)
150+
.doOnSubscribe(s -> LOGGER.debug("Negotiating using refresh token"))
151+
// fall back to primary token in case the refresh_token grant fails
152+
// (expired, revoked, ...)
153+
.switchIfEmpty(
154+
primaryToken(connectionContext)
155+
.doOnSubscribe(
156+
s ->
157+
LOGGER.debug(
158+
"Falling back to token provider")));
159+
160+
return cacheResult(connectionContext, token);
161+
}
162+
136163
private static String extractAccessToken(Map<String, String> payload) {
137164
String accessToken = payload.get(ACCESS_TOKEN);
138165

@@ -227,8 +254,7 @@ private Consumer<Map<String, String>> extractRefreshToken(ConnectionContext conn
227254
});
228255
}
229256

230-
this.refreshTokens.put(
231-
connectionContext, Mono.just(refreshToken));
257+
this.refreshTokens.put(connectionContext, refreshToken);
232258
getRefreshTokenStream(connectionContext)
233259
.sink
234260
.emitNext(refreshToken, FAIL_FAST);
@@ -297,30 +323,16 @@ private void setAuthorization(HttpHeaders headers) {
297323
headers.set(AUTHORIZATION, String.format("Basic %s", encoded));
298324
}
299325

300-
private Mono<String> token(ConnectionContext connectionContext) {
301-
Mono<String> cached =
302-
this.refreshTokens
303-
.getOrDefault(connectionContext, Mono.empty())
304-
.flatMap(
305-
refreshToken ->
306-
refreshToken(connectionContext, refreshToken)
307-
.doOnSubscribe(
308-
s ->
309-
LOGGER.debug(
310-
"Negotiating using refresh"
311-
+ " token")))
312-
.switchIfEmpty(
313-
primaryToken(connectionContext)
314-
.doOnSubscribe(
315-
s ->
316-
LOGGER.debug(
317-
"Negotiating using token"
318-
+ " provider")));
319-
326+
/**
327+
* Cache the given mono. If {@link ConnectionContext#getCacheDuration()} is not null, use that
328+
* as the cache TTL. Otherwise, cache indefinitely.
329+
*/
330+
private static Mono<String> cacheResult(
331+
ConnectionContext connectionContext, Mono<String> token) {
320332
return connectionContext
321333
.getCacheDuration()
322-
.map(cached::cache)
323-
.orElseGet(cached::cache)
334+
.map(token::cache)
335+
.orElseGet(token::cache)
324336
.checkpoint();
325337
}
326338

0 commit comments

Comments
 (0)