close icon
Spring Boot

Secure Spring Boot Web Applications Using Authentication

Get Started with Spring Boot Authentication with Auth0 (Java)

September 23, 2021

Spring is arguably one of the most popular Java frameworks available today. It provides a convenient platform to build modern web applications, and offers native integration with a wide variety of other platforms, frameworks, and protocols, including OAuth 2.0 and OpenID Connect.

In this post, you'll create a web application using Spring Boot with Thymeleaf templates, and then secure access to the application by implementing authentication using Auth0. Users will have the ability to log in and log out, while the application will have the ability to make secure calls to an API to obtain protected resources.

The application code

To build the backend application, you'll need to have JDK 11 or above, which is available from many sources, including OpenJDK, AdoptOpenJDK, Azul, or Oracle.

Signing up for Auth0

The application will be authenticated through Auth0. You can sign up for a free Auth0 account.

Once logged in, create a new application by clicking Applications -> Application -> Create Application:

Creating an Auth0 application

Give the application a name, select Regular Web Application as the application type, and click Create:

Creating a Regular Web Application

Make a note of the Domain, Client ID, and Client Secret. You'll need these values later in the post to configure the Spring application:

The application domain, client ID, and client secret

Set the Allowed Callback URLs to http://localhost:8080/login/oauth2/code/auth0. You'll see later in the post that this URL is the default redirection endpoint configured by Spring.

Set the Allowed Logout URLs to http://localhost:8080. This is the path that users will be redirected to when logging out:

The application callback URLs

The Auth0 application is now configured, and you can start writing some code.

Building the initial Spring application

In this section you'll build a Spring application from the ground up, providing a foundation on which to add authentication and integrate with Auth0. This section calls out the notable aspects of the frontend code, describes how to build the first controller, and demonstrates the login page generated by Spring.

The code created in this section is available from GitHub in the starter branch. For those interested in implementing authentication in Spring, check out this branch and skip ahead to the Adding OAuth authentication section.

Bootstraping the application with Spring Initialzr

Spring offers a service called Spring Initializr that bootstraps a project by defining the basic properties and dependencies the project requires.

Configure Initializr to create an application built with Maven, based on the latest non-snapshot version of Spring, building a JAR file, using Java 11, and including the following dependencies:

  • Spring Web, which hosts a web server and provides a Model-View-Controller (MVC) framework for the web application.
  • Thymeleaf, which provides a template language for building dynamic web pages.
  • OAuth2 Client, which allows the application to interact with a OAuth authorization server.

Spring Initializr with the required dependencies

Click the Generate button to download a ZIP file with the basic project template populated from the selected dependencies.

Compile and run the provided code with the command:

./mvnw spring-boot:run

The web server will listen on port 8080 by default, and so you can open http://localhost:8080 in a browser to be taken to a login page.

This login page is automatically generated by Spring, and is used to expose the various supported login methods. In this default configuration the login page exposes a form based login, allowing credentials to be directly entered.

As you begin integrating Spring with OAuth you will see this login page change from a form based login to a list of links to external authentication providers.

Spring also supports mixed authentication providers, so you could for example support authenticating users locally as well as through external OAuth providers, in which case the generated login form would provide inputs and links for both types of logins.

Here is the default form based login that is configured by default:

The generate form based login page

The logs show a randomly generated password like this:

Using generated security password: e1f3db8d-f89e-4c82-961b-2c797a8b0fb6

You can log into the form with the username user and the randomly generated password. This will take you to the root URL, which in turn displays a type=Not Found, status=404 error because no pages have been defined.

To fix this, the next step is to add a controller and view to the web application.

Creating a controller and view

Controllers interpret user input and transform it into a model that is represented to the user by the view. Create a controller called HomeController to display a HTML page when the user requests the root URL. The complete code is shown below:

// src/main/java/com/matthewcasperson/auth0demo/controllers/HomeController.java

package com.matthewcasperson.auth0demo.controllers;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {
    @GetMapping("/")
    public String main() {
        return "index";
    }
}

This controller is deliberately simple. It is a Plain Old Java Object (POJO) with the @Controller annotation:

