Search code examples
springspring-bootspring-security

Spring Security 6 POST requests are unauthorised with permitAll()


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


Solution

  • 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);