Expected Behavior
From the spec quoted by OAuth2AccessTokenResponse (https://tools.ietf.org/html/rfc6749#section-5.1) the default value could differ depending on the AS.
expires_in RECOMMENDED. The lifetime in seconds of the access token. For example, the value "3600" denotes that the access token will expire in one hour from the time the response was generated. If omitted, the authorization server SHOULD provide the expiration time via other means or document the default value.
It would be nice if it was possible to describe the default value (or "expires_in":null) meaning through the config.
Current Behavior
OAuth2AccessTokenResponse assumes that if there is no expires_in or "expires_in":null the token will expire one second later.
Context I'm using Gitlab as an OIDC provider. Gitlab does not specify an expiration for access token (https://gitlab.com/gitlab-org/gitlab/-/issues/21745), and return
"expires_in": null,
in the token response. The access token is then always refreshed, while the access token is still valid.
Comment From: jgrandja
@benba Customizing OAuth2AccessTokenResponse.expiresIn() is already possible via OAuth2AccessTokenResponseHttpMessageConverter.
See the ref doc for configuration details.
Comment From: benba
Thanks for your answer @jgrandja
Actually that's what i did until now, but it's very hard to do for changing that behavior.
Unless Gitlab OIDC usage of expires_in is an isolated/rare case, it would have been nice to have a simpler way to do it.
For my use case I use a endpoint with an @RegisteredOAuth2AuthorizedClient parameter.
I want both the auth code flow response access token and the one that can come from the refresh token to have the same customized handling of expires_in (let's say 1 day for this example).
So I first need to configure the auth code response customization:
// For the AC flow
.oauth2Login()
.loginPage("/oauth2/authorization/gitlab")
.tokenEndpoint(c -> c.accessTokenResponseClient(authCodeCustomGitLabExpiresInAccessTokenResponseClientFor()));
Then assuming #8700 is fixed I would need to configure the authorizedClientManager for the refresh token customization through @RegisteredOAuth2AuthorizedClient:
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken(configurer -> configurer.accessTokenResponseClient(refreshCustomGitLabExpiresInAccessTokenResponseClient()))
.clientCredentials()
.password()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
And finally I need all this boilerplate
@Bean
public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authCodeCustomGitLabExpiresInAccessTokenResponseClientFor() {
DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient =
new DefaultAuthorizationCodeTokenResponseClient();
accessTokenResponseClient.setRestOperations(customGitLabExpiresInRestTemplate());
return accessTokenResponseClient;
}
@Bean
public OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshCustomGitLabExpiresInAccessTokenResponseClient() {
DefaultRefreshTokenTokenResponseClient refreshTokenTokenResponseClient =
new DefaultRefreshTokenTokenResponseClient();
refreshTokenTokenResponseClient.setRestOperations(customGitLabExpiresInRestTemplate());
return refreshTokenTokenResponseClient;
}
private static RestTemplate customGitLabExpiresInRestTemplate() {
OAuth2AccessTokenResponseHttpMessageConverter tokenResponseHttpMessageConverter =
new OAuth2AccessTokenResponseHttpMessageConverter();
tokenResponseHttpMessageConverter.setTokenResponseConverter(customGitLabExpiresInTokenResponseConverter());
RestTemplate restTemplate = new RestTemplate(Arrays.asList(
new FormHttpMessageConverter(), tokenResponseHttpMessageConverter));
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
return restTemplate;
}
private static Converter<Map<String, String>, OAuth2AccessTokenResponse> customGitLabExpiresInTokenResponseConverter() {
MapOAuth2AccessTokenResponseConverter mapOAuth2AccessTokenResponseConverter = new MapOAuth2AccessTokenResponseConverter();
return tokenResponseParameters -> {
OAuth2AccessTokenResponse original = mapOAuth2AccessTokenResponseConverter.convert(tokenResponseParameters);
String expiresIn = tokenResponseParameters.get(OAuth2ParameterNames.EXPIRES_IN);
if (expiresIn != null) {
return original;
}
OAuth2AccessToken accessToken = original.getAccessToken();
return OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
.tokenType(accessToken.getTokenType())
.scopes(accessToken.getScopes())
.expiresIn(Duration.ofDays(1).toSeconds())
.refreshToken(original.getRefreshToken().getTokenValue())
.additionalParameters(original.getAdditionalParameters())
.build();
};
}
Maybe my conf is not optimal but it took me a hard time to get this right (at least I hope it is), even if all the individual pieces are described in the documentation.
Comment From: jgrandja
@benba
Unless Gitlab OIDC usage of
expires_inis an isolated/rare case
It is a rare case. Most providers I've seen thus far provide the expires_in parameter.
I reviewed your config and this is exactly how you would go about customizing the token response. Looks good to me.
Comment From: benba
I reviewed your config and this is exactly how you would go about customizing the token response. Looks good to me.
@jgrandja Thank you for your feedback
Comment From: beccagaspard
I know this is a pretty old thread, but recently came across this issue due to Salesforce API not providing the expires_in parameter. I was able to implement a slightly simpler solution based on this stackoverflow answer and wanted to share here in case it helps someone else - or if there is an even better approach, please let me know! 🤓
@Bean
OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository registrationRepository, OAuth2AuthorizedClientService clientService, SalesforceClientCredentialsTokenResponseClient tokenResponseClient) {
OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials(configurer -> configurer.accessTokenResponseClient(tokenResponseClient))
.build();
AuthorizedClientServiceOAuth2AuthorizedClientManager manager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(registrationRepository, clientService);
manager.setAuthorizedClientProvider(authorizedClientProvider);
return manager;
}
@Component
public class SalesforceClientCredentialsTokenResponseClient implements OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {
private final DefaultClientCredentialsTokenResponseClient delegate = new DefaultClientCredentialsTokenResponseClient();
@Override
public OAuth2AccessTokenResponse getTokenResponse(OAuth2ClientCredentialsGrantRequest authorizationGrantRequest) {
OAuth2AccessTokenResponse response = delegate.getTokenResponse(authorizationGrantRequest);
return OAuth2AccessTokenResponse.withResponse(response)
.expiresIn(Duration.ofHours(2).toSeconds()) // or whatever your session duration should be
.build();
}
}