@Controller
public class HomeController {

The class defines a single method called main with the @GetMapping annotation configured to respond to GET requests for the root path:

    @GetMapping("/")
    public String main() {

The body of the method returns a string that matches the file src/main/resources/templates/index.html:

        return "index";
    }

The file at src/main/resources/templates/index.html is a placeholder for now:

<html>
    <body>Test</body>
</html>

Run the application again with the command:

./mvnw spring-boot:run

This time when you open http://localhost:8080 the HTML page will be displayed.

Including the frontend assets

The front end code used to interact with the Spring endpoints is a reasonably simple HTML and JavaScript based web application. It consists of a static home page, a page that displays information about the profile of the currently logged in user, and a page that is used to make a number of requests to an external API server.

Here is the homepage:

The web application homepage, saved as src/main/resources/templates/index.html

Here is the profile page:

The profile page, saved as src/main/resources/templates/profile.html

Here is the external API page:

The external API page, saved as src/main/resources/templates/externalapi.html

As this blog post is focused on integrating Spring with Auth0, it won't dive into the specifics of these pages and their associated scripts.

However, it is worth discussing some of the Thymeleaf template syntax that is embedded in these pages, as they require some special attention and dependencies to work.

Each page includes a login button in the top right hand corner directing users to the /login path, and a logout button directing users to the /logout path . The Thymeleaf HTML template that generates these two buttons is shown below:

<button class="button button--primary button--compact" id="loginButton" sec:authorize="isAnonymous()">Log In</button>
<button class="button button--primary button--compact" id="logoutButton" sec:authorize="isAuthenticated()">Log Out</button>

Thymeleaf takes a different approach than many template languages by using custom attributes to modify their parent HTML element. Here the sec:authorize attribute is used to conditionally show one button or the other depending on whether the user is currently logged in or not. The isAnonymous method returns true if the user is not logged in, and the isAuthenticated method returns true if the user is logged in.

This means the login button will be shown if the user is not logged in, and the logout button will be shown if the user is logged in.

The sec:authorize attribute requires an additional Maven dependency, defined in the pom.xml file:

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>

The profile page, provided by the file src/main/resources/templates/profile.html, displays an image for the logged in user. As you'll see later in the post, the user's details are exposed by a model attribute called user that has a number of properties, one of which, called picture, is a URL to an avatar image.

As you did before, use a custom Thymeleaf attribute to dynamically update the HTML. In this case use the th:src attribute, which in turn sets the src attribute of the parent img element:

<img class="profile__avatar"
    th:src="${user.picture}"
    alt="Profile"/>

To display the user's email address, set the text content of a HTML span element. This is performed with the Thymeleaf th:text attribute, which sets the element's text to the value contained in the user model attribute's email property:

<span class="profile__description" th:text="${user.email}">

Finally, display a JSON representation of the currently logged in user's access token. Again, use the Thymeleaf th:text attribute, but this time on a pre element. This code displays the contents of the code model attribute:

<pre class="code-snippet__body" th:text="${code}">

All other frontend code is vanilla HTML, JavaScript, or CSS.

The HTML pages are saved under src/main/resources/templates, and from there they are picked up by Thymeleaf and processed before being sent to the client.

The remaining JavaScript and CSS files are saved under src/main/resources/static, which is one of the default directories Spring will serve static files from. These files are served with no additional processing.

Adding OAuth authentication

The most obvious next step is to integrate the application with an OAuth provider like Auth0.

The application must bypass the generated login page provided by Spring. This generated page is incredibly useful if the application supports a range of authentication methods, like supporting form based logins against a local user database as well as authenticating with an external OAuth provider. However, this demo will only have the one login method, in which case the generated login page is an unnecessary step in the login process, as users must be redirected to the Auth0 login page.

Updating the application configuration

The first step is to configure Spring security via the src/main/resources/application.yml file. The complete file is shown below:

spring:
  security:
    oauth2:
      client:
        registration:
          auth0:
            client-id: ${CLIENT_ID}
            client-secret: ${CLIENT_SECRET}
            scope:
              - openid
              - profile
              - email
        provider:
          auth0:
            # trailing slash is important!
            issuer-uri: https://${DOMAIN}/

All the configuration exists under the spring.security.oauth2.client property:

spring:
  security:
    oauth2:
      client:

Next, register a client called auth0. This can have any name (so it could have called this authzero), but the chosen name will be important when bypassing the generated login page, and must also match the callback URL configured in the Auth0 application:

        registration:
          auth0:

The environment variables CLIENT_ID and CLIENT_SECRET set the client-id and client-secret properties. These values are found in the Client ID and Client Secret fields from the Auth0 application:

            client-id: ${CLIENT_ID}
            client-secret: ${CLIENT_SECRET}

The scope object defines the user attributes received when a user logs in:

  • The openid scope includes details that uniquely identify the user.
  • The profile scope includes details like the user's name and avatar image.
  • The email scope returns the user's email address.

You can find more information on these scopes in the Auth0 documentation:

            scope:
              - openid
              - profile
              - email

Next, define a provider. Call this provider auth0 to match the registration above, and define the issuer-uri from the DOMAIN environment variable. The domain is found in the Auth0 application Domain field:

        provider:
          auth0:
            # trailing slash is important!
            issuer-uri: https://${DOMAIN}/

Compile and run the application with the following bash:

export CLIENT_ID=ClientIDGoesHere
export CLIENT_SECRET=ClientSecretGoesHere
export DOMAIN=ApplicationDomainGoesHere
./mvnw spring-boot:run

Or the following PowerShell:

$env:CLIENT_ID="ClientIDGoesHere"
$env:CLIENT_SECRET="ClientSecretGoesHere"
$env:DOMAIN="ApplicationDomainGoesHere"
.\mvnw spring-boot:run

Then open http://localhost:8080/login. You'll see the generated login page:

The generated OAuth login page

Notice the form based login has been replaced with a link to Auth0.

However, the URL presented on the screen is not actually where the link sends you to. Clicking on the link will first take you to http://localhost:8080/oauth2/authorization/auth0. This is also the callback URL configured in the Auth0 application.

Because there will only ever be one login method, users opening /login are directed to /oauth2/authorization/auth0. This redirection is performed in a controller called LoginController.

Adding the login controller

Add a new class called LoginController that responds to GET requests to the /login path. The complete code for this class is shown below:

// src/main/java/com/matthewcasperson/auth0demo/controllers/LoginController.java

package com.matthewcasperson.auth0demo.controllers;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import javax.servlet.http.HttpServletRequest;

@Controller
public class LoginController {

    @GetMapping("/login")
    public String loginPage(HttpServletRequest request) {
        return "redirect:/oauth2/authorization/auth0";
    }
}

Previous sections described classes annotated with @Controller and methods annotated with @GetMapping, and this controller is no different:

@Controller
public class LoginController {

    @GetMapping("/login")
    public String loginPage() {

Redirect the user to /oauth2/authorization/auth0 with the redirect prefix:

            return "redirect:/oauth2/authorization/auth0";
        }

At this point Spring will still present the generated login page. To have the controller handle the request, the Spring security configuration must be overridden.

This configuration is defined in a class called AuthSecurityConfig. The complete class is shown below:

// src/main/java/com/matthewcasperson/auth0demo/configuration/AuthSecurityConfig.java

package com.matthewcasperson.auth0demo.configuration;

import org.springframework.beans.factory.annotation.Autowired;
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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;

@EnableWebSecurity
@Configuration
public class AuthSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private ClientRegistrationRepository clientRegistrationRepository;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .and()
            .oauth2Login()
                .loginPage("/login").permitAll();

    }
}

The combination of the @EnableWebSecurity and @Configuration annotations on a class extending WebSecurityConfigurerAdapter allows you to control the security settings of the application. To quote from the @EnableWebSecurity API documentation:

Add this annotation to an @Configuration class to have the Spring Security configuration defined in any WebSecurityConfigurer or more likely by extending the WebSecurityConfigurerAdapter base class and overriding individual methods:

@EnableWebSecurity
@Configuration
public class AuthSecurityConfig extends WebSecurityConfigurerAdapter {

The security configuration is defined in the configure method:

    @Override
    protected void configure(HttpSecurity http) throws Exception {

Spring exposes a fluent interface through the HttpSecurity class to configure security settings.

Start by calling authorizeRequests, which matches the URLs to authorize. Calling authorizeRequests is also a requirement of calling oauth2Login, and without it oauth2Login throws an exception at runtime.

Then call oauth2Login to customize the OAuth configuration. The loginPage method customizes the login page. The loginPage method implements a number of changes, but specifically:

when authentication is required, redirect the browser to /login

we are in charge of rendering the login page when /login is requested

The permitAll method allows both authenticated and unauthenticated users to access the login page.

With this configuration in place, our controllers are charge of providing the login page, which means the LoginController will be called when a user opens /login:

        http
            .authorizeRequests()
            .and()
            .oauth2Login()
                .loginPage("/login").permitAll();

Run the application with the following bash:

export CLIENT_ID=ClientIDGoesHere
export CLIENT_SECRET=ClientSecretGoesHere
export DOMAIN=ApplicationDomainGoesHere
./mvnw spring-boot:run

Or the following PowerShell:

$env:CLIENT_ID="ClientIDGoesHere"
$env:CLIENT_SECRET="ClientSecretGoesHere"
$env:DOMAIN="ApplicationDomainGoesHere"
.\mvnw spring-boot:run

Open http://localhost:8080/login. Notice that you are taken directly to the Auth0 login page, bypassing the login page generated by Spring.

With the login process implemented, the next step is to add the ability to log out.

Implement a logout

To support the frontend code, the user must be able to log out by visiting /logout.

By default, Spring performs a logout via a HTTP POST operation. There are many reasons for this, but primarily:

the HTTP/1.1 RFC clearly states that GET methods should only be used to return content and the user cannot be held responsible for any side-effects of a GET request

Having said that, this simple application will override the default behavior and allow a GET request to the /logout page to perform a logout. This change is implemented in the AuthSecurityConfig class. The complete code for the class is shown below:

// src/main/java/com/matthewcasperson/auth0demo/configuration/AuthSecurityConfig.java

package com.matthewcasperson.auth0demo.configuration;

import org.springframework.beans.factory.annotation.Autowired;
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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@EnableWebSecurity
@Configuration
public class AuthSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private ClientRegistrationRepository clientRegistrationRepository;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .and()
            .oauth2Login()
                .loginPage("/login").permitAll()
                .and()
                .logout()
                    .logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET"))
                    .invalidateHttpSession(true)
                    .clearAuthentication(true)
                    .logoutSuccessUrl("/")
                    .deleteCookies("JSESSIONID")
                    .permitAll();
    }
}

Continue to build upon the fluent interface exposed by HttpSecurity to configure the logout process.

Calling logout exposes the log out configuration.

Call logoutRequestMatcher to allow a log out to be performed by a GET request to the /logout page. Redirect the user to the home page by calling logoutSuccessUrl. The cookie used to hold the session ID, called JSESSIONID, is cleared on logout with the call to deleteCookies. Finally, permit everyone access to the /logout path by calling permitAll:

                .logout()
                    .logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET"))
                    .logoutSuccessUrl("/")
                    .deleteCookies("JSESSIONID")
                    .permitAll();

Users now have the ability to log in and out of the application. The next step is to implement the profile page to display information about the logged in user.

Adding the profile page

The process of adding new pages to the application should start to look familiar now. Create a class with the @Controller annotation, and apply a @GetMapping annotation to a method inside.

The class called ProfileController responds to requests to the /profile path. The complete code is shown below:

// src/main/java/com/matthewcasperson/auth0demo/controllers/ProfileController.java

package com.matthewcasperson.auth0demo.controllers;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class ProfileController {
    @GetMapping("/profile")
    public ModelAndView profile() {
        Authentication authentication =
                SecurityContextHolder
                        .getContext()
                        .getAuthentication();

        OAuth2AuthenticationToken oauthToken =
                (OAuth2AuthenticationToken) authentication;

        String code = getUserAsJSON(oauthToken.getPrincipal().getAttributes());

        ModelAndView mav = new ModelAndView("profile");
        mav.addObject("user", oauthToken.getPrincipal().getAttributes());
        mav.addObject("code", code);
        return mav;
    }

    private String getUserAsJSON(Object attributes) {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JavaTimeModule());
        try {
            return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(attributes);
        } catch (JsonProcessingException e) {
            return "{\"message\":\"" + e + "\"}";
        }
    }
}

