The new Certification Class of Learn Spring Security is out:

>> CHECK OUT THE COURSE

1. Overview

In this quick tutorial, we’ll focus on setting up OpenID Connect with a Spring Security OAuth2 implementation.

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

And, more specifically, we’ll learn how to authenticate users using the OpenID Connect implementation from Google.

2. Maven Configuration

First, we need to add the following dependencies to our Spring Boot application:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
</dependency>

3. The Id Token

Before we dive into the implementation details, let’s have a quick look at how OpenID works, and how we’ll interact with it.

At this point, it’s, of course, important to already have an understanding of OAuth2, since OpenID is built on top of OAuth.

First, in order to use the identity functionality, we’ll make use of a new OAuth2 scope called openid. This will result in an extra field in our Access Token – “id_token“.

The id_token is a JWT (JSON Web Token) that contains identity information about the user, signed by identity provider (in our case Google).

Finally, both server(Authorization Code) and implicit flows are the most commonly used ways of obtaining id_token, in our example, we will use server flow.

3. OAuth2 Client Configuration

Next, let’s configure our OAuth2 client – as follows:

@Configuration
@EnableOAuth2Client
public class GoogleOpenIdConnectConfig {
    @Value("${google.clientId}")
    private String clientId;

    @Value("${google.clientSecret}")
    private String clientSecret;

    @Value("${google.accessTokenUri}")
    private String accessTokenUri;

    @Value("${google.userAuthorizationUri}")
    private String userAuthorizationUri;

    @Value("${google.redirectUri}")
    private String redirectUri;

    @Bean
    public OAuth2ProtectedResourceDetails googleOpenId() {
        AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
        details.setClientId(clientId);
        details.setClientSecret(clientSecret);
        details.setAccessTokenUri(accessTokenUri);
        details.setUserAuthorizationUri(userAuthorizationUri);
        details.setScope(Arrays.asList("openid", "email"));
        details.setPreEstablishedRedirectUri(redirectUri);
        details.setUseCurrentUri(false);
        return details;
    }

    @Bean
    public OAuth2RestTemplate googleOpenIdTemplate(OAuth2ClientContext clientContext) {
        return new OAuth2RestTemplate(googleOpenId(), clientContext);
    }
}

And here is application.properties:

google.clientId=<your app clientId>
google.clientSecret=<your app clientSecret>
google.accessTokenUri=https://www.googleapis.com/oauth2/v3/token
google.userAuthorizationUri=https://accounts.google.com/o/oauth2/auth
google.redirectUri=http://localhost:8081/google-login

Note that:

  • You first need to obtain OAuth 2.0 credentials for your Google web app from Google Developers Console.
  • We used scope openid to obtain id_token.
  • we also used an extra scope email to include user email in id_token identity information.
  • The redirect URI http://localhost:8081/google-login is the same one used in our Google web app.

4. Custom OpenID Connect Filter

Now, we need to create our own custom OpenIdConnectFilter to extract authentication from id_token – as follows:

public class OpenIdConnectFilter extends AbstractAuthenticationProcessingFilter {

    public OpenIdConnectFilter(String defaultFilterProcessesUrl) {
        super(defaultFilterProcessesUrl);
        setAuthenticationManager(new NoopAuthenticationManager());
    }
    @Override
    public Authentication attemptAuthentication(
      HttpServletRequest request, HttpServletResponse response) 
      throws AuthenticationException, IOException, ServletException {
        OAuth2AccessToken accessToken;
        try {
            accessToken = restTemplate.getAccessToken();
        } catch (OAuth2Exception e) {
            throw new BadCredentialsException("Could not obtain access token", e);
        }
        try {
            String idToken = accessToken.getAdditionalInformation().get("id_token").toString();
            Jwt tokenDecoded = JwtHelper.decode(idToken);
            Map<String, String> authInfo = new ObjectMapper().readValue(tokenDecoded.getClaims(), Map.class);

            OpenIdConnectUserDetails user = new OpenIdConnectUserDetails(authInfo, accessToken);
            return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
        } catch (InvalidTokenException e) {
            throw new BadCredentialsException("Could not obtain user details from token", e);
        }
    }
}

And here is our simple OpenIdConnectUserDetails:

public class OpenIdConnectUserDetails implements UserDetails {
    private String userId;
    private String username;
    private OAuth2AccessToken token;

    public OpenIdConnectUserDetails(Map<String, String> userInfo, OAuth2AccessToken token) {
        this.userId = userInfo.get("sub");
        this.username = userInfo.get("email");
        this.token = token;
    }
}

Note that:

  • Spring Security JwtHelper to decode id_token.
  • id_token always contains “sub” field which is a unique identifier for the user.
  • id_token will also contain “email” field as we added email scope to our request.

5. Security Configuration

Next, let’s discuss our security configuration:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private OAuth2RestTemplate restTemplate;

    @Bean
    public OpenIdConnectFilter openIdConnectFilter() {
        OpenIdConnectFilter filter = new OpenIdConnectFilter("/google-login");
        filter.setRestTemplate(restTemplate);
        return filter;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
        .addFilterAfter(new OAuth2ClientContextFilter(), 
          AbstractPreAuthenticatedProcessingFilter.class)
        .addFilterAfter(OpenIdConnectFilter(), 
          OAuth2ClientContextFilter.class)
        .httpBasic()
        .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/google-login"))
        .and()
        .authorizeRequests()
        .anyRequest().authenticated();
    }
}

Note that:

  • We added our custom OpenIdConnectFilter after OAuth2ClientContextFilter
  • We used a simple security configuration to redirect users to “/google-login” to get authenticated by Google

6. User Controller

Next, here is a simple controller to test our app:

@Controller
public class HomeController {
    @RequestMapping("/")
    @ResponseBody
    public String home() {
        String username = SecurityContextHolder.getContext().getAuthentication().getName();
        return "Welcome, " + username;
    }
}

Sample response (after redirect to Google to approve app authorities) :

Welcome, [email protected]

7. Sample OpenID Connect Process

Finally, let’s take a look at a sample OpenID Connect authentication process.

First, we’re going to send an Authentication Request:

https://accounts.google.com/o/oauth2/auth?
    client_id=sampleClientID
    response_type=code&
    scope=openid%20email&
    redirect_uri=http://localhost:8081/google-login&
    state=abc

The response (after user approval) is a redirect to:

http://localhost:8081/google-login?state=abc&code=xyz

Next, we’re going to exchange the code for an Access Token and id_token:

POST https://www.googleapis.com/oauth2/v3/token 
    code=xyz&
    client_id= sampleClientID&
    client_secret= sampleClientSecret&
    redirect_uri=http://localhost:8081/google-login&
    grant_type=authorization_code

Here’s a sample Response:

{
    "access_token": "SampleAccessToken",
    "id_token": "SampleIdToken",
    "token_type": "bearer",
    "expires_in": 3600,
    "refresh_token": "SampleRefreshToken"
}

Finally, here’s what the information of the actual id_token looks like:

{
    "iss":"accounts.google.com",
    "at_hash":"AccessTokenHash",
    "sub":"12345678",
    "email_verified":true,
    "email":"[email protected]",
     ...
}

So you can immediately see just how useful the user information inside the token is for providing identity information to our own application.

8. Conclusion

In this quick intro tutorial, we learned how to authenticate users using the OpenID Connect implementation from Google.

And, as always, you can find the source code over on GitHub.

Go deeper into Spring Security with the course:

>> LEARN SPRING SECURITY

  • kobdani

    I tried your code. it can do only one step of authentication, and cannot convert the access token to user info.

    • Hey Kodbani,
      That’s interesting – but unfortunately it’s not easy to reproduce. Feel free to open an issue over on Github with a bit more info to be able to reproduce this one.
      Cheers,
      Eugen.

  • Gil9091 Gutti (Gaara)

    As could be the login with Office365. Is that I try to do it following this example but when I log in gives me this error

    error=”access_denied”, error_description=”Access token denied.”

    Caused by: error=”invalid_request”, error_description=”AADSTS90014: The request body must contain the following parameter: ‘client_id’.
    Trace ID: cdb3fd35-5963-4cf4-8815-331dd1df0e00
    Correlation ID: bc0dc582-95d1-404e-a7f0-4d7f6d0d7190

    • Hey Gaara – that’s an interesting question, and also quite a code-heavy one – so the comments here are probably not the best place for it.
      My suggestion is – do a quick writeup of it on StackOverflow and send me the link (either here or via email) – and I’d be happy to have a look. Cheers,
      Eugen.