Description

I am experiencing a problem with Spring Security 6.2 in a Spring Boot 3.2 application, particularly when handling POST requests with Multipart Forms.

Configuration

My SecurityConfig class is set up as follows:

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable);
        http.authorizeHttpRequests((auth) -> {
                    auth.requestMatchers("/admin/**").hasRole("ADMIN");
                    auth.anyRequest().authenticated();
                })
                .formLogin(withDefaults());
        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(com.example.CustomUserDetailsService customUserDetailsService) {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(customUserDetailsService);
        return new ProviderManager(daoAuthenticationProvider);
    }

    @Bean
    com.example.CustomUserDetailsService customUserDetailsService(UserRepository userRepository) {
        return  new CustomUserDetailsService(userRepository);
    }
}

Issue

GET requests to unauthorized endpoints are correctly intercepted and redirected to the login page. POST requests using Form URL Encoded work as expected. However, POST requests with Multipart Form (e.g., POST localhost:8080/admin/test) hang for an extended period, eventually leading to the error: "Error: Number of redirects hit maximum amount."

Additional Observations

Debugging reveals that for POST requests with Multipart Form, loadUserByUsername in CustomUserDetailsService is invoked with an empty string "" as the phoneNumber parameter. After experiencing this error, the first login attempt results in: "org.apache.tomcat.util.http.fileupload.impl.InvalidContentTypeException: the request doesn't contain a multipart/form-data or multipart/mixed stream, content type header is application/x-www-form-urlencoded". Subsequent login attempts after this error proceed normally.

Minimal Reproducible Example

I have created a minimal reproducible example to demonstrate this issue: Minimal Reproducible Example

Comment From: jzheaux

Thanks for reaching out, @LL145. The sample you provided gives the following error at startup:

java.lang.IllegalArgumentException: URL must start with 'jdbc'

I believe the current app requires DB_URL to be an application property. Are you able to update the GitHub sample to not require external configuration? Ideally, I'd be able to run it with gw bootRun to investigate the issue.

Also, I didn't see any multipart controller methods in the sample application. Can you please indicate what would be an expected request for your application? It also may help for clarity's sake to add a controller endpoint that processes multipart data in the way you would like things to work.

Comment From: LL145

@jzheaux Thank you for your attention to my issue.

  1. Update on Minimal Reproducible Example: I have updated the Minimal Reproducible Example in the repository. The database has been switched to H2, which should allow for the application to be launched without any additional configurations.

  2. Steps to Reproduce the Issue: After starting the application, if you make a POST request to localhost:8080/admin/test using a multipart form, you will encounter the error: "Error: Number of redirects hit maximum amount." I have included a screenshot of this behavior here. Untitled

  3. Expected Behavior: I anticipate the behavior should be consistent with that observed when using GET requests or accessing the endpoint with Form URL Encoded - namely, being redirected back to the login page.

Looking forward to any insights or suggestions you might have regarding this issue.

Comment From: jzheaux

@LL145, thanks for the updates.

Sorry, I don't use Insomnia and I do not know what the other part of the request is from the screenshot. Typically a multipart request contains more than one part. If all you are sending is a userId, then why do you need it to be a multipart request?

What I'll need to proceed is a multipart request representative of what you want your application to support.

Comment From: LL145

@jzheaux Thank you for your response.

  1. Updated Minimal Reproducible Example: I have further updated the Minimal Reproducible Example in the repository. It now includes an endpoint that specifically receives multipart data: localhost:8080/admin/test2. However, the issue still persists with this new endpoint.

  2. Issue Unrelated to Controller Implementation: I believe that this issue is not related to the specific implementation of the controller. Even for a controller that accepts @RequestParam as parameters, users might send requests in multipart format, which then triggers this same issue. My concern is about handling such multipart requests consistently, as they are with GET or Form URL Encoded requests, by redirecting back to the login page.

Looking forward to further guidance or suggestions on this matter.

Comment From: jzheaux

I had to alter the sample to add an admin user. Having done that, here is how I proceeded:

  1. Login
POST /login HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 34
Content-Type: application/x-www-form-urlencoded; charset=utf-8
Host: localhost:8080
User-Agent: HTTPie/3.2.2

username=5551212&password=password

which gives the response:

HTTP/1.1 302 
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: keep-alive
Content-Length: 0
Date: Tue, 19 Dec 2023 22:24:56 GMT
Expires: 0
Keep-Alive: timeout=60
Location: http://localhost:8080/
Pragma: no-cache
Set-Cookie: JSESSIONID=0EA2E36464FA3613D713A9477E686B14; Path=/; HttpOnly
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 0
  1. Then, I make a multipart request to /admin/test like so:
POST /admin/test HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 288
Content-Type: multipart/form-data; boundary=84e2d0e4711841a4bdf60d094a4579cc
Cookie: JSESSIONID=0EA2E36464FA3613D713A9477E686B14
Host: localhost:8080
User-Agent: HTTPie/3.2.2

--84e2d0e4711841a4bdf60d094a4579cc
Content-Disposition: form-data; name="userId"

10
--84e2d0e4711841a4bdf60d094a4579cc
Content-Disposition: form-data; name="cv"; filename="settings.gradle"

rootProject.name = 'minimal-reproducible-example'

--84e2d0e4711841a4bdf60d094a4579cc--

which then gives the response:

HTTP/1.1 200 
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: keep-alive
Content-Length: 0
Date: Tue, 19 Dec 2023 22:23:39 GMT
Expires: 0
Keep-Alive: timeout=60
Pragma: no-cache
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 0

It appears that the multipart request returns fine without a 302.

Can you please do the following:

  • add to the sample the minimum necessary for authentication to function
  • give more detail on the sequence of requests that replicates the behavior

Comment From: LL145

@jzheaux

Thank you for your detailed response and for testing the scenario.

To better illustrate the issue, I have recorded a short video demonstrating how to reproduce the problem. In the video, I first access the endpoint using the GET method, which successfully redirects me back to the login page. However, when I attempt to access the same endpoint using the POST method, the error occurs. Please see the demonstration here: https://www.youtube.com/watch?v=2uJkSRAzj8U

Comment From: jzheaux

Thanks, @LL145.

It appears that what Insomnia is trying to do is to a multipart POST to /login, which is a bad request. Since it is a bad request, Spring Security redirects to /login?error. Insomnia follows with the same multipart POST to /login?error and the cycle ensues.

I'm not clear on why Insomnia is choosing to send a multipart to /login in the first place, but that may be an indication of an Insomnia misconfiguration or bug.

Spring Security consistently responds to clients to please redirect to /login?error when there is an error with login. Given that, I'm going to close this issue. Please feel free to comment further if there is something you see for Spring Security to do.

Comment From: LL145

@jzheaux Thank you for the assistance and for helping me identify the root cause of the problem. It turns out the issue was related to the behavior of the Insomnia testing tool. I agree with the decision to close this issue. Spring Security's response to redirect to /login?error on login errors is consistent and expected behavior.