Logo

web

Spring Boot with Thymeleaf

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

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

ItemDescription
/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
/.envEnvironment variables used by the docker-compose.yml file
/README.mdReadme file for the repo
/docker-compose.ymlA docker-compose config file for standing up a FusionAuth server, a Postgres database, and an Elastic instance

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:

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.

UserApplicationView HomepageClick Login LinkShow Login FormFill Out and Submit Login FormAuthenticates UserDisplay User's Account or OtherInfoUserApplication

Request flow during login before FusionAuth

The login flow will look like this after FusionAuth is introduced.

UserApplicationFusionAuthView HomepageClick Login Link (to FusionAuth)View Login FormShow Login FormFill Out and Submit Login FormAuthenticates UserGo to Redirect URIRequest the Redirect URIIs User Authenticated?User is AuthenticatedDisplay User's Account or OtherInfoUserApplicationFusionAuth

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.

If you ever want to reset the FusionAuth system, delete the volumes created by docker-compose by executing docker-compose down -v.

FusionAuth will be initially configured with these settings:

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:

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:

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

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:

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!