Like the other controllers, this class has a method with the @GetMapping annotation. However, the profile method has a slightly different signature to the previous methods: it returns a ModelAndView instead of a String. This provides the ability to populate a model to be provided to the view:

    @GetMapping("/profile")
    public ModelAndView profile() {

First, get access to the Authentication object held in the Spring security context. This object represents the currently authenticated user:

        Authentication authentication =
                SecurityContextHolder
                        .getContext()
                        .getAuthentication();

Because the application only logs in via OAuth, cast the Authentication object to a OAuth2AuthenticationToken:

        OAuth2AuthenticationToken oauthToken =
                (OAuth2AuthenticationToken) authentication;

The details of the OAuth user are return by calling getPrincipal, and the attributes (or claims) assigned to the user are returned by calling getAttributes.

To display the claims in the frontend application, serialize them to a JSON blob by calling getUserAsJSON:

        String code = getUserAsJSON(oauthToken.getPrincipal().getAttributes());

Now begin the process of building the model and view. Information about the template to be rendered by this page (i.e. the view) and the model that provides data to the template are contained in a ModelAndView:

        ModelAndView mav = new ModelAndView("profile");

The claims returned by the OAuth server are assigned to a model attribute called user. If you recall from the Including the frontend assets section, the profile page reads the email address and avatar image from the user model attribute:

        mav.addObject("user", oauthToken.getPrincipal().getAttributes());

The JSON blob is assigned to the model attribute called code:

        mav.addObject("code", code);

The ModelAndView object is then returned:

        return mav;
    }

The getUserAsJSON method serializes a Java object into JSON. It makes use of the Jackson library. Note that the JavaTimeModule module must be registered, as the user claims includes fields from the Java 8 java.time namespace, which are not supported in Jackson by default:

    private String getUserAsJSON(Object attributes) {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JavaTimeModule());
        try {
            return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(attributes);
        } catch (JsonProcessingException e) {
            return "{\"message\":\"" + e + "\"}";
        }
    }

This Jackson module requires an additional dependency included in the pom.xml file:

        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
        </dependency>

Run the application with the following bash:

export CLIENT_ID=ClientIDGoesHere
export CLIENT_SECRET=ClientSecretGoesHere
export DOMAIN=ApplicationDomainGoesHere
./mvnw spring-boot:run

Or the following PowerShell:

$env:CLIENT_ID="ClientIDGoesHere"
$env:CLIENT_SECRET="ClientSecretGoesHere"
$env:DOMAIN="ApplicationDomainGoesHere"
.\mvnw spring-boot:run

Open http://localhost:8080/login to log in, and then open http://localhost:8080/profile. The page will display the current user's email address, avatar image, and the JSON blob containing the claims associated with the user.

The final step is to extend the application to make API calls to an external service.

Calling an external service

The final functionality to implement is the ability to call an external API that itself is protected by a JWT token. This means the Spring application must receive a JWT access token from Auth0 and pass it along to the external service.

There are multiple projects that implement a compatible API. See this GitHub repository for an implementation written in Kotlin, and this GitHub repository for an implementation written in Node.js. Download, build, and run either of these implementations to provide a service for the code to query.

Returning a JWT access token

By default, the token generated by Auth0 is an opaque token. Opaque tokens are:

Tokens in a proprietary format that typically contain some identifier to information in a server’s persistent storage.

Opaque tokens are fine for authenticating users within the context of the Spring application, but opaque tokens cannot be passed to a downstream service that specifically demands a JWT token. JWT tokens are:

Tokens that conform to the JSON Web Token standard and contain information about an entity in the form of claims.

To receive a JWT token, the audience parameter must be included with the token request. Adding additional parameters involves hooking into the Spring OAuth process to inject additional parameters. This is done in the AuthSecurityConfig class. The complete code is shown below:

