Skip to content

The browser accesses a nonexistent resource during login, leading to /error after login #17686

@SellerJoke

Description

@SellerJoke

Describe the bug
When the user is not logged in and the browser accesses the protected resource, the browser will jump to the login page. If the browser automatically visits a nonexistent resource (e.g., favicon.ico) on the login page, the browser will automatically navigate to the /error path after login.

To Reproduce
Create a Spring Boot Project with dependencies 'org.springframework.boot:spring-boot-starter-web' and 'org.springframework.boot:spring-boot-starter-security'.
In the security configuration, a form login is provided and requires login to access all paths except the /favicon.ico path, which allows an anonymous login.
The most important thing is that there is no favicon.ico in this project.
Then visit 127.0.0.1:8080/hello or other secured resources in your browser. The browser will be redirect to /login. Enter username and password(admin:admin) and click the login button. The browser will be redirected to /error instead of /hello.

Expected behavior
After logged in, the browser redirects to the url before /login or home page instead of /error.

Sample
Here's the contents of the application.yml file. It provides a user with a username and password of admin.

spring:
    security:
    user:
      name: admin
      password: admin

Here are the security configuration classes. It's configured for form login. It allows anonymous access to /favicon.ico and requires a login for all other paths.

package example.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfiguration {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.formLogin(Customizer.withDefaults());
        http.authorizeRequests(authorize -> authorize
                .requestMatchers("/favicon.ico").permitAll()
                .anyRequest().authenticated()
        );
        return http.build();
    }
}

A controller.

package org.anonym.authorizationserver.conntroller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class HelloController {
    @GetMapping("/hello")
    @ResponseBody
    public String hello() {
        return "hello";
    }   
}

Analyse
org.springframework.security.web.savedrequest.HttpSessionRequestCache#saveRequest(HttpServletRequest, HttpServletResponse) method is the key to this problem. When an anonymous user accesses a protected resource, the backend will store the path to the protected resource in the RequestCache and instruct the browser to navigate to the /login path. Some request paths the RequestCache will not save are /favicon.ico requests, XHR requests, JSON requests, and file upload requests.
When the browser navigates to /login, it will automatically request /favicon.ico for a website icon that doesn't exist in the application. The application then forwards the request to the /error path. Since the /error path requires a login to access it, the /error path is stored in the RequestCache instead of the pre-login path. When the user logs in, the application fetches the saved /error path from the RequestCache and tells the browser to jump to it.

Improvement
The value of the Sec-Fetch-Dest header for active requests initiated by the browser is document, and the Sec-Fetch-Dest header for non-active requests is not document. Also, the dispatcherType value forwarded to the /error path is ERROR. The above two conditions can be used to determine whether the request path should be saved.
The change to HttpSessionRequestCache is in the public void saveRequest(HttpServletRequest, HttpServletResponse) method.

package org.springframework.security.web.savedrequest;
// other code...
public class HttpSessionRequestCache implements RequestCache {
        // other code...
	@Override
	public void saveRequest(HttpServletRequest request, HttpServletResponse response) {
                boolean activeRequest =  "document".equals(request.getHeader("Sec-Fetch-Dest"));
                boolean dispatchToError = DispatcherType.ERROR.equals(request.getDispatcherType());
		if ((!activeRequest && dispatchToError) || !this.requestMatcher.matches(request)) {
			if (this.logger.isTraceEnabled()) {
				this.logger
					.trace(LogMessage.format("Did not save request since it did not match [%s]", this.requestMatcher));
			}
			return;
		}
                // other code...
        }
        // other code...
}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions