Substituição do Spring Security 5 para OAuth2RestTemplate


14

Em spring-security-oauth2:2.4.0.RELEASEclasses como OAuth2RestTemplate, OAuth2ProtectedResourceDetailse ClientCredentialsAccessTokenProviderforam todos marcados como obsoletos.

No javadoc dessas classes, ele aponta para um guia de migração de segurança de primavera que insinua que as pessoas devem migrar para o projeto principal de segurança de primavera 5. No entanto, estou tendo problemas para descobrir como implementaria meu caso de uso neste projeto.

Toda a documentação e exemplos falam sobre a integração com um provedor OAuth de terceira parte, se você deseja que as solicitações recebidas para seu aplicativo sejam autenticadas e deseja usar o provedor OAuth de terceiros para verificar a identidade.

No meu caso de uso, tudo o que quero fazer é solicitar com um RestTemplateserviço externo protegido por OAuth. Atualmente, crio um OAuth2ProtectedResourceDetailscom meu ID de cliente e um segredo que eu passo para um OAuth2RestTemplate. Também tenho um personalizado ClientCredentialsAccessTokenProvideradicionado ao OAuth2ResTemplateque apenas adiciona alguns cabeçalhos extras à solicitação de token exigidos pelo provedor OAuth que estou usando.

Na documentação do spring-security 5, encontrei uma seção que menciona a personalização da solicitação de token , mas, novamente, parece estar no contexto de autenticação de uma solicitação de entrada com um provedor OAuth de terceiros. Não está claro como você usaria isso em combinação com algo como a ClientHttpRequestInterceptorpara garantir que cada solicitação de saída para um serviço externo obtenha primeiro um token e depois adicione isso à solicitação.

Também no guia de migração vinculado acima, há uma referência à OAuth2AuthorizedClientServicequal ela diz ser útil para uso em interceptores, mas, novamente, parece que ela se baseia em coisas como a ClientRegistrationRepositoryque parece estar onde mantém registros para provedores de terceiros, se você quiser usar que fornecem para garantir que uma solicitação recebida seja autenticada.

Existe alguma maneira de fazer uso da nova funcionalidade do spring-security 5 para registrar provedores OAuth para obter um token a ser adicionado às solicitações de saída do meu aplicativo?

Respostas:


15

Os recursos do cliente OAuth 2.0 do Spring Security 5.2.x não são compatíveis RestTemplate, mas apenas WebClient. Consulte Referência de segurança da primavera :

Suporte ao cliente HTTP

  • WebClient integração para ambientes de servlet (para solicitar recursos protegidos)

Além disso, RestTemplateserá preterido em uma versão futura. Consulte RestTemplate javadoc :

NOTA: A partir do 5.0, o reativo sem bloqueio org.springframework.web.reactive.client.WebClientoferece uma alternativa moderna ao RestTemplatesuporte eficiente para sincronização e assíncrona, além de cenários de streaming. O RestTemplateserá descontinuado em uma versão futura e não terá os novos recursos principais adicionados daqui para frente. Consulte a WebClientseção da documentação de referência do Spring Framework para obter mais detalhes e código de exemplo.

Portanto, a melhor solução seria abandonar a RestTemplatefavor de WebClient.


Usando WebClientpara fluxo de credenciais do cliente

Configure o registro e o provedor do cliente de forma programática ou usando a configuração automática do Spring Boot:

spring:
  security:
    oauth2:
      client:
        registration:
          custom:
            client-id: clientId
            client-secret: clientSecret
            authorization-grant-type: client_credentials
        provider:
          custom:
            token-uri: http://localhost:8081/oauth/token

… E o OAuth2AuthorizedClientManager @Bean:

@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
        ClientRegistrationRepository clientRegistrationRepository,
        OAuth2AuthorizedClientRepository authorizedClientRepository) {

    OAuth2AuthorizedClientProvider authorizedClientProvider =
            OAuth2AuthorizedClientProviderBuilder.builder()
                    .clientCredentials()
                    .build();

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

    return authorizedClientManager;
}

Configure a WebClientinstância para usar ServerOAuth2AuthorizedClientExchangeFilterFunctioncom o fornecido OAuth2AuthorizedClientManager:

