Keamanan Musim Semi dan OpenID Connect

Perhatikan bahwa artikel ini telah diperbarui ke tumpukan OAuth 2.0 Spring Security baru. Namun, tutorial menggunakan tumpukan warisan masih tersedia.

1. Ikhtisar

Dalam tutorial singkat ini, kami akan fokus pada menyiapkan OpenID Connect (OIDC) dengan Keamanan Musim Semi.

Kami akan menyajikan berbagai aspek spesifikasi ini, kemudian kami akan melihat dukungan yang ditawarkan Spring Security untuk menerapkannya pada Klien OAuth 2.0.

2. Pendahuluan Quick OpenID Connect

OpenID Connect adalah lapisan identitas yang dibangun di atas protokol OAuth 2.0.

Oleh karena itu, sangat penting untuk mengetahui OAuth 2.0 sebelum mempelajari OIDC, terutama alur Kode Otorisasi.

Paket spesifikasi OIDC sangat luas; ini mencakup fitur inti dan beberapa kemampuan opsional lainnya, yang disajikan dalam kelompok berbeda. Yang utama adalah:

  • Inti: otentikasi dan penggunaan Klaim untuk mengkomunikasikan informasi Pengguna Akhir
  • Penemuan: menetapkan bagaimana klien dapat secara dinamis menentukan informasi tentang Penyedia OpenID
  • Pendaftaran Dinamis: menentukan bagaimana klien dapat mendaftar dengan penyedia
  • Manajemen Sesi: menentukan cara mengelola sesi OIDC

Selain itu, dokumen tersebut membedakan Server Autentikasi OAuth 2.0 yang menawarkan dukungan untuk spesifikasi ini, menyebutnya sebagai "Penyedia OpenID" (OP) dan Klien OAuth 2.0 yang menggunakan OIDC sebagai Pihak Bergantung (RP). Kami akan mengikuti terminologi ini di artikel ini.

Perlu diketahui juga bahwa klien dapat meminta penggunaan ekstensi ini dengan menambahkan cakupan openid dalam Permintaan Otorisasi.

Terakhir, satu aspek lain yang berguna untuk dipahami untuk tutorial ini adalah fakta bahwa OP memancarkan informasi Pengguna Akhir sebagai JWT yang disebut "ID Token".

Sekarang ya, kami siap untuk terjun lebih dalam ke dunia OIDC.

3. Pengaturan Proyek

Sebelum fokus pada pengembangan sebenarnya, kita harus mendaftarkan Klien OAuth 2.o ke Penyedia OpenID kita.

Dalam kasus ini, kami akan menggunakan Google sebagai Penyedia OpenID. Kami dapat mengikuti petunjuk ini untuk mendaftarkan aplikasi klien kami di platform mereka. Perhatikan bahwa lingkup openid ada secara default.

URI Pengalihan yang kami siapkan dalam proses ini adalah titik akhir dalam layanan kami: // localhost: 8081 / login / oauth2 / code / google.

Kita harus mendapatkan Id Klien dan Rahasia Klien dari proses ini.

3.1. Konfigurasi Maven

Kami akan mulai dengan menambahkan dependensi ini ke file pom proyek kami:

 org.springframework.boot spring-boot-starter-oauth2-client 2.2.6.RELEASE 

Artefak starter menggabungkan semua dependensi terkait Spring Security Client, termasuk:

  • yang semi-keamanan-OAuth2-klien ketergantungan untuk OAuth 2.0 Login dan fungsi Client
  • perpustakaan JOSE untuk dukungan JWT

Seperti biasa, kami dapat menemukan versi terbaru dari artefak ini menggunakan mesin pencari Maven Central.

4. Konfigurasi Dasar Menggunakan Spring Boot

Pertama, kita akan mulai dengan mengkonfigurasi aplikasi kita untuk menggunakan pendaftaran klien yang baru saja kita buat dengan Google.

Menggunakan Spring Boot membuatnya sangat mudah, karena yang harus kita lakukan adalah menentukan dua properti aplikasi:

spring: security: oauth2: client: registration: google: client-id:  client-secret: 

Mari luncurkan aplikasi kita dan coba akses titik akhir sekarang. Kami akan melihat bahwa kami dialihkan ke halaman Login Google untuk Klien OAuth 2.0 kami.

