Skip to content

OAuth2AccessTokenResponse should allow to customize behavior in absence of "expires_in" parameter #8701

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
benba opened this issue Jun 17, 2020 · 5 comments
Assignees
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: invalid An issue that we don't feel is valid

Comments

@benba
Copy link
Contributor

benba commented Jun 17, 2020

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.

@benba benba added status: waiting-for-triage An issue we've not yet triaged type: enhancement A general enhancement labels Jun 17, 2020
@jgrandja
Copy link
Contributor

@benba Customizing OAuth2AccessTokenResponse.expiresIn() is already possible via OAuth2AccessTokenResponseHttpMessageConverter.

See the ref doc for configuration details.

@jgrandja jgrandja added in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: invalid An issue that we don't feel is valid and removed status: waiting-for-triage An issue we've not yet triaged type: enhancement A general enhancement labels Jun 22, 2020
@benba
Copy link
Contributor Author

benba commented Jun 22, 2020

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.

@jgrandja
Copy link
Contributor

jgrandja commented Jun 25, 2020

@benba

Unless Gitlab OIDC usage of expires_in is 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.

@benba
Copy link
Contributor Author

benba commented Jun 26, 2020

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

@beccagaspard
Copy link

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();
    }

}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: invalid An issue that we don't feel is valid
Projects
None yet
Development

No branches or pull requests

3 participants