Securing Spring Boot Microservices

Published on April 30, 2024

Overview

Ready to level up your microservices security game? This lab shows how to secure your Spring Boot apps and microservices using the powerful combination of OAuth, OpenID Connect (OIDC), and popular Identity Providers (IdP) like Auth0 and Keycloak.

Securing Spring Boot starts with a gentle introduction to Spring Boot, OAuth 2.0, and OpenID Connect (OIDC) by having you build an API that can be accessed with an access token. From there, you'll see how to leverage OIDC to implement login and logout features, using Thymeleaf for the view layer. Then, you'll learn about role-based access control and how to map your Auth0 user roles to Spring Security authorities.

Once you have the security building blocks in place, you'll move to building a secure microservice architecture with Spring Cloud and the applications you created in the previous steps. Finally, you'll learn how to move beyond passwords with passkeys.

This lab requires you to navigate sequentially through it, completing each step as you go.

Connect with the author Deepu Sasidharan

Deepu Sasidharan is a Software Engineer by passion and profession. He is a Java Champion working as a Staff Developer Advocate at Auth0 by Okta. He is the co-chair of JHipster and the creator of KDash, JWT-UI, and JDL Studio. He is a polyglot programmer working with Java, Rust, JavaScript, Go, etc. He is also a cloud technology advocate and an open-source software aficionado. He has authored books on Full-stack development and frequently writes about Java, Rust, JavaScript, Go, DevOps, Kubernetes, Linux, and so on, on his blog.

In this lab, you'll learn how to use Java and Spring Boot to build web apps and microservices secured with OAuth 2.0 and OIDC. But first, let's do a crash course for those who are new to these concepts.

What you will learn:

  • Basics of Oauth 2.0
  • Basics of OpenID Connect (OIDC).

Why use Spring to build apps?

Spring Boot is one of the most popular frameworks for developing Java web applications and REST APIs. It's used by companies around the world to simplify development and ease testing. If you want to be employed for years to come, Java is a good language to learn. Put those two together, and you have a winning combination!

Spring Boot provides a streamlined development experience by simplifying configuration and reducing boilerplate code. With Spring Boot, you can quickly create standalone, production-ready applications with minimal setup.

Here are some key features of Spring Boot:

  • Auto-configuration: Spring Boot automatically configures the application based on the dependencies present in the classpath. This eliminates the need for manual configuration and reduces development time.
  • Starter dependencies: Spring Boot provides a set of starter dependencies that include all the necessary libraries and configurations for specific use cases. You can easily add these starters to your project and get started quickly. For example, the okta-spring-boot starter is a thin wrapper around Spring Security's resource server, OIDC login, and OAuth client support. It makes enabling OAuth 2.0 and OIDC in your Spring Boot application a breeze.
  • Embedded server: Spring Boot includes an embedded server (Tomcat, Jetty, or Undertow) that allows you to run your application as a standalone executable JAR file. This makes the deployment and distribution of your application simple.
  • Actuator: Spring Boot Actuator provides production-ready features to monitor and manage your application. It includes endpoints for health checks, metrics, logging, and more.
  • Spring Initializr: Spring Initializr is a web-based tool that allows you to quickly create a new Spring Boot project with the desired dependencies. You can choose the project type, language, build tool, and dependencies, and Spring Initializr generates a project structure for you.
  • Spring Cloud: It is a companion project that allows you to build microservices quickly using the Spring framework. It provides a set of tools for common patterns in distributed systems, such as configuration management, service discovery, circuit breakers, intelligent routing, and more.

What is OAuth 2.0 and how does it work?

Authorization is the process of determining whether a user has the necessary permissions to access a resource. OAuth 2.0 is the industry-standard protocol for delegated authorization. OAuth 2.0 allows a user to grant a third-party website or application access to the user's protected resources without necessarily revealing their long-term credentials or even their identity. OAuth 2.0 provides specific authorization flows for web applications, desktop applications, mobile applications, and other types of clients. Let's see a quick summary of the OAuth 2.0 concepts and how it works.

For a more detailed explanation, check out the OAuth 2.0 introduction and OAuth 2.0 guide on the Auth0 website. You can also visit the OAuth 2.0 website.

OAuth 2.0 concepts

The key concepts in OAuth 2.0 are:

  • System Roles: These define the essential components of an OAuth 2.0 system.
    • Resource Owner: The entity that can grant access to a protected resource. Typically, this is the end-user.
    • Resource Server: The server that hosts the protected resources. The resource server is capable of validating and responding to protected resource requests using access tokens.
    • Client: The application or system that wants to access the protected resource on behalf of the user.
    • Authorization Server: The server that authenticates the user and issues access tokens. The authorization server exposes two endpoints: the authorization endpoint, which handles interactive authentication and consent of the user, and the token endpoint, which handles the exchange of an authorization code for an access token between the client and the authorization server.
  • Access Token: A piece of data that represents authorization to access a resource on behalf of the end-user. The access token is used by the client to make API requests on behalf of the user. JSON Web Tokens (JWTs) are a commonly used format for access tokens.
  • Authorization Code: An authorization code is a short-lived token that the client exchanges for an access token. After the user has authenticated and approved the requested access, the client obtains the authorization code.
  • Refresh Token: A refresh token is a long-lived token that the client can use to obtain a new access token when the current access token expires. Refresh tokens are used to maintain access to a resource without needing any user interaction, such as re-authentication.
  • Claims: A claim is an assertion. It's a key-value pair containing information about a user, like name or email. It is found on a token.
  • Scope: A scope is the permission or group of claims that the client requests from the user. It limits the client's access to the user's resources.

OAuth 2.0 grants

Another important concept in OAuth 2.0 is the grant type. The grant type defines the OAuth2 flow with which the client can obtain an access token. OAuth 2.0 defines several grant types, each suited to different use cases:

  • Authorization Code Grant: The authorization server issues an authorization code to the client after the user has authenticated and approved the requested access. The client exchanges the authorization code for an access token. Used by applications that can securely store the client secret.
  • Implicit Grant: The client receives an access token directly from the authorization server. Used by single-page applications (SPAs) and native applications.
  • Client Credentials Grant: Used by confidential clients to obtain an access token without user authentication. The client authenticates with the authorization server using its client ID and secret.
  • Resource Owner Password Credentials Grant: Used by trusted clients to obtain an access token by directly providing the user's credentials to the authorization server.
  • Refresh Token Grant: Used by clients to obtain a new access token by presenting a refresh token to the authorization server.

OAuth 2.1 drops the Implicit and Resource Owner Password Credentials grant types due to their low-security profile. It also makes Proof Key for Code Exchange (PKCE) mandatory for Authorization Code flow making it more secure for native applications and SPAs.

Some additional extension grants like Device Authorization Grant and Token Exchange Grant are widely used as well.

How OAuth 2.0 works

Let's see how OAuth 2.0 works for Authorization Code Grant, the most common flow:

  1. The client acquires credentials (client ID and client secret) from the authorization server to identify itself. This is a one-time step.
  2. The client requests authorization from the authorization server. The authorization request includes the client ID, response type, the requested scope, code challenge (only for PKCE), and a redirect URI to return to.
  3. The authorization server authenticates the client and verifies the requested scopes.
  4. The resource owner authenticates with the authorization server and authorizes the requested access.
  5. The authorization server redirects back to the client with an authorization code.
  6. The client calls the token endpoint with the authorization code, client ID, client secret (not required for PKCE), and code verifier (only for PKCE) to exchange it for an access token and an optional refresh token.
  7. The client uses the access token to make API requests on behalf of the user to the resource server.

Authorization Code Grant Flow with PKCE

OAuth 2.0 Authorization Code Grant Flow with PKCE

Other grant flows use a subset of these steps. For example, both Credentials Grant flows and Refresh Token Grant flow doesn't require steps 1 to 4. The Implicit Grant flow doesn't have step 4 and gets an access token directly.

Client Credential Grant Flow

OAuth 2.0 Client Credential Grant Flow

What is OpenID Connect and how does it work?

Authentication is the process of verifying the identity of a user. OAuth was designed with authorization in mind, but it lacked a standard way to authenticate users. OpenID Connect (OIDC) is an identity layer built on top of the OAuth 2.0 framework. It allows third-party applications to verify the end user's identity and obtain user profile information optionally.

The OIDC flow is very identical to the OAuth 2.0 flow above with a few add-ons. OIDC introduces the concept of an ID token, which is a JWT that contains user information.

  • In step 1, A specific scope of openid is sent to the OIDC-capable Identity Provider.
  • In step 5, The ID token is returned to the client along with the access token after the user has authenticated and authorized the requested access.
  • In step 6, The ID token is used by the client to verify the identity of the user and to obtain basic user profile information.
  • Step 7, Optionally the client can request more user information using the UserInfo endpoint and access token.

OIDC using Authorization Code Grant Flow with PKCE

OIDC using Authorization Code Grant Flow with PKCE

Pre-Requisites and Development Environment Setup

The focus of this lab is on securing the apps with OAuth 2.0, and OIDC. You should have a basic understanding of Java, Spring Boot, and REST APIs.

This guide is using Spring Boot version 3.2.4.

Before you start building your Spring Boot applications, you need to set up your development environment. You'll need the following tools:

  • Use your favorite text editor or IDE. We recommend using IntelliJ IDEA.
  • Ensure that you have Java 17+ installed in your system. You can easily install it using SDKMAN! or manually via pre-built binaries.
  • Ensure you have Docker and Docker Compose installed.
  • Windows commands in this guide are written for PowerShell version 5.0 or later. If you're using an older version, you may need to upgrade.

Create an Auth0 account

If you already have an Auth0 account, you can log in to your tenant and continue to the next step.

Otherwise, sign up for a free Auth0 account.

During the sign-up process, you create something called an Auth0 Tenant, where you configure your use of Auth0.

Once you sign in, Auth0 takes you to the Auth0 Dashboard, where you can configure and manage Auth0 assets, such as applications, APIs, connections, and user profiles.

Set up the Auth0 CLI

If you are not familiar with the Auth0 CLI, you can follow the "Auth0 CLI Basics" lab to learn how to build, manage, and test your Auth0 integrations from the command line.

There are different ways to install the Auth0 CLI, depending on your operating system.

LOADING...

Create a directory

Create a directory to hold all your projects.

COMMAND
mkdir spring-boot-microservices
cd spring-boot-microservices

Part 1: Create a Spring Boot API Secured with OAuth 2.0

What you will learn:

  • Learn how to build a Spring Boot API with Java.
  • Learn how to secure your API with OAuth 2.0.
  • Test your protected API endpoints.

Create a Spring Boot app using start.spring.io's REST API. This is a simple Car Service that uses Spring Data REST to serve up a REST API of cars. Dependencies used are Spring Boot Actuator, Spring Data JPA, Rest Repositories, PostgreSQL Driver, Spring Web, Validation, Spring Boot DevTools, and Docker Compose Support.

To create a Gradle project, run the following command:

LOADING...

Run the below commands if you want to use Maven instead of Gradle.

LOADING...

Create a car service

Configure the application.properties file to use port 8090, to have an application name, and to create the database automatically.

car-service/src/main/resources/application.properties
spring.application.name=car-service
server.port=8090
spring.jpa.hibernate.ddl-auto=update

Create a data package and a Car entity in the package with id and name properties.

LOADING...

Add the following code:

car-service/src/main/java/com/example/carservice/data/Car.java
package com.example.carservice.data;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.validation.constraints.NotNull;
import java.util.Objects;
@Entity
public class Car {
public Car() {
}
public Car(String name) {
this.name = name;
}
@Id
@GeneratedValue
private Long id;
@NotNull
private String name;
// generate getters and setters with your IDE
// create equals(), hashCode(), and toString() with your IDE
}

Don't forget to generate getters, setters, toString(), equals(), and hashCode() methods using your IDE.

Create a CarRepository interface in the same package:

LOADING...

Add the following code:

car-service/src/main/java/com/example/carservice/data/CarRepository.java
package com.example.carservice.data;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CarRepository extends JpaRepository<Car, Long> {
}

Modify CarServiceApplication to create a default set of cars when the application loads.

car-service/src/main/java/com/example/carservice/CarServiceApplication.java
package com.example.carservice;
import com.example.carservice.data.Car;
import com.example.carservice.data.CarRepository;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import java.util.stream.Stream;
@SpringBootApplication
public class CarServiceApplication {
public static void main(String[] args) {
SpringApplication.run(CarServiceApplication.class, args);
}
@Bean
ApplicationRunner init(CarRepository repository) {
repository.deleteAll();
return args -> {
Stream.of("Ferrari", "Jaguar", "Porsche", "Lamborghini", "Bugatti",
"AMC Gremlin", "Triumph Stag", "Ford Pinto", "Yugo GV").forEach(name -> {
repository.save(new Car(name));
});
repository.findAll().forEach(System.out::println);
};
}
}

Create a CarController class in the web package to expose a /cars endpoint.

LOADING...

Add the below code:

car-service/src/main/java/com/example/carservice/web/CarController.java
package com.example.carservice.web;
import com.example.carservice.data.Car;
import com.example.carservice.data.CarRepository;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
class CarController {
private final CarRepository repository;
public CarController(CarRepository repository) {
this.repository = repository;
}
@GetMapping("/cars")
public List<Car> getCars() {
return repository.findAll();
}
}

Test the service

There's a compose.yaml file in the root directory to start a PostgreSQL instance. This will be automatically started when you start the application.

car-service/compose.yaml
services:
postgres:
image: 'postgres:latest'
environment:
- 'POSTGRES_DB=mydatabase'
- 'POSTGRES_PASSWORD=secret'
- 'POSTGRES_USER=myuser'
ports:
- '5432'

Start the car service application:

GRADLE
MAVEN
./gradlew bootRun

Confirm you can access the /cars endpoint:

LOADING...

Secure the API with OAuth 2.0

We will use Auth0 as the Identity Provider (IdP) for OAuth and OIDC. To make your API an OAuth2 resource server, you need to add the okta-spring-boot-starter dependency to your project. The Okta Spring Boot starter is a thin wrapper around Spring Security's resource server, OIDC login, and OAuth client support. It secures all endpoints by default and makes enabling OAuth 2.0 and OIDC in your Spring Boot application a breeze.

GRADLE
MAVEN
// # Modify the `build.gradle` file:
implementation 'com.okta.spring:okta-spring-boot-starter:3.0.6'

Create an Auth0 application

Open a terminal and run auth0 login to configure the Auth0 CLI to get an API key for your tenant. Then, run auth0 apps create to register an OIDC app with the appropriate URLs:

LOADING...

Copy the domain, client ID, and client secret of your app and paste it into the following input boxes:

When you enter a value in the input fields present on this page, any code snippet that uses such value updates to reflect it. Using the input fields makes copying and pasting code as you follow along easy.

For security, these configuration values are stored in memory and only used locally. They are gone as soon as you refresh the page!

Configure OAuth2

Create an application.properties file.

LOADING...

Add the below configuration.

car-service/application.properties
# trailing slash is important for issuer
okta.oauth2.issuer=https://AUTH0-DOMAIN/
okta.oauth2.audience=${okta.oauth2.issuer}api/v2/
  • issuer: The URL of the authorization server. This is the base URL of the Auth0 tenant. This tells the application where to find the authorization server.
  • audience: The intended consumer of the token (the audience of the authorization server). Ideally, this should be the URL of your API server and should match the aud claim in the access token. In this case, we are using the default API from your Auth0 tenant for simplicity as it is the default aud claim in the access token. For production applications, you should create a new API for your application in Auth0.
The car service doesn't need the client ID and client secret because it's acting as a resource server and simply validates the access token, without communicating with Auth0. We will use these values later on in the lab.

Restart the car service application and confirm you can access the /cars endpoint:

LOADING...

You should see a 401 Unauthorized response because the API is now secured with OAuth 2.0. You need to obtain an access token from Auth0 and include it in the request to access the /cars endpoint.

Get an access token

You can get an access token using the Auth0 CLI to test making a secure call to your protected API endpoint:

COMMAND
auth0 test token -a https://AUTH0-DOMAIN/api/v2/ -s openid

Select any available client when prompted. You will be prompted to open a browser window and log in with a user credential. You can sign up as a new user using an email and password or using the Google social login.

If you'd like to see what's in the access token you created, you can copy and paste it into JWT.io or use JWT UI.

You can also get an access token using the Authorization Code Flow.

Paste the access token value in the following field so that you can use it to test your resource server:

Run the following command to make an authenticated request to your resource server:

LOADING...

You should receive a 200 OK response with the list of cars.

Stop the resource server using Ctrl+C.

So far, you learned how to build a Spring Boot API with Java, secure it with OAuth 2.0, and learned how to make authenticated requests from the command line.

Part 2: Create a Spring Boot Web Application Secured with OpenID Connect

What you will learn:

  • How to build a Spring Boot web app with Java.
  • How to secure your app and log in with OpenID Connect.
  • How to add a logout feature.

Create a Spring Boot app in the spring-boot-microservices folder using start.spring.io's REST API. This is a web app with a UI and will serve as our API gateway later on. The dependencies used are Spring Web, Okta, and Thymeleaf.

To create a Gradle project, run the following command:

LOADING...

Run the below commands if you want to use Maven instead of Gradle.

LOADING...

The Okta Spring Boot starter secures all endpoints by default.

Configure OIDC with Auth0

We will use the Auth0 application already created in the previous step to secure the web application with OIDC.

If you haven't already, copy the domain, client ID, and client secret of your app and paste them into the following input boxes to copying snippets easier.

For security, these configuration values are stored in memory and only used locally. They are gone as soon as you refresh the page!

Create an application.properties file file in the gateway directory.

LOADING...

Add the below configuration to configure the Okta Spring Boot starter:

gateway/application.properties
# trailing slash is important for issuer
okta.oauth2.issuer=https://AUTH0-DOMAIN/
okta.oauth2.client-id=AUTH0-CLIENT-ID
okta.oauth2.client-secret=AUTH0-CLIENT-SECRET

Add this file to .gitignore so you don't accidentally check it into source control:

gateway/.gitignore
application.properties
You can also put these values in src/main/resources/application.properties. However, we recommend you DO NOT include the client secret in this file for security reasons.

Create a profile view

Create a web folder and a HomeController.java class:

LOADING...

Populate with the following code to return the user's claims.

gateway/src/main/java/com/example/gateway/web/HomeController.java
package com.example.gateway.web;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
import java.util.Collections;
@RestController
class HomeController {
@GetMapping("/")
public ModelAndView home(@AuthenticationPrincipal OidcUser user) {
return new ModelAndView("home", Collections.singletonMap("claims", user.getClaims()));
}
}
These will be the claims in the ID token, not the access token.

Add dependencies for the Thymeleaf Spring Security extension.