Ini terlihat sangat sederhana, tetapi ada cukup banyak hal yang terjadi di balik terpal di sini. Selanjutnya, kita akan menjelajahi bagaimana Keamanan Musim Semi melakukan ini.

Sebelumnya, di postingan Dukungan WebClient dan OAuth 2, kami menganalisis internal tentang cara Keamanan Spring menangani Server dan Klien Otorisasi OAuth 2.0.

Di sana, kami melihat bahwa kami harus menyediakan data tambahan, selain Id Klien dan Rahasia Klien, untuk mengonfigurasi instance ClientRegistration dengan sukses. Jadi, bagaimana cara kerjanya?

Jawabannya adalah, Google adalah penyedia terkenal, dan oleh karena itu kerangka kerja menawarkan beberapa properti yang telah ditentukan sebelumnya untuk mempermudah.

Kita dapat melihat konfigurasi tersebut di enum CommonOAuth2Provider .

Untuk Google, jenis enumerasi mendefinisikan properti seperti:

  • cakupan default yang akan digunakan
  • titik akhir Otorisasi
  • titik akhir Token
  • titik akhir UserInfo, yang juga merupakan bagian dari spesifikasi OIDC Core

4.1. Mengakses Informasi Pengguna

Spring Security menawarkan representasi berguna dari Principal pengguna yang terdaftar di Penyedia OIDC, entitas OidcUser .

Selain metode OAuth2AuthenticatedPrincipal dasar , entitas ini menawarkan beberapa fungsionalitas yang berguna:

  • mengambil nilai Token ID dan Klaim yang dikandungnya
  • dapatkan Klaim yang disediakan oleh titik akhir UserInfo
  • menghasilkan gabungan dari dua set

Kami dapat dengan mudah mengakses entitas ini di pengontrol:

@GetMapping("/oidc-principal") public OidcUser getOidcUserPrincipal( @AuthenticationPrincipal OidcUser principal) { return principal; }

Atau dengan menggunakan SecurityContextHolder dalam kacang:

Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication.getPrincipal() instanceof OidcUser) { OidcUser principal = ((OidcUser) authentication.getPrincipal()); // ... }

Jika kita memeriksa kepala sekolah, kita akan melihat banyak informasi berguna di sini, seperti nama pengguna, email, gambar profil, dan lokal.

Selain itu, penting untuk diperhatikan bahwa Spring menambahkan otoritas ke prinsipal berdasarkan cakupan yang diterima dari penyedia, diawali dengan “ SCOPE_ “. Misalnya, cakupan openid menjadi SCOPE_openid yang diberikan otoritas.

Otoritas ini dapat digunakan untuk membatasi akses ke sumber daya tertentu, misalnya :

@EnableWebSecurity public class MappedAuthorities extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) { http .authorizeRequests(authorizeRequests -> authorizeRequests .mvcMatchers("/my-endpoint") .hasAuthority("SCOPE_openid") .anyRequest().authenticated() ); } }

5. OIDC beraksi

Sejauh ini, kita telah mempelajari bagaimana kita dapat dengan mudah mengimplementasikan solusi OIDC Login menggunakan Spring Security

Kami telah melihat manfaat yang dibawanya dengan mendelegasikan proses identifikasi pengguna ke Penyedia OpenID, yang, pada gilirannya, memberikan informasi berguna yang terperinci, bahkan dengan cara yang dapat diskalakan.

Namun kenyataannya, kami tidak harus berurusan dengan aspek khusus OIDC sejauh ini. Ini berarti Spring melakukan sebagian besar pekerjaan untuk kami.

Karenanya, kita akan melihat apa yang terjadi di balik layar untuk memahami lebih baik bagaimana spesifikasi ini diterapkan dan dapat memaksimalkannya.

5.1. Proses Login

Untuk melihat ini dengan jelas, mari aktifkan log RestTemplate untuk melihat permintaan yang dilakukan layanan:

logging: level: org.springframework.web.client.RestTemplate: DEBUG

Jika kita memanggil endpoint aman sekarang, kita akan melihat layanan menjalankan Alur Kode Otorisasi OAuth 2.0 reguler. Itu karena, seperti yang kami katakan, spesifikasi ini dibuat di atas OAuth 2.0. Bagaimanapun, ada beberapa perbedaan.

Pertama, bergantung pada penyedia yang kami gunakan dan cakupan yang telah kami konfigurasikan, kami mungkin melihat bahwa layanan melakukan panggilan ke titik akhir UserInfo yang kami sebutkan di awal.

