Spring Security 5 Sostituzione per OAuth2RestTemplate


14

In spring-security-oauth2:2.4.0.RELEASEclassi come OAuth2RestTemplate, OAuth2ProtectedResourceDetailse ClientCredentialsAccessTokenProvidersono stati tutti contrassegnati come obsoleti.

Dal javadoc su queste classi punta a una guida alla migrazione della sicurezza primaverile che insinua che le persone dovrebbero migrare al progetto core 5 di sicurezza primaverile. Tuttavia, ho difficoltà a trovare il modo in cui implementare il mio caso d'uso in questo progetto.

Tutta la documentazione e gli esempi parlano dell'integrazione con un provider OAuth di terza parte se si desidera autenticare le richieste in arrivo per l'applicazione e si desidera utilizzare il provider OAuth di terze parti per verificare l'identità.

Nel mio caso d'uso tutto quello che voglio fare è fare una richiesta con un RestTemplateservizio esterno protetto da OAuth. Attualmente creo un OAuth2ProtectedResourceDetailscon il mio ID cliente e il segreto che passo in un OAuth2RestTemplate. Ho anche ClientCredentialsAccessTokenProvideraggiunto un'aggiunta personalizzata a quella OAuth2ResTemplateche aggiunge solo alcune intestazioni extra alla richiesta di token richiesta dal provider OAuth che sto usando.

Nella documentazione di Spring Security 5 ho trovato una sezione che menziona la personalizzazione della richiesta di token , ma che sembra essere nel contesto dell'autenticazione di una richiesta in arrivo con un provider OAuth di terze parti. Non è chiaro come lo useresti in combinazione con qualcosa di simile a ClientHttpRequestInterceptorper garantire che ogni richiesta in uscita a un servizio esterno ottenga prima un token e poi lo aggiunga alla richiesta.

Anche nella guida alla migrazione collegata sopra c'è un riferimento a quello OAuth2AuthorizedClientServiceche dice sia utile per l'uso negli intercettori, ma ancora una volta sembra che si basi su cose come quella ClientRegistrationRepositoryche sembra essere dove mantiene le registrazioni per i fornitori di terze parti se si desidera utilizzare che forniscono per garantire che una richiesta in arrivo sia autenticata.

È possibile utilizzare le nuove funzionalità di Spring-Security 5 per la registrazione dei provider OAuth per ottenere un token da aggiungere alle richieste in uscita dalla mia applicazione?

Risposte:


15

Le funzionalità client OAuth 2.0 di Spring Security 5.2.x non supportano RestTemplate, ma solo WebClient. Vedi riferimento sulla sicurezza di primavera :

Supporto client HTTP

  • WebClient integrazione per ambienti servlet (per richiedere risorse protette)

Inoltre, RestTemplatesarà deprecato in una versione futura. Vedi RestTemplate javadoc :

NOTA: A partire da 5.0, il reattivo non bloccante org.springframework.web.reactive.client.WebClientoffre un'alternativa moderna al RestTemplatesupporto efficiente sia per la sincronizzazione che per l'asincronizzazione, nonché per gli scenari di streaming. Il RestTemplatesarà obsoleta in una versione futura e non avrà più importanti nuove funzionalità aggiunte in futuro. Vedere la WebClientsezione della documentazione di riferimento di Spring Framework per ulteriori dettagli e un codice di esempio.

Pertanto, la soluzione migliore sarebbe abbandonare RestTemplatea favore di WebClient.


Utilizzo WebClientper flusso credenziali client

Configurare la registrazione client e il provider a livello di codice o utilizzando la configurazione automatica di 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 il 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;
}

Configurare l' WebClientistanza da utilizzare ServerOAuth2AuthorizedClientExchangeFilterFunctioncon il fornito OAuth2AuthorizedClientManager:

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

Ora, se provi a fare una richiesta usando questa WebClientistanza, prima richiederà un token dal server di autorizzazione e lo includerà nella richiesta.


Grazie, questo chiarisce alcune cose, ma da tutta la documentazione collegata sopra sto ancora lottando per trovare un esempio in cui un intercettore (o qualunque sia la nuova terminologia WebClientè) o qualcosa di simile viene usato per recuperare un token OAuth da un provider OAuth personalizzato (non uno di quelli supportati OoTB come Facebook / Google) per aggiungerlo a una richiesta in uscita. Tutti gli esempi sembrano concentrarsi sull'autenticazione delle richieste in arrivo con altri provider. Hai qualche suggerimento per qualche buon esempio?
Matt Williams,

1
@MattWilliams Ho aggiornato la risposta con un esempio di come utilizzare WebClientcon il tipo di concessione delle credenziali del client.
Anar Sultanov il

Perfetto, ora tutto ha molto più senso, grazie mille. Potrei non avere la possibilità di provarlo per alcuni giorni, ma sicuramente tornerò e contrassegnerò come una risposta corretta una volta che ho avuto un tentativo
Matt Williams

1
Questo è deprecato ora troppo lol ... almeno UnAuthenticatedServerOAuth2AuthorizedClientRepository is ...
SledgeHammer

Grazie @SledgeHammer, ho aggiornato la mia risposta.
Anar Sultanov

