Use Thymeleaf Templates with Spring WebFlux to Secure Your Apps

The Thymeleaf library has been around at least for 10 years and it is still actively maintained as of today. It is designed to allow stronger collaboration between design and developer teams for some use cases, as Thymeleaf templates look like HTML and can be displayed in the browser as static prototypes.

In this tutorial you will learn how to create a simple Spring WebFlux application with Thymeleaf and Okta OIDC authentication, addressing the security concerns of preventing CSRF when submitting forms, and protecting functionality based on the user authorities and authentication status.

This tutorial was created with the following frameworks and tools:

Table of Contents

What is Thymeleaf?

Thymeleaf Logo

Thymeleaf is an open-source server-side Java template engine for standalone and web applications, that was created by Daniel Fernández. The templates look like HTML and can be integrated with Spring MVC and Spring Security among other popular development frameworks. Although documentation about integration with Spring WebFlux is hard to find, it is currently supported and the Thymeleaf starter dependency performs the auto-configuration of the template engine, template resolver and reactive view resolver.

Among its features, Thymeleaf allows you to:

  • Work with fragments: only a part of a template will be rendered. This can be useful for updating part of a page from responses in AJAX calls. It also provides a componentization tool, as the fragments can be included in multiple templates.
  • Handle forms with model objects containing their fields
  • Render variables and externalized text messages through its Standard Expression Syntax
  • Perform iterations and conditional evaluations

Create a Spring WebFlux application with Thymeleaf

Create a simple monolithic Spring Boot reactive application, that will use Thymeleaf for page templates. You can use Spring Initializr, from its web UI, or download the starter app with the following HTTPie command:

https -d start.spring.io/starter.zip bootVersion==2.6.4 \
  baseDir==thymeleaf-security \
  groupId==com.okta.developer.thymeleaf-security \
  artifactId==thymeleaf-security \
  name==thymeleaf-security \
  packageName==com.okta.developer.demo \
  javaVersion==11 \
  dependencies==webflux,okta,thymeleaf,devtools

Extract the Maven project and some additional dependencies. The thymeleaf-extras-springsecurity5 dependency is required to include security conditionals in the templates, and spring-security-test has mock login features that will help when writing controller tests.

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>

Add authentication with OpenID Connect

Before you begin, you’ll need a free Okta developer account. Install the Okta CLI and run okta register to sign up for a new account. If you already have an account, run okta login. Then, run okta apps create. Select the default app name, or change it as you see fit. Choose Web and press Enter.

Select Okta Spring Boot Starter. Accept the default Redirect URI values provided for you. That is, a Login Redirect of http://localhost:8080/login/oauth2/code/okta and a Logout Redirect of http://localhost:8080.

What does the Okta CLI do?

The Okta CLI will create an OIDC Web App in your Okta Org. It will add the redirect URIs you specified and grant access to the Everyone group. You will see output like the following when it’s finished:

Okta application configuration has been written to: 
  /path/to/app/src/main/resources/application.properties

Open src/main/resources/application.properties to see the issuer and credentials for your app.

okta.oauth2.issuer=https://dev-133337.okta.com/oauth2/default
okta.oauth2.client-id=0oab8eb55Kb9jdMIr5d6
okta.oauth2.client-secret=NEVER-SHOW-SECRETS

NOTE: You can also use the Okta Admin Console to create your app. See Create a Spring Boot App for more information.

The Okta configuration is now in application.properties. Rename it to application.yml and add the following additional properties:

spring:
  thymeleaf:
    prefix: file:src/main/resources/templates/  
  security:
    oauth2:
      client:
        provider:
          okta:
            user-name-attribute: email

okta:
  oauth2:
    issuer: https://{yourOktaDomain}/oauth2/default
    client-id: {clientId}
    client-secret: {clientSecret}
    scopes:
      - email
      - openid

Notice the profile scope is not requested for the first test. Only openid is a required scope to perform an OpenID Connect request. The thymeleaf.prefix property enables the hot-reloading of the templates, when the spring-boot-devtools dependency is included in the project.

Add some Thymeleaf templates

Create a src/main/resources/templates folder for the templates you are going to create. Create a home.html template with the following content:

<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>User Details</title>
    <!--/*/ <th:block th:include="head :: head"/> /*/-->
</head>
<body id="samples">
<div th:replace="menu :: menu"></div>

<div id="content" class="container">
    <h2>Okta Hosted Login + Spring Boot Example</h2>

    <div th:unless="${#authorization.expression('isAuthenticated()')}" class="text fw-light fs-6 lh-1">
        <p>Hello!</p>
        <p>If you're viewing this page then you have successfully configured and started this example server.</p>
        <p>This example shows you how to use the <a href="https://github.com/okta/okta-spring-boot">Okta Spring Boot
            Starter</a> to add the <a
            href="https://developer.okta.com/docs/guides/implement-grant-type/authcode/main/">Authorization
            Code Flow</a> to your application.</p>
        <p>When you click the login button below, you will be redirected to the login page on your Okta org. After you
            authenticate, you will be returned to this application.</p>
    </div>

    <div th:if="${#authorization.expression('isAuthenticated()')}" class="text fw-light fs-6 lh-1">
        <p>Welcome home, <span th:text="${#authentication.principal.name}">Joe Coder</span>!</p>
        <p>You have successfully authenticated against your Okta org, and have been redirected back to this 
          application.</p>
    </div>

    <form th:unless="${#authorization.expression('isAuthenticated()')}" method="get" 
          th:action="@{/oauth2/authorization/okta}">
        <button id="login-button" class="btn btn-primary" type="submit">Sign In</button>
    </form>

</div>
</body>
<!--/*/ <th:block th:include="footer :: footer"/> /*/-->
</html>

In the template above, the commented out <th:block/> allows to include a header and footer fragments defined in header.html and footer.html. They contain the Bootstrap dependencies for the templates styling. There is also a menu fragment that will replace the <div th:replace ...> element.

The th:if and th:unless conditionals are used to evaluate the authentication status. If the visitor is not authenticated, the Sign In button will be displayed. Otherwise, a greeting by username will be displayed.

Add a head.html template:

<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="head">
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" 
          integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css">
</head>
<body>
<p>Nothing to see here, move along.</p>
</body>
</html>

Create a footer.html template:

<html xmlns:th="http://www.thymeleaf.org">
<head>
</head>
<body>
<p>Nothing to see here, move along.</p>
</body>
<footer th:fragment="footer">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"  
            integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" 
            crossorigin="anonymous"></script>
</footer>
</html>

Then, add a menu.html template for the menu fragment:

<html xmlns:th="http://www.thymeleaf.org">

<body id="samples">
<nav class="navbar border mb-4 navbar-expand-lg navbar-light bg-light" th:fragment="menu">
    <div class="container-fluid">
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse"
                data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent"
                aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarSupportedContent">
            <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                <li class="nav-item"><a class="nav-link" th:href="@{/}">Home</a></li>
            </ul>
            <form class="d-flex" method="post" th:action="@{/logout}"
                  th:if="${#authorization.expression('isAuthenticated()')}">
                <input class="form-control me-2" type="hidden" th:name="${_csrf.parameterName}"
                       th:value="${_csrf.token}"/>
                <button id="logout-button" type="submit" class="btn btn-danger">Logout</button>
            </form>
        </div>
    </div>
</nav>
</body>
</html>

Create the first controller class

A Controller is required to access the home page. Add a HomeController class under src/main/java in the com.okta.developer.demo package, with the following content:

package com.okta.developer.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.reactive.result.view.Rendering;
import reactor.core.publisher.Mono;

import java.util.List;
import java.util.stream.Collectors;

@Controller
public class HomeController {

    private static Logger logger = LoggerFactory.getLogger(HomeController.class);

    @GetMapping("/")
    public Mono<Rendering> home(Authentication authentication) {
        List<String> authorities = authentication.getAuthorities()
                .stream()
                .map(scope -> scope.toString())
                .collect(Collectors.toList());
        return Mono.just(Rendering.view("home").modelAttribute("authorities", authorities).build());
    }
}

The controller will render the home view and will set the authorities as a model attribute, for security checks that will be added soon.

Tweak the security configuration

The default Okta starter auto-configuration will request authentication to access any page, so to customize the security, add a SecurityConfiguration class in the same package as before.

package com.okta.developer.demo;

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.logout.RedirectServerLogoutSuccessHandler;
import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;

import java.net.URI;

@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfiguration {

    @Bean
    public ServerLogoutSuccessHandler logoutSuccessHandler(){
        RedirectServerLogoutSuccessHandler handler = new RedirectServerLogoutSuccessHandler();
        handler.setLogoutSuccessUrl(URI.create("/"));
        return handler;
    }

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        http
            .authorizeExchange().pathMatchers("/").permitAll().and().anonymous()
            .and().authorizeExchange().anyExchange().authenticated()
            .and().oauth2Client()
            .and().oauth2Login()
            .and().logout().logoutSuccessHandler(logoutSuccessHandler());

        return http.build();
    }
}

In the configuration above, access to the home / page is allowed to all with anonymous authentication, which is required for the authentication expression in the Thymeleaf home.html template to work. An authentication object must be in the security context.

Run the application

Run the application with Maven:

./mvnw spring-boot:run

Go to http://localhost:8080 and you should see the home page and a Sign In button. Click the button and sign in with your Okta credentials. After signing in, you should be redirected back to the home page and see the content for authenticated users.

Home Page Authenticated

Protect content areas with authorization

Let’s add a userProfile.html template that will display the claims contained in the ID token returned from Okta, as well as the authorities that Spring Security derives from the token.

<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>User Details</title>
    <!--/*/ <th:block th:include="head :: head"/> /*/-->
</head>
<body id="samples">
<div th:replace="menu :: menu"></div>

<div id="content" class="container">

    <div>
        <h2>My Profile</h2>
        <p>Hello, <span th:text="${#authentication.principal.attributes['name']}">Joe Coder</span>. Below is the 
            information that was read with your <a 
            href="https://developer.okta.com/docs/api/resources/oidc.html#get-user-information">ID Token</a>.
        </p>
        <p>This route is protected with the annotation <code>@PreAuthorize("hasAuthority('SCOPE_profile')")</code>, 
            which will ensure that this page cannot be accessed until you have authenticated, and have the scope <code>profile</code>.</p>
    </div>
    <table class="table table-striped">
        <thead>
        <tr>
            <th>Claim</th>
            <th>Value</th>
        </tr>
        </thead>
        <tbody>
        <tr th:each="item : ${details}">
              <td th:text="${item.key}">Key</td>
              <td th:id="${'claim-' + item.key}" th:text="${item.value}">Value</td>
          </tr>
        </tbody>
    </table>

    <table class="table table-striped">
        <thead>
        <tr>
            <th>Spring Security Authorities</th>
        </tr>
        </thead>
        <tbody>
        <tr th:each="scope : ${#authentication.authorities}">
              <td><code th:text="${scope}">Authority</code></td>
        </tr>
        </tbody>
    </table>

</div>
</body>
<!--/*/ <th:block th:include="footer :: footer"/> /*/-->
</html>

Add the route mapping in the HomeController:

@GetMapping("/profile")
@PreAuthorize("hasAuthority('SCOPE_profile')")
public Mono<Rendering> userDetails(OAuth2AuthenticationToken authentication) {
    return Mono.just(Rendering.view("userProfile")
        .modelAttribute("details", authentication.getPrincipal().getAttributes())
        .build());
}

The @PreAuthorize annotation allows you to define an authorization predicate that can be written in SpEL (Spring Expression Language). It will be checked before the method execution. Only users with the authority SCOPE_PROFILE can request a display of the userProfile page. This is server-side protection.

For the client-side, add a link in the home.html template to access the userProfile page, below the “You successfully …” paragraph. The link will also only display for users that have the SCOPE_profile authority.

<p>You have successfully authenticated against your Okta org, and have been redirected back to this application.</p>
<p th:if="${#lists.contains(authorities, 'SCOPE_profile')}">Visit the <a th:href="@{/profile}">My Profile</a> page in this application to view the information retrieved with your OAuth Access Token.</p>

IMPORTANT NOTE: The authorization conditional is implemented in this way, because authorization-oriented expressions like ${#authorization.expression('hasRole(''SCOPE_profile'')')} are restricted in WebFlux applications due to a lack of support in the reactive side of Spring Security (as of Spring Security 5.6). Only a minimal set of security expressions is allowed: [isAuthenticated(), isFullyAuthenticated(), isAnonymous(), isRememberMe()].

Run the application again. After signing in, you still won’t see the new link, and if you go to http://localhost:8080/profile, you will get HTTP ERROR 403, which means forbidden. This is because in application.yml, as part of the Okta configuration, only email and openid scopes are requested, and profile scope is not returned in the access token claims. Add the missing scope in the application.yml, restart, and the userProfile view should now be visible:

Profile Page

As you can see, Spring Security assigns the groups contained in the claim as well as the requested scopes as authorities. Scopes are prefixed with SCOPE_. ROLE_ADMIN, and ROLE_USER groups are created by default when you create a client application with Okta CLI, and your user is assigned to them.

Prevent CSRF when submitting forms

CSRF stands for Cross-Site Request Forgery, and is a form of cyber-attack through the submission of a form from a malicious site to a known site, exploiting the browser behavior by which the malicious request is sent along with the known site cookies, passing as an authenticated request.

Spring Security CSRF protection is enabled by default for both Servlet and WebFlux applications. The predominant protection mechanism is the Synchronizer Token Pattern, which ensures each HTTP request must contain a secure random generated value, the CSRF token. The token must be required in a part of the request that is not populated automatically by the browser. For example, you can use an HTTP parameter or header.

Let’s verify the CSRF protection is active by creating a quiz form to the application. Create the template quiz.html with the following content:

<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Thymeleaf Quiz</title>
    <!--/*/ <th:block th:include="head :: head"/> /*/-->
</head>
<body id="samples">
<div th:replace="menu :: menu"></div>

<div id="content" class="container">
    <div>
        <h2>Select the right answer</h2>
    </div>
    <form action="#" th:action="@{/quiz}" th:object="${quiz}"
          method="post" class="col-md-4 fw-light">
        <ul>
            <li th:errors="*{answer}" />
        </ul>
        <div class="col-md-12">
            <h3>What is Thymeleaf?</h3>
        </div>
        <div class="col-md-12 form-check">
            <input class="form-check-input" type="radio" th:field="*{answer}" value="A" id="check-1-1"/>
            <label class="form-check-label" for="check-1-1">
                <strong>A.</strong> A server-side Java template engine
            </label>
        </div>
        <div class="col-md-12 form-check">
            <input class="form-check-input" type="radio" th:field="*{answer}" value="B" id="check-1-2"/>
            <label class="form-check-label" for="check-1-2">
                <strong>B.</strong> A markup language
            </label>
        </div>
        <div class="col-md-12 form-check">
            <input class="form-check-input" type="radio" th:field="*{answer}" value="C" id="check-1-3"/>
            <label class="form-check-label" for="check-1-3">
                <strong>C.</strong> A web framework
            </label>
        </div>
        <div class="col-md-12 mt-4 mb-4">
            <p>Your CSRF token is: <span th:text="${_csrf.token}"/></p>
        </div>
        <div class="col-md-12">
            <button type="submit" class="btn btn-primary">Submit</button>
        </div>
    </form>
</div>
</body>
<!--/*/ <th:block th:include="footer :: footer"/> /*/-->
</html>

The CSRF token is available as a request attribute and will be displayed in the quiz.html template for learning purposes.

Add a template for the quiz result with the name result.html:

<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Thymeleaf Quiz Submission</title>
    <!--/*/ <th:block th:include="head :: head"/> /*/-->
</head>
<body id="samples">
<div th:replace="menu :: menu"></div>
<div class="container" id="content">
    <div class="text-center">
        <i class="bi-balloon-heart-fill" style="font-size: 6rem; color: green;" th:if=${quiz.answer=='A'}></i>
        <i class="bi-x-circle-fill" style="font-size: 6rem; color: red;" th:unless=${quiz.answer=='A'}></i>
        <div class="panel mt-4 text-center">
            <div class="panel-body">
                <h4>Your selected answer is <strong>
                    <span th:text="${quiz.answer}"></span>
                </strong></h4>
                <p th:if=${quiz.answer=='A'}>Good Job!</p>
            </div>
        </div>
        <div class="panel mt-4 text-center" th:unless=${quiz.answer=='A'}>
            <div class="panel-body">
                <p>It is not the right answer</p>
                <p><a th:href="@{/quiz}">Try again!</a></p>
            </div>
        </div>
    </div>
</div>
</body>
<!--/*/ <th:block th:include="footer :: footer"/> /*/-->
</html>

Add a QuizSubmission data class, for holding the quiz answer:

package com.okta.developer.demo;

public class QuizSubmission {

    private String answer;

    public String getAnswer() {
        return answer;
    }

    public void setAnswer(String answer) {
        this.answer = answer;
    }
}

Add a QuizController for displaying the quiz and processing the form submission:

package com.okta.developer.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.reactive.result.view.Rendering;
import reactor.core.publisher.Mono;

@Controller
public class QuizController {

    private static Logger logger = LoggerFactory.getLogger(QuizController.class);

    @GetMapping("/quiz")
    @PreAuthorize("hasAuthority('SCOPE_quiz')")
    public Mono<Rendering> showQuiz() {
        return Mono.just(Rendering.view("quiz").modelAttribute("quiz", new QuizSubmission()).build());
    }

    @PostMapping(path = "/quiz", consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE})
    @PreAuthorize("hasAuthority('SCOPE_quiz')")
    public Mono<Rendering> saveQuiz(QuizSubmission quizSubmission) {
        return Mono.just(Rendering.view("result").modelAttribute("quiz", quizSubmission).build());
    }
}

In the new controller and templates, the quiz is authorized for users that have SCOPE_quiz authority. Add a protected link to the home.html template, below the profile link:

<p>You have successfully authenticated against your Okta org, and have been redirected back to this application.</p>
<p th:if="${#lists.contains(authorities, 'SCOPE_profile')}">Visit the <a th:href="@{/profile}">My Profile</a> page in this application to view the information retrieved with your OAuth Access Token.</p>
<p th:if="${#lists.contains(authorities, 'SCOPE_quiz')}">Visit the <a th:href="@{/quiz}">Thymeleaf Quiz</a> to test Cross-Site Request Forgery (CSRF) protection.</p>

Before running the application again, let’s verify CSRF protection with a controller test. Add QuizControllerTest to src/test/java under the package com.okta.developer.demo:

package com.okta.developer.demo;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;

import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockOidcLogin;

@WebFluxTest
public class QuizControllerTest {

    @Autowired
    private WebTestClient client;

    @Test
    void testPostQuiz_noCSRFToken() throws Exception {
        QuizSubmission quizSubmission = new QuizSubmission();
        this.client.mutateWith(mockOidcLogin())
                .post().uri("/quiz")
                .exchange()
                .expectStatus().isForbidden()
                .expectBody().returnResult()
                .toString().contains("An expected CSRF token cannot be found");
    }

    @Test
    void testPostQuiz() throws Exception {
        this.client.mutateWith(csrf()).mutateWith(mockOidcLogin())
                .post().uri("/quiz")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .exchange().expectStatus().isOk();
    }

    @Test
    void testGetQuiz_noAuth() throws Exception {
        this.client.get().uri("/quiz").exchange().expectStatus().is3xxRedirection();
    }

    @Test
    void testGetQuiz() throws Exception {
        this.client.mutateWith(mockOidcLogin())
                .get().uri("/quiz").exchange().expectStatus().isOk();
    }
}

In the test class above, the first test testPostQuiz_noCSRFToken() verifies the quiz cannot be submitted without the CSRF token, even if the user did sign in. In the second test testPostQuiz(), the CSRF token is added to the mock request with mutateWith(csrf()), so the expected response status is HTTP 200 OK. The third test testGetQuiz_noAuth() verifies a request of the quiz will be redirected (to the Okta sign-in form) if the user is not authenticated. And the last test testGetQuiz() verifies the quiz can be accessed if the user has been authenticated with an OIDC login.

As quiz is not a standard scope or a scope defined in Okta, you have to define it for the default authorization server before running the application. Sign in to the Okta Admin Console, and in the left menu, go to Security > API, the choose the default authorization server. In the Scopes tab, click Add Scope. Set the scope name as quiz and add a description, leave all the remaining fields with default values and click on Create. Now the quiz scope can be required during the OIDC login.

Add Scope Page

Run the application without adding the quiz scope to the application.yml file, sign in, and you should not see the quiz link yet. If you make a GET request with the browser to http://localhost:8080/quiz, the response will be 403 Forbidden.

Now add quiz to the list of scopes in the Okta configuration in application.yml. The final configuration should look like this:

spring:
  security:
    oauth2:
      client:
        provider:
          okta:
            user-name-attribute: email

okta:
  oauth2:
    issuer: https://{yourOktaDomain}/oauth2/default
    client-id: {clientId}
    client-secret: {clientSecret}
    scopes:
      - email
      - openid
      - profile
      - quiz

Run the application again and you should see the quiz link “Visit the Thymeleaf Quiz to test Cross-Site Request Forgery (CSRF) protection”. Click the link and the quiz will be displayed:

Thymeleaf Quiz Form

The CSRF token is displayed, but if you turn on developer tools, you can also find it as a hidden attribute <input type="hidden" name="_csrf" value="..."> that Spring Security adds to the form.

You can try the POST request with HTTPie, and you will again verify the POST request is rejected without the CSRF token.

$ http POST http://localhost:8080/

HTTP/1.1 403 Forbidden
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: text/plain
Expires: 0
Pragma: no-cache
Referrer-Policy: no-referrer
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1 ; mode=block
content-length: 38

An expected CSRF token cannot be found

The interesting fact here is that it seems CSRF protection takes precedence over authentication in the Spring Security filter chain.

Learn more about Spring Boot and Spring Security

I hope you enjoyed this brief introduction to Thymeleaf and learned how to secure content and implement authorization on the server-side using Spring Security. You could also experience how fast and easy is to integrate OIDC Authentication using Okta. To learn more about Spring Boot Security and OIDC, check out the following links:

You can find the complete source code for this tutorial on GitHub.

Be sure to follow us on Twitter and subscribe to our YouTube Channel so that you never miss any of our excellent content!

Okta Developer Blog Comment Policy

We welcome relevant and respectful comments. Off-topic comments may be removed.