Spring Security 5 – OAuth2 Login

1. Overview

Spring Security 5 introduces a new OAuth2LoginConfigurer class that we can use for configuring an external Authorization Server.

In this article, we’ll explore some of the various configuration options available for the oauth2Login() element.

2. Maven Dependencies

In addition to the standard Spring and Spring Security dependencies, we’ll also need to add the spring-security-oauth2-client and spring-security-oauth2-jose dependencies:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.security</groupId>
   <artifactId>spring-security-oauth2-jose</artifactId>
</dependency>

In our example, dependencies are managed by the Spring Boot starter parent, version 2.x, which corresponds to version 5.x of the Spring Security artifacts.

3. Clients Setup

In a Spring Boot project, all we need to do is add a few standard properties for each client we want to configure.

Let’s set up our project for login with clients registered with Google and Facebook as authentication providers.

3.1. Obtaining Client Credentials

To obtain client credentials for Google OAuth2 authentication, head on over to the Google API Console – section “Credentials”.

Here we’ll create credentials of type “OAuth2 Client ID” for our web application. This results in Google setting up a client id and secret for us.

We also have to configure an authorized redirect URI in the Google Console, which is the path that users will be redirected to after they successfully login with Google.

By default, Spring Boot configures this redirect URI as /login/oauth2/code/{registrationId}. Therefore, for Google we’ll add the URI:

http://localhost:8081/login/oauth2/code/google

To obtain the client credentials for authentication with Facebook, we need to register an application on the Facebook for Developers website and set up the corresponding URI as a “Valid OAuth redirect URI”:

http://localhost:8081/login/oauth2/code/facebook

3.3. Security Configuration

Next, we need to add client credentials in the application.properties file. The Spring Security properties are prefixed with “spring.security.oauth2.client.registration” followed by the client name, then the name of the client property:

spring.security.oauth2.client.registration.google.client-id=<your client id>
spring.security.oauth2.client.registration.google.client-secret=<your client secret>

spring.security.oauth2.client.registration.facebook.client-id=<your client id>
spring.security.oauth2.client.registration.facebook.client-secret=<your client secret>

Adding these properties for at least one client will enable the Oauth2ClientAutoConfiguration class which sets up all the necessary beans.

The automatic web security configuration is equivalent to defining a simple oauth2Login() element:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
         .anyRequest().authenticated()
         .and()
         .oauth2Login();
    }
}

Here, we can see the oauth2Login() element is used in a similar manner to already known httpBasic() and formLogin() elements.

Now, when we try to access a protected URL, the application will display an auto-generated login page with two clients:

image

3.4. Other Clients

Note that in addition to Google and Facebook, the Spring Security project also contains default configurations for GitHub and Okta. These default configurations provide all the necessary information for authentication, which is what allows us to only enter the client credentials.

If we want to use a different authentication provider not configured in Spring Security, we’ll need to define the full configuration, with information such as authorization URI and token URI. Here‘s a look at the default configurations in Spring Security to have an idea of the properties needed.

4. Setup in a Non-Boot Project


==== 4.1. Creating a ClientRegistrationRepository Bean

If we’re not working with a Spring Boot application, we’ll need to define a ClientRegistrationRepository bean that contains an internal representation of the client information owned by the authorization server:

@Configuration
@EnableWebSecurity
@PropertySource("classpath:application.properties")
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private static List<String> clients = Arrays.asList("google", "facebook");

    @Bean
    public ClientRegistrationRepository clientRegistrationRepository() {
        List<ClientRegistration> registrations = clients.stream()
          .map(c -> getRegistration(c))
          .filter(registration -> registration != null)
          .collect(Collectors.toList());

        return new InMemoryClientRegistrationRepository(registrations);
    }
}

Here we’re creating an InMemoryClientRegistrationRepository with a list of ClientRegistration objects.

4.2. Building ClientRegistration Objects

Let’s see the getRegistration() method that builds these objects:

private static String CLIENT_PROPERTY_KEY
  = "spring.security.oauth2.client.registration.";

@Autowired
private Environment env;