Yaitu, jika Respons Otorisasi mengambil setidaknya satu profil , email , alamat , atau cakupan telepon , kerangka kerja akan memanggil titik akhir UserInfo untuk mendapatkan informasi tambahan.

Meskipun semuanya akan menunjukkan bahwa Google harus mengambil profil dan cakupan email - karena kami menggunakannya dalam Permintaan Otorisasi - OP mengambil mitra kustom mereka, //www.googleapis.com/auth/userinfo.email dan / /www.googleapis.com/auth/userinfo.profile , sehingga Spring tidak memanggil titik akhir.

Artinya semua informasi yang kami peroleh adalah bagian dari ID Token.

Kita dapat beradaptasi dengan perilaku ini dengan membuat dan menyediakan contoh OidcUserService kita sendiri :

@Configuration public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { Set googleScopes = new HashSet(); googleScopes.add( "//www.googleapis.com/auth/userinfo.email"); googleScopes.add( "//www.googleapis.com/auth/userinfo.profile"); OidcUserService googleUserService = new OidcUserService(); googleUserService.setAccessibleScopes(googleScopes); http .authorizeRequests(authorizeRequests -> authorizeRequests .anyRequest().authenticated()) .oauth2Login(oauthLogin -> oauthLogin .userInfoEndpoint() .oidcUserService(googleUserService)); } }

Perbedaan kedua yang akan kita amati adalah panggilan ke JWK Set URI. Seperti yang kami jelaskan di posting JWS dan JWK kami, ini digunakan untuk memverifikasi tanda tangan ID Token berformat JWT.

Selanjutnya, kami akan menganalisis Token ID secara rinci.

5.2. Token ID

Secara alami, spesifikasi OIDC mencakup dan beradaptasi dengan banyak skenario berbeda. Dalam kasus ini, kami menggunakan alur Kode Otorisasi, dan protokol menunjukkan bahwa Token Akses dan Token ID akan diambil sebagai bagian dari respons Token Endpoint.

Seperti yang kami katakan sebelumnya, entitas OidcUser berisi Klaim yang terdapat dalam Token ID, dan token berformat JWT yang sebenarnya, yang dapat diperiksa menggunakan jwt.io.

Selain itu, Spring menawarkan banyak pengambil yang berguna untuk mendapatkan Klaim standar yang ditentukan oleh spesifikasi dengan cara yang bersih.

