Thymeleaf + Spring WebFlux + Spring Security


Thymeleaf has been around for a long time, at least 10 years ago, but it is still very popular and actively maintained. Thymeleaf templates are convenient because they look like normal HTML pages when simply opened in a browser and can be used as a static application prototype.

In this article, we’ll see how to create a simple Spring WebFlux application with Thymeleaf, Okta OIDC authentication, CSRF protection, and permission control.

We will use the following frameworks and tools:

What is Thymeleaf?

Thymeleaf is an open source server-side templating engine for various types of web and non-web applications created by Daniel Fernández. Templates are similar to HTML and can be used with Spring MVC, Spring Security, and other popular frameworks. Including there is integration with Spring WebFlux, but at the moment there is quite a bit of information about this. Thymeleaf-starter performs automatic tuning template engine, template resolver and reactive view resolver.

Thymeleaf features include:

  • Working with fragments: rendering only part of the template. Can be used when updating part of the page when responding to AJAX requests. There is also a “component” mechanism: fragments can be included in several different templates.

  • Processing forms using model objects containing form fields.

  • Rendering Variables and External Text Messages with the Thymeleaf Expression Language Standard Expression Syntax.

  • The presence of cycles and conditional structures.

Spring WebFlux Application with Thymeleaf

We will write a simple monolithic reactive Spring Boot application with Thymeleaf. Application stub can be created via web interface Spring Initializr or 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

We will have a Maven project. Unpack it and add a couple of dependencies: thymeleaf-extras-springsecurity5 to support Spring Security in templates and spring-security-test for 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>

Authentication with OpenID Connect

You will need a free Okta developer account. Install Okta CLI and run okta register to create a new account. If you already have an account, then use okta login. To create a new application run okta apps create.

You can leave the Application name at the default or change it to your liking. Application type (Type of Application) select Web. Framework of Application – Okta Spring Boot Starter. Leave the Redirect URI as default: Login Redirect to http://localhost:8080/login/oauth2/code/okta and exit (Logout Redirect) to http://localhost:8080.

The Okta CLI will create an OIDC Web App in your Okta Org, add the redirect URIs you specified, and grant access to the Everyone group. Upon completion, a message similar to this should appear:

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

Access details of your application will be in the file src/main/resources/application.properties.

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

You can also use the Okta Admin Console to create an application. For more information, see the section Create a Spring Boot App in the documentation.

Let’s rename application.properties in application.yml and add the following options:

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

Note that we don’t need a scope yet profile. For OpenID Connect requests only openid is required. Property thymeleaf.prefix allows hot reloading of templates if a dependency is included in the project spring-boot-devtools.

Thymeleaf Templates

Create a folder for templates src/main/resources/templates and in it the file home.html 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 tag <th:block/> allows you to include header and footer snippets defined in header.html and footer.html. They contain Bootstrap dependencies for styling templates. Also instead of <div th:replace ...> the menu fragment will be inserted.

Conditional expressions th:if and th:unless are used to check the authentication status. If the user is not authenticated, the “sign in“. Otherwise, a greeting with the username.

Next, create 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>

And footer.html:

<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>

Also a template menu.html menu snippet:

<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="https://habr.com/ru/company/otus/blog/665952/@{/}">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>

Controller

To access the page home a controller is required. Create in a package com.okta.developer.demo Class HomeController 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());
    }
}

This controller renders a view home and fills in the model attribute of the authority (authorities) for further verification of access rights.

Security setup

Okta starter is configured by default to authenticate access to all pages. We need to tweak this a little, so add a class SecurityConfiguration 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();
    }
}

Here we allow anonymous access for all users to the root page (/) so that they can log in.

Application launch

Run the application using Maven:

./mvnw spring-boot:run

Go to address http://localhost:8080 – you will see the home page and the button “sign in“. Click the button and log in using your Okta credentials. Upon successful login, you should be redirected to the home page and see the content for authenticated users.

Content protection with authorization

Next, let’s add a template userProfile.htmlwhich will display information about claimcontained in the token ID returned by Okta, as well as the authorities received by Spring Security 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>

Customize in HomeController mapping:

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

annotation @PreAuthorize allows you to define authorization rules using SpEL (Spring Expression Language). The rules are checked before the method is executed. In this case, only users with permissions SCOPE_profile can access the page userProfile. This is server side protection.

On the client side, add in the template home.html link to access the page userProfile after “You successfully …”. The link will only be displayed to users with authority. SCOPE_profile.

<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="https://habr.com/ru/company/otus/blog/665952/@{/profile}">My Profile</a> page in this application to view the information retrieved with your OAuth Access Token.</p>