private ClientRegistration getRegistration(String client) {
    String clientId = env.getProperty(
      CLIENT_PROPERTY_KEY + client + ".client-id");

    if (clientId == null) {
        return null;
    }

    String clientSecret = env.getProperty(
      CLIENT_PROPERTY_KEY + client + ".client-secret");

    if (client.equals("google")) {
        return CommonOAuth2Provider.GOOGLE.getBuilder(client)
          .clientId(clientId).clientSecret(clientSecret).build();
    }
    if (client.equals("facebook")) {
        return CommonOAuth2Provider.FACEBOOK.getBuilder(client)
          .clientId(clientId).clientSecret(clientSecret).build();
    }
    return null;
}

Here, we’re reading the client credentials from a similar application.properties file, then using the CommonOauth2Provider enum already defined in Spring Security for the rest of the client properties for Google and Facebook clients.

Each ClientRegistration instance corresponds to a client.

4.3. Registering the ClientRegistrationRepository

Finally, we have to create an OAuth2AuthorizedClientService bean based on the ClientRegistrationRepository bean and register both with the oauth2Login() element:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests().anyRequest().authenticated()
      .and()
      .oauth2Login()
      .clientRegistrationRepository(clientRegistrationRepository())
      .authorizedClientService(authorizedClientService());
}

@Bean
public OAuth2AuthorizedClientService authorizedClientService() {

    return new InMemoryOAuth2AuthorizedClientService(
      clientRegistrationRepository());
}

As evidenced here, we can use the clientRegistrationRepository() method of oauth2Login() to register a custom registration repository.

We’ll also have to define a custom login page, as it won’t be automatically generated anymore. We’ll see more information on this in the next section.

Let’s continue with further customization of our login process.

5. Customizing oauth2Login()

There are several elements that the OAuth 2 process uses and that we can customize using oauth2Login() methods.

Note that all these elements have default configurations in Spring Boot and explicit configuration isn’t required.

Let’s see how we can customize these in our configuration.

5.1. Custom Login Page

Even though Spring Boot generates a default login page for us, we’ll usually want to define our own customized page.

Let’s start with configuring a new login URL for the oauth2Login() element by using the loginPage() method:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
      .antMatchers("/oauth_login")
      .permitAll()
      .anyRequest()
      .authenticated()
      .and()
      .oauth2Login()
      .loginPage("/oauth_login");
}

Here, we’ve set up our login URL to be /oauth_login.

Next, let’s define a LoginController with a method that maps to this URL:

@Controller
public class LoginController {

    private static String authorizationRequestBaseUri
      = "oauth2/authorization";
    Map<String, String> oauth2AuthenticationUrls
      = new HashMap<>();

    @Autowired
    private ClientRegistrationRepository clientRegistrationRepository;

    @GetMapping("/oauth_login")
    public String getLoginPage(Model model) {
        // ...

        return "oauth_login";
    }
}

This method has to send a map of the clients available and their authorization endpoints to the view, which we’ll obtain from the ClientRegistrationRepository bean:

public String getLoginPage(Model model) {
    Iterable<ClientRegistration> clientRegistrations = null;
    ResolvableType type = ResolvableType.forInstance(clientRegistrationRepository)
      .as(Iterable.class);
    if (type != ResolvableType.NONE &&
      ClientRegistration.class.isAssignableFrom(type.resolveGenerics()[0])) {
        clientRegistrations = (Iterable<ClientRegistration>) clientRegistrationRepository;
    }

    clientRegistrations.forEach(registration ->
      oauth2AuthenticationUrls.put(registration.getClientName(),
      authorizationRequestBaseUri + "/" + registration.getRegistrationId()));
    model.addAttribute("urls", oauth2AuthenticationUrls);

    return "oauth_login";
}

Finally, we need to define our oauth_login.html page:

<h3>Login with:</h3>
<p th:each="url : ${urls}">
    <a th:text="${url.key}" th:href="${url.value}">Client</a>
</p>

This is a simple HTML page which displays links to authenticate with each client.

After adding some styling to it, we can have a much nicer looking login page:

image

5.2. Custom Authentication Success and Failure Behavior

We can control the post-authentication behavior by using different methods:

  • defaultSuccessUrl() and failureUrl() – to redirect the user to a given URL

  • successHandler() and failureHandler() – to execute custom logic following the authentication process

Let’s see how we can set custom URL’s to redirect the user to:

.oauth2Login()
  .defaultSuccessUrl("/loginSuccess")
  .failureUrl("/loginFailure");

If the user visited a secured page before authenticating, they will be redirected to that page after logging in; otherwise, they will be redirected to /loginSuccess.

