I'm working with an old project that uses a custom MVC servlet path, was packaged as a war, and deployed in a tomcat. I wanted to migrate to jar packaging, but when trying to log in using form login I'm presented with an infinite redirect.

Sample code, with spring boot 2.4.5 and just spring-web and spring-security dependencies:

@RestController
@SpringBootApplication
public class FormloginApplication extends SpringBootServletInitializer {

    public static void main(String[] args) {
        SpringApplication.run(FormloginApplication.class, args);
    }

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(FormloginApplication.class);
    }

    @Bean
    public SecurityFilterChain formLoginFilterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeRequests(authorize -> authorize.anyRequest().authenticated())
                .formLogin().and()
                .build();
    }

    @GetMapping("/hello")
    public String hello(@AuthenticationPrincipal User user) {
        return "Hello " + user.getUsername() + "!";
    }

}
server:
  servlet:
    context-path: /formlogin

spring:
  mvc:
    servlet:
      path: /api

When packaging as formlogin.war, everything works fine. Accessing localhost:8080/formlogin/api/hello I am redirected to http://localhost:8080/formlogin/login, login page loads, I log in with the generated credentials, and I am greeted with "Hello user!".

When packaging as jar or using the spring-boot:run maven goal, I still get redirected to http://localhost:8080/formlogin/login, but the login page doesn't load, instead it tries to redirect me again to http://localhost:8080/formlogin/login. After a few rounds, the browser gives up and displays the error "this page doesn't work".

Removing the spring.mvc.servlet.path configuration makes it work again, but it's not a viable solution for me.

Sample repo: https://github.com/gbaso/spring-security-9684

Comment From: marcusdacoregio

Thanks for submitting this bug @gbaso.

I am taking a look into it and will provide some answers soon.

Comment From: marcusdacoregio

Hi @gbaso. It looks like this issue requires a little bit more research. While we continue investigating further into this bug I have adapted a sample to provide you a workaround.

Basically what seems to be happening is that the embedded tomcat is not recognizing the /formlogin/login as a valid endpoint for the application, so it returns a 404 response status and Spring Boot redirects to the default /error endpoint. The /error endpoint is secure by default, so it redirects you again to the /formlogin/login.

The solution, for now, is to map the login page under the /api servlet path, so this way the application server knows that there's a Servlet that can handle it.

You can see the changes to the sample in this commit and it may help you while we figure it out.

Comment From: gbaso

Thanks a lot @marcusdacoregio, I'll give it a try!

Comment From: marcusdacoregio

Hey @gbaso, the solution to your problem seems to be related to https://github.com/spring-projects/spring-boot/issues/22915.

server:
  servlet:
    context-path: /formlogin
    register-default-servlet: true #tells spring boot to register the default servlet on / path

spring:
  mvc:
    servlet:
      path: /api

You can see why the default value is false for this property in that issue. For reference, the external tomcat, on its conf/web.xml file, defines a default servlet that serves static resources, it processes all requests that are not mapped to other servlets with servlet mappings. Since almost all the Spring Boot applications register the DispatcherServlet to the default / path, there is no need to always register the default servlet.

The login page is not necessarily served by the DispatcherServlet, so when the server tried to find something under the /formlogin/login endpoint, it couldn't, because there wasn't any servlet that handles requests to /formlogin/*, only to /formlogin/api/*, thus throwing a 404.

I'm closing this as solved but feel free if you want to discuss anything else about it.

Comment From: gbaso

Thank you @marcusdacoregio, registering the default servlet indeed solves the problem.

Maybe spring.mvc.servlet.path should advise to consider registering the default servlet, like it warns about the PathPatternParser incompatibility. What do you think?

Comment From: marcusdacoregio

I don't see spring.mvc.servlet.path advising about it. Maybe when using a login page mapped to a different path than the servlet path we could warn or even throw an error asking for registering the default servlet. What do you think @jzheaux ?