// src/main/java/com/matthewcasperson/auth0demo/configuration/AuthSecurityConfig.java

package com.matthewcasperson.auth0demo.configuration;

import org.springframework.beans.factory.annotation.Autowired;
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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import java.util.function.Consumer;

@EnableWebSecurity
@Configuration
public class AuthSecurityConfig extends WebSecurityConfigurerAdapter {

    private ClientRegistrationRepository clientRegistrationRepository;

    @Autowired
    public AuthSecurityConfig(ClientRegistrationRepository clientRegistrationRepository) {
        this.clientRegistrationRepository = clientRegistrationRepository;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .and()
            .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET"))
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                .logoutSuccessUrl("/")
                .deleteCookies("JSESSIONID")
                .permitAll()
            .and()
            .oauth2Login()
                .loginPage("/login").permitAll()
                .authorizationEndpoint()
                .authorizationRequestResolver(
                  authorizationRequestResolver(this.clientRegistrationRepository)
                );
    }

    private OAuth2AuthorizationRequestResolver authorizationRequestResolver(
            ClientRegistrationRepository clientRegistrationRepository) {

        DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver =
                new DefaultOAuth2AuthorizationRequestResolver(
                        clientRegistrationRepository, "/oauth2/authorization");
        authorizationRequestResolver.setAuthorizationRequestCustomizer(
                customizer -> customizer
                        .additionalParameters(params -> params.put("audience", System.getenv().get("AUDIENCE"))));

        return authorizationRequestResolver;
    }
}

Use constructor injection to gain access to a ClientRegistrationRepository object. This object is used to access the client registrations, such as the registration called auth0 defined in the application.yaml file:

    private ClientRegistrationRepository clientRegistrationRepository;

    @Autowired
    public AuthSecurityConfig(ClientRegistrationRepository clientRegistrationRepository) {
        this.clientRegistrationRepository = clientRegistrationRepository;
    }

Call the method called authorizationEndpoint exposed by oauth2Login, and from there call authorizationRequestResolver. The first parameter to the authorizationRequestResolver method is an instance of the interface OAuth2AuthorizationRequestResolver, returned by the method authorizationRequestResolver:

                .authorizationEndpoint()
                  .authorizationRequestResolver(
                    authorizationRequestResolver(this.clientRegistrationRepository)
                  );

The OAuth2AuthorizationRequestResolver interface is used to build a class to customize the request parameters sent to the OAuth server. To quote from the Spring documentation:

One of the primary use cases an OAuth2AuthorizationRequestResolver can realize is the ability to customize the Authorization Request with additional parameters above the standard parameters defined in the OAuth 2.0 Authorization Framework.

For example, OpenID Connect defines additional OAuth 2.0 request parameters for the Authorization Code Flow extending from the standard parameters defined in the OAuth 2.0 Authorization Framework.

The code below creates an instance of DefaultOAuth2AuthorizationRequestResolver (which implements OAuth2AuthorizationRequestResolver), and then calls setAuthorizationRequestCustomizer.

The parameter passed to setAuthorizationRequestCustomizer is a lambda that adds the additional audience parameter, whose value is found from the environment variable called AUDIENCE:

    private OAuth2AuthorizationRequestResolver authorizationRequestResolver(
            ClientRegistrationRepository clientRegistrationRepository) {

        DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver =
                new DefaultOAuth2AuthorizationRequestResolver(
                        clientRegistrationRepository, "/oauth2/authorization");
        authorizationRequestResolver.setAuthorizationRequestCustomizer(
                customizer -> customizer
                        .additionalParameters(params -> params.put("audience", System.getenv().get("AUDIENCE"))));

        return authorizationRequestResolver;
    }

The end result of this change is that the token request to Auth0 includes the audience parameter, which in turn means the resulting access token is a JWT.

Calling the external API

If the browser was to directly call an external API hosted on a different hostname or port, the external API must return Cross-Origin Resource Sharing (CORS) headers, otherwise the browser will block the request.

To avoid this requirement on the external API, the Spring application exposes endpoints for the frontend to call, and then proxies those requests to the external API. This is possible because Java, unlike a web browser, can call whatever network resources it likes.

The ApiProxyController controller exposes the external API endpoints. The complete code is shown below:

// src/main/java/com/matthewcasperson/auth0demo/controllers/ApiProxyController.java

package com.matthewcasperson.auth0demo.controllers;

import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletResponse;

@Controller
public class ApiProxyController {

    private OAuth2AuthorizedClientService clientService;

    @Autowired
    public ApiProxyController(OAuth2AuthorizedClientService clientService) {
        this.clientService = clientService;
    }

    private String getAccessToken() {
        Authentication authentication =
                SecurityContextHolder
                        .getContext()
                        .getAuthentication();

        OAuth2AuthenticationToken oauthToken =
                (OAuth2AuthenticationToken) authentication;

        OAuth2AuthorizedClient client =
                clientService.loadAuthorizedClient(
                        oauthToken.getAuthorizedClientRegistrationId(),
                        oauthToken.getName());

        return client.getAccessToken().getTokenValue();
    }

    private String accessAPI(String message) {
        try {
            try (CloseableHttpClient httpClient = HttpClients.custom().build()) {
                HttpUriRequest request = RequestBuilder.get()
                        .setUri("http://" + System.getenv().get("EXTERNALAPI") + "/api/messages/" + message)
                        .setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + getAccessToken())
                        .build();
                try (CloseableHttpResponse response = httpClient.execute(request)) {
                    return EntityUtils.toString(response.getEntity());
                }
            }
        } catch (Exception e) {
            return "{\"message\": \"" + e + "\"}";
        }
    }

    @RequestMapping(value = "/api/messages/{message}", method = RequestMethod.GET)
    @ResponseBody
    public String proxyMessageRequest(HttpServletResponse response, @PathVariable("message") String message) {
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        return accessAPI(message);
    }
}

This controller uses constructor injection to gain access to an instance of the Spring OAuth2AuthorizedClientService interface. This service is required to extract the JWT access token returned by the OAuth server:

    private OAuth2AuthorizedClientService clientService;

    @Autowired
    public ApiProxyController(OAuth2AuthorizedClientService clientService) {
        this.clientService = clientService;
    }

The getAccessToken method uses clientService to extract the JWT token and return it as a String:

    private String getAccessToken() {
        Authentication authentication =
                SecurityContextHolder
                        .getContext()
                        .getAuthentication();

        OAuth2AuthenticationToken oauthToken =
                (OAuth2AuthenticationToken) authentication;

        OAuth2AuthorizedClient client =
                clientService.loadAuthorizedClient(
                        oauthToken.getAuthorizedClientRegistrationId(),
                        oauthToken.getName());

        return client.getAccessToken().getTokenValue();
    }

The accessAPI method makes a HTTP call to the external API, configured through the EXTERNALAPI environment variable. The external API is expected to respond on three paths:

  • /api/messages/public
  • /api/messages/protected
  • /api/messages/admin

The JWT token is passed in the Authorization header with the value Bearer: <JWT>. The response from the external API is then returned as a String. If there was an exception, place the exception text into a JSON blob and return that to the client:

    private String accessAPI(String message) {
        try {
            try (CloseableHttpClient httpClient = HttpClients.custom().build()) {
                HttpUriRequest request = RequestBuilder.get()
                        .setUri("http://" + System.getenv().get("EXTERNALAPI") + "/api/messages/" + message)
                        .setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + getAccessToken())
                        .build();
                try (CloseableHttpResponse response = httpClient.execute(request)) {
                    return EntityUtils.toString(response.getEntity());
                }
            }
        } catch (Exception e) {
            return "{\"message\": \"" + e + "\"}";
        }
    }

Now expose a method to respond to GET requests to any path starting with /api/messages/. Notice here that this method does not use the @GetMapping annotation, but instead uses the more generic @RequestMapping to match the incoming requests, and @ResponseBody to indicate the method return value is to be included in the HTTP response body.

The value assigned to the @RequestMapping annotation is the path that this method responds to. The string /api/messages/{message} indicates this method responds to any request to a path starting with /api/messages/, with the final element of the path being any value.

