Skip to content

OAuth2 Client Credentials Flow: Getting access tokens in the service/data tier #6780

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
fritzdj opened this issue Apr 15, 2019 · 9 comments
Closed
Assignees
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) type: enhancement A general enhancement
Milestone

Comments

@fritzdj
Copy link
Contributor

fritzdj commented Apr 15, 2019

Summary
Currently there is a way to auto-wire OAuth2AuthorizedClientService in a component in the service/data tier to lookup the OAuth2AuthorizedClient. This works, but you would still need to use OAuth2AuthorizedClientArgumentResolver to resolve OAuth2AuthorizedClient in web-tier first. My suggestion is to make things more self-contained in service/data tier by allowing OAuth2AuthorizedClientService to get new access tokens if needed. I think Spring should allow new access tokens to be retrieved either way (in service/data tier OR web tier). We tried this with a custom service bean and it is working using some of the logic in OAuth2AuthorizedClientArgumentResolver.

fyi @jgrandja - see #6609

Example
Here is an example of a bean that could be created and used by OAuth2AuthorizedClientArgumentResolver (in the web tier) OR used by a class in the service/data tier to get new access tokens. This also has a temporary workaround to get around #6609 (where access token is never refreshed).

public class OAuth2AuthorizedClientService {

    private ClientRegistrationRepository clientRegistrationRepository;
    private OAuth2AuthorizedClientRepository authorizedClientRepository;
    private HttpServletRequest httpServletRequest;
    private HttpServletResponse httpServletResponse;
    private DefaultClientCredentialsTokenResponseClient defaultClientCredentialsTokenResponseClient = new DefaultClientCredentialsTokenResponseClient();

    public OAuth2AuthorizedClientService(
            ClientRegistrationRepository clientRegistrationRepository,
            OAuth2AuthorizedClientRepository authorizedClientRepository,
            HttpServletRequest httpServletRequest,
            HttpServletResponse httpServletResponse) {
        this.clientRegistrationRepository = clientRegistrationRepository;
        this.authorizedClientRepository = authorizedClientRepository;
        this.httpServletRequest = httpServletRequest;
        this.httpServletResponse = httpServletResponse;
    }

    public OAuth2AuthorizedClient getAuthorizedClient(String audience) {
        Authentication principal = SecurityContextHolder.getContext().getAuthentication();
        ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId(audience);

        OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient(
            audience, principal, httpServletRequest);
        if (authorizedClient != null && authorizedClient.getAccessToken().getExpiresAt().isAfter(Instant.now().plusSeconds(300))) {
            return authorizedClient;
        }

        OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest =
            new OAuth2ClientCredentialsGrantRequest(clientRegistration);

        defaultClientCredentialsTokenResponseClient
            .setRequestEntityConverter(new Auth0ClientCredentialsGrantRequestEntityConverter(
                audience));
        OAuth2AccessTokenResponse tokenResponse =
            defaultClientCredentialsTokenResponseClient.getTokenResponse(clientCredentialsGrantRequest);

        authorizedClient = new OAuth2AuthorizedClient(
            clientRegistration,
            principal.getName(),
            tokenResponse.getAccessToken());

        this.authorizedClientRepository.saveAuthorizedClient(
            authorizedClient,
            principal,
            httpServletRequest,
            httpServletResponse);

        return authorizedClient;
    }


    public void setDefaultClientCredentialsTokenResponseClient(DefaultClientCredentialsTokenResponseClient defaultClientCredentialsTokenResponseClient) {
        this.defaultClientCredentialsTokenResponseClient = defaultClientCredentialsTokenResponseClient;
    }
}
@jgrandja
Copy link
Contributor

@fritzdj Are you aware of ServletOAuth2AuthorizedClientExchangeFilterFunction (Servlet) and ServerOAuth2AuthorizedClientExchangeFilterFunction (Reactive) which provides integration with WebClient?

These ExchangeFilterFunction's are capable of fetching new (or refresh expired) access tokens (for authorization_code) and also fetch new access tokens for client_credentials. The WebClient can be used in the service-tier for your use case. See the sample

@jgrandja jgrandja self-assigned this Apr 18, 2019
@jgrandja jgrandja added in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: waiting-for-feedback We need additional information before we can continue labels Apr 18, 2019
@fritzdj
Copy link
Contributor Author

fritzdj commented Apr 22, 2019

Thanks @jgrandja. It does look like this would meet our needs if we were to do some refactoring and use ServletOAuth2AuthorizedClientExchangeFilterFunction (Servlet) with WebClient instead of using RestTemplate. However, it may make sense to put something similar in place for projects where RestTemplate is used. While converting to WebClient may seem trivial, it could be more of a large change for some projects based on the number of classes / tests affected.

@jgrandja
Copy link
Contributor

