Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fails to work with Spring Security after upgrading to Spring Boot 2.5 #707

Open
ieu opened this issue Nov 10, 2021 · 9 comments · May be fixed by #728 or #802
Open

Fails to work with Spring Security after upgrading to Spring Boot 2.5 #707

ieu opened this issue Nov 10, 2021 · 9 comments · May be fixed by #728 or #802
Labels

Comments

@ieu
Copy link

ieu commented Nov 10, 2021

problem-spring-web fails to work with Spring Security after upgrading to Spring Boot 2.5

Description

After introducing spring-security as dependency:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

problem-spring-web will no longer work.

Expected Behavior

Work as expected.

For example, normally with problem-spring-web, a validation failure message looks like below:

{
    "title": "Bad Request",
    "status": 400,
    "detail": "Required request parameter 'message' for method parameter type String is not present"
}

Actual Behavior

  1. Authentication failure will cause empty response with status 200.

  2. Application will simply respond as Spring Boot's default beheavior will do on other expections thrown:

{
    "timestamp": "2021-11-10T00:00:00.000+00:00",
    "status": 400,
    "error": "Bad Request",
    "path": "/echo"
}

Possible Fix

I personally have no idea about this

Steps to Reproduce

  1. Create a new project using Spring Initializr
  2. Include problem-spring-web as dependency:
<dependency>
	<groupId>org.zalando</groupId>
	<artifactId>problem-spring-web-starter</artifactId>
	<version>0.27.0</version>
</dependency>
  1. Prepare a ValueObject
@Data(staticConstructor = "of")
@AllArgsConstructor
public class EchoMessage {
    String message;
}
  1. Prepare a RestController
@RestController
public class EchoController {
    @GetMapping(value = {"/echo", "/authorized/echo"})
    public EchoMessage echo(@RequestParam @NotEmpty String message) {
        return EchoMessage.of(message);
    }
}
  1. Configure Spring security
@Configuration
public class SecurityConfiguration {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // @formatter:off
        return http
                .authorizeRequests()
                    .antMatchers("/authorized/**")
                        .authenticated()
                    .anyRequest()
                        .anonymous()
                .and()
                .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .csrf()
                    .disable()
                .httpBasic()
                    .disable()
                .build();
        // @formatter:on
    }
}
  1. Write tests
@WebMvcTest
@ContextConfiguration(classes = SecurityTestConfiguration.class)
public class EchoControllerTest {
    private final MockMvc mockMvc;

    @Autowired
    public EchoControllerTest(MockMvc mockMvc) {
        this.mockMvc = mockMvc;
    }

    @Test
    public void testEcho() throws Exception {
        this.mockMvc.perform(get("/echo?message={message}", "Hello"))
                .andExpect(status().is(200))
                .andExpect(jsonPath("$.message", is("Hello")));
    }

    @Test
    public void testEchoWithoutMessage() throws Exception {
        this.mockMvc.perform(get("/echo"))
                .andExpect(header().string("Content-Type", "application/problem+json"))
                .andExpect(status().is(400))
                .andExpect(jsonPath("$.title", is("Bad Request")))
                .andExpect(jsonPath("$.status", is(400)));
    }

    @Test
    public void testEchoPost() throws Exception {
        this.mockMvc.perform(post("/echo?message={message}", "Hello"))
                .andExpect(header().string("Content-Type", "application/problem+json"))
                .andExpect(status().is(405))
                .andExpect(jsonPath("$.title", is("Method Not Allowed")))
                .andExpect(jsonPath("$.status", is(405)));
    }

    @Test
    public void testAuthorizedEcho() throws Exception {
        this.mockMvc.perform(get("/authorized/echo?message={message}", "Hello"))
                .andExpect(header().string("Content-Type", "application/problem+json"))
                .andExpect(status().is(401))
                .andExpect(jsonPath("$.title", is("Unauthorized")))
                .andExpect(jsonPath("$.status", is(401)));
    }
}

All tests fails except for testEcho.

I also tried simulating requests to /echo using curl, and got the same result.

Context

I find that if I register my own AdivceTrait bean in SecurityConfiguration, problem-spring-web will work again:

@Configuration
public class SecurityConfiguration {
      @ControllerAdvice
      public static class SecurityExceptionHandling implements ProblemHandling, SecurityAdviceTrait {}

      @Bean
      public AdviceTrait securityExceptionHandling() {
          return new SecurityExceptionHandling();
      }

    // Other beans
}

Your Environment

  • Spring Boot version: 2.5.6
  • problem-spring-web version: 0.27.0
  • Java version: Adopt OpenJDK 11

AND here is the runnable demo project attached: demo.zip

@ieu ieu added the Bug label Nov 10, 2021
@ieu
Copy link
Author

ieu commented Nov 11, 2021

After another few hours of digging into this problem, I think it may be caused by issue of dependencies between beans. For some reason I haven't figured out yet, Instantiation priority of HandlerExceptionResolver required by SecurityProblemSupport is evelated and HandlerExceptionResolver is initialized before SecurityExceptionHandling, making SecurityExceptionHandling failing to be discovered by HandlerExceptionResolver.

The best way to fix it I've found is to replace return type of securityExceptionHandling bean with concrete type SecurityExceptionHandling. Doing so, Spring will be able to obtain annotations through bean definition before actually instantiate the bean and we don't have to care about order of bean creation.

@HendrikJanssen
Copy link

Any status update on this? This seems like a complete showstopper

@aafwu00 aafwu00 linked a pull request Dec 30, 2021 that will close this issue
6 tasks
@on-delete
Copy link

