Description
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:
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 ofInMemoryReactiveOAuth2AuthorizedClientService
, but with the following modification toloadAuthorizedClient
:
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: