Spring Security en OpenID Connect

Merk op dat dit artikel is bijgewerkt naar de nieuwe Spring Security OAuth 2.0-stack. De tutorial met behulp van de legacy-stack is echter nog steeds beschikbaar.

1. Overzicht

In deze korte tutorial zullen we ons concentreren op het instellen van OpenID Connect (OIDC) met Spring Security.

We zullen verschillende aspecten van deze specificatie presenteren, en dan zullen we de ondersteuning zien die Spring Security biedt om deze op een OAuth 2.0 Client te implementeren.

2. Snelle OpenID Connect Connect-introductie

OpenID Connect is een identiteitslaag die bovenop het OAuth 2.0-protocol is gebouwd.

Het is dus erg belangrijk om OAuth 2.0 te kennen voordat je in OIDC duikt, vooral de autorisatiecode-stroom.

De OIDC-specificatiesuite is uitgebreid; het bevat kernfuncties en verschillende andere optionele mogelijkheden, gepresenteerd in verschillende groepen. De belangrijkste zijn:

  • Kern: authenticatie en gebruik van claims om informatie over de eindgebruiker te communiceren
  • Ontdekking: bepaalt hoe een klant informatie over OpenID-providers dynamisch kan bepalen
  • Dynamische registratie: bepaalt hoe een klant zich kan registreren bij een provider
  • Sessiebeheer: definieert hoe OIDC-sessies worden beheerd

Bovendien onderscheiden de documenten de OAuth 2.0 Authentication Servers die ondersteuning bieden voor deze specificatie, door naar hen te verwijzen als “OpenID Providers” (OP's) en de OAuth 2.0 Clients die OIDC gebruiken als Relying Parties (RP's). In dit artikel houden we ons aan deze terminologie.

Het is de moeite waard om te weten dat een klant het gebruik van deze extensie kan aanvragen door de extensie openid scope in zijn autorisatieverzoek.

Ten slotte is een ander aspect dat nuttig is om te begrijpen voor deze tutorial het feit dat de OP's eindgebruikersinformatie uitzenden als een JWT, een "ID Token" genaamd.

Nu ja, we zijn klaar om dieper in de OIDC-wereld te duiken.

3. Projectconfiguratie

Voordat we ons concentreren op de daadwerkelijke ontwikkeling, moeten we een OAuth 2.o-client registreren bij onze OpenID-provider.

In dit geval gebruiken we Google als OpenID-provider. We kunnen deze instructies volgen om onze clienttoepassing op hun platform te registreren. Merk op dat de openid scope is standaard aanwezig.

De omleidings-URI die we in dit proces hebben ingesteld, is een eindpunt in onze service: // localhost: 8081 / login / oauth2 / code / google.

We moeten van dit proces een klant-ID en een klantgeheim verkrijgen.

3.1. Maven-configuratie

We beginnen met het toevoegen van deze afhankelijkheden aan ons project pom-bestand:

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

Het startartefact verzamelt alle Spring Security Client-gerelateerde afhankelijkheden, waaronder:

  • de spring-security-oauth2-client afhankelijkheid voor OAuth 2.0 Login en Client-functionaliteit
  • de JOSE-bibliotheek voor JWT-ondersteuning

Zoals gewoonlijk kunnen we de nieuwste versie van dit artefact vinden met behulp van de Maven Central-zoekmachine.

4. Basisconfiguratie met Spring Boot

Ten eerste beginnen we met het configureren van onze applicatie om de clientregistratie te gebruiken die we zojuist met Google hebben gemaakt.

Het gebruik van Spring Boot maakt dit erg eenvoudig, omdat we alleen maar twee applicatie-eigenschappen hoeven te definiëren:

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

Laten we onze applicatie starten en nu proberen toegang te krijgen tot een eindpunt. We zullen zien dat we worden omgeleid naar een Google-inlogpagina voor onze OAuth 2.0-client.

Het ziet er heel simpel uit, maar er gebeuren hier nogal wat dingen onder de motorkap. Vervolgens zullen we onderzoeken hoe Spring Security dit voor elkaar krijgt.

Voorheen analyseerden we in onze WebClient- en OAuth 2-ondersteuningspost de internals over hoe Spring Security omgaat met OAuth 2.0-autorisatieservers en -clients.

Daarin zagen we dat we naast de Client-ID en het Clientgeheim aanvullende gegevens moeten verstrekken om een Klantregistratie instantie succesvol. Dus, hoe werkt dit?

Het antwoord is, Google is een bekende provider en daarom biedt het framework een aantal vooraf gedefinieerde eigenschappen om dingen gemakkelijker te maken.

We kunnen die configuraties bekijken in het CommonOAuth2Provider opsomming.

