Spring Security 5 Penggantian untuk OAuth2RestTemplate

14

Di spring-security-oauth2:2.4.0.RELEASEkelas-kelas seperti OAuth2RestTemplate, OAuth2ProtectedResourceDetailsdan ClientCredentialsAccessTokenProvidersemuanya telah ditandai sebagai usang.

Dari javadoc pada kelas-kelas ini itu menunjuk ke panduan migrasi keamanan pegas yang mengisyaratkan bahwa orang harus bermigrasi ke proyek inti-keamanan 5 musim semi. Namun saya mengalami kesulitan menemukan bagaimana saya akan menerapkan use case saya dalam proyek ini.

Semua dokumentasi dan contoh berbicara tentang integrasi dengan penyedia OAuth bagian ke-3 jika Anda ingin permintaan masuk ke aplikasi Anda diautentikasi dan Anda ingin menggunakan penyedia OAuth pihak ke-3 untuk memverifikasi identitas.

Dalam kasus penggunaan saya, yang ingin saya lakukan adalah membuat permintaan dengan RestTemplateke layanan eksternal yang dilindungi oleh OAuth. Saat ini saya membuat OAuth2ProtectedResourceDetailsdengan id klien dan rahasia yang saya berikan menjadi OAuth2RestTemplate. Saya juga memiliki kustom yang ClientCredentialsAccessTokenProviderditambahkan ke OAuth2ResTemplateyang hanya menambahkan beberapa header tambahan ke permintaan token yang diperlukan oleh penyedia OAuth yang saya gunakan.

Dalam dokumentasi spring-security 5 saya telah menemukan bagian yang menyebutkan menyesuaikan permintaan token , tetapi sekali lagi yang terlihat dalam konteks mengotentikasi permintaan masuk dengan penyedia OAuth pihak ke-3. Tidak jelas bagaimana Anda akan menggunakan ini dalam kombinasi dengan sesuatu seperti ClientHttpRequestInterceptoruntuk memastikan bahwa setiap permintaan keluar ke layanan eksternal pertama mendapat token dan kemudian mendapatkan yang ditambahkan ke permintaan.

Juga dalam panduan migrasi yang ditautkan di atas ada referensi OAuth2AuthorizedClientServiceyang dikatakan berguna untuk digunakan dalam pencegat, tetapi sekali lagi ini sepertinya bergantung pada hal-hal seperti ClientRegistrationRepositoryyang tampaknya berada di mana ia mengelola pendaftaran untuk penyedia pihak ketiga jika Anda ingin menggunakan yang menyediakan untuk memastikan permintaan yang masuk dikonfirmasi.

Apakah ada cara saya dapat menggunakan fungsionalitas baru di spring-security 5 untuk mendaftarkan penyedia OAuth untuk mendapatkan token untuk ditambahkan ke permintaan keluar dari aplikasi saya?

Matt Williams
sumber

Jawaban:

15

Fitur Klien OAuth 2.0 dari Spring Security 5.2.x tidak mendukung RestTemplate, tetapi hanya WebClient. Lihat Referensi Keamanan Pegas :

Dukungan Klien HTTP

  • WebClient integrasi untuk Lingkungan Servlet (untuk meminta sumber daya yang dilindungi)

Selain itu, RestTemplateakan ditinggalkan dalam versi mendatang. Lihat RestTemplate javadoc :

CATATAN: Pada 5.0, non-blocking, reaktif org.springframework.web.reactive.client.WebClientmenawarkan alternatif modern RestTemplatedengan dukungan efisien untuk sinkronisasi dan async, serta skenario streaming. Ini RestTemplateakan ditinggalkan dalam versi masa depan dan tidak akan memiliki fitur baru yang ditambahkan di masa depan. Lihat WebClientbagian dokumentasi referensi Kerangka Kerja Spring untuk detail lebih lanjut dan kode contoh.

Oleh karena itu, solusi terbaik adalah dengan meninggalkan RestTemplatemendukung WebClient.