@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
    ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
            new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
    oauth2Client.setDefaultClientRegistrationId("custom");
    return WebClient.builder()
            .apply(oauth2Client.oauth2Configuration())
            .build();
}

Agora, se você tentar fazer uma solicitação usando esta WebClientinstância, ele primeiro solicitará um token do servidor de autorização e o incluirá na solicitação.


Obrigado, isso esclarece algumas coisas, mas de toda a documentação vinculada acima, ainda estou lutando para encontrar um exemplo em que um interceptador (ou seja qual for a nova terminologia WebClient) ou algo semelhante é usado para buscar um token OAuth de um provedor OAuth personalizado (não um dos suportados pelo OoTB como o Facebook / Google) para adicioná-lo a uma solicitação de saída. Todos os exemplos parecem focar na autenticação de solicitações recebidas com outros provedores. Você tem alguma indicação de bons exemplos?
Matt Williams

11
@MattWilliams Atualizei a resposta com um exemplo de como usar WebClientcom o tipo de concessão de credenciais do cliente.
Anar Sultanov

Perfeito, tudo isso faz muito mais sentido agora, muito obrigado. Eu poderia não ter a chance de experimentá-lo por alguns dias, mas não se esqueça de voltar e marcar este como uma resposta correta uma vez que eu tinha um ir
Matt Williams

11
Isso está obsoleto agora muito lol ... pelo menos UnAuthenticatedServerOAuth2AuthorizedClientRepository is ...
SledgeHammer

Obrigado @SledgeHammer, atualizei minha resposta.
Anar Sultanov 31/03

1

A resposta acima de @Anar Sultanov me ajudou a chegar a esse ponto, mas como tive que adicionar alguns cabeçalhos adicionais à minha solicitação de token OAuth, pensei em fornecer uma resposta completa sobre como resolvi o problema no meu caso de uso.

Configurar detalhes do provedor

Adicione o seguinte a application.properties

spring.security.oauth2.client.registration.uaa.client-id=${CLIENT_ID:}
spring.security.oauth2.client.registration.uaa.client-secret=${CLIENT_SECRET:}
spring.security.oauth2.client.registration.uaa.scope=${SCOPE:}
spring.security.oauth2.client.registration.uaa.authorization-grant-type=client_credentials
spring.security.oauth2.client.provider.uaa.token-uri=${UAA_URL:}

Implementar personalizado ReactiveOAuth2AccessTokenResponseClient

Como esta é uma comunicação servidor a servidor, precisamos usar o ServerOAuth2AuthorizedClientExchangeFilterFunction. Isso aceita apenas um ReactiveOAuth2AuthorizedClientManager, não o não reativo OAuth2AuthorizedClientManager. Portanto, quando usamos ReactiveOAuth2AuthorizedClientManager.setAuthorizedClientProvider()(para que o provedor use para fazer a solicitação do OAuth2), precisamos dar a ele um em ReactiveOAuth2AuthorizedClientProvidervez do não-reativo OAuth2AuthorizedClientProvider. De acordo com a documentação de referência de segurança da mola, se você usar um não reativo, DefaultClientCredentialsTokenResponseClientpoderá usar o .setRequestEntityConverter()método para alterar a solicitação de token OAuth2, mas o equivalente reativo WebClientReactiveClientCredentialsTokenResponseClientnão fornece esse recurso, portanto, precisamos implementar o nosso próprio (podemos usar a WebClientReactiveClientCredentialsTokenResponseClientlógica existente ).

Minha implementação foi chamada UaaWebClientReactiveClientCredentialsTokenResponseClient(implementação omitida, pois altera apenas ligeiramente os métodos headers()e body()do padrão WebClientReactiveClientCredentialsTokenResponseClientpara adicionar alguns campos adicionais de cabeçalhos / corpo, não altera o fluxo de autenticação subjacente).

Configurar WebClient

O ServerOAuth2AuthorizedClientExchangeFilterFunction.setClientCredentialsTokenResponseClient()método foi descontinuado, portanto, seguindo os conselhos de descontinuação desse método:

