question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

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

See original GitHub issue

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

Issue Analytics

  • State:open
  • Created 2 years ago
  • Reactions:5
  • Comments:9

github_iconTop GitHub Comments

3reactions
xak2000commented, Aug 10, 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.

3reactions
on-deletecommented, Jan 6, 2022

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.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Upgrading from Spring 2.5 to 4.x made Spring Security not ...
I'm updating dependencies from a legacy spring project on 2.5.6 to 4.1.6. I may, when working, try to upgrade to lastest Spring 4...
Read more >
Spring CORS Configuration is ignored after spring-boot 2.5.5 ...
Hello. I have a project using spring-boot with spring-security. As I was trying to update my versions, my CORS configuration stopped working ......
Read more >
Spring Security without the WebSecurityConfigurerAdapter
In Spring Security 5.7.0-M2 we deprecated the WebSecurityConfigurerAdapter , as we encourage users to move towards a component-based security ...
Read more >
Spring Boot Security Auto-Configuration - Baeldung
The application will fail to start if it's missing. Also, notice that we need to use the PasswordEncoder to set the passwords when...
Read more >
TLS Https error after upgrading Spring boot 2.3.2 to 2.5.12 ...
Coding example for the question TLS Https error after upgrading Spring boot 2.3.2 to 2.5.12-Springboot.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found