GRADLE
MAVEN
// # Modify the `build.gradle` file:
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'

Add a template to render the page at src/main/resources/templates/home.html:

LOADING...

Paste in the following code:

gateway/src/main/resources/templates/home.html
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spring Boot + Auth0</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
</head>
<body>
<div class="container">
<h2>Spring Boot + Auth0 Example</h2>
<div th:unless="${#authorization.expression('isAuthenticated()')}">
<p>Hello!</p>
<p>If you're viewing this page then you have successfully configured and started this application.</p>
<p>When you click the login button below, you will be redirected to login. After you
authenticate, you will be returned to this application.</p>
</div>
<div th:if="${#authorization.expression('isAuthenticated()')}">
<p>Welcome home, <span th:text="${#authentication.principal.attributes['name']}">Mary Coder</span>!</p>
<p>You have successfully authenticated with Auth0, and have been redirected back to this application.</p>
<p>Here are your user's attributes:</p>
<table class="table table-striped">
<thead>
<tr>
<th>Claim</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr th:each="item : ${claims}">
<td th:text="${item.key}">Key</td>
<td th:id="${'claim-' + item.key}" th:text="${item.value}">Value</td>
</tr>
</tbody>
</table>
</div>
<form method="get" th:action="@{/oauth2/authorization/okta}"
th:unless="${#authorization.expression('isAuthenticated()')}">
<button id="login" class="btn btn-primary" type="submit">Login</button>
</form>
<form method="post" th:action="@{/logout}" th:if="${#authorization.expression('isAuthenticated()')}">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<button id="logout" class="btn btn-danger" type="submit">Logout</button>
</form>
</div>
</body>
</html>

Create a SecurityConfiguration.java class in the config package to configure logout:

LOADING...

Paste in the following code:

gateway/src/main/java/com/example/gateway/config/SecurityConfiguration.java
package com.example.gateway.config;
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.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
public class SecurityConfiguration {
private final ClientRegistrationRepository clientRegistrationRepository;
public SecurityConfiguration(ClientRegistrationRepository clientRegistrationRepository) {
this.clientRegistrationRepository = clientRegistrationRepository;
}
private LogoutSuccessHandler logoutSuccessHandler() {
OidcClientInitiatedLogoutSuccessHandler logoutSuccessHandler =
new OidcClientInitiatedLogoutSuccessHandler(this.clientRegistrationRepository);
// Sets the location that the end user's User Agent will be redirected to
// After the logout has been performed at the Provider
logoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}");
return logoutSuccessHandler;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
.oauth2Login(withDefaults())
.logout(logout -> logout.logoutSuccessHandler(logoutSuccessHandler()));
return http.build();
}
}

Note: Ensure that RP-Initiated Logout End Session Endpoint Discovery is enabled in your Auth0 tenant. This is enabled by default for tenants created after 14 November 2023.

Test OIDC authentication

Run the app with the following command:

GRADLE
MAVEN
./gradlew bootRun

Open the http://localhost:8080 URL in your favorite browser. You'll be prompted to log in. Log in with an existing user or create a new user.

Now you will be able to see your OIDC profile information and log out.

When you log out, you'll be prompted to log in again with Auth0. That's because all routes are protected.

With minimal effort, you have enabled OIDC Authentication using Auth0.

Part 3: Enable Role-Based Access Control

What you will learn:

  • How to use Auth0 Actions to convert Auth0 roles to Spring Security authorities.
  • How to secure methods with Spring Security's @PreAuthorize.

Role-Based Access Control refers to the idea of assigning permissions to users based on their role within an organization. It offers a simple, manageable approach to access management that is less prone to error than assigning permissions to users individually.

Use an Auth0 login action to add roles

To add a new administrator role, use the Auth0 CLI:

COMMAND
auth0 roles create --name ROLE_ADMIN --description "Administrators"

Find your user ID with auth0 users search; Enter the full email address you used to log in previously.

Assign the role you just created to your user. You must use quotes around the user-id in the command below.

COMMAND
auth0 users roles assign "<user-id>"

Auth0 Actions are functions written in Node.js that execute at certain points within the Auth0 platform. They are used to customize and extend your authentication and authorization flows.

Create a Login Action:

COMMAND
auth0 actions create --name "Add Roles" --trigger post-login
You can change the text editor used for editing templates, rules, and actions. Set the environment variable EDITOR to your preferred editor. For example export EDITOR="nano"

When the editor opens, use the following code in the onExecutePostLogin() function. This will set a https://spring-boot.example.com/roles claim in both the ID and access token.

ACTION
exports.onExecutePostLogin = async (event, api) => {
const namespace = "https://spring-boot.example.com";
if (event.authorization) {
api.idToken.setCustomClaim("preferred_username", event.user.email);
api.idToken.setCustomClaim(`${namespace}/roles`, event.authorization.roles);
api.accessToken.setCustomClaim(`${namespace}/roles`, event.authorization.roles);
}
};

