Expected Behavior Ideally there would be a programatic way to (manually) refresh an OAuth2 access token using its corresponding refresh token.
Current Behavior
This behaviour seems to be present in ServletOAuth2AuthorizedClientExchangeFilterFunction and in OAuth2AuthorizedClientArgumentResolver, with the help of an OAuth2AuthorizedClientManager. However, I've found this through inspection of the code and several issues posted both here and on stack overflow. And in these resources the code is not reusable from the outside. Moreover, it seems to be a common issue and is not yet covered in any tutorial/docs:
- https://stackoverflow.com/questions/54165375/refresh-oauth2-access-token-manually
- https://stackoverflow.com/questions/51790152/spring-security-with-oidc-refresh-the-tokens
- https://stackoverflow.com/questions/52533221/how-to-refresh-oauth2-token-with-spring-security-5-oauth2-client-and-resttemplat
- https://stackoverflow.com/questions/59144160/spring-oauth2-client-automatically-refresh-expired-access-token (contains a lot of code, hopefully there's a more elegant way)
Context
In our case we have multiple authentication mechanisms, one if which is against an external OIDC-compliant identity provider. We do that manually, by using OAuth2LoginAuthenticationFilter and OidcAuthorizationCodeAuthenticationProvider. Ultimately we get OAuth2AuthenticationToken, but before that we can obtain to the access/refresh tokens from the OAuth2LoginAuthenticationToken. Now the question is, when we implement the refresh endpoint, how can we use the refresh token to try to renew the access token (and store it again for further refreshes)?
Not aware of any workarounds besides the one posted in the last SO post that I linked to (but it seems untested and we haven't done that ourselves as well).
Comment From: jgrandja
@milanov The reference doc shows how to configure the refresh_token grant with the OAuth2AuthorizedClientProvider.
Now the question is, when we implement the refresh endpoint, how can we use the refresh token to try to renew the access token (and store it again for further refreshes)
This is automatically handled by DefaultOAuth2AuthorizedClientManager and the configured RefreshTokenOAuth2AuthorizedClientProvider.
Does this help?
Comment From: milanov
It really does, thanks for the quick and to-the-point response. From my point of view this issue can be closed.
One more quick question, if I may. Why is it necessary to pass the access token when constructing the OAuth2RefreshTokenGrantRequest? As far as I've read, only the refresh token is needed when the call to the refresh "endpoint" is made. I also tried to construct the request with a stubbed new OAuth2AccessToken() token and it still works.
Comment From: jgrandja
@milanov
Why is it necessary to pass the access token when constructing the
OAuth2RefreshTokenGrantRequest
There are scenarios where the refresh_token grant needs to be customized depending on the provider being used. The OAuth2RefreshTokenGrantRequest supplies ClientRegistration, OAuth2AccessToken and OAuth2RefreshToken so all related objects are available to implement whatever customization is necessary.
I'm glad you got it working. I'll close this issue.
Comment From: anthonynovatsis
@milanov Came across this issue and had read the same SO posts - above is very helpful. Thanks!
In our case we obtain and securely store a refresh token (after user authentication). We start our service, where can we provide our refresh token? You mention constructing OAuth2RefreshTokenGrantRequest but where is it constructed?
Thanks in advance!
Comment From: hannah23280
@milanov The reference doc shows how to configure the
refresh_tokengrant with theOAuth2AuthorizedClientProvider.Now the question is, when we implement the refresh endpoint, how can we use the refresh token to try to renew the access token (and store it again for further refreshes)
This is automatically handled by
DefaultOAuth2AuthorizedClientManagerand the configuredRefreshTokenOAuth2AuthorizedClientProvider.Does this help?
Does this mean we developer do not have to do configure anything to make the backend application send refresh token whenever access token is expired? I am still wondering how can we check (e.g via log) that the spring security is really sending refresh token behind the scene?
Comment From: rakheen-dama
You can create a wrapper class for the OAuth2AuthorizedClientManager and ensure the expired token is removed before it is used
@RequiredArgsConstructor
public class OAuth2ClientManagerWrapper implements OAuth2AuthorizedClientManager {
private final OAuth2AuthorizedClientManager manager;
private final OAuth2AuthorizedClientService clientService;
@Override
public OAuth2AuthorizedClient authorize(final OAuth2AuthorizeRequest authorizeRequest) {
if (authorizeRequest.getAuthorizedClient() != null &&
Objects.requireNonNull(authorizeRequest.getAuthorizedClient().getRefreshToken()).getExpiresAt()
.isBefore(Instant.now().plus(1, ChronoUnit.MINUTES))) {
String registrationId = authorizeRequest.getAuthorizedClient().getClientRegistration().getRegistrationId();
clientService.removeAuthorizedClient(registrationId, authorizeRequest.getPrincipal().getName());
}
return manager.authorize(authorizeRequest);
}
}
``` @Bean public OAuth2AuthorizedClientManager authorizedClientManager( final ClientRegistrationRepository clientRegistrationRepository, final OAuth2AuthorizedClientService oAuth2AuthorizedClientService) { final String username = env.getProperty("spring.security.oauth2.client.registration.foo.username"); final String password = env.getProperty("spring.security.oauth2.client.registration.foo.password");
var provider = OAuth2AuthorizedClientProviderBuilder.builder()
.password()
.refreshToken(rt -> rt.clockSkew(Duration.ofMinutes(1)).build())
.build();
var authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, oAuth2AuthorizedClientService);
var authManager = new OAuth2ClientManagerWrapper(authorizedClientManager, oAuth2AuthorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(provider);
authorizedClientManager.setContextAttributesMapper(
c -> Map.of(
OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, Objects.requireNonNull(username),
OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, Objects.requireNonNull(password))
);
return authManager;
}
```
Comment From: benallard
I can't find a way to force a refresh, even if the access_token is still valid. Is that something to be prevented at all cost? Any pointer where I could inject my customization to allow that? any documentation on that subject?
Comment From: dtrunk90
@benallard hasTokenExpired is done here. It has a skew of hardcoded 60s. So, there's currently no real way of forcing a refresh. The only workaround I can think of is extending DefaultOAuth2AuthorizedClientManager + RefreshTokenOAuth2AuthorizedClientProvider#authorize.