Summary

Injected Authentication as @Controller method parameter is null in a @WebMvcTest when using a (Mockito) mocked authentication.

I tried with both an annotation and a request post processor.

Note that I have not the same problem in @WebFluxTest, using the same annotation or a WebTestClient Configurer with implementation very similar to above mentioned post-processor.

Actual / Expected Behavior

The mocked authentication from the TestSecurityContext is not injected when it should

Version

Versions pulled by boot 2.2.6.Release (same issue at least with 2.2.4 and 2.2.5)

Sample

You can clone https://github.com/ch4mpy/spring-addons which contains * the code for the @WithMockAuthentication annotation and its security context factory * the code for the MockAuthenticationRequestPostProcessor MockMvc request post-processor * sample of failing @WebMvcTest using both @WithMockAuthentication and MockAuthenticationRequestPostProcessor * sample of passing @WebFluxTest using @WithMockAuthentication * sample of passing @WebFluxTest using MockAuthenticationWebTestClientConfigurer

What I'd like to use:

    @Test
    @WithMockAuthentication(
            authType = OidcIdAuthenticationToken.class,
            name = "Ch4mpy",
            authorities = "ROLE_AUTHORIZED_PERSONNEL")
    public void greetCh4mpyWithAnnotation() throws Exception {
        api.get("/greet")
                .andExpect(content().string("Hello Ch4mpy! You are granted with [ROLE_AUTHORIZED_PERSONNEL]."));
    }

    @Test
    public void greetCh4mpyWithRequestPostProcessor() throws Exception {
        api.with(
                mockAuthentication(OidcIdAuthenticationToken.class).name("Ch4mpy")
                        .authorities("ROLE_AUTHORIZED_PERSONNEL"))
                .get("/greet")
                .andExpect(content().string("Hello Ch4mpy! You are granted with [ROLE_AUTHORIZED_PERSONNEL]."));
    }

Whith a controller like:

@RestController
public class GreetingController {
    private final MessageService<OidcIdAuthenticationToken> messageService;

    @Autowired
    public GreetingController(MessageService<OidcIdAuthenticationToken> messageService) {
        this.messageService = messageService;
    }

    @GetMapping("/greet")
    public String greet(OidcIdAuthenticationToken auth) {
        return messageService.greet(auth);
    }
}

Comment From: rwinch

Thanks for the report. Can you please reduce this to a minimal sample needed to reproduce the problem? The sample should likely contain less than 10 classes and not require any third party jars that Spring Security does not already use.

Comment From: ch4mpy

https://github.com/ch4mpy/issue-8228

@rwinch just run the tests, failure should demo the issue.

As demoed in the @Service test, TestSecurityContext is correctly populated. Maybe the issue is not related to the test framework but to the way Authentication is provided to the @Controller method, but I don't know where to search exactly...

Comment From: rwinch

It feels like this is a question that would be better suited to Stack Overflow. As mentioned in the guidelines for contributing, we prefer to use GitHub issues only for bugs and enhancements. Feel free to update this issue with a link to the re-posted question (so that other people can find it) or produce a minimal sample that demonstrates the issue is in Spring vs the sample if you feel this is a genuine bug.

Comment From: ch4mpy

@rwinch to me this is a "genuine" bug, and provided sample shows it (I just found a way to make the sample even smaller and expressive, so please pull and visit again failing test and WithMockAuthentication.Factory).

This passes

    @Test
    @WithMockAuthentication(useMock = false, name = "Rob", authorities = { "ROLE_USER", "ROLE_AUTHORIZED" })
    void robCanAccessToMethodWithAuthenticationStub() throws Exception {
        mockMvc.perform(get("/method")).andExpect(content().string("Hey Rob, how are you?"));
    }

This fails

    @Test
    @WithMockAuthentication(useMock = true, name = "Rob", authorities = { "ROLE_USER", "ROLE_AUTHORIZED" })
    void robCanAccessToMethodWithAuthenticationMock() throws Exception {
        mockMvc.perform(get("/method")).andExpect(content().string("Hey Rob, how are you?"));
    }

The difference between the two being

public Authentication bogousAuthentication(WithMockAuthentication annotation) {
    var auth = mock(Authentication.class);
    when(auth.getName()).thenReturn(annotation.name());
    when(auth.getAuthorities()).thenReturn((Collection) Stream.of(annotation.authorities()).map(SimpleGrantedAuthority::new).collect(Collectors.toSet()));
    when(auth.isAuthenticated()).thenReturn(true);
    return auth;
}

public Authentication workingAuthentication(WithMockAuthentication annotation) {
    return new TestAuthentication(
        annotation.name(), Stream.of(annotation.authorities()).map(SimpleGrantedAuthority::new).collect(Collectors.toSet()));
}

Comment From: ch4mpy

https://stackoverflow.com/questions/60940334/why-do-i-get-null-authentication-as-controller-method-parameter-in-webmvcte

Comment From: rwinch

I've answered on StackOverflow https://stackoverflow.com/a/60942496/618087

For what it is worth, I don't feel like your statement on StackOverflow (emphasis mine) is fair:

I'm pretty sure I face a bug. Enough to create an issue in spring-security project, but it seems that Spring team team has no time to investigate...

I closed this issue when you did not produce a minimal sample as I requested. This guideline is expected on StackOverflow and I think it is reasonable to expect the same in issue trackers.

Comment From: ch4mpy

@rwinch please accept my apologies if you fell offended. I understuntand you have a lot of work AND issues to investigate. At the moment you closed the issue, I had created a specific project (with 9 classes) and a dedicated Github repo. I really felt I had produced the requested minimal project ... until figured out later I could remove one more class.

Comment From: rwinch

@ch4mpy Thanks for the apology. I just didn't feel like the statement was fair or necessary for solving your issue.

At the moment you closed the issue, I had created a specific project (with 9 classes) and a dedicated Github repo.

Perhaps this was a bit of a real world "race condition". At the time I closed it, I did not see the simplified sample posted to the issue.

In any case, I'm glad that we were able to get your question answered. Thank you for producing the simplified sample. It made a huge difference.

please accept my apologies if you fell offended.

The apology is warmly accepted. Let's put it behind us :smile: Looking forward to (virtually) seeing you around soon!

Comment From: ch4mpy

@rwinch by the way, have you noticed how convenient this @WithMockAuthentication is?

It solves most use cases missing in spring-security-test, specifically regarding OAuth2: testing of - secured @Services - Authentication impl you don't have hands on such as KeycloakAuthenticationToken - works both for webmvc and webflux

Sample taken from https://github.com/ch4mpy/spring-addons/tree/master/spring-security-oauth2-test-webmvc-addons/src/test/java/com/c4_soft/springaddons/tests/webmvc

    @Test
    @WithMockAuthentication(authType = JwtAuthenticationToken.class, authorities = "ROLE_AUTHORIZED_PERSONNEL")
    public void secretWithScopeAuthorizedPersonnelAuthority() {
        assertThat(messageService.getSecret()).isEqualTo("Secret message");
    }

Comment From: rwinch

Can you elaborate on why you care about the type? I believe in most cases the type of authentication doesn't matter. When it does matter, I suspect there are likely custom properties which cannot be dynamically added to an annotation.

Comment From: ch4mpy

For use cases such as this one https://stackoverflow.com/questions/49144953/mocking-a-keycloak-token-for-testing-a-spring-controller

If you need to further configure authentication mock, you can then access it from test security context like I do here

@Test
@WithMockAuthentication(authType = JwtAuthenticationToken.class, name = "ch4mpy", authorities = "ROLE_USER")
public void greetWithMockAuthentication() {
    final var auth = (JwtAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
    // more conf here

    assertThat(messageService.greet(auth)).isEqualTo("Hello ch4mpy! You are granted with [ROLE_USER].");
}

Comment From: ch4mpy

@rwinch off course this additional configuration is easier using a MockMvc post-processor or WebTestClient configurer, provided that it's a @Controller under test (and not a @Service)

@Test
public void securedRouteWithoutAuthorizedPersonnelIsForbidden() throws Exception {
    api.with(mockAuthentication(KeycloakAuthenticationToken.class)
            .configure(auth -> when(auth.getPrincipal()).thenReturn("foo")))
        .get("/secured-route")
        .andExpect(status().isForbidden());
}

Comment From: rwinch

I'd suggest using with(authentication(authentication)) as illustrated in the reference. This is more powerful since any object (mock, stub, or actual) can be passed into it.

KeycloakAuthenticationToken authentication = mock(KeycloakAuthenticationToken.class);
// ...
mvc
    .perform(get("/").with(authentication(authentication)))

Comment From: ch4mpy

True. Just one more line and slightly less fluent "flow" API.

But, as mentioned two comments ago, main drawback is limitation to @Controller testing when annotation enables to unit-test any secured @Component such as @Service & @Repo

Comment From: rwinch

I don't see an advantage to the annotation if you are modifying it directly, just set it directly too.

SecurityContextHolder.getContext().setAuthentication(mock)

Comment From: ch4mpy

Tests code homogeneity. Most frequently, the annotation is enough. When I need further mock configuration than name or authorities, then I access it.

Comment From: rwinch

If the annotation is enough, why do you need it to be the specific type?

Comment From: ch4mpy

Because sometimes it is accessed as

@GetMapping("foo")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<String> getFoo(KeycloakAuthentication auth) {
...
}

or

@PreAuthorize("isAuthenticated()")
public String greet() {
    var auth = (KeycloakAuthentication) SecurityContextHolder.getContext().getAuthentication();
    ...
}

Comment From: rwinch

Why would you access it that way if you are not setting custom properties? Just use Authentication.

Comment From: ch4mpy

Original reason why only some tests need to do advanced configuration on the mock.

In "real world", I'd wrap authentication extraction:

public static KeycloakAuthenticationToken authentication() {
    return (KeycloakAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
}

and client code would be like

@PreAuthorize("isAuthenticated()")
public String greetSimple() {
    // simple, no need to further configure authentication mock in tests
    return String.format("Hello, %s!", authentication().getName());
}

@PreAuthorize("isAuthenticated()")
public String greetComplex() {
    // more complex, further configure authentication mock is required in tests
    var oidcKeycloakAccount = authentication().getAccount();
    return String.format("Hello, %s!, you are granted with %s.", oidcKeycloakAccount.getPrincipal().getName(), oidcKeycloakAccount.getRoles());
}

P.S. Please note I'm not requesting mocked authentication in spring-framework. I have it in a lib of mine I publish on maven-central and as so can use in any project I want since you explained me how to work around a Spring framework "expectation" about getPrincipal() having to be non nul for Authentication to be resolved as controller method argument.

I find it useful enough to maintain it, I'd understand you don't.