We also depend on this bugfix. We have found a workaround for spring-problem-web v0.26.2 to just create an ExceptionHandling with ControllerAdvice ourselfs. Would be great if this bugfix could be released soon.

@piefel
Copy link

piefel commented Mar 15, 2022

I suspect this is actually another manifestation of the same issue as #696

@agorgl
Copy link

agorgl commented May 22, 2022

Vanilla spring boot project with spring-boot-starter-web, spring-boot-starter-security and problem-spring-web-starter, produces same weird results :(

@agorgl
Copy link

agorgl commented May 23, 2022

Could the recent deprecation of WebSecurityConfigurerAdapter in Spring Security (https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter) also affect this somehow?

@awattez
Copy link

awattez commented May 30, 2022

Vanilla spring boot project with spring-boot-starter-web, spring-boot-starter-security :
Spring Initializer Characteristics
with problem-spring-web-starter, produces same weird results :(


Environment :

  • Spring Boot version: 2.7.0
  • problem-spring-web version: 0.27.0
  • Java version: Java(TM) SE Runtime Environment (build 17.0.2+8-LTS-86)

Authentication failure will cause empty response with status 200.


Same problem with problem-spring-web-starter version 0.28.0-RC.0 :

<dependency>
	<groupId>org.zalando</groupId>
	<artifactId>problem-spring-web-starter</artifactId>
	<version>0.28.0-RC.0</version>
</dependency>

My workaround or fix is to add a @ControllerAdvice class :

package com.example.demo;

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.zalando.problem.spring.web.advice.ProblemHandling;
import org.zalando.problem.spring.web.advice.security.SecurityAdviceTrait;

@ControllerAdvice
public class ProblemSecurityAdvice implements ProblemHandling, SecurityAdviceTrait {
}

We find the expected behavior. Authentication failure will cause JSON response with status 401.

{
    "title": "Unauthorized",
    "status": 401,
    "detail": "Full authentication is required to access this resource"
}

I think that the comment of @ieu is good here, in any case we are closed to the problem, the instantiation of this bean is problematic even if it represents exactly the same thing as the class described above. On the other hand, there is most certainly a problem with the order of the beans to be instantiated.

ProblemSecurityAutoConfiguration.java

@awattez
Copy link

awattez commented May 30, 2022

Why this PR #728 is not merged if it fixes the problem ?

@xak2000
Copy link

xak2000 commented Aug 9, 2022

I think the root of the problem is introduced in this PR #413.

Before this change (in version 0.25.2 and below) there was SpringSecurityExceptionHandling bean that was directly annotated with @ControllerAdvice and was imported using spring.factories.

This way spring has registered it as a bean and also registered it as a @ControllerAdvice.

The mechanism was changed in mentioned PR (0.26.0+).

Now, it's SecurityExceptionHandling class that is also annotated with @ControllerAdvice, but the subtle difference is that this class is not a bean (it's marked as @Component through annotation inheritance, but it's not subject to package scan, so this mark is ignored). There is another ProblemSecurityAutoConfiguration class that registers SecurityExceptionHandling as a bean. So far so good.

But here is the problem. The bean type is AdviceTrait, that is not directly annotated with @ControllerAdvice (and any parent of this type is also not annotated). This makes Spring to register it as a bean, but not as a @ControllerAdvice. So, this is just a bean that sits in the container and does nothing (no-one calls it's methods for any reason).

If we change the return type of bean-factory method to SecurityExceptionHandling, the problem will be fixed. Spring will find @ControllerAdvice annotation on the bean type and will register this advice, so all @ExceptionHandler methods (including the method, that handles Throwable) will be registered.

Basically, the code should be changed to this:

    @Bean
    @ConditionalOnMissingBean(AdviceTrait.class)
    public SecurityExceptionHandling securityExceptionHandling() {
        return new SecurityExceptionHandling();
    }

The PR #413 contains some tests, but they are not fully cover the case, e.g.:

    @Test
    void shouldConfigureExceptionHandling(
            @Autowired final AdviceTrait trait) {
        assertThat(trait).isExactlyInstanceOf(SecurityExceptionHandling.class);
    }

This test checks that bean is present in the context and that it's the correct implementation, but it doesn't check that @ControllerAdvice is registered in Spring MVC (and it isn't!).

Also I found another test that should catch this problem, but it doesn't. It doesn't catch the problem because test class and advice both live in the same package, that falls under component auto-scan. I confirmed that by removing the code that registers SecurityExceptionHandling bean from ProblemSecurityAutoConfiguration and the advice still works. The solution is to move all tests to the package, that will not catch main classes by auto-scan (e.g. into a sibling package). That would be a good thing to do as we are trying to test autoconfigurations here, but when they are subject to auto-scan, our test are flawed.

As soon as I moved test classes into a sibling package the test started to fail, showing that SecurityExceptionHandling doesn't work. It's good, as now the test really does its job. And to make the test pass we could, e.g. change the return type of @Bean method to SecurityExceptionHandling instead of AdviceTrait, as I mentioned early.

It's interesting, that there is no problem with ExceptionHandling, that should be registered as @ControllerAdvice when there is no spring-security in the context. It is registered fine although the return type is also AdviceTrait here. I suppose it's something with bean registration ordering and controller advice registration ordering. In some cases Spring can't determine the exact type of the bean and looks on it's declared type when tries to find @ControllerAdvice annotation.

@xak2000 xak2000 linked a pull request Aug 10, 2022 that will close this issue
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
7 participants