Voor Google definieert het opgesomde type eigenschappen zoals:

  • de standaardbereiken die zullen worden gebruikt
  • het autorisatie-eindpunt
  • het token-eindpunt
  • het UserInfo-eindpunt, dat ook deel uitmaakt van de OIDC Core-specificatie

4.1. Toegang tot gebruikersinformatie

Spring Security biedt een handige weergave van een gebruikers-Principal die is geregistreerd bij een OIDC-provider, de OidcUser entiteit.

Afgezien van de basis OAuth2AuthenticatedPrincipal methoden biedt deze entiteit enkele nuttige functionaliteit:

  • haal de ID Token-waarde op en de claims die deze bevat
  • verkrijg de claims die zijn verstrekt door het UserInfo-eindpunt
  • genereer een totaal van de twee sets

We hebben gemakkelijk toegang tot deze entiteit in een controller:

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

Of door de SecurityContextHolder in een boon:

Authenticatie authenticatie = SecurityContextHolder.getContext (). GetAuthentication (); if (authentication.getPrincipal () instantie van OidcUser) {OidcUser principal = ((OidcUser) authentication.getPrincipal ()); // ...}

Als we de opdrachtgever inspecteren, zien we hier veel nuttige informatie, zoals de naam van de gebruiker, het e-mailadres, de profielfoto en de landinstelling.

Verder is het belangrijk op te merken dat Spring bevoegdheden toevoegt aan de principal op basis van de scopes die het van de provider heeft ontvangen, voorafgegaan door 'TOEPASSINGSGEBIED_“. Bijvoorbeeld de openid scope wordt een SCOPE_openid verleende autoriteit.

Deze bevoegdheden kunnen bijvoorbeeld worden gebruikt om de toegang tot bepaalde bronnen te beperken:

@EnableWebSecurity openbare klasse MappedAuthorities breidt WebSecurityConfigurerAdapter uit {protected void configure (HttpSecurity http) {http .authorizeRequests (authorizeRequests -> authorizeRequests .mvcMatchers ("/ my-endpoint") .hasAuthority ("SCOPE_openequests"). ; }}

5. OIDC in actie

Tot nu toe hebben we geleerd hoe we eenvoudig een OIDC Login-oplossing kunnen implementeren met Spring Security

We hebben het voordeel gezien dat het met zich meebrengt door het gebruikersidentificatieproces te delegeren aan een OpenID-provider, die op zijn beurt gedetailleerde nuttige informatie levert, zelfs op een schaalbare manier.

Maar de waarheid is dat we tot nu toe niet met een OIDC-specifiek aspect te maken hebben gehad. Dit betekent dat Spring het meeste werk voor ons doet.

Daarom zullen we zien wat er achter de schermen gebeurt om beter te begrijpen hoe deze specificatie in praktijk wordt gebracht en om er het meeste uit te kunnen halen.

5.1. Het aanmeldingsproces

Laten we, om dit duidelijk te zien, het RestTemplate logboeken om de verzoeken te zien die de service uitvoert:

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

Als we nu een beveiligd eindpunt aanroepen, zien we dat de service de reguliere OAuth 2.0-autorisatiecodestroom uitvoert. Dat komt omdat, zoals we al zeiden, deze specificatie bovenop OAuth 2.0 is gebouwd. Er zijn sowieso enkele verschillen.

Ten eerste, afhankelijk van de provider die we gebruiken en de scopes die we hebben geconfigureerd, kunnen we zien dat de service een oproep doet naar het UserInfo-eindpunt dat we aan het begin noemden.

Namelijk, als het autorisatiereactie ten minste één van de profiel, e-mail, adres of telefoon scope, roept het framework het UserInfo-eindpunt aan om aanvullende informatie te verkrijgen.

Ook al zou alles erop wijzen dat Google het profiel en de e-mail bereik - aangezien we ze gebruiken in het autorisatieverzoek - haalt het OP in plaats daarvan hun aangepaste tegenhangers op, //www.googleapis.com/auth/userinfo.email en //www.googleapis.com/auth/userinfo.profile, dus Spring noemt het eindpunt niet.

Dit betekent dat alle informatie die we verkrijgen deel uitmaakt van het ID-token.

We kunnen ons aanpassen aan dit gedrag door ons eigen gedrag te creëren en aan te bieden OidcUserService voorbeeld:

@Configuration openbare klasse OAuth2LoginSecurityConfig breidt WebSecurityConfigurerAdapter uit {@Override protected void configure (HttpSecurity http) gooit uitzondering {Set googleScopes = new HashSet (); googleScopes.add ("//www.googleapis.com/auth/userinfo.email"); googleScopes.add ("//www.googleapis.com/auth/userinfo.profile"); OidcUserService googleUserService = nieuwe OidcUserService (); googleUserService.setAccessibleScopes (googleScopes); http .authorizeRequests (authorizeRequests -> authorizeRequests .anyRequest (). geauthenticeerd ()) .oauth2Login (oauthLogin -> oauthLogin .userInfoEndpoint () .oidcUserService (googleUserService)); }}

Het tweede verschil dat we zullen zien, is een aanroep naar de JWK Set URI. Zoals we hebben uitgelegd in onze JWS- en JWK-post, wordt dit gebruikt om de JWT-geformatteerde ID Token-handtekening te verifiëren.

Vervolgens zullen we het ID-token in detail analyseren.

5.2. Het ID-token

Uiteraard dekt de OIDC-specificatie en past deze zich aan veel verschillende scenario's aan. In dit geval gebruiken we de autorisatiecode-stroom, en het protocol geeft aan dat zowel het toegangstoken als het ID-token zullen worden opgehaald als onderdeel van de reactie van het tokeneindpunt.

Zoals we al eerder zeiden, de OidcUser entiteit bevat de claims die zijn vervat in het ID-token en het daadwerkelijke JWT-opgemaakte token, dat kan worden geïnspecteerd met behulp van jwt.io.

Bovendien biedt Spring vele handige getters om op een schone manier de standaard Claims gedefinieerd door de specificatie te verkrijgen.

We kunnen zien dat het ID-token enkele verplichte claims bevat:

  • de ID van de uitgever in de vorm van een URL (bijv. '//accounts.google.com“)
  • een subject-id, die een referentie is van de eindgebruiker die is opgenomen door de uitgever
  • de vervaltijd van het token
  • het tijdstip waarop het token is uitgegeven
  • de doelgroep, die de OAuth 2.0 Client-ID zal bevatten die we hebben geconfigureerd

En ook veel OIDC-standaardclaims zoals degene die we eerder noemden (naam, locale, afbeelding, e-mail).

Omdat deze standaard zijn, kunnen we van veel providers verwachten dat ze ten minste een aantal van deze velden terugvinden en daardoor de ontwikkeling van eenvoudigere oplossingen vergemakkelijken.

5.3. Claims en reikwijdte

Zoals we ons kunnen voorstellen, komen de claims die worden opgehaald door het OP overeen met de scopes die we (of Spring Security) hebben geconfigureerd.

OIDC definieert enkele bereiken die kunnen worden gebruikt om de door OIDC gedefinieerde claims aan te vragen:

  • profiel, die kan worden gebruikt om standaardprofielclaims aan te vragen (bijv. naam, voorkeursgebruikersnaam,afbeelding, enzovoort)
  • e-mail, om toegang te krijgen tot het e-mail en email geverifieerd Claims
  • adres
  • telefoon, om het telefoonnummer en phone_number_verified Claims

Hoewel Spring het nog niet ondersteunt, staat de specificatie toe om enkele claims aan te vragen door ze op te geven in het autorisatieverzoek.

6. Lente-ondersteuning voor OIDC Discovery

Zoals we in de inleiding hebben uitgelegd, bevat OIDC naast het kerndoel veel verschillende functies.

De mogelijkheden die we in deze sectie en de volgende gaan analyseren, zijn optioneel in OIDC. Daarom is het belangrijk om te begrijpen dat er mogelijk OP's zijn die deze niet ondersteunen.

De specificatie definieert een ontdekkingsmechanisme voor een RP om het OP te ontdekken en informatie te verkrijgen die nodig is om ermee te communiceren.

Kort gezegd: OP's bieden een JSON-document met standaardmetadata. De informatie moet worden bediend door een bekend eindpunt van de locatie van de uitgevende instelling, /.well-known/openid-configuration.

Spring profiteert hiervan door ons in staat te stellen een Klantregistratie met slechts één eenvoudige eigenschap, de locatie van de uitgevende instelling.

Maar laten we meteen naar een voorbeeld springen om dit duidelijk te zien.

We zullen een gewoonte definiëren Klantregistratie voorbeeld:

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

Nu kunnen we onze applicatie opnieuw opstarten en de logboeken controleren om te bevestigen dat de applicatie het openid-configuratie eindpunt in het opstartproces.

We kunnen zelfs door dit eindpunt bladeren om de informatie van Google te bekijken:

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

We kunnen bijvoorbeeld de autorisatie, de token en de UserInfo-eindpunten zien die de service moet gebruiken, en de ondersteunde scopes.

Een bijzonder relevante opmerking hier is het feit dat als het Discovery-eindpunt niet beschikbaar is op het moment dat de service wordt gestart, onze app het opstartproces niet met succes kan voltooien.

7. OpenID Connect-sessiebeheer

Deze specificatie vormt een aanvulling op de kernfunctionaliteit door het volgende te definiëren:

  • verschillende manieren om de inlogstatus van de Eindgebruiker bij het OP doorlopend te volgen, zodat de RP een Eindgebruiker kan uitloggen die is uitgelogd bij de OpenID-provider
  • de mogelijkheid om RP-uitlog-URI's te registreren bij het OP als onderdeel van de klantregistratie, om te worden geïnformeerd wanneer de eindgebruiker uitlogt bij het OP
  • een mechanisme om het OP te laten weten dat de eindgebruiker is uitgelogd van de site en mogelijk ook wil uitloggen bij het OP

Natuurlijk ondersteunen niet alle OP's al deze items, en sommige van deze oplossingen kunnen alleen worden geïmplementeerd in een front-end implementatie via de User-Agent.

In deze tutorial zullen we ons concentreren op de mogelijkheden die Spring biedt voor het laatste item van de lijst, RP-geïnitieerde Logout.

Als we op dit punt inloggen op onze applicatie, hebben we normaal gesproken toegang tot elk eindpunt.

Als we uitloggen (het /uitloggen endpoint) en we doen daarna een verzoek aan een beveiligde bron, we zullen zien dat we het antwoord kunnen krijgen zonder opnieuw in te loggen.

Dit is echter niet waar; als we het tabblad Netwerk in de debugconsole van de browser inspecteren, zullen we zien dat wanneer we het beveiligde eindpunt de tweede keer raken, we worden omgeleid naar het OP-autorisatie-eindpunt, en aangezien we daar nog steeds zijn aangemeld, wordt de stroom transparant voltooid , die vrijwel onmiddellijk in het beveiligde eindpunt belandt.

Dit kan in sommige gevallen natuurlijk niet het gewenste gedrag zijn. Laten we eens kijken hoe we dit OIDC-mechanisme kunnen implementeren om hiermee om te gaan.

7.1. De OpenID-providerconfiguratie

In dit geval zullen we een Okta-instantie configureren en gebruiken als onze OpenID-provider. We zullen niet ingaan op details over het maken van de instantie, maar we kunnen de stappen van deze handleiding volgen en in gedachten houden dat het standaard callback-eindpunt van Spring Security zal zijn / login / oauth2 / code / okta.

In onze applicatie kunnen we de klantregistratiegegevens definiëren met eigenschappen:

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

OIDC geeft aan dat het OP-uitlog-eindpunt kan worden gespecificeerd in het Discovery-document, als het end_session_endpoint element.

7.2. De LogoutSuccessHandler Configuratie

Vervolgens moeten we het HttpSecurity logout logica door een aangepast LogoutSuccessHandler voorbeeld:

@Override protected void configure (HttpSecurity http) genereert uitzondering {http .authorizeRequests (authorizeRequests -> authorizeRequests .mvcMatchers ("/ home"). AllowAll () .anyRequest (). Authenticated ()) .oauth2Login -> oauthLogin -> oauthLogin -> oauthLogin ()) .logout (logout -> logout .logoutSuccessHandler (oidcLogoutSuccessHandler ())); }

Laten we nu eens kijken hoe we een LogoutSuccessHandler voor dit doel met behulp van een speciale klasse die wordt aangeboden door Spring Security, de OidcClientInitiatedLogoutSuccessHandler:

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

Daarom moeten we deze URI instellen als een geldige uitlog-omleidings-URI in het OP Client-configuratiescherm.

Het is duidelijk dat de OP-uitlogconfiguratie is opgenomen in de configuratie van de clientregistratie, aangezien we alleen de ClientRegistrationRepository boon aanwezig in de context.

Dus, wat gaat er nu gebeuren?

Nadat we zijn ingelogd op onze applicatie, kunnen we een verzoek sturen naar de /uitloggen eindpunt geleverd door Spring Security.

Als we de netwerklogboeken in de debugconsole van de browser controleren, we zullen zien dat we zijn omgeleid naar een OP-uitlog-eindpunt voordat we eindelijk toegang hebben tot de omleidings-URI die we hebben geconfigureerd.

De volgende keer dat we toegang krijgen tot een eindpunt in onze applicatie dat authenticatie vereist, moeten we verplicht opnieuw inloggen op ons OP-platform om machtigingen te krijgen.

8. Conclusie

Samenvattend hebben we in deze tutorial veel geleerd over de oplossingen die worden aangeboden door OpenID Connect, en hoe we sommige ervan kunnen implementeren met Spring Security.

Zoals altijd zijn alle complete voorbeelden te vinden in onze GitHub-opslagplaats.