Remplacement de Spring Security 5 pour OAuth2RestTemplate

14

Dans des spring-security-oauth2:2.4.0.RELEASEclasses telles que OAuth2RestTemplate, OAuth2ProtectedResourceDetailset ClientCredentialsAccessTokenProvideront toutes été marquées comme obsolètes.

À partir du javadoc sur ces classes, il pointe vers un guide de migration de sécurité de printemps qui insinue que les gens doivent migrer vers le projet principal de sécurité de printemps 5. Cependant, j'ai du mal à trouver comment j'implémenterais mon cas d'utilisation dans ce projet.

Toute la documentation et les exemples parlent de l'intégration avec un fournisseur OAuth tiers si vous souhaitez que les demandes entrantes à votre application soient authentifiées et que vous souhaitez utiliser le fournisseur OAuth tiers pour vérifier l'identité.

Dans mon cas d'utilisation, tout ce que je veux faire est de faire une demande avec un RestTemplateà un service externe protégé par OAuth. Actuellement, je crée un OAuth2ProtectedResourceDetailsavec mon identifiant client et mon secret que je passe dans un OAuth2RestTemplate. J'ai également ClientCredentialsAccessTokenProviderajouté une coutume OAuth2ResTemplatequi ajoute simplement des en-têtes supplémentaires à la demande de jeton qui sont requis par le fournisseur OAuth que j'utilise.

Dans la documentation de spring-security 5, j'ai trouvé une section qui mentionne la personnalisation de la demande de jeton , mais qui semble être dans le contexte de l'authentification d'une demande entrante avec un fournisseur OAuth tiers. Il n'est pas clair comment vous utiliseriez cela en combinaison avec quelque chose comme un ClientHttpRequestInterceptorpour vous assurer que chaque demande sortante à un service externe obtient d'abord un jeton, puis obtient celui ajouté à la demande.

Dans le guide de migration lié ci-dessus, il est également fait référence à un document OAuth2AuthorizedClientServicequi, selon lui, est utile pour les intercepteurs, mais encore une fois, il semble qu'il s'appuie sur des choses comme le ClientRegistrationRepositoryqui semble être l'endroit où il maintient les enregistrements pour les fournisseurs tiers si vous souhaitez utiliser qui fournissent pour garantir qu'une demande entrante est authentifiée.

Existe-t-il un moyen d'utiliser les nouvelles fonctionnalités de Spring-Security 5 pour enregistrer les fournisseurs OAuth afin d'obtenir un jeton à ajouter aux demandes sortantes de mon application?

Matt Williams
la source

Réponses:

15

Les fonctionnalités client OAuth 2.0 de Spring Security 5.2.x ne sont pas prises en charge RestTemplate, mais uniquement WebClient. Voir Référence de sécurité Spring :

Prise en charge du client HTTP

  • WebClient intégration pour les environnements de servlet (pour demander des ressources protégées)

En outre, RestTemplatesera déconseillé dans une future version. Voir RestTemplate javadoc :

REMARQUE: à partir de la version 5.0, la version non bloquante et réactive org.springframework.web.reactive.client.WebClientoffre une alternative moderne à la RestTemplateprise en charge efficace de la synchronisation et de l'async, ainsi que des scénarios de streaming. Le RestTemplatesera obsolète dans une future version et il n'y aura pas de nouvelles fonctionnalités majeures ajoutées à l'avenir. Consultez la WebClientsection de la documentation de référence de Spring Framework pour plus de détails et un exemple de code.

Par conséquent, la meilleure solution serait d'abandonner RestTemplateen faveur de WebClient.


Utilisation WebClientdu flux d'informations d'identification du client

Configurez l'enregistrement client et le fournisseur par programme ou à l'aide de la configuration automatique 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

… Et le 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;
}

Configurez l' WebClientinstance à utiliser ServerOAuth2AuthorizedClientExchangeFilterFunctionavec les éléments fournis OAuth2AuthorizedClientManager:

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

Maintenant, si vous essayez de faire une demande à l'aide de cette WebClientinstance, il demandera d'abord un jeton au serveur d'autorisation et l'inclura dans la demande.

Anar Sultanov
la source
Merci, cela clarifie certaines choses, mais à partir de toute la documentation liée ci-dessus, j'ai encore du mal à trouver un exemple où un intercepteur (ou quelle que soit la nouvelle terminologie WebClient) ou quelque chose de similaire est utilisé pour récupérer un jeton OAuth à partir d'un fournisseur OAuth personnalisé (pas l'un de ceux pris en charge OoTB comme Facebook / Google) afin de l'ajouter à une demande sortante. Tous les exemples semblent se concentrer sur l'authentification des demandes entrantes avec d'autres fournisseurs. Avez-vous des conseils pour de bons exemples?
Matt Williams
1
@MattWilliams J'ai mis à jour la réponse avec un exemple d'utilisation WebClientavec le type d'octroi des informations d'identification client.
Anar Sultanov
Parfait, tout cela a beaucoup plus de sens maintenant, merci beaucoup. Je n'aurai peut-être pas la chance de l'essayer pendant quelques jours, mais je serai sûr de revenir et de marquer cela comme une bonne réponse une fois que j'aurai essayé
Matt Williams
1
C'est obsolète maintenant trop lol ... au moins UnAuthenticatedServerOAuth2AuthorizedClientRepository est ...
SledgeHammer
Merci @SledgeHammer, j'ai mis à jour ma réponse.
Anar Sultanov
1

La réponse ci-dessus de @Anar Sultanov m'a aidé à arriver à ce point, mais comme je devais ajouter des en-têtes supplémentaires à ma demande de jeton OAuth, j'ai pensé fournir une réponse complète sur la façon dont j'ai résolu le problème pour mon cas d'utilisation.

Configurer les détails du fournisseur

Ajoutez ce qui suit à 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:}