Descontinuada. Use em ServerOAuth2AuthorizedClientExchangeFilterFunction(ReactiveOAuth2AuthorizedClientManager)vez disso. Crie uma instância ClientCredentialsReactiveOAuth2AuthorizedClientProviderconfigurada com uma WebClientReactiveClientCredentialsTokenResponseClient(ou uma personalizada) e depois forneça-a DefaultReactiveOAuth2AuthorizedClientManager.

Isso termina com a configuração parecida com:

@Bean("oAuth2WebClient")
public WebClient oauthFilteredWebClient(final ReactiveClientRegistrationRepository 
    clientRegistrationRepository)
{
    final ClientCredentialsReactiveOAuth2AuthorizedClientProvider
        clientCredentialsReactiveOAuth2AuthorizedClientProvider =
            new ClientCredentialsReactiveOAuth2AuthorizedClientProvider();
    clientCredentialsReactiveOAuth2AuthorizedClientProvider.setAccessTokenResponseClient(
        new UaaWebClientReactiveClientCredentialsTokenResponseClient());

    final DefaultReactiveOAuth2AuthorizedClientManager defaultReactiveOAuth2AuthorizedClientManager =
        new DefaultReactiveOAuth2AuthorizedClientManager(clientRegistrationRepository,
            new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
    defaultReactiveOAuth2AuthorizedClientManager.setAuthorizedClientProvider(
        clientCredentialsReactiveOAuth2AuthorizedClientProvider);

    final ServerOAuth2AuthorizedClientExchangeFilterFunction oAuthFilter =
        new ServerOAuth2AuthorizedClientExchangeFilterFunction(defaultReactiveOAuth2AuthorizedClientManager);
    oAuthFilter.setDefaultClientRegistrationId("uaa");

    return WebClient.builder()
        .filter(oAuthFilter)
        .build();
}

Use WebClientnormalmente

Agora, o oAuth2WebClientbean está pronto para ser usado para acessar recursos protegidos por nosso provedor OAuth2 configurado da maneira que você faria qualquer outra solicitação usando a WebClient.


Como passar programaticamente um ID do cliente, um segredo do cliente e um ponto final oauth?
Monti

Eu não tentei isso, mas parece que você pode criar instâncias de ClientRegistrations com os detalhes necessários e passá-las ao construtor para InMemoryReactiveClientRegistrationRepository(a implementação padrão de ReactiveClientRegistrationRepository). Você, então, usar esse recém-criado InMemoryReactiveClientRegistrationRepositoryfeijão no lugar do meu autowired clientRegistrationRepositoryque é passado para o oauthFilteredWebClientmétodo
Matt Williams

Ah, mas não consigo me registrar diferente ClientRegistrationem tempo de execução, não é? Tanto quanto eu entendi, eu preciso criar um bean ClientRegistrationna inicialização.
Monti

Ah, ok, eu pensei que você só queria não declará-los no application.propertiesarquivo. A implementação própria ReactiveOAuth2AccessTokenResponseClientpermite que você faça qualquer solicitação que você deseja obter um token OAuth2, mas não sei como você poderia fornecer um "contexto" dinâmico por solicitação. O mesmo vale se você implementou seu próprio filtro inteiro. você daria acesso à solicitação de saída, portanto, a menos que você possa inferir o que precisa a partir daí, não tenho certeza de quais são suas opções.Qual é o seu caso de uso? Por que você não conhece os possíveis registros na inicialização?
Matt Williams

1

Eu achei a resposta @matt Williams bastante útil. Embora eu queira adicionar caso alguém queira passar programaticamente clientId e secret para a configuração do WebClient. Aqui está como isso pode ser feito.

 @Configuration
    public class WebClientConfig {

    public static final String TEST_REGISTRATION_ID = "test-client";

    @Bean
    public ReactiveClientRegistrationRepository clientRegistrationRepository() {
        var clientRegistration = ClientRegistration.withRegistrationId(TEST_REGISTRATION_ID)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .clientId("<client_id>")
                .clientSecret("<client_secret>")
                .tokenUri("<token_uri>")
                .build();
        return new InMemoryReactiveClientRegistrationRepository(clientRegistration);
    }

    @Bean
    public WebClient testWebClient(ReactiveClientRegistrationRepository clientRegistrationRepo) {

        var oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepo,  new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
        oauth.setDefaultClientRegistrationId(TEST_REGISTRATION_ID);

        return WebClient.builder()
                .baseUrl("https://.test.com")
                .filter(oauth)
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
    }
}

0

Olá, talvez seja tarde demais, no entanto, o RestTemplate ainda é suportado no Spring Security 5, para o aplicativo não reativo RestTemplate ainda é usado, o que você precisa fazer é configurar apenas a segurança do Spring corretamente e criar um interceptador, conforme mencionado no guia de migração

Use a seguinte configuração para usar o fluxo client_credentials

application.yml

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: ${okta.oauth2.issuer}/v1/keys
      client:
        registration:
          okta:
            client-id: ${okta.oauth2.clientId}
            client-secret: ${okta.oauth2.clientSecret}
            scope: "custom-scope"
            authorization-grant-type: client_credentials
            provider: okta
        provider:
          okta:
            authorization-uri: ${okta.oauth2.issuer}/v1/authorize
            token-uri: ${okta.oauth2.issuer}/v1/token

Configuração para OauthResTemplate

@Configuration
@RequiredArgsConstructor
public class OAuthRestTemplateConfig {

    public static final String OAUTH_WEBCLIENT = "OAUTH_WEBCLIENT";

    private final RestTemplateBuilder restTemplateBuilder;
    private final OAuth2AuthorizedClientService oAuth2AuthorizedClientService;
    private final ClientRegistrationRepository clientRegistrationRepository;

    @Bean(OAUTH_WEBCLIENT)
    RestTemplate oAuthRestTemplate() {
        var clientRegistration = clientRegistrationRepository.findByRegistrationId(Constants.OKTA_AUTH_SERVER_ID);

        return restTemplateBuilder
                .additionalInterceptors(new OAuthClientCredentialsRestTemplateInterceptorConfig(authorizedClientManager(), clientRegistration))
                .setReadTimeout(Duration.ofSeconds(5))
                .setConnectTimeout(Duration.ofSeconds(1))
                .build();
    }

    @Bean
    OAuth2AuthorizedClientManager authorizedClientManager() {
        var authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
                .clientCredentials()
                .build();

        var authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, oAuth2AuthorizedClientService);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        return authorizedClientManager;
    }

}

