Part 2: The client side

An OAuth2/OIDC client

OAuth2 / OpenID Connect Client

Now we will implement the corresponding client for the product server to show the product list in a web UI.

Tip: You may look into the Spring Boot Reference Documentation and the Spring Security Reference Documentation on how to implement a client.

To start with this tutorial part, navigate to the project initial/ui in your IDE.

First, just run this unfinished client. Please make sure that you also have started the product server from the previous part.

Just run class com.example.UiApplication. Then navigate your web browser to http://localhost:9095/client. You should see the following screen.

Now try to click the link for Products. This should lead to the following whitelabel error screen:

This is because our initial client still only sends a basic authentication header to authenticate the request for getting the product list. But the product server now requires a JWT token instead. This is why we now get a 401 HTTP status error (unauthorized).

So let's start with fixing this issue by implementing an OAuth2/OIDC client.

Step 1: Change Maven dependencies for the client

Add the following dependency to the existing maven pom.xml file:

pom.xml:

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

This adds the spring boot starter dependency for building an OAuth2/OIDC client. This includes all required classes to manage the authorization code flow of OAuth2 and handle all JWT token-related tasks.

Step 2: Add the required properties for the client

The client requires several configuration parameters from the identity server to be used. Thanks to the OpenID Connect discovery specification most identity servers publish all required parameters at a well-known server endpoint /.well-known/openid-configuration. In the case of Auth0 the URL is https://access-me.eu.auth0.com/.well-known/openid-configuration.

This is why one parameter of spring (see below) is requiring the issuer-uri. This points to the base URL address of the identity server (i.e. without the /.well-known/openid-configuration part).

Spring security provides predefined properties to configure the application as an OAuth2/OIDC client:

  • The property spring.security.oauth2.client.provider.auth0.issuer-uri specifies the URI for loading the required configuration to set up an OAuth2/OIDC client for the Auth0 identity provider.

  • The property spring.security.oauth2.client.provider.user-name-attribute specifies the attribute claim to use for mapping user data retrieved from the user info endpoint in OAuth2/OIDC client for Auth0.

  • The property spring.security.oauth2.client.registration.auth0.client-id specifies the client id as it is has been registered at the Auth0 identity provider.

  • The property spring.security.oauth2.client.registration.auth0.client-secret specifies the client secret to authorize the application to act as a registered at the Auth0 identity provider.

  • The property spring.security.oauth2.client.registration.auth0.authorizationGrantType specifies which OAuth2/OIDC grant flow should be used for the client.

  • The property spring.security.oauth2.client.registration.auth0.clientAuthenticationMethod specifies the authentication method to use when calling the token endpoint at the Auth0 identity provider. The value of NONE specifies that no client_secret is specified. Instead, the dynamic Proof Key for Key Exchange (PKCE) is used instead.

  • The property spring.security.oauth2.client.registration.auth0.redirect-uri specifies the redirect URI to call our client application with the authorization code from the Auth0 identity provider. Spring also provides predefined placeholders for the base URL and the registration id.

  • The property spring.security.oauth2.client.registration.auth0.scope specifies the scopes to be used for the OAuth2 login. The value of openid enables the OpenID Connect mode and profile/email specifies which attribute claims to include in the token.

After adding the required new properties the updated application.yml should look like this:

application.yml:

spring:
  security:
    oauth2:
      client:
        provider:
          auth0:
            issuer-uri: https://access-me.eu.auth0.com/
            user-name-attribute: sub
        registration:
          auth0:
            client-id: 'v13BSQLEZnw4N96V36dDdsGRd022isKe'
            authorizationGrantType: authorization_code
            clientAuthenticationMethod: NONE
            redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'
            scope:
              - openid
              - profile
              - email

Important: Please check that all indents are correct. Otherwise, you may get strange runtime errors when starting the application. The client secret is noted here just for the purpose of this tutorial. In your real productive applications, you should NEVER publish sensitive data like this client secret or any other sensitive data!!

