Skip to content

InMemory[Reactive]OAuth2AuthorizedClientService does not support changes to the ClientRegistration at runtime #15511

Closed
@kzander91

Description

@kzander91

Describe the bug
We're calling an API that requires JWTs for authentication. The JWTs are obtained from an authorization server using Client Credentials Grant.
The authorization server requires us to periodically rotate the client secret. We store the secret in a separate configuration service (where we can update it out-of-band) and we have implemented a custom ReactiveClientRegistrationRepository that retrieves the secret from there.

This setup works fine until the secret is changed: We noticed that our app keeps using the previous secret, even though our ReactiveClientRegistrationRepository is returning the new secret.

We have tracked this down to InMemoryReactiveOAuth2AuthorizedClientService#loadAuthorizedClient which internally uses our repository implementation:

public <T extends OAuth2AuthorizedClient> Mono<T> loadAuthorizedClient(String clientRegistrationId,
String principalName) {
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
Assert.hasText(principalName, "principalName cannot be empty");
return (Mono<T>) this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId)
.map((clientRegistration) -> new OAuth2AuthorizedClientId(clientRegistrationId, principalName))
.flatMap((identifier) -> Mono.justOrEmpty(this.authorizedClients.get(identifier)));
}

Note how the ClientRegistration retrieved from our clientRegistrationRepository (containing the current secret) is effectively ignored in favor of the ClientRegistration cached in the authorizedClients map (as a member of OAuth2AuthorizedClient). This means that the client secret that was current at the time of the first token request is used forever, even if the ClientRegistration changes afterwards.
After we rotated the secret, the authorization server rejects the previous one, resulting in all token requests to fail shortly after a secret rotation.

Expected behavior
The InMemoryReactiveOAuth2AuthorizedClientService should consider the fact that the ClientRegistration may change at runtime and thus not ignore it after fetching it from the registration repository.

Workaround

  • First quick workaround was to reboot our app after every secret rotation.
  • Next, we implemented our own ReactiveOAuth2AuthorizedClientService that is basically a copy of InMemoryReactiveOAuth2AuthorizedClientService, but with the following modification to loadAuthorizedClient:
return this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId)
  .mapNotNull(clientRegistration -> {
    OAuth2AuthorizedClientId id = new OAuth2AuthorizedClientId(clientRegistrationId, principalName);
    OAuth2AuthorizedClient cachedAuthorizedClient = this.authorizedClients.get(id);
    if (cachedAuthorizedClient == null) {
      return null;
    }
    // Use current registration here  vvvvvvvvvvvvvvvvvv
    return new OAuth2AuthorizedClient(clientRegistration, cachedAuthorizedClient.getPrincipalName(), cachedAuthorizedClient.getAccessToken(), cachedAuthorizedClient.getRefreshToken());
  });

Note that the non-reactive version has the same behaviour, so this isn't specific to the reactive implementation.


Is there a specific reason for why the service behaves that way? What would be the recommended way to implement an OAuth2 client with secrets that can change at runtime?

The R2DBC implementation for example does behave as expected and always returns the ClientRegistration from the repository, see here:

private Mono<OAuth2AuthorizedClient> getAuthorizedClient(OAuth2AuthorizedClientHolder authorizedClientHolder) {
return this.clientRegistrationRepository.findByRegistrationId(authorizedClientHolder.getClientRegistrationId())
.switchIfEmpty(Mono.error(dataRetrievalFailureException(authorizedClientHolder.getClientRegistrationId())))
.map((clientRegistration) -> new OAuth2AuthorizedClient(clientRegistration,
authorizedClientHolder.getPrincipalName(), authorizedClientHolder.getAccessToken(),
authorizedClientHolder.getRefreshToken()));
}

Metadata

Metadata

Assignees

Labels

in: oauth2An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose)status: duplicateA duplicate of another issuetype: bugA general bug

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions