Reputation: 1701
I want to write unit tests for my spring controller. I'm using keycloak's openid flow to secure my endpoints.
In my tests I'm using the @WithMockUser
annotation to mock an authenticated user. My problem is that I'm reading the userId from the token of the principal. My unit test now fails because the userId
I read from the token is null;
if (principal instanceof KeycloakAuthenticationToken) {
KeycloakAuthenticationToken authenticationToken = (KeycloakAuthenticationToken) principal;
SimpleKeycloakAccount account = (SimpleKeycloakAccount) authenticationToken.getDetails();
RefreshableKeycloakSecurityContext keycloakSecurityContext = account.getKeycloakSecurityContext();
AccessToken token = keycloakSecurityContext.getToken();
Map<String, Object> otherClaims = token.getOtherClaims();
userId = otherClaims.get("userId").toString();
}
Is there anything to easily mock the KeycloakAuthenticationToken
?
Upvotes: 22
Views: 27791
Reputation: 33
I don't like to add extra dependency, a specially when it related only for test case scenario. Also adding dependency in some project is a big process with security checks and needs to be approved by many Managers, seniors etc. So this is my solution that allows to mock Keycloak security context without instance of keycloak and other extra dependecies. This is copied form my project so adjustments are required. Hope it help.
@Test
void shouldFooOnProtectedEndpoint() throws Exception {
//given
AccessToken token = new AccessToken();
// by username i was differentiate is it allowed
token.setPreferredUsername(SUBMITTER_USERNAME);
KeycloakSecurityContext keycloakSecurityContext = mock(KeycloakSecurityContext.class);
given(keycloakSecurityContext.getToken()).willReturn(token);
KeycloakPrincipal principal = mock(KeycloakPrincipal.class);
given(principal.getKeycloakSecurityContext()).willReturn(keycloakSecurityContext);
Authentication auth = mock(Authentication.class);
given(auth.getPrincipal()).willReturn(principal);
SecurityContextHolder.getContext().setAuthentication(auth);
... test logic
}
Upvotes: 0
Reputation: 12629
@WithmockUser
configures the security-context with a UsernamePasswordAuthenticationToken
. This can be just fine for most use-cases but when your app relies on another Authentication implementation (like your code does), you have to build or mock an instance of the right type and put it in the test security-context: SecurityContextHolder.getContext().setAuthentication(authentication);
Of course, you'll soon want to automate this, building your own annotation or RequestPostProcessor
... or ...
take one "off the shelf", like in this lib of mine, which is available from maven-central:
<dependency>
<!-- just enough for @WithMockKeycloackAuth -->
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-security-oauth2-test-addons</artifactId>
<version>3.0.1</version>
<scope>test</scope>
</dependency>
<dependency>
<!-- required only for WebMvc "fluent" API -->
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-security-oauth2-test-webmvc-addons</artifactId>
<version>3.0.1</version>
<scope>test</scope>
</dependency>
You can use it either with @WithMockKeycloackAuth
annotations:
@RunWith(SpringRunner.class)
@WebMvcTest(GreetingController.class)
@ContextConfiguration(classes = GreetingApp.class)
@ComponentScan(basePackageClasses = { KeycloakSecurityComponents.class, KeycloakSpringBootConfigResolver.class })
public class GreetingControllerTests extends ServletUnitTestingSupport {
@MockBean
MessageService messageService;
@Test
@WithMockKeycloackAuth("TESTER")
public void whenUserIsNotGrantedWithAuthorizedPersonelThenSecretRouteIsNotAccessible() throws Exception {
mockMvc().get("/secured-route").andExpect(status().isForbidden());
}
@Test
@WithMockKeycloackAuth("AUTHORIZED_PERSONNEL")
public void whenUserIsGrantedWithAuthorizedPersonelThenSecretRouteIsAccessible() throws Exception {
mockMvc().get("/secured-route").andExpect(content().string(is("secret route")));
}
@Test
@WithMockKeycloakAuth(
authorities = { "USER", "AUTHORIZED_PERSONNEL" },
claims = @OpenIdClaims(
sub = "42",
email = "[email protected]",
emailVerified = true,
nickName = "Tonton-Pirate",
preferredUsername = "ch4mpy",
otherClaims = @Claims(stringClaims = @StringClaim(name = "foo", value = "bar"))))
public void whenAuthenticatedWithKeycloakAuthenticationTokenThenCanGreet() throws Exception {
mockMvc().get("/greet")
.andExpect(status().isOk())
.andExpect(content().string(startsWith("Hello ch4mpy! You are granted with ")))
.andExpect(content().string(containsString("AUTHORIZED_PERSONNEL")))
.andExpect(content().string(containsString("USER")));
}
Or MockMvc fluent API (RequestPostProcessor):
@RunWith(SpringRunner.class)
@WebMvcTest(GreetingController.class)
@ContextConfiguration(classes = GreetingApp.class)
@ComponentScan(basePackageClasses = { KeycloakSecurityComponents.class, KeycloakSpringBootConfigResolver.class })
public class GreetingControllerTest extends ServletKeycloakAuthUnitTestingSupport {
@MockBean
MessageService messageService;
@Test
public void whenUserIsNotGrantedWithAuthorizedPersonelThenSecretMethodIsNotAccessible() throws Exception {
mockMvc().with(authentication().roles("TESTER")).get("/secured-method").andExpect(status().isForbidden());
}
@Test
public void whenUserIsGrantedWithAuthorizedPersonelThenSecretMethodIsAccessible() throws Exception {
mockMvc().with(authentication().roles("AUTHORIZED_PERSONNEL")).get("/secured-method")
.andExpect(content().string(is("secret method")));
}
}
Upvotes: 19