Save the file using your editor. List the available actions with the following command:

COMMAND
auth0 actions list

Save your action ID into an environment variable and deploy the action you just created:

LOADING...

Once the action is deployed, you must attach it to the login flow. You can do this with Auth0 Management API for Actions:

LOADING...

Update application.properties of the gateway application to use the claim name that you defined in your action.

gateway/application.properties
okta.oauth2.groupsClaim=https://spring-boot.example.com/roles

Restart the application and log in again. You should see the roles added to the https://spring-boot.example.com/roles claim.

Secure methods with @PreAuthorize

Now that you have Spring Security authorities mapped from Auth0 roles, you can use Spring Security's @PreAuthorize annotation to secure methods. But first, you have to enable method-level security.

Enable @EnableMethodSecurity annotation in the gateway app's SecurityConfiguration.java.

gateway/src/main/java/com/example/gateway/config/SecurityConfiguration.java
...
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
...
@Configuration
@EnableMethodSecurity
public class SecurityConfiguration {
...
}

Add a new method to gateway app's HomeController that is secured by authority and update the home method to require the profile scope.

gateway/src/main/java/com/example/gateway/web/HomeController.java
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.stream.Collectors;
...
@GetMapping("/")
@PreAuthorize("hasAuthority('SCOPE_profile')")
public ModelAndView home(@AuthenticationPrincipal OidcUser user) {
return new ModelAndView("home", Collections.singletonMap("claims", user.getClaims()));
}
@GetMapping("/admin")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public String admin(@AuthenticationPrincipal OidcUser user) {
var authentication = SecurityContextHolder.getContext().getAuthentication();
var authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.toSet());
return "Hello, Admin!<br/><br/>User: " + user.getFullName() + "!<br/><br/>Authorities: " + authorities;
}

The admin() method requires you to be authenticated and have an ROLE_ADMIN role. The home() method is secured with @PreAuthorize("hasAuthority('SCOPE_profile')") annotation to ensure that the / route cannot be accessed until you have authenticated with the profile scope. The Okta Spring Boot starter sends openid, email, profile scopes by default.

Restart your app, log in with the user to whom you assigned the administrator role, and confirm you can access http://localhost:8080 and http://localhost:8080/admin successfully.

This proves that your Auth0 roles have been converted to Spring Security authorities, and included in your ID token.

Verify roles are in your access token

To prove that roles have been added to your access token, create a new access token with the Auth0 CLI:

COMMAND
auth0 test token -a https://AUTH0-DOMAIN/api/v2/ -s openid

Select any available client when prompted. You will be prompted to open a browser window and log in with a user credential. On success, an ID token and access token will be generated and printed to the console.

Copy the access token value and paste it into JWT.io. If you would rather prefer a command line tool, you can install JWT UI and paste the access token there.

You can also get an access token using the Authorization Code Flow.

You should see the roles in the https://spring-boot.example.com/roles claim of the access token.

Part 4: Create Spring Boot Microservices Secured with OAuth 2.0 and OpenID Connect

What you will learn:

  • How to build a Spring Boot microservices architecture.
  • How to build an API gateway with Spring Cloud Gateway.
  • How to secure your microservices architecture with OAuth and OpenID Connect.

We already have some of the building blocks required for a microservices architecture. We have a car service that acts as a microservice and a web app that acts as an API gateway. We will now create a new service that will act as the discovery service. We will then connect everything using Spring Cloud.

Create a discovery service

Create a new Spring Boot project using the Spring Initializr with the following dependencies: Eureka Server, Config Client, Actuator, Web, DevTools, and Okta.

Create a Spring Boot app using start.spring.io's REST API. This is a Netflix Eureka server used for service discovery. The dependencies used is Eureka Server.

LOADING...

Run the below commands if you want to use Maven instead of Gradle.

LOADING...

Enable service discovery with Netflix Eureka

In the discovery-service project, configure the application.properties file to use port 8761 and turn off registration with Eureka.

discovery-service/src/main/resources/application.properties
server.port=8761
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false

Add the @EnableEurekaServer annotation to the DiscoveryServiceApplication class.

discovery-service/src/main/java/com/example/discoveryservice/DiscoveryServiceApplication.java
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableEurekaServer
@SpringBootApplication
public class DiscoveryServiceApplication { ... }

Start the discovery service application:

GRADLE
MAVEN
./gradlew bootRun

You can access the Eureka instance at http://localhost:8761/.

Add Spring Cloud dependencies to the gateway and car service

Update dependencies for the car-service project.

GRADLE
MAVEN
// # Modify the `build.gradle` file and replace the dependencies section with the below:
ext {
set('springCloudVersion', "2023.0.1")
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-rest'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'com.okta.spring:okta-spring-boot-starter:3.0.6'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
developmentOnly 'org.springframework.boot:spring-boot-docker-compose'
runtimeOnly 'org.postgresql:postgresql'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}

