Note that this article has been updated to the new Spring Security OAuth 2.0 stack. The tutorial using the legacy stack is still available, though.

1. Overview

In this tutorial, we’ll focus on setting up OpenID Connect (OIDC) with Spring Security.

We’ll present different aspects of this specification, and then we’ll see the support that Spring Security offers to implement it on an OAuth 2.0 Client.

2. Quick OpenID Connect Introduction

OpenID Connect is an identity layer built on top of the OAuth 2.0 protocol.

So, it’s really important to know OAuth 2.0 before diving into OIDC, especially the Authorization Code flow.

The OIDC specification suite is extensive. It includes core features and several other optional capabilities, presented in different groups. Here are the main ones:

  • Core – authentication and use of Claims to communicate End User information
  • Discovery – stipulate how a client can dynamically determine information about OpenID Providers
  • Dynamic Registration – dictate how a client can register with a provider
  • Session Management – define how to manage OIDC sessions

On top of this, the documents distinguish the OAuth 2.0 Authentication Servers that offer support for this spec, referring to them as OpenID Providers (OPs) and the OAuth 2.0 Clients that use OIDC as Relying Parties (RPs). We’ll be using this terminology in this article.

It’s also worth noting that a client can request the use of this extension by adding the openid scope in its Authorization Request.

Finally, for this tutorial, it’s useful to know that the OPs emit End User information as a JWT called an ID Token.

Now we’re ready to dive deeper into the OIDC world.

3. Project Setup

Before focusing on the actual development, we’ll have to register an OAuth 2.0 Client with our OpenID Provider.

In this case, we’ll use Google as the OpenID Provider. We can follow these instructions to register our client application on their platform. Notice that the openid scope is present by default.

The Redirect URI we set up in this process is an endpoint in our service: http://localhost:8081/login/oauth2/code/google.

We should obtain a Client ID and a Client Secret from this process.

3.1. Maven Configuration

We’ll start by adding these dependencies to our project pom file:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
    <version>2.2.6.RELEASE</version>
</dependency>

The starter artifact aggregates all Spring Security Client-related dependencies, including

  • the spring-security-oauth2-client dependency for OAuth 2.0 Login and Client functionality
  • the JOSE library for JWT support

As usual, we can find the latest version of this artifact using the Maven Central search engine.

4. Basic Configuration Using Spring Boot

First, we’ll start by configuring our application to use the client registration we just created with Google.

Using Spring Boot makes this very easy since all we have to do is define two application properties:

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

Let’s launch our application and try to access an endpoint now. We’ll see that we get redirected to a Google Login page for our OAuth 2.0 Client.

It looks really simple, but there are quite a lot of things going on under the hood here. Next, we’ll explore how Spring Security pulls this off.

Formerly, in our WebClient and OAuth 2 Support post, we analyzed the internals on how Spring Security handles OAuth 2.0 Authorization Servers and Clients.

There we saw that we have to provide additional data, apart from the Client ID and the Client Secret, to configure a ClientRegistration instance successfully.

So, how is this working?

Google is a well-known provider, and therefore the framework offers some predefined properties to make things easier.

We can have a look at those configurations in the CommonOAuth2Provider enum.

For Google, the enumerated type defines properties such as

  • the default scopes that will be used
  • the Authorization endpoint
  • the Token endpoint
  • the UserInfo endpoint, which is also part of the OIDC Core specification

4.1. Accessing User Information

Spring Security offers a useful representation of a user Principal registered with an OIDC Provider, the OidcUser entity.

Apart from the basic OAuth2AuthenticatedPrincipal methods, this entity offers some useful functionality:

  • Retrieve the ID Token value and the Claims it contains
  • Obtain the Claims provided by the UserInfo endpoint
  • Generate an aggregate of the two sets

We can easily access this entity in a controller:

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

Or we can use the SecurityContextHolder in a bean:

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

If we inspect the principal, we’ll see a lot of useful information here, such as the user’s name, email, profile picture and locale.

5. OIDC in Action

So far, we’ve learned how we can easily implement an OIDC Login solution using Spring Security.

We’ve seen the benefit it carries by delegating the user identification process to an OpenID Provider, which in turn supplies detailed useful information, even in a scalable manner.

But the truth is that we didn’t have to deal with any OIDC-specific aspect so far. This means that Spring is doing most of the work for us.

So, let’s look at what’s going on behind the scenes to understand better how this specification is put into action and be able to get the most out of it.

5.1. The Login Process

In order to see this clearly, let’s enable the RestTemplate logs to see the requests the service is performing:

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 some differences.

First, 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, https://www.googleapis.com/auth/userinfo.email and https://www.googleapis.com/auth/userinfo.profile, so 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 {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        Set<String> googleScopes = new HashSet<>();
        googleScopes.add("https://www.googleapis.com/auth/userinfo.email");
        googleScopes.add("https://www.googleapis.com/auth/userinfo.profile");

        OidcUserService googleUserService = new OidcUserService();
        googleUserService.setAccessibleScopes(googleScopes);

        http.authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
            .oauth2Login(oauthLogin -> oauthLogin.userInfoEndpoint(userInfoEndpointConfig ->
                    userInfoEndpointConfig.oidcUserService(googleUserService)));
        return http.build();
    }
}

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., “https://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

It also contains many OIDC Standard Claims such as 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 facilitate 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_usernamepicture, etc.)
  • email, to access to the email and email_verified Claims
  • address
  • phone, to request 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. So, 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-id>
            client-secret: <secret>
        provider:
          custom-google:
            issuer-uri: https://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:

https://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.

It’s especially relevant to note here that if the Discovery endpoint is not available when the service launches, 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 the following:

  • 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, in order 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 log out (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, 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-id>
            client-secret: <secret>
        provider:
          okta:
            issuer-uri: https://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:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(authorizeRequests -> authorizeRequests
                    .requestMatchers("/home").permitAll()
                    .anyRequest().authenticated())
        .oauth2Login(AbstractAuthenticationFilterConfigurer::permitAll)
        .logout(logout -> logout.logoutSuccessHandler(oidcLogoutSuccessHandler()));
    return http.build();
}

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("http://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 log in to our application, we can send a request to the /logout endpoint provided by Spring Security.

If we check the Network logs in the browser debug console, we’ll see we got redirected to an OP logout endpoint before finally accessing the Redirect URI we configured.

Next time we access an endpoint in our application that requires authentication, we’ll mandatorily need to log in again in our OP platform to get permissions.

8. Conclusion

To summarize, in this article, we learned a lot about the solutions offered by OpenID Connect and how we can implement some of them using Spring Security.

As always, all the complete examples can be found over on GitHub.