Spring Security 5 Reemplazo para OAuth2RestTemplate

14

En spring-security-oauth2:2.4.0.RELEASEclases como OAuth2RestTemplate, OAuth2ProtectedResourceDetailsy ClientCredentialsAccessTokenProvidertodas han sido marcadas como obsoletas.

Desde el javadoc en estas clases, apunta a una guía de migración de seguridad de primavera que insinúa que las personas deben migrar al proyecto central de spring-security 5. Sin embargo, tengo problemas para encontrar cómo implementaría mi caso de uso en este proyecto.

Toda la documentación y ejemplos hablan sobre la integración con un proveedor de OAuth de tercera parte si desea que las solicitudes entrantes a su aplicación se autentiquen y desea usar el proveedor de OAuth de terceros para verificar la identidad.

En mi caso de uso, todo lo que quiero hacer es hacer una solicitud con un RestTemplateservicio externo protegido por OAuth. Actualmente creo un OAuth2ProtectedResourceDetailscon mi ID de cliente y secreto que paso a un OAuth2RestTemplate. También tengo una costumbre ClientCredentialsAccessTokenProvideragregada a la OAuth2ResTemplateque solo agrega algunos encabezados adicionales a la solicitud de token que requiere el proveedor de OAuth que estoy usando.

En la documentación de spring-security 5, encontré una sección que menciona la personalización de la solicitud de token , pero nuevamente parece estar en el contexto de autenticar una solicitud entrante con un proveedor de OAuth de terceros. No está claro cómo usaría esto en combinación con algo como a ClientHttpRequestInterceptorpara garantizar que cada solicitud saliente a un servicio externo primero obtenga un token y luego se agregue a la solicitud.

También en la guía de migración vinculada anteriormente hay una referencia a una OAuth2AuthorizedClientServiceque dice que es útil para usar en interceptores, pero nuevamente parece que depende de cosas como la ClientRegistrationRepositoryque parece ser donde mantiene registros para proveedores de terceros si desea usar que proporcionan para garantizar que se autentique una solicitud entrante.

¿Hay alguna forma de utilizar la nueva funcionalidad en spring-security 5 para registrar proveedores de OAuth y obtener un token para agregar a las solicitudes salientes de mi aplicación?

Matt Williams
fuente

Respuestas:

15

Las características del cliente OAuth 2.0 de Spring Security 5.2.x no son compatibles RestTemplate, sino solo WebClient. Consulte la referencia de seguridad de Spring :

Soporte de cliente HTTP

  • WebClient integración para entornos Servlet (para solicitar recursos protegidos)

Además, RestTemplatequedará en desuso en una versión futura. Ver RestTemplate javadoc :

NOTA: A partir de 5.0, el reactivo sin bloqueo org.springframework.web.reactive.client.WebClientofrece una alternativa moderna al RestTemplatesoporte eficiente para sincronización y asíncrono, así como escenarios de transmisión. El RestTemplateestará en desuso en una versión futura y no tendrá nuevas características importantes agregadas en el futuro. Consulte la WebClientsección de la documentación de referencia de Spring Framework para obtener más detalles y un código de ejemplo.

Por lo tanto, la mejor solución sería abandonar RestTemplatea favor de WebClient.


Uso WebClientpara el flujo de credenciales del cliente

Configure el registro y el proveedor del cliente mediante programación o mediante la configuración automática de 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

... y el 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 la WebClientinstancia para usar ServerOAuth2AuthorizedClientExchangeFilterFunctioncon lo proporcionado OAuth2AuthorizedClientManager:

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

Ahora, si intenta realizar una solicitud utilizando esta WebClientinstancia, primero solicitará un token del servidor de autorización y lo incluirá en la solicitud.

Anar Sultanov
fuente
Gracias, eso aclara algunas cosas, pero de toda la documentación vinculada anteriormente, todavía estoy luchando por encontrar un ejemplo en el que WebClientse use un interceptor (o para lo que sea la nueva terminología ) o algo similar para obtener un token OAuth de un proveedor de OAuth personalizado (ninguno de los que admite OoTB como Facebook / Google) para agregarlo a una solicitud saliente. Todos los ejemplos parecen centrarse en autenticar las solicitudes entrantes con otros proveedores. ¿Tienes alguna sugerencia para algún buen ejemplo?
Matt Williams
1
@MattWilliams Actualicé la respuesta con un ejemplo de cómo usar WebClientcon el tipo de concesión de credenciales del cliente.
Anar Sultanov
Perfecto, todo tiene mucho más sentido ahora, muchas gracias. Es posible que no tenga la oportunidad de probarlo durante unos días, pero me aseguraré de volver y marcarlo como una respuesta correcta una vez que haya tenido una oportunidad
Matt Williams
1
Eso está en desuso ahora también lol ... al menos UnAuthenticatedServerOAuth2AuthorizedClientRepository es ...
SledgeHammer
Gracias @SledgeHammer, actualicé mi respuesta.
Anar Sultanov
1