Note that the authorization condition is implemented in this way, since expressions like ${#authorization.expression('hasRole(''SCOPE_profile'')')} do not work in WebFlux due to lack of support in reactive Spring Security (Spring Security 5.6). Only the minimum set of security check expressions is supported: [isAuthenticated(), isFullyAuthenticated(), isAnonymous(), isRememberMe()].

Run the application again. You won’t see the new link after logging in, but if you go to http://localhost:8080/profile, you will get HTTP ERROR 403 Forbidden – access denied. This is due to the fact that in application.yml we set up only getting the scope for email and openid, and profile is not returned in the access token. Add the missing scope to application.yml, restart. Now the view userProfile should be available:

As you can see, Spring Security assigns the groups contained in claim, as well as the requested scope as the authorities. scope has a prefix SCOPE_. When creating an application through Okta CLI, groups are created by default ROLE_ADMIN and ROLE_USERand your account is included in those groups.

Protection against CSRF attacks

A CSRF (Cross-site request forgery) attack allows you to send data from a form on an attacker’s page to a victim site where the user is already authenticated and perform malicious actions on behalf of the user.

CSRF protection in Spring Security is enabled by default for both Servlet and WebFlux applications. The main way to protect Synchronizer Token Pattern. A randomly generated value is placed in each HTTP request – a CSRF token. The token must be in a part of the request that is not automatically filled in by the browser. For example, you can use an HTTP parameter or a header for this.

Let’s test our CSRF protection by building a simple survey application. Create a 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="https://habr.com/ru/company/otus/blog/665952/@{/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 an attribute of the request, for training purposes we will display it in the template quiz.html.

Also add template result.html to display the poll result:

<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="https://habr.com/ru/company/otus/blog/665952/@{/quiz}">Try again!</a></p>
            </div>
        </div>
    </div>
</div>
</body>
<!--/*/ <th:block th:include="footer :: footer"/> /*/-->
</html>

Next class QuizSubmission to store response:

package com.okta.developer.demo;

public class QuizSubmission {

    private String answer;

    public String getAnswer() {
        return answer;
    }

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

and controller QuizController to display the survey and process the form data:

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, only users with permissions to access the survey are allowed to SCOPE_quiz. Add a secure link to the template home.html after 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="https://habr.com/ru/company/otus/blog/665952/@{/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="https://habr.com/ru/company/otus/blog/665952/@{/quiz}">Thymeleaf Quiz</a> to test Cross-Site Request Forgery (CSRF) protection.</p>

Before running the application again, let’s check the CSRF protection with a test. Create QuizControllerTest in src/test/java in 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();
    }
}

Test testPostQuiz_noCSRFToken() checks that the survey cannot be sent without a CSRF token, even if the user is logged in. Second test testPostQuiz() – the CSRF token is added to the dummy request using mutateWith(csrf()). Here, the expected response status is HTTP 200 OK. Third test testGetQuiz_noAuth() checks that the request will be redirected (to the Okta login form) if the user is not authenticated. And the last test testGetQuiz() checks that the survey can be accessed if the user is authenticated using OIDC.

Insofar as quiz is not a default scope or scope defined in Okta, you need to define it for the default authorization server before running the application. Go to Okta Admin Console in the menu Security > API, select the default authorization server. On the tab scopes click Add Scope. Enter the name (Name) of the quiz and a description (Display phrase). Leave the rest of the fields with default values ​​and click Create. Now, when logging in via OIDC, you can require scope quiz.

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

Now add quiz to scopes list in Okta’s configuration 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. You should see a link “Visit the Thymeleaf Quiz to test Cross-Site Request Forgery (CSRF) protection”. Click on the link – you will be redirected to a page with a quiz:

Spring Security adds CSRF token to form as hidden attribute <input type="hidden" name="_csrf" value="...">.

You can make a POST request using HTTPie and verify once again that the CSRF protection is working.

$ 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

An interesting fact is that CSRF protection takes precedence over authentication in the Spring Security filter chain.

More about Spring Boot and Spring Security

I hope you enjoyed this brief introduction to Thymeleaf and learned how to secure content and implement server-side authorization with Spring Security. You also saw how quick and easy it is to integrate OIDC authentication with Okta. You can learn more about Spring Boot Security and OIDC in the following articles:

You can find the source code from the article at GitHub.


We invite everyone who has read the article to the end to the open lesson “Validation Framework in Spring”. In the lesson, we will look at how to validate various objects using javax.validation, in Spring projects with features. Registration – link.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *