Skip to content

OAuth2: No new token is requested when only password grant is used #10056

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
geobreze opened this issue Jul 9, 2021 · 6 comments
Closed

OAuth2: No new token is requested when only password grant is used #10056

geobreze opened this issue Jul 9, 2021 · 6 comments
Assignees
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: declined A suggestion or change that we don't feel we should currently apply

Comments

@geobreze
Copy link

geobreze commented Jul 9, 2021

Describe the bug

When RefreshTokenOAuth2AuthorizedClientProvider is not registered, but PasswordOAuth2AuthorizedClientProvider does, next issue may happen. In case when accessToken expired, but there is refreshToken in the context, no authorization is performed.

This happens because of next block which assumes that RefreshTokenOAuth2AuthorizedClientProvider presents, which is not always true.

if (authorizedClient != null && hasTokenExpired(authorizedClient.getAccessToken())
&& authorizedClient.getRefreshToken() != null) {
// If client is already authorized and access token is expired and a refresh
// token is available, than return and allow
// RefreshTokenOAuth2AuthorizedClientProvider to handle the refresh
return null;
}

To Reproduce

  • When building provider viaOAuth2AuthorizedClientProviderBuilder, use password, but not refreshToken providers
  • Get an expired token or wait until token expires
  • Now all requests will have no authorized client

Expected behavior

Password provider should get new token in case when there is no RefreshTokenOAuth2AuthorizedClientProvider.

@geobreze geobreze added status: waiting-for-triage An issue we've not yet triaged type: bug A general bug labels Jul 9, 2021
@geobreze geobreze changed the title OAuth2: No new token OAuth2: No new token is requested when only password grant is used Jul 11, 2021
@sjohnr sjohnr added the in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) label Jul 12, 2021
@sjohnr sjohnr self-assigned this Jul 12, 2021
@sjohnr
Copy link
Contributor

sjohnr commented Jul 12, 2021

Hi @geobreze, thanks for the report. Perhaps I'm misunderstanding and you can help clarify: Based on the code snippet from PasswordOAuth2AuthorizedClientProvider, why would you have a refresh token but not have the refresh token provider enabled? Or said differently, why would authorizedClient.getRefreshToken() != null be true in your case?

@sjohnr sjohnr added status: waiting-for-feedback We need additional information before we can continue and removed status: waiting-for-triage An issue we've not yet triaged labels Jul 12, 2021
@geobreze
Copy link
Author

geobreze commented Jul 12, 2021

Hi @sjohnr, In our case, oauth2 backend had some issues with token refresh, but it was sending us both accessToken and refreshToken. So we temporary removed RefreshTokenOAuth2AuthorizedClientProvider from our configuration and after that this issue appeared.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Jul 12, 2021
@sjohnr
Copy link
Contributor

sjohnr commented Jul 12, 2021

Thanks for the clarification there, that clears it up for me. Unfortunately, I don't see an easy way for the framework to solve your case while still providing the delegation-based strategy (loosely coupled) between different providers. Since yours is more of an edge case, I would suggest removing the refreshToken from the response. This can be accomplished with something like the following configuration:

    @Bean
    public OAuth2AuthorizedClientManager authorizedClientManager(
            ClientRegistrationRepository clientRegistrationRepository,
            OAuth2AuthorizedClientRepository authorizedClientRepository) {
        OAuth2AuthorizedClientProvider authorizedClientProvider =
            OAuth2AuthorizedClientProviderBuilder.builder()
                .password((builder) -> builder
                    .accessTokenResponseClient(createPasswordAccessTokenResponseClient())
                    .build())
                .build();

        DefaultOAuth2AuthorizedClientManager authorizedClientManager =
            new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        return authorizedClientManager;
    }

    private OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> createPasswordAccessTokenResponseClient() {
        // Deprecated in 5.6. See note.
        MapOAuth2AccessTokenResponseConverter defaultAccessTokenResponseConverter =
            new MapOAuth2AccessTokenResponseConverter();

        Converter<Map<String, String>, OAuth2AccessTokenResponse> noRefreshTokenConverter =
            defaultAccessTokenResponseConverter
                .andThen((accessTokenResponse) ->
                    OAuth2AccessTokenResponse.withResponse(accessTokenResponse)
                        .refreshToken(null)
                        .build());

        OAuth2AccessTokenResponseHttpMessageConverter accessTokenResponseHttpMessageConverter =
            new OAuth2AccessTokenResponseHttpMessageConverter();
        accessTokenResponseHttpMessageConverter.setTokenResponseConverter(noRefreshTokenConverter);

        RestTemplate restTemplate = new RestTemplate(
            Arrays.asList(new FormHttpMessageConverter(), accessTokenResponseHttpMessageConverter));
        restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());

        DefaultPasswordTokenResponseClient passwordTokenResponseClient = new DefaultPasswordTokenResponseClient();
        passwordTokenResponseClient.setRestOperations(restTemplate);

        return passwordTokenResponseClient;
    }

Note: MapOAuth2AccessTokenResponseConverter is deprecated in 5.6. due to gh-9685. The new class name is DefaultMapOAuth2AccessTokenResponseConverter.

Is this an acceptable workaround for your case?

@geobreze
Copy link
Author

Thanks for the solution! We've applied another workaround for this case (error handling for token response, as advised in #10016). I just left this issue here to let you know about this (maybe, incredibly rare) edge case. Thanks for your input and your time investment, we will consider this workaround if something fails on production again 😃

@sjohnr
Copy link
Contributor

sjohnr commented Jul 12, 2021

Thanks. I will keep this in mind, as there is a theme around these APIs in the community right now. In this case though, I think the framework seems to be doing the sensible thing out of the box, and there are ways (some possibly even simpler than my suggestion) to customize for special cases.

I'm going to close this for now, but feel free to re-open or add additional comments if you think of anything we missed in the discussion.

@sjohnr sjohnr closed this as completed Jul 12, 2021
@sjohnr sjohnr added status: declined A suggestion or change that we don't feel we should currently apply and removed status: feedback-provided Feedback has been provided type: bug A general bug labels Jul 12, 2021
@rakheen-dama
Copy link

rakheen-dama commented Oct 5, 2023

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

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: declined A suggestion or change that we don't feel we should currently apply
Projects
None yet
Development

No branches or pull requests

4 participants