Spring Security y OpenID Connect

Tenga en cuenta que este artículo se ha actualizado a la nueva pila Spring Security OAuth 2.0. Sin embargo, el tutorial que usa la pila heredada todavía está disponible.

1. Información general

En este tutorial rápido, nos centraremos en configurar OpenID Connect (OIDC) con Spring Security.

Presentaremos diferentes aspectos de esta especificación y luego veremos el soporte que ofrece Spring Security para implementarla en un cliente OAuth 2.0.

2. Introducción rápida a OpenID Connect

OpenID Connect es una capa de identidad construida sobre el protocolo OAuth 2.0.

Por lo tanto, es muy importante conocer OAuth 2.0 antes de sumergirse en OIDC, especialmente el flujo del Código de autorización.

El conjunto de especificaciones OIDC es extenso; incluye características principales y varias otras capacidades opcionales, presentadas en diferentes grupos. Los principales son:

  • Núcleo: autenticación y uso de reclamos para comunicar información al usuario final
  • Descubrimiento: estipula cómo un cliente puede determinar dinámicamente información sobre proveedores OpenID
  • Registro dinámico: dicta cómo un cliente puede registrarse con un proveedor
  • Gestión de sesiones: define cómo gestionar las sesiones OIDC

Además de esto, los documentos distinguen los Servidores de autenticación OAuth 2.0 que ofrecen soporte para esta especificación, refiriéndose a ellos como “Proveedores OpenID” (OP) y los Clientes OAuth 2.0 que usan OIDC como Partes confiables (RP). Nos adheriremos a esta terminología en este artículo.

Vale la pena saber también que un cliente puede solicitar el uso de esta extensión agregando el alcance openid en su Solicitud de autorización.

Finalmente, otro aspecto que es útil comprender para este tutorial es el hecho de que los OP emiten información del usuario final como un JWT llamado “ID Token”.

Ahora sí, estamos listos para sumergirnos más profundamente en el mundo de OIDC.

3. Configuración del proyecto

Antes de centrarnos en el desarrollo real, tendremos que registrar un Cliente OAuth 2.o con nuestro Proveedor OpenID.

En este caso, usaremos Google como proveedor de OpenID. Podemos seguir estas instrucciones para registrar nuestra aplicación cliente en su plataforma. Tenga en cuenta que el ámbito de openid está presente de forma predeterminada.

El URI de redireccionamiento que configuramos en este proceso es un punto final en nuestro servicio: // localhost: 8081 / login / oauth2 / code / google.

Debemos obtener una identificación de cliente y un secreto de cliente de este proceso.

3.1. Configuración de Maven

Comenzaremos agregando estas dependencias a nuestro archivo pom de proyecto:

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

El artefacto de inicio agrega todas las dependencias relacionadas con Spring Security Client, que incluyen:

  • la dependencia spring-security-oauth2-client para la funcionalidad de inicio de sesión y cliente de OAuth 2.0
  • la biblioteca JOSE para soporte JWT

Como es habitual, podemos encontrar la última versión de este artefacto utilizando el motor de búsqueda de Maven Central.

4. Configuración básica con Spring Boot

En primer lugar, comenzaremos configurando nuestra aplicación para usar el registro de cliente que acabamos de crear con Google.

Usar Spring Boot hace que esto sea muy fácil, ya que todo lo que tenemos que hacer es definir dos propiedades de la aplicación:

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

Iniciemos nuestra aplicación e intentemos acceder a un punto final ahora. Veremos que nos redirigen a una página de inicio de sesión de Google para nuestro cliente OAuth 2.0.

Parece realmente simple, pero hay muchas cosas que suceden debajo del capó aquí. A continuación, exploraremos cómo Spring Security logra esto.

Anteriormente, en nuestra publicación de WebClient y OAuth 2 Support, analizamos los aspectos internos sobre cómo Spring Security maneja los servidores y clientes de autorización OAuth 2.0.

Allí, vimos que tenemos que proporcionar datos adicionales, además del ID de cliente y el secreto del cliente, para configurar una instancia de ClientRegistration con éxito. Entonces, ¿cómo está funcionando esto?

La respuesta es que Google es un proveedor conocido y, por lo tanto, el marco ofrece algunas propiedades predefinidas para facilitar las cosas.

Podemos echar un vistazo a esas configuraciones en la enumeración CommonOAuth2Provider .

Para Google, el tipo enumerado define propiedades como:

  • los ámbitos predeterminados que se utilizarán
  • el punto final de autorización
  • el punto final de Token
  • el punto final UserInfo, que también es parte de la especificación OIDC Core

4.1. Acceso a la información del usuario

Spring Security ofrece una representación útil de un usuario principal registrado con un proveedor OIDC, la entidad OidcUser .

Además de los métodos básicos de OAuth2AuthenticatedPrincipal , esta entidad ofrece algunas funciones útiles:

  • recuperar el valor del token de identificación y las reclamaciones que contiene
  • obtener las reclamaciones proporcionadas por el extremo UserInfo
  • generar un agregado de los dos conjuntos

Podemos acceder fácilmente a esta entidad en un controlador:

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

O usando SecurityContextHolder en un bean:

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

Si inspeccionamos el director, veremos mucha información útil aquí, como el nombre del usuario, el correo electrónico, la foto de perfil y la configuración regional.

Además, es importante tener en cuenta que Spring agrega autoridades al principal en función de los ámbitos que recibió del proveedor, con el prefijo " SCOPE_ ". Por ejemplo, el ámbito de openid se convierte en una autoridad otorgada SCOPE_openid .

Estas autorizaciones se pueden utilizar para restringir el acceso a determinados recursos, por ejemplo :

@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 en acción

Hasta ahora, hemos aprendido cómo podemos implementar fácilmente una solución de inicio de sesión OIDC utilizando Spring Security

Hemos visto el beneficio que conlleva delegar el proceso de identificación del usuario a un proveedor OpenID, que, a su vez, proporciona información útil detallada, incluso de manera escalable.

Pero la verdad es que no hemos tenido que ocuparnos de ningún aspecto específico de OIDC hasta ahora. Esto significa que Spring está haciendo la mayor parte del trabajo por nosotros.

Por lo tanto, veremos qué sucede entre bastidores para comprender mejor cómo se pone en práctica esta especificación y poder aprovecharla al máximo.

5.1. El proceso de inicio de sesión

Para ver esto claramente, habilitemos los registros de RestTemplate para ver las solicitudes que está realizando el servicio:

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.

Si verificamos los registros de red en la consola de depuración del navegador, veremos que nos redirigen a un punto final de cierre de sesión OP antes de acceder finalmente al URI de redireccionamiento que configuramos.

La próxima vez que accedamos a un punto final en nuestra aplicación que requiera autenticación, obligatoriamente tendremos que iniciar sesión nuevamente en nuestra plataforma OP para obtener permisos.

8. Conclusión

En resumen, en este tutorial aprendimos mucho sobre las soluciones que ofrece OpenID Connect, y cómo podemos implementar algunas de ellas usando Spring Security.

Como siempre, todos los ejemplos completos se pueden encontrar en nuestro repositorio de GitHub.