Interceptor

public class OAuthClientCredentialsRestTemplateInterceptor implements ClientHttpRequestInterceptor {

    private final OAuth2AuthorizedClientManager manager;
    private final Authentication principal;
    private final ClientRegistration clientRegistration;

    public OAuthClientCredentialsRestTemplateInterceptor(OAuth2AuthorizedClientManager manager, ClientRegistration clientRegistration) {
        this.manager = manager;
        this.clientRegistration = clientRegistration;
        this.principal = createPrincipal();
    }

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        OAuth2AuthorizeRequest oAuth2AuthorizeRequest = OAuth2AuthorizeRequest
                .withClientRegistrationId(clientRegistration.getRegistrationId())
                .principal(principal)
                .build();
        OAuth2AuthorizedClient client = manager.authorize(oAuth2AuthorizeRequest);
        if (isNull(client)) {
            throw new IllegalStateException("client credentials flow on " + clientRegistration.getRegistrationId() + " failed, client is null");
        }

        request.getHeaders().add(HttpHeaders.AUTHORIZATION, BEARER_PREFIX + client.getAccessToken().getTokenValue());
        return execution.execute(request, body);
    }

    private Authentication createPrincipal() {
        return new Authentication() {
            @Override
            public Collection<? extends GrantedAuthority> getAuthorities() {
                return Collections.emptySet();
            }

            @Override
            public Object getCredentials() {
                return null;
            }

            @Override
            public Object getDetails() {
                return null;
            }

            @Override
            public Object getPrincipal() {
                return this;
            }

            @Override
            public boolean isAuthenticated() {
                return false;
            }

            @Override
            public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
            }

            @Override
            public String getName() {
                return clientRegistration.getClientId();
            }
        };
    }
}

Isso gerará access_token na primeira chamada e sempre que o token expirar. OAuth2AuthorizedClientManager gerenciará tudo isso para você

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.