Spring Security and OpenID Connect

1. Overview

In this quick 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.

Thus, 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. The main ones are:

  • Core: authentication and use of Claims to communicate End-User information
  • Discovery: stipulates how a client can dynamically determine information about OpenID Providers
  • Dynamic Registration: dictates how a client can register with a provider
  • Session Management: defines 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 adhering to this terminology in this article.

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

Finally, one other aspect that is useful to understand for this tutorial is the fact that the OPs emit End-User information as a JWT called an “ID Token”.

Now yes, 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.o 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

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

Using Spring Boot makes this very easy, as 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.

In 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?

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

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

For Google, the enumerated type defines properties like:

  • 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 by using 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, like the user’s name, email, profile picture, and locale.

Furthermore, it’s important to note that Spring adds authorities to the principal based on the scopes it received from the provider, prefixed with “SCOPE_“. For example, the openid scope becomes a SCOPE_openid granted authority.

These authorities can be used to restrict access to certain resources, for example:

@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 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, 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.

Hence, we’ll see 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, 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 profileemailaddress 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, 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<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
          .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. “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

And also many OIDC Standard Claims like the ones we mentioned before (namelocalepictureemail).

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, pictureetcetera)
  • 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-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.

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-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:

@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("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 login 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 tutorial 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 in our GitHub repo.

Related posts:

Spring Boot - CORS Support
Generating Random Numbers in a Range in Java
Guide to Selenium with JUnit / TestNG
Toán tử instanceof trong java
Java Program to Implement EnumMap API
Encode/Decode to/from Base64
Guide to Character Encoding
Sorting in Java
Từ khóa static và final trong java
Apache Commons Collections MapUtils
A Guide to Java HashMap
Spring Autowiring of Generic Types
Java Program to Generate All Subsets of a Given Set in the Lexico Graphic Order
Spring Security Remember Me
Java Program to Create a Balanced Binary Tree of the Incoming Data
Hướng dẫn Java Design Pattern – DAO
Hướng dẫn Java Design Pattern – Memento
Java Program to Check Whether an Input Binary Tree is the Sub Tree of the Binary Tree
Một số tính năng mới về xử lý ngoại lệ trong Java 7
Spring Boot - Building RESTful Web Services
Java Program to Implement Naor-Reingold Pseudo Random Function
A Quick Guide to Spring Cloud Consul
Java Program to Implement ArrayBlockingQueue API
Spring Boot - Creating Docker Image
Spring WebClient vs. RestTemplate
Java Program to Check if a Given Graph Contain Hamiltonian Cycle or Not
Java Program to Construct an Expression Tree for an Postfix Expression
Java Web Services – Jersey JAX-RS – REST và sử dụng REST API testing tools với Postman
Hướng dẫn Java Design Pattern – Prototype
Chuyển đổi giữa các kiểu dữ liệu trong Java
The Modulo Operator in Java
Java Program to Check Whether it is Weakly Connected or Strongly Connected for a Directed Graph