Update dependencies for the gateway project.

GRADLE
MAVEN
// # Modify the `build.gradle` file and replace the dependencies section with the below:
ext {
set('springCloudVersion', "2023.0.1")
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'com.okta.spring:okta-spring-boot-starter:3.0.6'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
implementation 'org.springframework.cloud:spring-cloud-starter-gateway-mvc'
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}

Enable service discovery and routing in the gateway project

In the gateway project, rename the application.properties file to application.yml and add routing for the car service. Using YAML makes further configuration easier.

gateway/src/main/resources/application.yml
spring:
application:
name: gateway
cloud:
gateway:
discovery:
locator:
enabled: true
default-filters:
- TokenRelay
mvc:
routes:
- id: car-service
uri: lb://car-service
predicates:
# proxy paths in car service
- Path=/cars/**
openfeign:
oauth2:
enabled: true
clientRegistrationId: okta

The configuration above:

  • Enables service discovery.
  • Sets a default token relay filter so that the gateway can forward the access token to the car service.
  • Configures a route for the car service so that we can access endpoints from the car service through the gateway.
  • Enables OAuth 2.0 for the Feign client.

Update GatewayApplication.java to enable service discovery and feign clients:

gateway/src/main/java/com/example/gateway/GatewayApplication.java
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication
public class GatewayApplication { ... }

Add a CoolCarController in the web package to fetch and filter cars from the car microservice. This approach will be useful for aggregating data in an UI.

LOADING...

Add the following code to the CoolCarController:

gateway/src/main/java/com/example/gateway/web/CoolCarController.java
package com.example.gateway.web;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collection;
import java.util.Collections;
@RestController
class CoolCarController {
private final CarClient carClient;
public CoolCarController(CarClient carClient) {
this.carClient = carClient;
}
@GetMapping("/cool-cars")
public String coolCars() {
var cars = carClient.readCars()
.stream()
.filter(this::isCool)
// Add fire emoji and line break
.map(car -> "\uD83D\uDD25" + car.name() + "<br/>")
.reduce("", String::concat);
return "These are cool cars!<br/><br/>" + cars;
}
private boolean isCool(Car car) {
return !car.name().equals("AMC Gremlin") &&
!car.name().equals("Triumph Stag") &&
!car.name().equals("Ford Pinto") &&
!car.name().equals("Yugo GV");
}
}
record Car(String name) {
}
@FeignClient(name = "car-service", fallback = Fallback.class)
interface CarClient {
@GetMapping("/cars")
Collection<Car> readCars();
}
@Component
class Fallback implements CarClient {
@Override
public Collection<Car> readCars() {
return Collections.emptyList();
}
}

The CoolCarController class fetches cars from the car service and filters out the uncool cars. The CarClient interface is a Feign client that fetches cars from the car service. The Fallback class is a fallback implementation for the Feign client.

Update the application.properties file to add the audience.

gateway/application.properties
okta.oauth2.audience=https://AUTH0-DOMAIN/api/v2/

Finally, we need to update the SecurityConfiguration class to send the audience value in the authorization request and to enable OAuth2 resource server support so that we can call REST APIs with access tokens.

gateway/src/main/java/com/example/gateway/config/SecurityConfiguration.java
...
import org.springframework.beans.factory.annotation.Value;
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 java.util.function.Consumer;
@Configuration
@EnableMethodSecurity
public class SecurityConfiguration {
@Value("${okta.oauth2.audience}")
private String audience;
...
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
.oauth2Login(oauth2 -> oauth2
.authorizationEndpoint(authorization -> authorization
.authorizationRequestResolver(
authorizationRequestResolver(this.clientRegistrationRepository)
)
)
)
.oauth2ResourceServer(jwt -> jwt.jwt(withDefaults()))
.logout(logout -> logout.logoutSuccessHandler(logoutSuccessHandler()));
return http.build();
}
private OAuth2AuthorizationRequestResolver authorizationRequestResolver(
ClientRegistrationRepository clientRegistrationRepository) {
DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver =
new DefaultOAuth2AuthorizationRequestResolver(
clientRegistrationRepository, "/oauth2/authorization");
authorizationRequestResolver.setAuthorizationRequestCustomizer(
authorizationRequestCustomizer());
return authorizationRequestResolver;
}
private Consumer<OAuth2AuthorizationRequest.Builder>
authorizationRequestCustomizer() {
return customizer -> customizer
.additionalParameters(params -> params.put("audience", audience));
}
}

Start the discover-service, car-service, and gateway applications in this order.

Confirm you can access all the endpoints from the gateway after authentication:

Verify that you can access the car-service endpoints directly through the gateway:

Get an access token:

COMMAND
auth0 test token -a https://AUTH0-DOMAIN/api/v2/ -s openid

Paste the access token value in the following field:

Run the following command to make an authenticated request to the gateway:

LOADING...

Voila! You have successfully created a microservices architecture with Spring Boot and secured it with OAuth 2.0 and OpenID Connect.

Bonus 1: Move beyond Passwords with Passkeys

What you will learn:

  • How to enable passkeys support in your Auth0 tenant.

Passkeys are FIDO credentials that are discoverable by browsers or housed in hardware authenticators like your mobile device, laptop, or security keys for passwordless authentication. Passkeys replace passwords with cryptographic key pairs for phishing-resistant sign-in security and an improved user experience. The cryptographic keys are used from end-user devices (computers, phones, or security keys) for user authentication. Any passwordless FIDO credential is a passkey. Learn more about passkeys.

Enable passkeys on your Auth0 tenant

  1. Log in to your Auth0 Dashboard and navigate to Authentication > Database > Username-Password-Authentication.
    1. If the second tab says Authentication Methods, your tenant supports passkeys, proceed to the next step.
    2. If the second tab says Password Policy, your tenant doesn't support passkeys, Create a new tenant and proceed to the next step.
  2. Navigate to Authentication > Authentication Profile and select Identifier First. Save your changes.
  3. Navigate to Authentication > Database > Username-Password-Authentication and select the Authentication Methods tab and enable Passkey.

Test passkeys

Log out of the gateway application and go to http://localhost:8080 again. You will be prompted to log in. Click Sign up and create a new account. You will be prompted to create a passkey. Follow the instructions to create a passkey. You can log out and log in again with passkeys to test it.

Bonus 2: The Okta Spring Boot Starter and Keycloak

What you will learn:

  • How to use the Okta Spring Boot Starter with Keycloak.

If you find yourself in a situation where you don't have an internet connection, it can be handy to run Keycloak locally in a Docker container. Since the Okta Spring Boot starter is a thin wrapper around Spring Security, it works with Keycloak, too.

The Okta Spring Boot starter does validate the issuer to ensure it's an Okta URL, so you must use Spring Security's properties instead of the okta.oauth2.* properties when using Keycloak.

An easy way to get a pre-configured Keycloak instance is to use JHipster's jhipster-sample-app-oauth2 application. It gets updated with every JHipster release. Clone it with the following command:

COMMAND
git clone https://github.com/jhipster/jhipster-sample-app-oauth2.git --depth=1
cd jhipster-sample-app-oauth2

Start Keycloak with Docker Compose:

COMMAND
docker compose -f src/main/docker/keycloak.yml up -d

Configure the gateway to use Keycloak by removing the okta.oauth2.* properties and using Spring Security's in application.properties:

gateway/application.properties
spring.security.oauth2.client.provider.okta.issuer-uri=http://localhost:9080/realms/jhipster
spring.security.oauth2.client.registration.okta.client-id=web_app
spring.security.oauth2.client.registration.okta.client-secret=web_app
spring.security.oauth2.client.registration.okta.scope=openid,profile,email,offline_access
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9080/realms/jhipster
okta.oauth2.groupsClaim=roles
okta.oauth2.audience=account

Update the car service to use Keycloak by removing the okta.oauth2.* properties and using Spring Security's in application.properties:

car-service/application.properties
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9080/realms/jhipster
spring.security.oauth2.resourceserver.jwt.audiences=account

Restart both apps, open http://localhost:8080, and you'll be able to log in with Keycloak.

Use admin/admin for credentials, and you can access http://localhost:8080/cool-cars as you did before.

Conclusion and Next Steps

In this lab, you learned how to secure a Spring Boot application with OAuth 2.0 and OpenID Connect using Auth0. You also learned how to create a microservices architecture with Spring Cloud and secure it with OAuth 2.0 and OpenID Connect. You also learned how to enable passkeys for passwordless authentication and how to use the Okta Spring Boot Starter with Keycloak.

You can find the complete code for this lab in the GitHub repository. Check the commit history to see chapter-wise changes.

Check out our other Java guides:

Be sure to visit the Okta Spring Boot Starter's GitHub repository to stay informed on the latest developments and join the growing community of Spring Boot users who use Okta.

Legal Disclosure

This document and any recommendations within are not legal, privacy, security, compliance, or business advice. This document is intended for general informational purposes only and may not reflect the most current security, privacy, and legal developments or all relevant issues. You are responsible for obtaining legal, security, privacy, compliance, or business advice from your lawyer or other professional advisor and should not rely on the recommendations herein. Okta is not liable to you for any loss or damages that may result from your implementation of any recommendations in this document. Okta makes no representations, warranties, or other assurances regarding the content of this document. Information regarding Okta's contractual assurances to its customers can be found at okta.com/agreements.