Grego
Grego

Reputation: 327

Thymeleaf Webflux Security, Csrf not added to views

I am trying to add csrf tags to forms however it seems like it works differently than it did in mvc.

So what I did was adding <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />

to login form however the _csrf attribute is not present even though these annotations are present

@EnableWebFluxSecurity
@EnableReactiveMethodSecurity

Here's my SecurityWebFilterChain:

 http
                .authorizeExchange().pathMatchers(
                "/landing",
                "/",
                "/register",
                "/login",
                "/favicon.ico",
                "/js/**",
                "/fonts/**",
                "/assets/**",
                "/css/**",
                "/webjars/**").permitAll()
                .anyExchange().authenticated()
                .and()
                .httpBasic()
                .and()
                .formLogin().loginPage("/login")
                .and().logout()

What am I missing?

UPDATE: Added the dependencies I am using that are related.

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.3.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
    <thymeleaf.version>3.0.9.RELEASE</thymeleaf.version>
    <thymeleaf-layout-dialect.version>2.0.0</thymeleaf-layout-dialect.version>

</properties>
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.integration</groupId>
            <artifactId>spring-integration-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>net.sourceforge.nekohtml</groupId>
            <artifactId>nekohtml</artifactId>
            <version>1.9.22</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity4</artifactId>
            <version>3.0.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>io.github.jpenren</groupId>
            <artifactId>thymeleaf-spring-data-dialect</artifactId>
            <version>3.3.1</version>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf-spring5</artifactId>
            <version>3.0.9.RELEASE</version>
        </dependency>
    </dependencies>

UPDATE When I include the hidden input tag with csrf to the login form:

<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />

I get this error:

org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: "_csrf.parameterName" (template: "public/login" - line 75, col 17)

Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1007E: Property or field 'parameterName' cannot be found on null

Because _csrf is null for some reason, even though the annotations are in place.

Login controller:

 @GetMapping("/login")
    public String login(Model model) {
        return "public/login";
    }

Also tried adding a controller advice like this:

@ControllerAdvice
public class SecurityAdvice {

    @ModelAttribute("_csrf")
    Mono<CsrfToken> csrfToken(final ServerWebExchange exchange) {

        final Mono<CsrfToken> csrfToken = exchange.getAttribute(CsrfToken.class.getName());

        return csrfToken.doOnSuccess(token -> exchange.getAttributes()
                .put(DEFAULT_CSRF_ATTR_NAME, token)).log();
    }
}

Similarly as it was used here: https://github.com/daggerok/csrf-spring-webflux-mustache

However this results in

java.lang.NullPointerException: null
    at com.a.Config.SecurityAdvice.csrfToken(SecurityAdvice.java:23) ~[classes/:na]

This line is the return part of the last snippet.

Upvotes: 5

Views: 1421

Answers (4)

BW1337
BW1337

Reputation: 81

It appears the CsrfToken information is not actually added to the model by default in WebFlux. (see https://github.com/spring-projects/spring-security/issues/6046)

The workaround is to add a ControllerAdvice subscribing to he token information and referencing it via ComponentScan or declaring it as an explicit bean like so:

(I'm using Kotlin but this should work the same way in Java)

import org.springframework.security.web.reactive.result.view.CsrfRequestDataValueProcessor.DEFAULT_CSRF_ATTR_NAME
import org.springframework.security.web.server.csrf.CsrfToken
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ModelAttribute
import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono

@ControllerAdvice
class CsrfControllerAdvice {

    @ModelAttribute(value = DEFAULT_CSRF_ATTR_NAME)
    fun csrfToken(exchange: ServerWebExchange): Mono<CsrfToken> {
        return exchange.getAttributeOrDefault(CsrfToken::class.java.name, Mono.empty())
    }
}

Upvotes: 1

codependent
codependent

Reputation: 24472

The cleanest solution that will work across all your controllers is to use a @ControllerAdvice as detailed in the documentation:

@ControllerAdvice
public class SecurityControllerAdvice {
    @ModelAttribute
    Mono<CsrfToken> csrfToken(ServerWebExchange exchange) {
        Mono<CsrfToken> csrfToken = exchange.getAttribute(CsrfToken.class.getName());
        return csrfToken.doOnSuccess(token -> exchange.getAttributes()
                .put(CsrfRequestDataValueProcessor.DEFAULT_CSRF_ATTR_NAME, token));
    }
}

Upvotes: 1

Druckles
Druckles

Reputation: 3782

I had to include thymeleaf-extras-springsecurity5 (not springsecurity4) to get this working.

I couldn't find any CsrfToken, which meant the answer from @mk88 didn't help nor this guide.

Once I'd included the following dependency, it worked flawlessly as designed:

    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        <version>3.0.4.RELEASE</version>
    </dependency>

Output from Thymeleaf:

<form action="/members" method="POST" ><input type="hidden" name="_csrf" value="432033fd-1076-4620-9f99-d0220b6d3071"/>

Upvotes: 0

mk88
mk88

Reputation: 16

This is what i did to get it working.

@GetMapping("/login")
public Mono<String> login(ServerWebExchange exchange, Model model) {
    Mono<CsrfToken> token = exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty());
    return token.map(t -> {
        model.addAttribute("_csrf", t);
        return "login";
    });
}

Upvotes: 0

Related Questions