Spring-security: OAuth2 Client Credentials Flow: Getting access tokens in the service/data tier

Created on 15 Apr 2019  路  9Comments  路  Source: spring-projects/spring-security

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 https://github.com/spring-projects/spring-security/issues/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 https://github.com/spring-projects/spring-security/issues/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;
    }
}
oauth2 enhancement

Most helpful comment

@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.

All 9 comments

@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

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.

@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>.

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.

@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.

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?

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.

@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.

@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.

Was this page helpful?
0 / 5 - 0 ratings