Kita dapat melihat Token ID menyertakan beberapa Klaim wajib:

  • pengenal penerbit yang diformat sebagai URL (misalnya “ //accounts.google.com “)
  • id subjek, yang merupakan referensi Pengguna Akhir yang terkandung oleh penerbit
  • the expiration time for the token
  • time at which the token was issued
  • the audience, which will contain the OAuth 2.0 Client id we've configured

And also many OIDC Standard Claims like the ones we mentioned before (name, locale, picture, email).

As these are standard, we can expect many providers to retrieve at least some of these fields, and therefore facilitating the development of simpler solutions.

5.3. Claims and Scopes

As we can imagine, the Claims that are retrieved by the OP correspond with the scopes we (or Spring Security) configured.

OIDC defines some scopes that can be used to request the Claims defined by OIDC:

  • profile, which can be used to request default profile Claims (e.g. name, preferred_username,picture, etcetera)
  • email, to access to the email and email_verified Claims
  • address
  • phone, to requests the phone_number and phone_number_verified Claims

Even though Spring doesn't support it yet, the spec allows requesting single Claims by specifying them in the Authorization Request.

6. Spring Support for OIDC Discovery

As we explained in the introduction, OIDC includes many different features apart from its core purpose.

The capabilities we're going to analyze in this section and the following are optional in OIDC. Hence, it's important to understand that there might be OPs that don't support them.

The specification defines a Discovery mechanism for an RP to discover the OP and obtain information needed to interact with it.

In a nutshell, OPs provide a JSON document of standard metadata. The information must be served by a well-known endpoint of the issuer location, /.well-known/openid-configuration.

Spring benefits from this by allowing us to configure a ClientRegistration with just one simple property, the issuer location.

But let's jump right into an example to see this clearly.

We'll define a custom ClientRegistration instance:

spring: security: oauth2: client: registration: custom-google: client-id:  client-secret:  provider: custom-google: issuer-uri: //accounts.google.com

Now we can restart our application and check the logs to confirm the application is calling the openid-configuration endpoint in the startup process.

We can even browse this endpoint to have a look at the information provided by Google:

//accounts.google.com/.well-known/openid-configuration

We can see, for example, the Authorization, the Token and the UserInfo endpoints that the service has to use, and the supported scopes.

An especially relevant note here is the fact that if the Discovery endpoint is not available at the time the service launches, then our app won't be able to complete the startup process successfully.

7. OpenID Connect Session Management

This specification complements the Core functionality by defining:

  • different ways to monitor the End-User's login status at the OP on an ongoing basis so that the RP can log out an End-User who has logged out of the OpenID Provider
  • the possibility of registering RP logout URIs with the OP as part of the Client registration, so as to be notified when the End-User logs out of the OP
  • a mechanism to notify the OP that the End-User has logged out of the site and might want to log out of the OP as well

Naturally, not all OPs support all of these items, and some of these solutions can be implemented only in a front-end implementation via the User-Agent.

In this tutorial, we'll focus on the capabilities offered by Spring for the last item of the list, RP-initiated Logout.

At this point, if we log in to our application, we can normally access every endpoint.

If we logout (calling the /logout endpoint) and we make a request to a secured resource afterward, we'll see that we can get the response without having to log in again.

However, this is actually not true; if we inspect the Network tab in the browser debug console, we'll see that when we hit the secured endpoint the second time we get redirected to the OP Authorization Endpoint, and since we're still logged in there, the flow is completed transparently, ending up in the secured endpoint almost instantly.

Of course, this might not be the desired behavior in some cases. Let's see how we can implement this OIDC mechanism to deal with this.

7.1. The OpenID Provider Configuration

In this case, we'll be configuring and using an Okta instance as our OpenID Provider. We won't go into details on how to create the instance, but we can follow the steps of this guide, and keeping in mind that Spring Security's default callback endpoint will be /login/oauth2/code/okta.

In our application, we can define the client registration data with properties:

spring: security: oauth2: client: registration: okta: client-id:  client-secret:  provider: okta: issuer-uri: //dev-123.okta.com

OIDC indicates that the OP logout endpoint can be specified in the Discovery document, as the end_session_endpoint element.

7.2. The LogoutSuccessHandler Configuration

Next, we'll have to configure the HttpSecurity logout logic by providing a customized LogoutSuccessHandler instance:

@Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests(authorizeRequests -> authorizeRequests .mvcMatchers("/home").permitAll() .anyRequest().authenticated()) .oauth2Login(oauthLogin -> oauthLogin.permitAll()) .logout(logout -> logout .logoutSuccessHandler(oidcLogoutSuccessHandler())); }

Now let's see how we can create a LogoutSuccessHandler for this purpose using a special class provided by Spring Security, the OidcClientInitiatedLogoutSuccessHandler:

@Autowired private ClientRegistrationRepository clientRegistrationRepository; private LogoutSuccessHandler oidcLogoutSuccessHandler() { OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler = new OidcClientInitiatedLogoutSuccessHandler( this.clientRegistrationRepository); oidcLogoutSuccessHandler.setPostLogoutRedirectUri( URI.create("//localhost:8081/home")); return oidcLogoutSuccessHandler; }

Consequently, we'll need to set up this URI as a valid logout Redirect URI in the OP Client configuration panel.

Clearly, the OP logout configuration is contained in the client registration setup, since all we're using to configure the handler is the ClientRegistrationRepository bean present in the context.

So, what will happen now?

After we login to our application, we can send a request to the /logout endpoint provided by Spring Security.

Jika kami memeriksa log Jaringan di konsol debug browser, kami akan dialihkan ke titik akhir logout OP sebelum akhirnya mengakses URI Pengalihan yang kami konfigurasikan.

Lain kali kami mengakses titik akhir di aplikasi kami yang memerlukan otentikasi, kami wajib masuk lagi di platform OP kami untuk mendapatkan izin.

8. Kesimpulan

Untuk meringkas, dalam tutorial ini kami belajar banyak tentang solusi yang ditawarkan oleh OpenID Connect, dan bagaimana kami dapat mengimplementasikan beberapa di antaranya menggunakan Spring Security.

Seperti biasa, semua contoh lengkap dapat ditemukan di repo GitHub kami.