ch4mp
ch4mp

Reputation: 12629

Why do I get null `Authentication` as @Controller method parameter in `@WebMvcTest`?

The context: I created a test annotation @WithMockAuthentication to populate test security context with an Authentication instance, much like @WithMockUser does. The main difference being, in my case, the instance is a Mockito mock.

What I experience: As soon as I replace an actual instance with a mock, the Authentication instance provided as controller method parameter is null in annotated tests: in the WithSecurityContextFactory, if I replace:

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

with

    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;
    }

Then I get NPE in the controller tests at

    @RequestMapping("/method")
    @PreAuthorize("hasRole('ROLE_AUTHORIZED')")
    public ResponseEntity<String> securedMethod(Authentication auth) {
        // Here, auth is null if Authentication is a mock
        return ResponseEntity.ok(String.format("Hey %s, how are you?", auth.getName()));
    }

I've created a minimal sample to reproduce. Run the test to see the failure.

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...

[EDIT] This last statement is uselessly offensive and completely wrong as the answer is provided by Rob Winch, who is a major member of spring-security :/ My Bad

Upvotes: 1

Views: 2081

Answers (1)

Rob Winch
Rob Winch

Reputation: 21720

The argument for your SampleController is of type Authentication which is an instance of Principal and thus the ServletRequestMethodArgumentResolver will attempt to resolve the argument from HttpServletRequest.getUserPrincipal().

The mock that you are creating did not stub the Authentication.getPrincipal() method.

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;
}

For that reason, Authentication.getPrincipal() is null and thus SecurityContextHolderAwareRequestWrapper.getUserPrincipal() returns null. Why does it return null when the principal is null? I cannot be certain the original intention as the code was added before I was a team member. However, it makes sense in Spring Security's model. Authentication can represent both an authenticated user and credentials used for authenticating. The Javadoc of Authentication.getPrincipal() states (emphasis mine):

The identity of the principal being authenticated. In the case of an authentication request with username and password, this would be the username. Callers are expected to populate the principal for an authentication request.

The AuthenticationManager implementation will often return an Authentication containing richer information as the principal for use by the application. Many of the authentication providers will create a UserDetails object as the principal.

The null check is to ensure that the Authentication is indeed representing an authenticated user.

To fix it you must stub out the getPrincipal() method with something like when(auth.getPrincipal()).thenReturn("bogus");. The change can be seen below and in my pull request.

public Authentication bogousAuthentication(WithMockAuthentication annotation) {
    var auth = mock(Authentication.class);
    when(auth.getPrincipal()).thenReturn("bogus");
    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;
}

Upvotes: 2

Related Questions