La respuesta anterior de @Anar Sultanov me ayudó a llegar a este punto, pero como tuve que agregar algunos encabezados adicionales a mi solicitud de token OAuth, pensé que proporcionaría una respuesta completa sobre cómo resolví el problema para mi caso de uso.

Configurar detalles del proveedor

Agregue lo siguiente 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 se trata de comunicación de servidor a servidor, necesitamos usar el ServerOAuth2AuthorizedClientExchangeFilterFunction. Esto solo acepta a ReactiveOAuth2AuthorizedClientManager, no lo no reactivo OAuth2AuthorizedClientManager. Por lo tanto, cuando usamos ReactiveOAuth2AuthorizedClientManager.setAuthorizedClientProvider()(para darle el proveedor para que lo use para hacer la solicitud OAuth2) tenemos que darle un en ReactiveOAuth2AuthorizedClientProviderlugar de no reactivo OAuth2AuthorizedClientProvider. Según la documentación de referencia de Spring-Security, si utiliza un elemento no reactivo DefaultClientCredentialsTokenResponseClient, puede utilizar el .setRequestEntityConverter()método para alterar la solicitud del token OAuth2, pero el equivalente reactivo WebClientReactiveClientCredentialsTokenResponseClientno proporciona esta función, por lo que tenemos que implementar el nuestro (podemos hacer uso de La WebClientReactiveClientCredentialsTokenResponseClientlógica existente ).

Mi aplicación se llama UaaWebClientReactiveClientCredentialsTokenResponseClient(aplicación omite ya que sólo muy ligeramente altera las headers()y los body()métodos de la estándar WebClientReactiveClientCredentialsTokenResponseClientpara añadir algunas cabeceras extra / campos del cuerpo, que no cambia el subyacente de autenticación de flujo).

Configurar WebClient

El ServerOAuth2AuthorizedClientExchangeFilterFunction.setClientCredentialsTokenResponseClient()método ha quedado en desuso, por lo tanto, siguiendo los consejos de desuso de ese método:

Obsoleto. Usar en su ServerOAuth2AuthorizedClientExchangeFilterFunction(ReactiveOAuth2AuthorizedClientManager)lugar. Cree una instancia de ClientCredentialsReactiveOAuth2AuthorizedClientProviderconfigurado con un WebClientReactiveClientCredentialsTokenResponseClient(o uno personalizado) y luego suminístrelo DefaultReactiveOAuth2AuthorizedClientManager.

Esto termina con una configuración similar a la siguiente:

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

Usar WebClientcomo de costumbre

El oAuth2WebClientbean ahora está listo para ser utilizado para acceder a los recursos protegidos por nuestro proveedor de OAuth2 configurado de la misma forma en que haría cualquier otra solicitud utilizando a WebClient.

Matt Williams
fuente
¿Cómo paso un ID de cliente, un secreto de cliente y un punto final oauth programáticamente?
monti
No he intentado esto, pero parece que podría crear instancias de ClientRegistrations con los detalles requeridos y pasarlos al constructor para InMemoryReactiveClientRegistrationRepository(la implementación predeterminada de ReactiveClientRegistrationRepository). Luego usa ese InMemoryReactiveClientRegistrationRepositorybean recién creado en lugar de mi autoconectado clientRegistrationRepositoryque se pasa al oauthFilteredWebClientmétodo
Matt Williams
Mh, pero no puedo registrarme diferente ClientRegistrationen tiempo de ejecución, ¿verdad? Según tengo entendido, necesito crear un bean ClientRegistrationen el inicio.
monti
Ah ok, pensé que solo querías no declararlos en el application.propertiesarchivo. La implementación de la suya propia le ReactiveOAuth2AccessTokenResponseClientpermite realizar cualquier solicitud que desee para obtener un token OAuth2, pero no sé cómo podría proporcionarle un "contexto" dinámico por solicitud. Lo mismo ocurre si implementa su filtro completo. Todo esto le daría acceso a la solicitud saliente, así que a menos que pueda inferir lo que necesita a partir de ahí, no estoy seguro de cuáles son sus opciones. ¿Cuál es su caso de uso? ¿Por qué no conocería los posibles registros al inicio?
Matt Williams
1

La respuesta de @matt Williams me pareció bastante útil. Aunque me gustaría agregar en caso de que alguien quisiera pasar programáticamente clientId y secret para la configuración de WebClient. Así es como se puede hacer.

 @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);
    }
}
Persona que practica jogging
fuente
0

Hola, tal vez sea demasiado tarde, sin embargo, RestTemplate todavía es compatible con Spring Security 5, para la aplicación no reactiva RestTemplate todavía se usa, lo que debe hacer es configurar la seguridad de Spring correctamente y crear un interceptor como se menciona en la guía de migración

Use la siguiente configuración para usar el flujo de credenciales de cliente

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

Configuración a 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;
    }

}

Interceptador

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

Esto generará access_token en la primera llamada y cada vez que expire el token. OAuth2AuthorizedClientManager gestionará todo esto para usted

Leandro Assis
fuente