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:
Give the application a name, select Regular Web Application as the application type, and click Create:
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:
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 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.
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 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:
Here is the profile page:
Here is the external API page:
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:
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