The method takes an instance of HttpServletResponse as the first parameter. Use this parameter to customize the response to the client. The method also takes a string with the @PathVariable annotation, which links the variable defined in the path to the method parameter:

    @RequestMapping(value = "/api/messages/{message}", method = RequestMethod.GET)
    @ResponseBody
    public String proxyMessageRequest(HttpServletResponse response, @PathVariable("message") String message) {

The body of the method sets the response HTTP headers, and returns the JSON blob from the external API:

        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        return accessAPI(message);
    }

Providing the external API frontend

The frontend web application must now be exposed on the path /external-api. This is done with the controller ExternalApiController, which follows the now familiar pattern of an MVC controller.

The complete code is shown below:

// src/main/java/com/matthewcasperson/auth0demo/controllers/ExternalApiController.java

package com.matthewcasperson.auth0demo.controllers;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class ExternalApiController {
    @GetMapping("/external-api")
    public String externalApi() {
        return "externalapi";
    }
}

Run the application with the following bash:

export CLIENT_ID=ClientIDGoesHere
export CLIENT_SECRET=ClientSecretGoesHere
export DOMAIN=ApplicationDomainGoesHere
export AUDIENCE=AudienceGoesHere
export EXTERNALAPI=localhost:6060
./mvnw spring-boot:run

Or the following PowerShell:

$env:CLIENT_ID="ClientIDGoesHere"
$env:CLIENT_SECRET="ClientSecretGoesHere"
$env:DOMAIN="ApplicationDomainGoesHere"
$env:AUDIENCE="AudienceGoesHere"
$env:EXTERNALAPI="localhost:6060"
.\mvnw spring-boot:run

Ensure one of the external API projects noted under the Calling an external service heading have been started. Then perform a login by opening http://localhost:8080/login, and open http://localhost:8080/external-api. The page displays three tabs, and clicking on each of them makes a request to either /api/messages/public, /api/messages/protected, or /api/messages/admin. These requests are handled by ApiProxyController, which in turn makes a request to the same paths on an external API. The external API responses are then returned to the frontend web application, where they are displayed in the browser.

However, if this page is accessed while not logged in, an exception will be thrown, so the security configuration must be updated to ensure the profile and external API pages can only be accessed by authenticated users.

Restricting access to protected pages

There is one minor piece of housekeeping to address, which is to force the user to be logged in when accessing the profile and external API pages. This is configured in the AuthSecurityConfig class. The complete code is shown below:

// src/main/java/com/matthewcasperson/auth0demo/configuration/AuthSecurityConfig.java

package com.matthewcasperson.auth0demo.configuration;

import org.springframework.beans.factory.annotation.Autowired;
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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@EnableWebSecurity
@Configuration
public class AuthSecurityConfig extends WebSecurityConfigurerAdapter {

    private ClientRegistrationRepository clientRegistrationRepository;

    @Autowired
    public AuthSecurityConfig(ClientRegistrationRepository clientRegistrationRepository) {
        this.clientRegistrationRepository = clientRegistrationRepository;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/", "/*.js", "/*.css").permitAll()
                .anyRequest().authenticated()
            .and()
            .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET"))
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                .logoutSuccessUrl("/")
                .deleteCookies("JSESSIONID")
                .permitAll()
            .and()
            .oauth2Login()
                .loginPage("/login").permitAll()
                .authorizationEndpoint()
                .authorizationRequestResolver(
                        authorizationRequestResolver(this.clientRegistrationRepository)
                );
    }

    private OAuth2AuthorizationRequestResolver authorizationRequestResolver(
            ClientRegistrationRepository clientRegistrationRepository) {

        DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver =
                new DefaultOAuth2AuthorizationRequestResolver(
                        clientRegistrationRepository, "/oauth2/authorization");
        authorizationRequestResolver.setAuthorizationRequestCustomizer(
                customizer -> customizer
                        .additionalParameters(params -> params.put("audience", System.getenv().get("AUDIENCE"))));

        return authorizationRequestResolver;
    }
}

The code below ensures that anyone can open the root page, as well as any of the static JavaScript or CSS files. All other pages require the user to be authenticated:

```java

  • Twitter icon
  • LinkedIn icon
  • Faceboook icon