Spring Security und OpenID Connect

Beachten Sie, dass dieser Artikel auf den neuen OAuth 2.0-Stack von Spring Security aktualisiert wurde. Das Tutorial mit dem Legacy-Stack ist jedoch weiterhin verfügbar.

1. Übersicht

In diesem kurzen Tutorial konzentrieren wir uns auf das Einrichten von OpenID Connect (OIDC) mit Spring Security.

Wir werden verschiedene Aspekte dieser Spezifikation vorstellen und dann die Unterstützung sehen, die Spring Security bietet, um sie auf einem OAuth 2.0-Client zu implementieren.

2. Quick OpenID Connect Einführung

OpenID Connect ist eine Identitätsschicht, die auf dem OAuth 2.0-Protokoll aufbaut.

Daher ist es sehr wichtig, OAuth 2.0 zu kennen, bevor Sie in OIDC eintauchen, insbesondere in den Ablauf des Autorisierungscodes.

Die OIDC-Spezifikationssuite ist umfangreich. Es enthält Kernfunktionen und mehrere andere optionale Funktionen, die in verschiedenen Gruppen dargestellt werden. Die wichtigsten sind:

  • Kern: Authentifizierung und Verwendung von Ansprüchen zur Übermittlung von Endbenutzerinformationen
  • Ermittlung: Legt fest, wie ein Client Informationen zu OpenID-Anbietern dynamisch ermitteln kann
  • Dynamische Registrierung: Legt fest, wie sich ein Client bei einem Anbieter registrieren kann
  • Sitzungsverwaltung: Definiert, wie OIDC-Sitzungen verwaltet werden

Darüber hinaus unterscheiden die Dokumente die OAuth 2.0-Authentifizierungsserver, die diese Spezifikation unterstützen, und bezeichnen sie als „OpenID-Anbieter“ (OPs) und die OAuth 2.0-Clients, die OIDC als vertrauende Parteien (RPs) verwenden. Wir werden uns in diesem Artikel an diese Terminologie halten.

Es ist auch wichtig zu wissen, dass ein Client die Verwendung dieser Erweiterung anfordern kann, indem er den Bereich openid in seine Autorisierungsanforderung einfügt .

Ein weiterer Aspekt, der für dieses Lernprogramm hilfreich ist, ist die Tatsache, dass die OPs Endbenutzerinformationen als JWT ausgeben, das als „ID-Token“ bezeichnet wird.

Ja, wir sind bereit, tiefer in die OIDC-Welt einzutauchen.

3. Projekteinrichtung

Bevor wir uns auf die eigentliche Entwicklung konzentrieren können, müssen wir einen OAuth 2.o-Client bei unserem OpenID-Anbieter registrieren.

In diesem Fall verwenden wir Google als OpenID-Anbieter. Wir können diesen Anweisungen folgen, um unsere Kundenanwendung auf ihrer Plattform zu registrieren. Beachten Sie, dass der Bereich openid standardmäßig vorhanden ist.

Der in diesem Prozess eingerichtete Redirect-URI ist ein Endpunkt in unserem Dienst: // localhost: 8081 / login / oauth2 / code / google.

Wir sollten aus diesem Prozess eine Kunden-ID und ein Kundengeheimnis erhalten.

3.1. Maven-Konfiguration

Wir beginnen mit dem Hinzufügen dieser Abhängigkeiten zu unserer Projekt-POM-Datei:

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

Das Starter-Artefakt fasst alle Spring Security Client-bezogenen Abhängigkeiten zusammen, einschließlich:

  • Die Spring-Security-Oauth2-Client- Abhängigkeit für die OAuth 2.0 -Anmelde- und Client-Funktionalität
  • die JOSE-Bibliothek für JWT-Unterstützung

Wie üblich können wir die neueste Version dieses Artefakts mithilfe der Maven Central-Suchmaschine finden.

4. Grundkonfiguration mit Spring Boot

Zunächst konfigurieren wir unsere Anwendung so, dass sie die gerade bei Google erstellte Kundenregistrierung verwendet.

Die Verwendung von Spring Boot macht dies sehr einfach, da wir lediglich zwei Anwendungseigenschaften definieren müssen:

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

Lassen Sie uns unsere Anwendung starten und versuchen, jetzt auf einen Endpunkt zuzugreifen. Wir werden sehen, dass wir zu einer Google-Anmeldeseite für unseren OAuth 2.0-Client weitergeleitet werden.