Implémenter personnalisé ReactiveOAuth2AccessTokenResponseClient

Comme il s'agit d'une communication de serveur à serveur, nous devons utiliser le ServerOAuth2AuthorizedClientExchangeFilterFunction. Cela n'accepte qu'un ReactiveOAuth2AuthorizedClientManager, pas le non réactif OAuth2AuthorizedClientManager. Par conséquent, lorsque nous utilisons ReactiveOAuth2AuthorizedClientManager.setAuthorizedClientProvider()(pour lui donner le fournisseur à utiliser pour effectuer la demande OAuth2), nous devons lui donner un ReactiveOAuth2AuthorizedClientProviderau lieu du non réactif OAuth2AuthorizedClientProvider. Selon la documentation de référence de Spring-Security, si vous utilisez une méthode non réactive, DefaultClientCredentialsTokenResponseClientvous pouvez utiliser la .setRequestEntityConverter()méthode pour modifier la demande de jeton OAuth2, mais l'équivalent réactif WebClientReactiveClientCredentialsTokenResponseClientne fournit pas cette fonctionnalité, nous devons donc implémenter la nôtre (nous pouvons utiliser la WebClientReactiveClientCredentialsTokenResponseClientlogique existante ).

Mon implémentation a été appelée UaaWebClientReactiveClientCredentialsTokenResponseClient(implémentation omise car elle ne modifie que très légèrement les méthodes headers()et body()par défaut WebClientReactiveClientCredentialsTokenResponseClientpour ajouter des en-têtes / champs de corps supplémentaires, elle ne modifie pas le flux d'authentification sous-jacent).

Configurer WebClient

La ServerOAuth2AuthorizedClientExchangeFilterFunction.setClientCredentialsTokenResponseClient()méthode a été déconseillée. Suivez donc les conseils de dépréciation de cette méthode:

Obsolète. Utilisez ServerOAuth2AuthorizedClientExchangeFilterFunction(ReactiveOAuth2AuthorizedClientManager)plutôt. Créez une instance de ClientCredentialsReactiveOAuth2AuthorizedClientProviderconfiguré avec une WebClientReactiveClientCredentialsTokenResponseClient(ou une personnalisée) et fournissez-la à DefaultReactiveOAuth2AuthorizedClientManager.

Cela se termine par une configuration ressemblant à quelque chose comme:

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

Utiliser WebClientcomme d'habitude

Le oAuth2WebClientbean est maintenant prêt à être utilisé pour accéder aux ressources protégées par notre fournisseur OAuth2 configuré comme vous le feriez pour toute autre demande à l'aide de a WebClient.

Matt Williams
la source
Comment transmettre un identifiant client, un secret client et un point de terminaison oauth par programmation?
monti
Je n'ai pas essayé cela, mais il semble que vous puissiez créer des instances de ClientRegistrations avec les détails requis et les transmettre au constructeur de InMemoryReactiveClientRegistrationRepository(l'implémentation par défaut de ReactiveClientRegistrationRepository). Vous utilisez ensuite ce InMemoryReactiveClientRegistrationRepositorybean nouvellement créé à la place de mon câble automatique clientRegistrationRepositoryqui est passé dans la oauthFilteredWebClientméthode
Matt Williams
Mh, mais je ne peux pas m'inscrire différemment ClientRegistrationà l'exécution, n'est -ce pas? Pour autant que j'ai compris, je dois créer un bean ClientRegistrationau démarrage.
monti
Ah ok, je pensais que tu voulais juste ne pas les déclarer dans le application.propertiesfichier. Implémenter le vôtre ReactiveOAuth2AccessTokenResponseClientvous permet de faire la requête que vous souhaitez pour obtenir un jeton OAuth2, mais je ne sais pas comment vous pouvez lui fournir un "contexte" dynamique par requête. Il en va de même si vous avez implémenté votre propre filtre en entier. Tout cela vous donnerait accès à la demande sortante, donc à moins que vous ne puissiez en déduire ce dont vous avez besoin, je ne suis pas sûr de vos options. Quel est votre cas d'utilisation? Pourquoi ne sauriez-vous pas les enregistrements possibles au démarrage?
Matt Williams
1

J'ai trouvé la réponse de @matt Williams très utile. Bien que j'aimerais ajouter au cas où quelqu'un voudrait transmettre par programme clientId et secret pour la configuration WebClient. Voici comment cela peut être fait.

 @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);
    }
}
Joggeur
la source
0

Salut, il est peut-être trop tard, mais RestTemplate est toujours pris en charge dans Spring Security 5, pour une application non réactive RestTemplate est toujours utilisé ce que vous devez faire est de configurer correctement la sécurité Spring et de créer un intercepteur comme mentionné dans le guide de migration

Utilisez la configuration suivante pour utiliser le flux 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

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

}

Intercepteur

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

Cela générera access_token lors du premier appel et à chaque expiration du jeton. OAuth2AuthorizedClientManager gérera tout cela pour vous

Leandro Assis
la source