4

I am using Spring Boot 3, Spring Security 6. My Security configuration doesn't work properly. I have 2 paths on which any request should be permitted, and for everything else one needs to authenticate.

Both GET and POST method work on those that need authentication.

On those with permitAll(), only GET requests work. For POST, I get 401 Unauthorised.

I took care of CSRF, and anyway I expect all the POST requests to work, not only those with authentication.

On Postman, I selected POST, No Auth, put a RAW body and selected JSON. I really don't know why is it not working.

Here is my code:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }

 
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, KeycloakLogoutHandler keycloakLogoutHandler) throws Exception {

        CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
        XorCsrfTokenRequestAttributeHandler delegate = new XorCsrfTokenRequestAttributeHandler();
        // set the name of the attribute the CsrfToken will be populated on
        delegate.setCsrfRequestAttributeName("_csrf");
        // Use only the handle() method of XorCsrfTokenRequestAttributeHandler and the
        // default implementation of resolveCsrfTokenValue() from CsrfTokenRequestHandler
        CsrfTokenRequestHandler requestHandler = delegate::handle;
    
        http
                .authorizeHttpRequests().requestMatchers("/firstpath/**", "/secondpath/**", "/error/**").permitAll().and()
                .authorizeHttpRequests().anyRequest().authenticated().and()
                .oauth2ResourceServer(oauth2 -> oauth2.jwt());
        http.oauth2Login()
                .and()
                .logout()
                .addLogoutHandler(keycloakLogoutHandler)
                .logoutSuccessUrl("/");
        http.csrf((csrf) -> csrf
                .csrfTokenRepository(tokenRepository)
                .csrfTokenRequestHandler(requestHandler));
        return http.build();
    }
}
@Slf4j
@RestController
@RequestMapping("/firstpath")
public class NameitController {

    @PostMapping(value = "path", produces = WSConstants.JSON_MEDIATYPE)
    @ResponseBody
    public ResponseEntity saveMyObject(@RequestBody ObjectDTO dto) {
        [...] //my code
    }
}

I also tried http.authorizeHttpRequests().requestMatchers(HttpMethod.POST, "/firstpath/path").permitAll(), but at no use.

Edit: It still has to do with CSRF protection, because when I tired http.csrf().disable();, everything worked fine. But I still want CSRF protection, it seems like the token is not sent with permitAll()?...

post request with Postman

Edit2: After adding Spring Security logs:

enter image description here

Octavia
  • 198
  • 1
  • 13
  • This question is in a big of a state of disarray because you have tried some things and it's not working. The question also demonstrates you have not minimized the issue to just CSRF, which seems to be the main issue you're asking for help with. – Steve Riesenberg Jan 12 '23 at 18:24
  • Having said that (above comment), I'm wondering if this application was migrated from Spring Security 5.7 directly to 6.0? If so, please follow the steps in the [5.8 migration guide](https://docs.spring.io/spring-security/reference/5.8/migration/index.html) first to upgrade from 5.7 to 5.8. Next, look at the [CSRF](https://docs.spring.io/spring-security/reference/5.8/migration/servlet/exploits.html#_defer_loading_csrftoken) section. Only then would you migrate to 6.0. Does that help? – Steve Riesenberg Jan 12 '23 at 18:24
  • I migrated from Spring Boot 2.5.3 to 2.7.0 and then to 3.0.0, I am not sure which versions of Spring Security the older versions use. I will take a look at the links you said, thank you! – Octavia Jan 13 '23 at 08:07
  • I went trough the migration guides to 5.8 and to 6.0, I added CookieCsrfTokenRepository, still POST methods doesn't work in Swagger and on permitAll() paths in Postman.. for the authentication I am using Access Tokens and the POST methods work... – Octavia Jan 13 '23 at 10:24
  • It appears you did not upgrade to 5.8 as I suggested. I'm still not able to determine what from your question used to work in Spring Security 5.7 that only stopped working in 6.0. There are a couple of different possible issues. Please provide the complete minimal sample that works on Spring Security 5.7 (Spring Boot 2.7), and update the question to contain that example (full configuration) and I'll be happy to take a look. – Steve Riesenberg Jan 13 '23 at 15:57
  • @SteveRiesenberg I did the same thing on Spring Security 5.7 and it didn't work, which led me to the conclusion that I am not setting the XSRF-token properly in Postman. I had this problem only on permitAll() because for the rest of the endpoints I use OAuth 2.0 authentication, and from what I understood, then the CSRF protection is not requested. The only problem left, is I get a " Invalid CSRF token found for [...]" on POST request with Swagger, but I guess that's a different thing then my question. – Octavia Jan 16 '23 at 13:30
  • adding ``springdoc.swagger-ui.csrf.enabled=true`` to properties files was needed so that Swagger would work – Octavia Jan 16 '23 at 15:33
  • That makes sense. CSRF is required for POST requests regardless of whether they are permitAll or not. – Steve Riesenberg Jan 17 '23 at 05:52