Menggunakan WebClientuntuk Alur Kredensial Klien

Konfigurasikan pendaftaran dan penyedia klien baik secara terprogram atau menggunakan konfigurasi otomatis Boot Musim Semi:

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

… Dan 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;
}

Konfigurasikan WebClientinstance untuk digunakan ServerOAuth2AuthorizedClientExchangeFilterFunctiondengan yang disediakan OAuth2AuthorizedClientManager:

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

Sekarang, jika Anda mencoba membuat permintaan menggunakan WebClientinstance ini , pertama-tama ia akan meminta token dari server otorisasi dan memasukkannya ke dalam permintaan.

Anar Sultanov
sumber
Terima kasih, itu membereskan beberapa hal, tetapi dari semua dokumentasi terkait di atas, saya masih berjuang untuk menemukan contoh di mana pencegat (atau apa pun terminologinya WebClient), atau sesuatu yang serupa digunakan untuk mengambil token OAuth dari sebuah penyedia OAuth khusus (bukan yang mendukung OoTB seperti Facebook / Google) untuk menambahkannya ke permintaan keluar. Semua contoh tampaknya berfokus pada otentikasi permintaan masuk dengan penyedia lain. Apakah Anda punya petunjuk untuk contoh yang baik?
Matt Williams
1
@MattWilliams Saya memperbarui jawaban dengan contoh bagaimana menggunakan WebClientdengan jenis hibah kredensial klien.
Anar Sultanov
Sempurna, semua itu jauh lebih masuk akal sekarang, terima kasih banyak. Saya mungkin tidak mendapatkan kesempatan untuk mencobanya selama beberapa hari, tetapi pasti akan kembali dan menandainya sebagai jawaban yang benar setelah saya mencobanya
Matt Williams
1
Itu sudah usang sekarang juga lol ... setidaknya UnAuthenticatedServerOAuth2AuthorizedClientRepository is ...
SledgeHammer
Terima kasih @ HarrisHammer, saya memperbarui jawaban saya.
Anar Sultanov
1

Jawaban di atas dari @Anar Sultanov membantu saya sampai di titik ini, tetapi karena saya harus menambahkan beberapa header tambahan ke permintaan token OAuth saya, saya pikir saya akan memberikan jawaban lengkap untuk bagaimana saya memecahkan masalah untuk kasus penggunaan saya.

Konfigurasikan detail penyedia

Tambahkan yang berikut ke 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:}

Terapkan kebiasaan ReactiveOAuth2AccessTokenResponseClient

Karena ini adalah komunikasi server-ke-server, kita perlu menggunakan ServerOAuth2AuthorizedClientExchangeFilterFunction. Ini hanya menerima a ReactiveOAuth2AuthorizedClientManager, bukan yang non-reaktif OAuth2AuthorizedClientManager. Karena itu ketika kita menggunakan ReactiveOAuth2AuthorizedClientManager.setAuthorizedClientProvider()(untuk memberikannya kepada penyedia untuk digunakan untuk membuat permintaan OAuth2) kita harus memberikannya ReactiveOAuth2AuthorizedClientProvideralih - alih non-reaktif OAuth2AuthorizedClientProvider. Sesuai dengan dokumentasi pegas-keamanan jika Anda menggunakan non-reaktif, DefaultClientCredentialsTokenResponseClientAnda dapat menggunakan .setRequestEntityConverter()metode ini untuk mengubah permintaan token OAuth2, tetapi padanan reaktif WebClientReactiveClientCredentialsTokenResponseClienttidak menyediakan fasilitas ini, jadi kami harus mengimplementasikan sendiri (kami dapat menggunakan WebClientReactiveClientCredentialsTokenResponseClientlogika yang ada ).

Implementasi saya dipanggil UaaWebClientReactiveClientCredentialsTokenResponseClient(implementasi dihilangkan karena hanya sedikit mengubah metode headers()dan body()dari default WebClientReactiveClientCredentialsTokenResponseClientuntuk menambahkan beberapa header / bidang tambahan, itu tidak mengubah aliran auth yang mendasarinya).