@fritzdj

...if we were to do some refactoring and use ServletOAuth2AuthorizedClientExchangeFilterFunction (Servlet) with WebClient instead of using RestTemplate.

What re-factoring changes are you referring to? Can you be more specific and provide details. The only place RestTemplate is used in ServletOAuth2AuthorizedClientExchangeFilterFunction is indirectly via the injected OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest>.

@kujaomega
Copy link

Hi @jgrandja. The workaround you propose with ServletOAuth2AuthorizedClientExchangeFilterFunction which provides integration with WebClient works well if you only have one authenticated client.

I think that, what @fritzdj tries to propose is an easy way to get new access tokens if needed. I think there is no such way in spring security.

The case where you have lot's of clients. A client use Oauth2 authentication with a provider (for example "google"). Spring security receive this Access tokens and Refresh tokens and store it in OAuth2AuthorizedClientRepository (which is an InMemory repository). You need this Access token to use a certain service offline. Also, as the OAuth2AuthorizedClientRepository is an InMemory repository there is no easy way to access that InMemory repository for other servers.

There's the way to get the OAuth2AuthorizedClientRepository bean, get the access token and use it. This way, if the server reboot, you lost your access token. So, there should be a way to configure the access token and refresh token persistence.

Correct me if I'm wrong or if there are some easy solution to solve this problem.

@fritzdj
Copy link
Contributor Author

fritzdj commented Apr 26, 2019

@jgrandja, by "re-factoring" I meant in a web app (not Spring Security itself). In many legacy web applications, RestTemplate is used all over the place. Switching to WebClient would take some effort. I think there should be a way to just grab new access tokens in the service/data tier of a web application (for one to many authenticated clients) while using RestTemplate if desired.

Good point by @kujaomega - if this only supports one authenticated client that will likely not work for us either in all cases.

@mmaask
Copy link

mmaask commented Apr 26, 2019

I am not sure if it is planned in the future, but I'd also like to be able to obtain access token through different mechanism. For example we are using Feign Client instead, so using ServletOAuth2AuthorizedClientExchangeFilterFunction doesn't seem to be an option. Refactoring to Webclient would be our only option or somekind of custom interceptor for Feign like it used to be.

In Spring Cloud Security we have OAuth2FeignRequestInterceptor which basically does the same through ClientCredentialsAccessTokenProvider. Is there any simple implementation planned for Feign or should we keep using old Spring Cloud Security + OAuth2 for now?

@jgrandja
Copy link
Contributor

Sorry for my misunderstanding @fritzdj regarding the re-factoring you mentioned. I understand your goal and it makes sense. Keep an eye out on #6811 as the goal there is to address re-use and likely resolve this issue at the same time. I'll keep this issue open either way until #6811 is resolved, which I'm planning on starting this week.

@jgrandja
Copy link
Contributor

jgrandja commented Apr 29, 2019

@Vaelyr We're planning on addressing the re-use of the logic currently in ServletOAuth2AuthorizedClientExchangeFilterFunction outside of WebClient. The end-goal is to allow better reuse for Feign Client or RestTemplate. Please track #6811 for progress.

@jgrandja jgrandja added status: duplicate A duplicate of another issue type: enhancement A general enhancement and removed status: waiting-for-feedback We need additional information before we can continue status: duplicate A duplicate of another issue labels Apr 29, 2019
@jgrandja
Copy link
Contributor

@kujaomega

The workaround you propose with ServletOAuth2AuthorizedClientExchangeFilterFunction which provides integration with WebClient works well if you only have one authenticated client.

Yes, that is correct but this was a quick workaround I provided and would not have this limitation when the real solution is in place, which is planned via #6811, #6683, a new ticket or a combination of these.

if the server reboot, you lost your access token. So, there should be a way to configure the access token and refresh token persistence.

At the moment, we only provide an in-memory implementation of OAuth2AuthorizedClientRepository. If you require persistence, you can implement your own OAuth2AuthorizedClientRepository that provides persistence and simply register it as a @Bean.

FYI, oauth2-client requires an OAuth2AuthorizedClientService (or OAuth2AuthorizedClientRepository) and ClientRegistrationRepository @Bean regardless of the backing implementation.

@jgrandja jgrandja removed their assignment Jun 4, 2019
@jgrandja jgrandja added this to the 5.2.0.RC1 milestone Jul 26, 2019
@jgrandja jgrandja self-assigned this Aug 28, 2019
jgrandja added a commit to jgrandja/spring-security that referenced this issue Sep 4, 2019
@jzheaux jzheaux modified the milestones: 5.2.0.RC1, 5.2.0 Sep 5, 2019
@jgrandja jgrandja modified the milestones: 5.2.0, 5.2.0.RC1 Sep 6, 2019
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) type: enhancement A general enhancement
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants