Spring Security dan OpenID Connect

Perhatikan bahawa artikel ini telah dikemas kini ke timbunan Spring Security OAuth 2.0 yang baru. Tutorial menggunakan timbunan lama masih tersedia.

1. Gambaran keseluruhan

Dalam tutorial ringkas ini, kita akan fokus untuk menyiapkan OpenID Connect (OIDC) dengan Spring Security.

Kami akan membentangkan aspek yang berbeza dari spesifikasi ini, dan kemudian kami akan melihat sokongan yang ditawarkan Spring Security untuk melaksanakannya pada Pelanggan OAuth 2.0.

2. Pengenalan OpenID Connect Pantas

OpenID Connect adalah lapisan identiti yang dibina di atas protokol OAuth 2.0.

Oleh itu, sangat penting untuk mengetahui OAuth 2.0 sebelum menyelami OIDC, terutamanya aliran Kod Pengesahan.

Suite spesifikasi OIDC luas; ini merangkumi ciri teras dan beberapa keupayaan pilihan lain, yang disajikan dalam kumpulan yang berbeza. Yang utama adalah:

  • Teras: pengesahan dan penggunaan Tuntutan untuk menyampaikan maklumat Pengguna Akhir
  • Penemuan: menetapkan bagaimana pelanggan dapat menentukan maklumat secara dinamik mengenai Pembekal OpenID
  • Pendaftaran Dinamik: menentukan bagaimana pelanggan boleh mendaftar dengan pembekal
  • Pengurusan Sesi: menentukan bagaimana menguruskan sesi OIDC

Di atas semua ini, dokumen membezakan OAuth 2.0 Authentication Servers yang menawarkan sokongan untuk spesifikasi ini, merujuk kepada mereka sebagai "OpenID Providers" (OP) dan OAuth 2.0 Clients yang menggunakan OIDC sebagai Relying Parties (RP). Kami akan mematuhi terminologi ini dalam artikel ini.

Perlu diketahui juga bahawa pelanggan boleh meminta penggunaan pelanjutan ini dengan menambahkan ruang lingkup terbuka dalam Permintaan Kebenarannya.

Akhirnya, satu aspek lain yang berguna untuk difahami untuk tutorial ini adalah hakikat bahawa OP memancarkan maklumat Pengguna Akhir sebagai JWT yang disebut "ID Token".

Sekarang ya, kami bersedia untuk menyelami dunia OIDC dengan lebih mendalam.

3. Penyediaan Projek

Sebelum memberi tumpuan kepada pembangunan sebenar, kita harus mendaftarkan Pelanggan OAuth 2.o dengan Pembekal OpenID kami.

Dalam kes ini, kami akan menggunakan Google sebagai Pembekal OpenID. Kami boleh mengikuti arahan ini untuk mendaftarkan aplikasi pelanggan kami di platform mereka. Perhatikan bahawa ruang lingkup openid ada secara lalai.

URI Redirect yang kami tetapkan dalam proses ini adalah titik akhir dalam perkhidmatan kami: // localhost: 8081 / login / oauth2 / code / google.

Kita harus mendapatkan Id Pelanggan dan Rahsia Pelanggan dari proses ini.

3.1. Konfigurasi Maven

Kami akan memulakan dengan menambahkan kebergantungan ini ke fail pom projek kami:

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

Artifak pemula menggabungkan semua pergantungan Pelanggan Keselamatan Spring, termasuk:

  • yang bunga-keselamatan-OAuth2-pelanggan pergantungan untuk OAuth 2.0 Daftar dan fungsi Pelanggan
  • perpustakaan JOSE untuk sokongan JWT

Seperti biasa, kita dapat menemui versi terbaru artifak ini menggunakan mesin carian Maven Central.

4. Konfigurasi Asas Menggunakan Spring Boot

Pertama, kita akan mulakan dengan mengkonfigurasi aplikasi kita untuk menggunakan pendaftaran pelanggan yang baru kita buat dengan Google.

Menggunakan Spring Boot menjadikannya sangat mudah, kerana yang harus kita lakukan adalah menentukan dua sifat aplikasi:

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

Mari melancarkan aplikasi kami dan cuba mengakses titik akhir sekarang. Kami akan melihat bahawa kami diarahkan ke halaman Log masuk Google untuk Pelanggan OAuth 2.0 kami.

Ia kelihatan sangat sederhana, tetapi terdapat banyak perkara yang berlaku di sini. Seterusnya, kita akan meneroka bagaimana Spring Security mengeluarkannya.

Dahulu, di pos Sokongan WebClient dan OAuth 2 kami, kami menganalisis dalaman mengenai bagaimana Spring Security menangani Pelayan dan Pelanggan Pembenaran OAuth 2.0.

Di sana, kami melihat bahawa kami harus memberikan data tambahan, selain dari Client Id dan Client Secret, untuk mengkonfigurasi instance ClientRegistration dengan jayanya. Jadi, bagaimana ini berfungsi?

Jawapannya adalah, Google adalah penyedia terkenal, dan oleh itu kerangka kerja menawarkan beberapa sifat yang telah ditentukan untuk menjadikan semuanya lebih mudah.