1

La risposta precedente di @Anar Sultanov mi ha aiutato ad arrivare a questo punto, ma poiché ho dovuto aggiungere alcune intestazioni aggiuntive alla mia richiesta di token OAuth, ho pensato di fornire una risposta completa su come ho risolto il problema per il mio caso d'uso.

Configura i dettagli del provider

Aggiungi quanto segue 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:}

Implementa personalizzato ReactiveOAuth2AccessTokenResponseClient

Poiché si tratta di una comunicazione server-server, è necessario utilizzare ServerOAuth2AuthorizedClientExchangeFilterFunction. Questo accetta solo un ReactiveOAuth2AuthorizedClientManager, non il non reattivo OAuth2AuthorizedClientManager. Pertanto, quando utilizziamo ReactiveOAuth2AuthorizedClientManager.setAuthorizedClientProvider()(per dargli il provider da utilizzare per effettuare la richiesta OAuth2), dobbiamo dargli un ReactiveOAuth2AuthorizedClientProviderinvece del non reattivo OAuth2AuthorizedClientProvider. Secondo la documentazione di riferimento sulla sicurezza primaverile se si utilizza un non reattivo, DefaultClientCredentialsTokenResponseClientè possibile utilizzare il .setRequestEntityConverter()metodo per modificare la richiesta di token OAuth2, ma l'equivalente reattivo WebClientReactiveClientCredentialsTokenResponseClientnon fornisce questa funzione, quindi dobbiamo implementare il nostro (possiamo fare uso di la WebClientReactiveClientCredentialsTokenResponseClientlogica esistente ).

La mia implementazione è stata chiamata UaaWebClientReactiveClientCredentialsTokenResponseClient(implementazione omessa in quanto modifica leggermente i metodi headers()e body()dal valore predefinito WebClientReactiveClientCredentialsTokenResponseClientper aggiungere alcune intestazioni / campi body aggiuntivi, non cambia il flusso di autorizzazione sottostante).

Configurazione WebClient

Il ServerOAuth2AuthorizedClientExchangeFilterFunction.setClientCredentialsTokenResponseClient()metodo è stato deprecato, quindi seguendo il consiglio di deprecazione di quel metodo:

Deprecato. Usa ServerOAuth2AuthorizedClientExchangeFilterFunction(ReactiveOAuth2AuthorizedClientManager)invece. Creare un'istanza di ClientCredentialsReactiveOAuth2AuthorizedClientProviderconfigurata con una WebClientReactiveClientCredentialsTokenResponseClient(o una personalizzata) e fornirla a DefaultReactiveOAuth2AuthorizedClientManager.

Questo finisce con una configurazione simile a:

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

Usa WebClientcome di consueto

Il oAuth2WebClientbean è ora pronto per essere utilizzato per accedere alle risorse protette dal nostro provider OAuth2 configurato nel modo in cui si farebbe qualsiasi altra richiesta utilizzando a WebClient.


Come passare un ID client, client-secret e un endpoint oauth in modo programmatico?
monti,

Non l'ho provato, ma sembra che tu possa creare istanze di ClientRegistrations con i dettagli richiesti e passarle nel costruttore per InMemoryReactiveClientRegistrationRepository(l'implementazione predefinita di ReactiveClientRegistrationRepository). Quindi usi quel InMemoryReactiveClientRegistrationRepositorybean appena creato al posto del mio autowired clientRegistrationRepositoryche viene passato al oauthFilteredWebClientmetodo
Matt Williams

Mh, ma non riesco a registrarmi diversamente ClientRegistrationin fase di esecuzione, vero? Per quanto ho capito, devo creare un bean ClientRegistrationall'avvio.
monti,

Ah ok, pensavo volessi solo non dichiararli nel application.propertiesfile. L'implementazione del tuo ReactiveOAuth2AccessTokenResponseClientti permette di fare qualunque richiesta tu voglia ottenere un token OAuth2, ma non so come potresti fornirti un "contesto" dinamico per richiesta. Lo stesso vale se avessi implementato il tuo intero filtro. ti darebbe accesso a è la richiesta in uscita, quindi a meno che tu non possa dedurre ciò di cui hai bisogno da lì non sono sicuro di quali siano le tue opzioni. Qual è il tuo caso d'uso? Perché non dovresti conoscere le possibili registrazioni all'avvio?
Matt Williams

1

Ho trovato la risposta di @matt Williams abbastanza utile. Anche se vorrei aggiungere nel caso qualcuno volesse passare programmaticamente clientId e segreto per la configurazione di WebClient. Ecco come può essere fatto.

 @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

Salve, forse è troppo tardi, tuttavia RestTemplate è ancora supportato in Spring Security 5, per l'app non reattiva RestTemplate è ancora utilizzato ciò che devi fare è solo configurare correttamente la sicurezza di Spring e creare un intercettore come menzionato nella guida alla migrazione

Utilizzare la seguente configurazione per utilizzare il flusso 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

Configurazione su 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;
    }

}

intercettatore

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

Questo genererà access_token nella prima chiamata e ogni volta che il token è scaduto. OAuth2AuthorizedClientManager gestirà tutto questo per te

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.