2 Answers2

2

In your postman, I do not see X-XSRF-TOKEN header. If you’re not sending the XSRF-Token back to the server after fetching it from the cookie, you might wanna do it as shown in the end of the answer since it is one of the ways it is designed to protect against CSRF attacks and only works like that. In frameworks like angular, we can get it as a cookie from spring boot server and send it back as a header to differentiate malicious sites accessing the same URL, since such sites, inside the browser, cannot access the cookie associated with our genuine domain to send it back as a header.

Here is a simple working project which uses spring security 6 and crsf token with postman testing if it can help. It uses InMemoryUserDetailsManager, NoOpPasswordEncoder(Not recommended for production) and basic authentication.

SecurityConfig:

import java.util.function.Supplier;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRequestHandler;
import org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@Configuration
public class ProjectSecurityConfig {
    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        XorCsrfTokenRequestAttributeHandler delegate = new XorCsrfTokenRequestAttributeHandler();
        delegate.setCsrfRequestAttributeName("_csrf");
        CsrfTokenRequestHandler requestHandler = new CsrfTokenRequestHandler() {
            @Override
            public void handle(HttpServletRequest request, HttpServletResponse response,
                    Supplier<CsrfToken> csrfToken) {
                delegate.handle(request, response, csrfToken);
            }
        };
        return http
                .cors().disable() // disabled cors for simplicity in this example in case of testing through a ui
                .authorizeHttpRequests()
                .requestMatchers("/error").permitAll()
                .anyRequest().authenticated()
                .and()
                .csrf()
                .csrfTokenRequestHandler(requestHandler)
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .and().formLogin()
                .and().httpBasic()
                .and().build();
    }

    @Bean
    InMemoryUserDetailsManager userDetailsService() {
        UserDetails admin = User.withUsername("admin").password("pass").authorities("admin").build();
        return new InMemoryUserDetailsManager(admin);
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}

Controller:

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.learning.entity.DataObject;

@RestController
public class TestController {
    @PostMapping("/post")
    public String post(@RequestBody DataObject dataObject) {
        return "succesfull post";
    }
}

DataObject Model:

public class DataObject {
    private String data;

    public String getData() {
        return data;
    }

    public void setData(String data) {
        this.data = data;
    }
}

application.properties:

logging.level.org.springframework.security.web.csrf=TRACE

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.learning</groupId>
    <artifactId>spring-security-3-csrf-example</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-security-3-csrf-example</name>
    <description>spring learning</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Test using CSRF token in postman:

First add basic auth credentials-

adding basic auth credentials

Add data object json body-

adding object json body

Send a mock request to the server to get a XSRF Cookie

Use this Cookie value as a header with name "X-XSRF-TOKEN"-

enter image description here

Testing it-

enter image description here

Note:- Since version 6, Spring Security does not create sessions for basic authentication by default so no Cookie for session will be returned in this example.

UPDATE :-

Here is an article on a more sophisticated way to send XSRF-TOKEN through postman as pointed by @OctaviaAdler in the comments. TLDR in case the link goes down:- Create an environment in postman and add the variable "xsrf-token" in it. Inside the request, add the header X-XSRF-TOKEN with the value set to "{{xsrf-token}}" (name of the environment variable in double curly braces without quotes). Then add the following script inside the "Tests" tab -

var xsrfCookie = postman.getResponseCookie("XSRF-TOKEN");
postman.setEnvironmentVariable("xsrf-token", xsrfCookie.value);
  • 1
    thank you! it worked, I recommend this for how to send XSRF-TOKEN. https://www.baeldung.com/postman-send-csrf-token – Octavia Jan 16 '23 at 13:42
-1

THe order in which you define in security config please have a look order like this

  `@Override
   protected void configure(HttpSecurity http) throws Exception {
   http
        .authorizeRequests()
        .antMatchers(HttpMethod.POST, "/api/auth/**")
        .permitAll()
        .antMatchers("/",
                "/favicon.ico",
                "/**/*.png",
                "/**/*.gif",
                "/**/*.svg",
                "/**/*.jpg",
                "/**/*.html",
                "/**/*.css",
                "/**/*.js")
        .permitAll()                   
        .anyRequest()
        .authenticated()
        .and()
        .cors()
        .and()
        .exceptionHandling()
        .authenticationEntryPoint(this.jwtAuthenticationEntryPoint)
        .and()
        .sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        .csrf()
        .disable();

// Add our custom JWT security filter
http.addFilterBefore(jwtAuthenticationFilter(), 
 UsernamePasswordAuthenticationFilter.class);

 }`