If we want the user to always be sent to the /loginSuccess URL regardless if they were on a secured page before or not, we can use the method defaultSuccessUrl(“/loginSuccess”, true).

To use a custom handler, we would have to create a class that implements the AuthenticationSuccessHandler or AuthenticationFailureHandler interfaces, override the inherited methods, then set the beans using the successHandler() and failureHandler() methods.

5.3. Custom Authorization Endpoint

The authorization endpoint is the endpoint that Spring Security uses to trigger an authorization request to the external server.

First, let’s set new properties for the authorization endpoint:

.oauth2Login()
  .authorizationEndpoint()
  .baseUri("/oauth2/authorize-client")
  .authorizationRequestRepository(authorizationRequestRepository());

Here, we’ve modified the baseUri to /oauth2/authorize-client instead of the default /oauth2/authorization. We’re also explicitly setting an authorizationRequestRepository() bean that we have to define:

@Bean
public AuthorizationRequestRepository<OAuth2AuthorizationRequest>
  authorizationRequestRepository() {

    return new HttpSessionOAuth2AuthorizationRequestRepository();
}

In our example, we’ve used the Spring-provided implementation for our bean, but we could also provide a custom one.

5.4. Custom Token Endpoint

The token endpoint processes access tokens.

Let’s explicitly configure the tokenEndpoint() with the default response client implementation:

.oauth2Login()
  .tokenEndpoint()
  .accessTokenResponseClient(accessTokenResponseClient());

And here’s the response client bean:

@Bean
public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest>
  accessTokenResponseClient() {

    return new NimbusAuthorizationCodeTokenResponseClient();
}

This configuration is the same as the default one and is using the Spring implementation which is based on exchanging an authorization code with the provider.

Of course, we could also substitute a custom response client.

5.5. Custom Redirection Endpoint

This is the endpoint to redirect to after authentication with the external provider.

Let’s see how we can change the baseUri for the redirection endpoint:

.oauth2Login()
  .redirectionEndpoint()
  .baseUri("/oauth2/redirect")

The default URI is login/oauth2/code.

Note that if we change it, we also have to update the redirectUriTemplate property of each ClientRegistration and add the new URI as an authorized redirect URI for each client.

5.6. Custom User Information Endpoint

The user info endpoint is the location we can leverage to obtain user information.

We can customize this endpoint using the userInfoEndpoint() method. For this, we can use methods such as userService() and customUserType() to modify the way user information is retrieved.

6. Accessing User Information

A common task we may want to achieve is finding information about the logged-in user. For this, we can make a request to the user information endpoint.

First, we’ll have to get the client corresponding to the current user token:

@Autowired
private OAuth2AuthorizedClientService authorizedClientService;

@GetMapping("/loginSuccess")
public String getLoginInfo(Model model, OAuth2AuthenticationToken authentication) {
    OAuth2AuthorizedClient client = authorizedClientService
      .loadAuthorizedClient(
        authentication.getAuthorizedClientRegistrationId(),
          authentication.getName());
    //...
    return "loginSuccess";
}

Next, we’ll send a request to the client’s user info endpoint and retrieve the userAttributes Map:

String userInfoEndpointUri = client.getClientRegistration()
  .getProviderDetails().getUserInfoEndpoint().getUri();

if (!StringUtils.isEmpty(userInfoEndpointUri)) {
    RestTemplate restTemplate = new RestTemplate();
    HttpHeaders headers = new HttpHeaders();
    headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + client.getAccessToken()
      .getTokenValue());
    HttpEntity entity = new HttpEntity("", headers);
    ResponseEntity <map>response = restTemplate
      .exchange(userInfoEndpointUri, HttpMethod.GET, entity, Map.class);
    Map userAttributes = response.getBody();
    model.addAttribute("name", userAttributes.get("name"));
}

By adding the name property as a Model attribute, we can display it in the loginSuccess view as a welcome message to the user:

image

Besides the name, the userAttributes Map also contains properties such as email, family_name, picture, locale.

7. Conclusion

In this article, we’ve seen how we can use the oauth2Login() element in Spring Security to authenticate with different providers such as Google and Facebook. We’ve also gone through some common scenarios of customizing this process.

The full source code of the examples can be found over on GitHub.

Leave a Reply

Your email address will not be published.