Securing Spring Boot Microservices
Published on April 30, 2024Overview
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:
- The client acquires credentials (client ID and client secret) from the authorization server to identify itself. This is a one-time step.
- 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.
- The authorization server authenticates the client and verifies the requested scopes.
- The resource owner authenticates with the authorization server and authorizes the requested access.
- The authorization server redirects back to the client with an authorization code.
- 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.
- 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
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
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
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.
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.
Create a directory
Create a directory to hold all your projects.
mkdir spring-boot-microservicescd 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:
Run the below commands if you want to use Maven instead of Gradle.
Create a car service
Configure the application.properties
file to use port 8090
, to have an application name, and to create the database automatically.
spring.application.name=car-serviceserver.port=8090spring.jpa.hibernate.ddl-auto=update
Create a data
package and a Car
entity in the package with id
and name
properties.
Add the following code:
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;@Entitypublic class Car {public Car() {}public Car(String name) {this.name = name;}@Id@GeneratedValueprivate Long id;@NotNullprivate 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:
Add the following code:
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.
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;@SpringBootApplicationpublic class CarServiceApplication {public static void main(String[] args) {SpringApplication.run(CarServiceApplication.class, args);}@BeanApplicationRunner 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.
Add the below code:
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;@RestControllerclass 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.
services:postgres:image: 'postgres:latest'environment:- 'POSTGRES_DB=mydatabase'- 'POSTGRES_PASSWORD=secret'- 'POSTGRES_USER=myuser'ports:- '5432'
Start the car service application:
./gradlew bootRun
Confirm you can access the /cars
endpoint:
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.
// # 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:
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.
Configure OAuth2
Create an application.properties
file.
Add the below configuration.
# trailing slash is important for issuerokta.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 theaud
claim in the access token. In this case, we are using the default API from your Auth0 tenant for simplicity as it is the defaultaud
claim in the access token. For production applications, you should create a new API for your application in Auth0.
Restart the car service application and confirm you can access the /cars
endpoint:
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:
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.
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:
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:
Run the below commands if you want to use Maven instead of Gradle.
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.
Create an application.properties
file file in the gateway
directory.
Add the below configuration to configure the Okta Spring Boot starter:
# trailing slash is important for issuerokta.oauth2.issuer=https://AUTH0-DOMAIN/okta.oauth2.client-id=AUTH0-CLIENT-IDokta.oauth2.client-secret=AUTH0-CLIENT-SECRET
Add this file to .gitignore
so you don't accidentally check it into source control:
application.properties
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:
Populate with the following code to return the user's claims.
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;@RestControllerclass HomeController {@GetMapping("/")public ModelAndView home(@AuthenticationPrincipal OidcUser user) {return new ModelAndView("home", Collections.singletonMap("claims", user.getClaims()));}}
Add dependencies for the Thymeleaf Spring Security extension.
// # 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
:
Paste in the following code:
<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 youauthenticate, 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:
Paste in the following code:
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;@Configurationpublic 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 ProviderlogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}");return logoutSuccessHandler;}@Beanpublic 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:
./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.
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:
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.
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:
auth0 actions create --name "Add Roles" --trigger post-login
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.
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:
auth0 actions list
Save your action ID into an environment variable and deploy the action you just created:
Once the action is deployed, you must attach it to the login flow. You can do this with Auth0 Management API for Actions:
Update application.properties
of the gateway
application to use the claim name that you defined in your action.
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
.
...import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;...@Configuration@EnableMethodSecuritypublic 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.
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:
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.
Run the below commands if you want to use Maven instead of Gradle.
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.
server.port=8761eureka.client.register-with-eureka=falseeureka.client.fetch-registry=false
Add the @EnableEurekaServer
annotation to the DiscoveryServiceApplication
class.
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;@EnableEurekaServer@SpringBootApplicationpublic class DiscoveryServiceApplication { ... }
Start the discovery service application:
./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.
// # 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.
// # 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.
spring:application:name: gatewaycloud:gateway:discovery:locator:enabled: truedefault-filters:- TokenRelaymvc:routes:- id: car-serviceuri: lb://car-servicepredicates:# proxy paths in car service- Path=/cars/**openfeign:oauth2:enabled: trueclientRegistrationId: 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:
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;import org.springframework.cloud.openfeign.EnableFeignClients;@EnableDiscoveryClient@EnableFeignClients@SpringBootApplicationpublic 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.
Add the following code to the CoolCarController
:
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;@RestControllerclass 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();}@Componentclass Fallback implements CarClient {@Overridepublic 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.
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.
...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@EnableMethodSecuritypublic class SecurityConfiguration {@Value("${okta.oauth2.audience}")private String audience;...@Beanpublic 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:
- Profile page: http://localhost:8080/
- Admin page: http://localhost:8080/admin
- Cool Cars page: http://localhost:8080/cool-cars
Verify that you can access the car-service
endpoints directly through the gateway:
Get an access token:
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:
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
- Log in to your Auth0 Dashboard and navigate to Authentication > Database > Username-Password-Authentication.
- If the second tab says Authentication Methods, your tenant supports passkeys, proceed to the next step.
- If the second tab says Password Policy, your tenant doesn't support passkeys, Create a new tenant and proceed to the next step.
- Navigate to Authentication > Authentication Profile and select Identifier First. Save your changes.
- 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.
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:
git clone https://github.com/jhipster/jhipster-sample-app-oauth2.git --depth=1cd jhipster-sample-app-oauth2
Start Keycloak with Docker Compose:
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
:
spring.security.oauth2.client.provider.okta.issuer-uri=http://localhost:9080/realms/jhipsterspring.security.oauth2.client.registration.okta.client-id=web_appspring.security.oauth2.client.registration.okta.client-secret=web_appspring.security.oauth2.client.registration.okta.scope=openid,profile,email,offline_accessspring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9080/realms/jhipsterokta.oauth2.groupsClaim=rolesokta.oauth2.audience=account
Update the car service to use Keycloak by removing the okta.oauth2.*
properties and using Spring Security's in application.properties
:
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9080/realms/jhipsterspring.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:
- Authorization in Micronaut
- Authentication in Micronaut
- Role-Based Access Control in Micronaut
- Authorization in Quarkus
- Authentication in Quarkus
- Role-Based Access Control in Quarkus
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.