Step 3: Add OAuth2/OIDC client security configuration

To enable the client application to act as an OAuth2/OIDC client for Auth0 identity provider it is required to add a new security configuration.

To achieve this create a new class named WebSecurityConfiguration in package com.example.

com/example/WebSecurityConfiguration.java:

package com.example;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@EnableWebSecurity
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

  @Bean
  public SecurityFilterChain api(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorizeRequests ->
                        authorizeRequests
                                .anyRequest().authenticated()
                )
                .oauth2Client().and()
                .oauth2Login(withDefaults());
        return http.build();
  }
}

Step 4: Update the call to the resource server

We already extended the product server requiring a bearer token in the Authorization header with each request. To be able to call the server from the client we need to add the access token.

To achieve this we have to change the class ProductService to add the required header with the token.

com/example/ProductService.java:

package com.example;

import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;

@Service
public class ProductService {

  public Collection<Product> getAllProducts(OAuth2AccessToken oAuth2AccessToken) {

    RestTemplate template = new RestTemplate();

    ResponseEntity<Product[]> response =
            template.exchange(
                    "http://localhost:9090/server/products",
                    HttpMethod.GET,
                    new HttpEntity<Product[]>(createAuthorizationHeader(oAuth2AccessToken)),
                    Product[].class);

    if (response.getBody() != null) {
      return Arrays.asList(response.getBody());
    } else {
      return Collections.emptyList();
    }
  }

  private HttpHeaders createAuthorizationHeader(OAuth2AccessToken oAuth2AccessToken) {
    return new HttpHeaders() {
      {
        String authHeader = "Bearer " + oAuth2AccessToken.getTokenValue();
        set("Authorization", authHeader);
      }
    };
  }
}

In the ProductController class, we need to add a reference to an instance of the class OAuth2AuthorizedClientService. By using this instance we can retrieve the required access token.

In addition to this we also show the currently authenticated user by adding a new parameter of type org.springframework.security.oauth2.core.oidc.user.OidcUser annotated by @AuthenticationPrincipal.

com/example/ProductController:

package com.example;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

/** UI controller for products frontend. */
@Controller
public class ProductController {

  private final ProductService productService;
  private final OAuth2AuthorizedClientService authorizedClientService;

  @Autowired
  public ProductController(ProductService productService, OAuth2AuthorizedClientService authorizedClientService) {
    this.productService = productService;
    this.authorizedClientService = authorizedClientService;
  }

  @GetMapping(path = "/")
  public String index(@AuthenticationPrincipal OidcUser oidcUserInfo, Model model) {
    String fullName = oidcUserInfo.getUserInfo().getNickName();
    model.addAttribute("username", fullName);
    return "index";
  }

  @GetMapping(path = "/products")
  public String getAllProducts(Authentication authentication, Model model) {
    OAuth2AuthorizedClient authorizedClient =
            this.authorizedClientService.loadAuthorizedClient("auth0", authentication.getName());
    Iterable<Product> products = productService.getAllProducts(authorizedClient.getAccessToken());
    model.addAttribute("products", products);
    return "products";
  }
}

Please note that "auth0" refers to the corresponding id of the client configuration in application.yml.

Step 5: Run the client application

Now we can run the finished client as well. Please ensure that you have also started the product server from the last part.

Just run class com.example.UiApplication. Then navigate your web browser to http://localhost:9095/client.

If you have successfully followed and completed all steps you should be redirected to the login dialog of the identity server of Auth0.

To log in please use the following user credentials:

  • user: user@example.com

  • password: user_4demo!

Important: The user credentials are noted here just for the purpose of this tutorial. In your real productive applications, you should NEVER publish user credentials or any other sensitive data!!

After successful login, you should again be redirected back to the client application and you should see the main screen.

After clicking the Products link you should see the list of products.

This ends the whole tutorial.

Last updated