Reputation: 12629
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
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