Es sieht wirklich einfach aus, aber hier ist unter der Haube eine Menge los. Als nächstes werden wir untersuchen, wie Spring Security dies schafft.

Zuvor haben wir in unserem WebClient- und OAuth 2-Supportbeitrag die Interna analysiert, wie Spring Security mit OAuth 2.0-Autorisierungsservern und -Clients umgeht.

Dort haben wir gesehen, dass wir neben der Client-ID und dem Client-Geheimnis zusätzliche Daten bereitstellen müssen, um eine ClientRegistration- Instanz erfolgreich zu konfigurieren . Wie funktioniert das?

Die Antwort lautet: Google ist ein bekannter Anbieter. Daher bietet das Framework einige vordefinierte Eigenschaften, um die Arbeit zu vereinfachen.

Wir können uns diese Konfigurationen in der CommonOAuth2Provider-Enumeration ansehen .

Für Google definiert der Aufzählungstyp Eigenschaften wie:

  • Die Standardbereiche, die verwendet werden
  • der Autorisierungsendpunkt
  • der Token-Endpunkt
  • der UserInfo-Endpunkt, der auch Teil der OIDC Core-Spezifikation ist

4.1. Zugriff auf Benutzerinformationen

Spring Security bietet eine nützliche Darstellung eines Benutzer-Principals, der bei einem OIDC-Anbieter, der OidcUser- Entität , registriert ist .

Neben den grundlegenden OAuth2AuthenticatedPrincipal- Methoden bietet diese Entität einige nützliche Funktionen:

  • Rufen Sie den ID-Token-Wert und die darin enthaltenen Ansprüche ab
  • Erhalten Sie die vom UserInfo-Endpunkt bereitgestellten Ansprüche
  • Generieren Sie ein Aggregat der beiden Sätze

Wir können leicht auf diese Entität in einem Controller zugreifen:

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

Oder indem Sie den SecurityContextHolder in einer Bean verwenden:

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

Wenn wir den Auftraggeber überprüfen, sehen wir hier viele nützliche Informationen, wie den Namen des Benutzers, die E-Mail-Adresse, das Profilbild und das Gebietsschema.

Darüber hinaus ist zu beachten, dass Spring dem Principal Berechtigungen hinzufügt, basierend auf den vom Anbieter erhaltenen Bereichen mit dem Präfix „ SCOPE_ “. Beispielsweise wird der openid- Bereich zu einer SCOPE_openid- Berechtigung.

Mit diesen Berechtigungen kann der Zugriff auf bestimmte Ressourcen eingeschränkt werden, z. B .:

@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 in Aktion

Bisher haben wir gelernt, wie wir mit Spring Security einfach eine OIDC-Anmeldelösung implementieren können

Wir haben den Vorteil gesehen, den es mit sich bringt, den Benutzeridentifizierungsprozess an einen OpenID-Anbieter zu delegieren, der wiederum detaillierte nützliche Informationen liefert, selbst auf skalierbare Weise.

Aber die Wahrheit ist, wir mussten uns bisher mit keinem OIDC-spezifischen Aspekt befassen. Dies bedeutet, dass der Frühling den größten Teil der Arbeit für uns erledigt.

Daher werden wir sehen, was sich hinter den Kulissen abspielt, um besser zu verstehen, wie diese Spezifikation umgesetzt wird, und um das Beste daraus zu machen.

5.1. Der Anmeldevorgang

Um dies klar zu sehen, aktivieren wir die RestTemplate- Protokolle, um die Anforderungen anzuzeigen , die der Dienst ausführt:

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.

Wenn wir die Netzwerkprotokolle in der Browser-Debug-Konsole überprüfen, werden wir feststellen, dass wir zu einem OP-Abmeldeendpunkt umgeleitet wurden, bevor wir schließlich auf den von uns konfigurierten Redirect-URI zugreifen.

Wenn wir das nächste Mal auf einen Endpunkt in unserer Anwendung zugreifen, für den eine Authentifizierung erforderlich ist, müssen wir uns zwingend erneut bei unserer OP-Plattform anmelden, um Berechtigungen zu erhalten.

8. Fazit

Zusammenfassend haben wir in diesem Tutorial viel über die von OpenID Connect angebotenen Lösungen und darüber gelernt, wie wir einige davon mit Spring Security implementieren können.

Wie immer finden Sie alle vollständigen Beispiele in unserem GitHub-Repo.