Konfigurasikan WebClient

The ServerOAuth2AuthorizedClientExchangeFilterFunction.setClientCredentialsTokenResponseClient()Metode telah usang, sehingga mengikuti saran bantahan dari metode yang:

Usang. Gunakan ServerOAuth2AuthorizedClientExchangeFilterFunction(ReactiveOAuth2AuthorizedClientManager)sebagai gantinya. Buat instance dari yang ClientCredentialsReactiveOAuth2AuthorizedClientProviderdikonfigurasi dengan WebClientReactiveClientCredentialsTokenResponseClient(atau yang kustom) dan kemudian berikan DefaultReactiveOAuth2AuthorizedClientManager.

Ini berakhir dengan konfigurasi yang terlihat seperti:

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

Gunakan WebClientseperti biasa

The oAuth2WebClientkacang sekarang siap untuk digunakan untuk mengakses sumber daya yang dilindungi oleh penyedia OAuth2 dikonfigurasi kami dalam cara Anda akan membuat permintaan lain menggunakan WebClient.

Matt Williams
sumber
Bagaimana cara saya melewati titik klien-rahasia, klien-rahasia dan oauth secara terprogram?
monti
Saya belum mencoba ini, tetapi sepertinya Anda dapat membuat contoh ClientRegistrations dengan rincian yang diperlukan dan mengirimkannya ke konstruktor untuk InMemoryReactiveClientRegistrationRepository(implementasi default ReactiveClientRegistrationRepository). Anda kemudian menggunakan InMemoryReactiveClientRegistrationRepositorykacang yang baru dibuat itu sebagai pengganti autowired saya clientRegistrationRepositoryyang dimasukkan ke dalam oauthFilteredWebClientmetode
Matt Williams
Mh, tapi saya tidak bisa mendaftar berbeda ClientRegistrationsaat runtime, kan? Sejauh yang saya mengerti saya perlu membuat kacang ClientRegistrationsaat startup.
monti
Ah ok, saya pikir Anda hanya ingin tidak mendeklarasikannya dalam application.propertiesfile. Menerapkan sendiri ReactiveOAuth2AccessTokenResponseClientmemungkinkan Anda untuk membuat permintaan apa pun yang Anda inginkan untuk mendapatkan token OAuth2, tapi saya tidak tahu bagaimana Anda bisa memberikan "konteks 'dinamis untuk setiap permintaan. Hal yang sama berlaku jika Anda menerapkan seluruh filter Anda sendiri. Semua ini akan memberi Anda akses ke permintaan keluar, jadi kecuali Anda dapat menyimpulkan apa yang Anda butuhkan dari sana, saya tidak yakin apa pilihan Anda. Apa kasus penggunaan Anda? Mengapa Anda tidak tahu kemungkinan pendaftaran saat startup?
Matt Williams
1

Saya menemukan @matt Williams menjawab cukup membantu. Meskipun saya ingin menambahkan seandainya seseorang ingin secara program melewati clientId dan rahasia untuk konfigurasi WebClient. Ini dia bagaimana bisa dilakukan.

 @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);
    }
}
Pelari
sumber
0

Hai mungkin sudah terlambat namun RestTemplate masih didukung di Spring Security 5, untuk aplikasi non-reaktif RestTemplate masih digunakan yang harus Anda lakukan hanya mengkonfigurasi keamanan pegas dengan benar dan membuat interseptor seperti yang disebutkan pada panduan migrasi

Gunakan konfigurasi berikut untuk menggunakan aliran 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

Konfigurasi ke 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;
    }

}

Pencegat

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

Ini akan menghasilkan access_token dalam panggilan pertama dan kapan pun token itu kedaluwarsa. OAuth2AuthorizedClientManager akan mengelola semua ini untuk Anda

Leandro Assis
sumber