web
Spring Boot with Thymeleaf
Integrate Your Java Spring Application With FusionAuth
This Quickstart will take you through building a simple Spring Boot web application and integrating it with FusionAuth. It uses Spring Boot web and Thymeleaf. You’ll be building it for ChangeBank, a global leader in converting dollars into coins. It’ll have areas reserved for users who have logged in as well as public facing sections.
In this Quickstart you’ll create the Changebank application, which has
- A Home Page, which is where a logged-out user goes. It has a login button that allows a user to log in using FusionAuth
- An Account Page, which is where a user goes when they’re logged in. It has a Logout button that allows the user to log out with FusionAuth
- The associated back end routes to accomplish all of this
This quickstart will be referencing code in the example application in the Spring Boot Web Quickstart Repo. You can find a fully-working version of this application there.
Repository Contents
Item | Description |
---|---|
/complete-application/ | A completed application, with a working integration with FusionAuth |
/kickstart/ | A directory containing a kickstart file, which is used to configure FusionAuth |
/stub-application/ | A downloaded and unzipped but otherwise clean start to the Spring Boot template with dependencies |
/.env | Environment variables used by the docker-compose.yml file |
/README.md | Readme file for the repo |
/docker-compose.yml | A docker-compose config file for standing up a FusionAuth server, a Postgres database, and an Elastic instance |
- Java: Java can be installed via a variety of methods.
- Docker: The quickest way to stand up FusionAuth. Ensure you also have docker compose installed.
- (Alternatively, you can Install FusionAuth Manually).
The example repository has already set up a Spring Boot template application that includes a Maven wrapper. You can find out more about those by viewing:
- Spring Initializr: The Spring Boot template generator webpage.
- Maven Wrapper: A self-contained version of the Maven build tool which will download the application dependencies and compile the source code.
This app was built using Java 17. You can change which version to use by editing the project’s pom.xml
file. The code in this example should be compatible down to Java 8, though minor differences may occur.
General architecture
While this sample application doesn’t have login functionality without FusionAuth, a more typical integration will replace an existing login system with FusionAuth.
In that case, the system might look like this before FusionAuth is introduced.
Request flow during login before FusionAuth
The login flow will look like this after FusionAuth is introduced.
Request flow during login after FusionAuth
In general, you are introducing FusionAuth in order to normalize and consolidate user data. This helps make sure it is consistent and up-to-date as well as offloading your login security and functionality to FusionAuth.
FusionAuth Installation via Docker
The root of this project directory (next to this README) are two files, a Docker compose file and an environment variables configuration file. Assuming you have Docker installed on your machine, you can stand up FusionAuth up on your machine with:
git clone https://github.com/FusionAuth/fusionauth-quickstart-springboot-web.git
cd fusionauth-quickstart-springboot-web
docker-compose up -d
The FusionAuth configuration files also make use of a unique feature of FusionAuth, called Kickstart: when FusionAuth comes up for the first time, it will look at the Kickstart file located at kickstart/kickstart.json
and mimic API calls to configure FusionAuth for use when it is first run.
docker-compose down -v
.FusionAuth will be initially configured with these settings:
- Your client Id is:
e9fdb985-9173-4e01-9d73-ac2d60d1dc8e
- Your client secret is:
super-secret-secret-that-should-be-regenerated-for-production
- Your example username is
richard@example.com
and your password ispassword
. - Your admin username is
admin@example.com
and your password ispassword
. - Your fusionAuthBaseUrl is ’http://localhost:9011/’
You can log into the FusionAuth admin UI and look around if you want, but with Docker/Kickstart you don’t need to.
Create your Spring Boot Application with Thymeleaf
Overview of the Application
We are going to be building a version of the ChangeBank app. This app will allow you to log in and see your “balance”, as well as present a page that you can input a dollar amount and have that broken into change of dimes and pennies. You won’t be keeping track of any account state beyond the user in FusionAuth, so the only actual functionality you will implement is the api call to make change.
You are going to build an application with three views:
Home
: This is the welcome page than an unauthenticated user will see when first visiting your application.Account
: This is a protected view the user can only see after they have logged in.Make Change
: This view is also protected and demonstrates making an authenticated form post to the backend.
Downloading the template from Spring Initializr
We have already done this for you in the example repository, but if you wish to do so yourself you can go to the Initializr site at https://start.spring.io/ and download your own starter package. We will rely on three dependencies for this project:
- OAuth2 Client
- Thymeleaf
- Spring Web
This example was made with Maven
, Spring Boot version 2.7.12
, and Java 17. If you choose different options the configuration and code may be different.
If you choose you can download and unzip the package in any directory you wish and proceed to follow along with the rest of this guide. The directions will assume you are in stub-application
from the example repository. Please substitute that directory with wherever you unzipped the files from Spring.
Install the dependencies
From the stub-application
directory run the following to execute the Maven build tool to download dependencies and build your application.
./mvnw package
Note: if you are on Windows substitute ./mvnw
with .\mvnw.cmd
Configure Spring Application Properties
From the stub-application
directory open src/main/resources/application.properties
.
spring.thymeleaf.cache=false
spring.thymeleaf.enabled=true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.application.name=FusionAuth Spring Example
spring.security.oauth2.client.registration.fusionauth-client.client-id=e9fdb985-9173-4e01-9d73-ac2d60d1dc8e
spring.security.oauth2.client.registration.fusionauth-client.client-secret=super-secret-secret-that-should-be-regenerated-for-production
spring.security.oauth2.client.registration.fusionauth-client.scope=email,openid,profile
spring.security.oauth2.client.registration.fusionauth-client.redirect-uri=http://localhost:8080/login/oauth2/code/fusionauth
spring.security.oauth2.client.registration.fusionauth-client.client-name=fusionauth
spring.security.oauth2.client.registration.fusionauth-client.provider=fusionauth
spring.security.oauth2.client.registration.fusionauth-client.client-authentication-method=basic
spring.security.oauth2.client.registration.fusionauth-client.authorization-grant-type=authorization_code
spring.security.oauth2.client.provider.fusionauth.authorization-uri=http://localhost:9011/oauth2/authorize
spring.security.oauth2.client.provider.fusionauth.token-uri=http://localhost:9011/oauth2/token
spring.security.oauth2.client.provider.fusionauth.user-info-uri=http://localhost:9011/oauth2/userinfo?schema=openid
spring.security.oauth2.client.provider.fusionauth.user-name-attribute=name
spring.security.oauth2.client.provider.fusionauth.user-info-authentication-method=header
spring.security.oauth2.client.provider.fusionauth.jwk-set-uri=http://localhost:9011/.well-known/jwks.json
The properties with the spring.thymeleaf
prefix enable Thymeleaf for server-side template rendering and defines the location and suffix of the template files.
The spring.application.name
property defines the app name, which will show up in your application view.
The properties with the spring.security.oauth2.client.registration
prefix configure the settings for the OAuth2 Client. The client-id
, client-secret
, redirect-uri
and authorization-grant-type
all need to match what is configured in FusionAuth. If you used the Kickstart method these settings should be correct, however if they are not you can log in to http://localhost:9011/admin and check the OAuth Settings for your application.
The properties with the spring.security.oauth2.provider
prefix tell Spring where the important endpoints for FusionAuth are.
Add controllers
Find the base directory
when the main application java class is defined. In our example this is src/main/java/io/quickstart/springweb
and the main class is FusionAuthSpringApplication.java
.
Create the following new java classes in the base directory
:
HomeController.java
package io.fusionauth.quickstart.springweb;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class HomeController {
@Value("${spring.application.name}")
String appName;
@RequestMapping("/")
public String homePage(Model model) {
model.addAttribute("appName", appName);
return "home";
}
}
This controller serves the page for unauthenticated users on the /
route and shows the application name which is injected from the spring.application.name
property in the application.properties
file. The return string home
tells Spring to render the home.html
template.
AccountController.java
package io.fusionauth.quickstart.springweb;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class AccountController {
@RequestMapping("/account")
public String userPage(Model model, @AuthenticationPrincipal OidcUser principal) {
if (principal != null) {
model.addAttribute("profile", principal.getClaims());
}
return "account";
}
}
This controller serves the page for authenticated users on the /account
route and adds the claims from the OidcUser
returned from FusionAuth to the view model on the profile
attribute (more on that later). The return string account
tells Spring to render the account.html
template.
MakeChangeController.java
package io.fusionauth.quickstart.springweb;
import io.fusionauth.quickstart.springweb.model.Change;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import java.math.BigDecimal;
import java.math.RoundingMode;
@Controller
public class MakeChangeController {
private static final Logger logger = LoggerFactory.getLogger(MakeChangeController.class);
@GetMapping("/make-change")
public String get(Model model, @AuthenticationPrincipal OidcUser principal) {
model.addAttribute("change", new Change());
model.addAttribute("profile", principal.getClaims());
return "make-change";
}
@PostMapping("/make-change")
public String post(@ModelAttribute Change change, Model model, @AuthenticationPrincipal OidcUser principal) {
model.addAttribute("profile", principal.getClaims());
change.setError(null);
try {
if (change.getTotal() != null) {
BigDecimal totalValue = new BigDecimal(change.getTotal());
change.setNickels(totalValue
.divide(new BigDecimal("0.05"), RoundingMode.HALF_DOWN)
.intValue());
change.setPennies(totalValue
.subtract(new BigDecimal("0.05")
.multiply(new BigDecimal(change.getNickels())))
.multiply(new BigDecimal(100))
.intValue());
}
} catch (Exception e) {
logger.error(e.getMessage());
change.setError("Please enter a dollar amount.");
}
return "make-change";
}
}
This controller serves both GET and POST requests on the /make-change
route. Both methods add the claims to the profile
attribute like AccountController.java
does. The GetMapping
adds a new model object of Change
to view model as well. The PostMapping
takes the Change
object as a parameter and uses the total
to calculate how many nickels and pennies to return. If total
is not a valid dollar (BigDecimal) value it adds an error to the Change model object. The return string make-change
tells Spring to render the make-change.html
template.
Add the model object
We need to create the model object referenced in the controllers. From the base directory
create a new directory model
and in it add a new class Change.java
package io.fusionauth.quickstart.springweb.model;
public class Change {
private String error;
private String total;
private Integer nickels;
private Integer pennies;
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
public String getTotal() {
return total;
}
public void setTotal(String total) {
this.total = total;
}
public Integer getNickels() {
return nickels;
}
public void setNickels(Integer nickels) {
this.nickels = nickels;
}
public Integer getPennies() {
return pennies;
}
public void setPennies(Integer pennies) {
this.pennies = pennies;
}
public String getMessage() {
return String.format("We can make change for %s with %s nickels and %s pennies!", getTotal(), getNickels(), getPennies());
}
}
This model has four fields
error
: The error string if the input was invalid.total
: The total amount to divide into pennies and nickels.nickels
: The number of pennies returned from the “make change” operation.pennies
: The number of pennies returned from the “make change” operation.
It additionally has a method getMessage
which formats a message string to render on the template.
Add view templates
Now you need to add some views for Thymeleaf to render on the routes you just defined. Add three files into src/main/resources/templates
:
home.html
<html xmlns:th="http://www.w3.org/1999/xhtml" lang="en">
<head>
<title>Home Page</title>
<link rel="stylesheet" type="text/css" th:href="@{/css/styles.css}"/>
</head>
<body>
<div id="page-container">
<div id="page-header">
<div id="logo-header">
<img th:src="@{/images/example_bank_logo.svg}" />
<a class="button-lg" href="/account">Login</a>
</div>
<div id="menu-bar" class="menu-bar">
<a class="menu-link">About</a>
<a class="menu-link">Services</a>
<a class="menu-link">Products</a>
<a class="menu-link" style="text-decoration-line: underline;">Home</a>
</div>
</div>
<div style="flex: 1;">
<div class="column-container">
<div class="content-container">
<div style="margin-bottom: 100px;">
<h1>Welcome to Changebank</h1>
<p>To get started, <a href="/account">log in or create a new account</a>.</p>
</div>
</div>
<div style="flex: 0;">
<img th:src="@{/images/money.jpg}" style="max-width: 800px;"/>
</div>
</div>
</div>
</body>
</html>
Note that this template links to /account
for login. Spring will automatically know that the user is not logged in and redirect them to FusionAuth based on what was defined in application.properties
.
account.html
<html xmlns:th="http://www.w3.org/1999/xhtml" lang="en">
<head>
<title>User Profile</title>
<link rel="stylesheet" type="text/css" th:href="@{/css/styles.css}"/>
</head>
<div id="page-container">
<div id="page-header">
<div id="logo-header">
<img src="https://fusionauth.io/assets/img/samplethemes/changebank/changebank.svg" />
<div class="h-row">
<p class="header-email" th:text="${profile.email}"></p>
<a class="button-lg" href="/logout">Logout</a>
</div>
</div>
<div id="menu-bar" class="menu-bar">
<a class="menu-link inactive" href="/make-change">Make Change</a>
<a class="menu-link" href="/account">Account</a>
</div>
</div>
<div style="flex: 1;">
<!-- Application page -->
<div class="column-container">
<div class="app-container">
<h3>Your balance</h3>
<div class="balance">$0.00</div>
</div>
</div>
</div>
</html>
Similarly, this page has a href to /logout
that Spring will use to redirect the user to FusionAuth for logout. It also references the ${profile.email}
claim that was set in controller.
make-change.html
<html>
<head>
<meta charset="utf-8" />
<title>FusionAuth OpenID and PKCE example</title>
<link rel="stylesheet" type="text/css" th:href="@{/css/styles.css}"/>
</head>
<body>
<div id="page-container">
<div id="page-header">
<div id="logo-header">
<img th:src="@{/images/example_bank_logo.svg}" />
<div class="h-row">
<p class="header-email" th:text="${profile.email}"></p>
<a class="button-lg" href="/logout">Logout</a>
</div>
</div>
<div id="menu-bar" class="menu-bar">
<a class="menu-link" href="/make-change">Make Change</a>
<a class="menu-link inactive" href="/account">Account</a>
</div>
</div>
<div style="flex: 1;">
<div class="column-container">
<div class="app-container change-container">
<h3>We Make Change</h3>
<div th:if="${change.error != null}" class="error-message" th:text="${change.error}"></div>
<div th:unless="${change.error != null}" class="change-message" th:text="${change.message}">
</div>
<form method="post" action="#" th:action="@{/make-change}" th:object="${change}">
<div class="h-row">
<div class="change-label">Amount in USD: $</div>
<input class="change-input" name="amount" value="0.00" th:field="*{total}" />
<input class="change-submit" type="submit" value="Make Change" />
</div>
</form>
</div>
</div>
</div>
</body>
</html>
In addition to the references made above, this template adds a form which binds the ${change}
object as the model, submits the *{total}
as input by the user, and displays the message or error depending on what is defined in the Change
object.
Add static assets
There are some references to a stylesheet and images in those templates. You should add them here.
In src/main/resourcs/static/images
you can download our bank logo image as example_bank_logo.svg
and our money image as money.jpg
.
In src/main/resources/static/css
you should add styles.css
from the example styles or write your own.
Feel free to update and style as you see fit, just be sure to update the templates if you change images.
Configure Spring Web and Spring Security
Almost done!
In order for this application to work you will need two more configuration java classes. From the base directory
add a new directory config
and in it add the following:
SecurityConfiguration.java
package io.fusionauth.quickstart.springweb.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.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestCustomizers;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, ClientRegistrationRepository repo)
throws Exception {
String base_uri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI;
DefaultOAuth2AuthorizationRequestResolver resolver = new DefaultOAuth2AuthorizationRequestResolver(repo, base_uri);
resolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce());
http
.authorizeRequests(a -> a
.antMatchers("/", "/images/**", "/css/**")
.permitAll()
.anyRequest()
.authenticated())
.oauth2Login(login -> login.authorizationEndpoint().authorizationRequestResolver(resolver));
http.logout(logout -> logout
.logoutSuccessUrl("http://localhost:9011/oauth2/logout?client_id=e9fdb985-9173-4e01-9d73-ac2d60d1dc8e"));
return http.build();
}
}
This class uses the @Configuration
annotation to tell Spring it is providing additional configuration, in this case a SecurityFilterChain
which ultimately does two things:
- Tells Spring to require authentication for all requests except to
/
(i.e.home.html
),/images
, and/css
(implicitly referring to the directories undersrc/main/resources/static
). - Tells Spring to redirect the user on logout to FusionAuth to complete the logout.
WebConfig.java
package io.fusionauth.quickstart.springweb.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler(
"/images/**",
"/css/**")
.addResourceLocations(
"classpath:/static/images/",
"classpath:/static/css/");
}
}
This configuration class enables web mvc and sets up resource handlers for the static assets.
Run the app
Now from the root directory run
./mvnw spring-boot:run
You can open the application at http://localhost:8080
Congratulations! Have fun with FusionAuth!