Kita dapat melihat konfigurasi tersebut di enum CommonOAuth2Provider .

Untuk Google, jenis yang dihitung menentukan sifat seperti:

  • skop lalai yang akan digunakan
  • titik akhir Pengesahan
  • titik akhir Token
  • titik akhir UserInfo, yang juga merupakan sebahagian daripada spesifikasi Teras OIDC

4.1. Mengakses Maklumat Pengguna

Spring Security menawarkan perwakilan berguna Pengetua pengguna yang berdaftar dengan Penyedia OIDC, entiti OidcUser .

Selain daripada kaedah asas OAuth2AuthenticatedPrincipal , entiti ini menawarkan beberapa fungsi yang berguna:

  • dapatkan semula nilai Token ID dan Tuntutan yang terkandung di dalamnya
  • dapatkan Tuntutan yang diberikan oleh titik akhir UserInfo
  • menghasilkan agregat bagi dua set

Kita boleh mengakses entiti ini dengan mudah dalam pengawal:

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

Sekiranya kami memeriksa pengetua, kami akan melihat banyak maklumat berguna di sini, seperti nama pengguna, e-mel, gambar profil, dan tempat.

Selain itu, penting untuk diperhatikan bahawa Spring menambahkan pihak berkuasa kepada prinsipal berdasarkan ruang lingkup yang diterima dari penyedia, yang diawali dengan " SCOPE_ ". Contohnya, skop openid menjadi autoriti yang diberi SCOPE_openid .

Pihak berkuasa ini dapat digunakan untuk menyekat 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 dalam Tindakan

Sejauh ini, kami telah belajar bagaimana kami dapat dengan mudah melaksanakan penyelesaian Login OIDC menggunakan Spring Security

Kami telah melihat faedah yang diberikannya dengan mendelegasikan proses pengenalan pengguna kepada Pembekal OpenID, yang seterusnya memberikan maklumat berguna terperinci, bahkan dengan cara yang dapat ditingkatkan.

Tetapi sebenarnya, kita tidak perlu menangani aspek khusus OIDC setakat ini. Ini bermakna Spring melakukan sebahagian besar pekerjaan untuk kita.

Oleh itu, kita akan melihat apa yang berlaku di belakang tabir untuk memahami dengan lebih baik bagaimana spesifikasi ini dilaksanakan dan dapat memanfaatkannya sepenuhnya.

5.1. Proses Log Masuk

Untuk melihatnya dengan jelas, mari aktifkan log RestTemplate untuk melihat permintaan yang dilakukan oleh perkhidmatan:

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

If we call a secured endpoint now, we'll see the service is carrying out the regular OAuth 2.0 Authorization Code Flow. That's because, as we said, this specification is built on top of OAuth 2.0. There are, anyway, some differences.

Firstly, depending on the provider we're using and the scopes we've configured, we might see that the service is making a call to the UserInfo endpoint we mentioned at the beginning.

Namely, if the Authorization Response retrieves at least one of profile, email, address or phone scope, the framework will call the UserInfo endpoint to obtain additional information.

Even though everything would indicate that Google should retrieve the profile and the email scope – since we're using them in the Authorization Request – the OP retrieves their custom counterparts instead, //www.googleapis.com/auth/userinfo.email and //www.googleapis.com/auth/userinfo.profile, thus Spring doesn't call the endpoint.

This means that all the information we're obtaining is part of the ID Token.

We can adapt to this behavior by creating and providing our own OidcUserService instance:

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

The second difference we'll observe is a call to the JWK Set URI. As we explained in our JWS and JWK post, this is used to verify the JWT-formatted ID Token signature.

Next, we'll analyze the ID Token in detail.

5.2. The ID Token

Naturally, the OIDC spec covers and adapts to a lot of different scenarios. In this case, we're using the Authorization Code flow, and the protocol indicates that both the Access Token and the ID Token will be retrieved as part of the Token Endpoint response.

As we said before, the OidcUser entity contains the Claims contained in the ID Token, and the actual JWT-formatted token, which can be inspected using jwt.io.

On top of this, Spring offers many handy getters to obtain the standard Claims defined by the specification in a clean manner.

We can see the ID Token includes some mandatory Claims:

  • the issuer identifier formatted as a URL (e.g. “//accounts.google.com“)
  • a subject id, which is a reference of the End-User contained by the issuer
  • 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.

Sekiranya kita memeriksa log Rangkaian di konsol debug penyemak imbas, kita akan melihat kita diarahkan ke titik akhir log keluar OP sebelum akhirnya mengakses URI Redirect yang kita konfigurasikan.

Lain kali kita mengakses titik akhir dalam aplikasi kita yang memerlukan pengesahan, kita mesti masuk lagi di platform OP kita untuk mendapatkan kebenaran.

8. Kesimpulannya

Ringkasnya, dalam tutorial ini kita belajar banyak tentang penyelesaian yang ditawarkan oleh OpenID Connect, dan bagaimana kita dapat menerapkan beberapa dari mereka menggunakan Spring Security.

Seperti biasa, semua contoh lengkap boleh didapati di repo GitHub kami.