Issue
I am using keycloak to authenticate my spring boot application as below:
@Configuration
public class CustomKeycloakSpringBootConfigResolver extends KeycloakSpringBootConfigResolver {
private final KeycloakDeployment keycloakDeployment;
CustomKeycloakSpringBootConfigResolver(KeycloakSpringBootProperties properties) {
keycloakDeployment = KeycloakDeploymentBuilder.build(properties);
}
@Override
public KeycloakDeployment resolve(HttpFacade.Request facade) {
return keycloakDeployment;
}
@KeycloakConfiguration
class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Autowired
void configureGlobal(AuthenticationManagerBuilder auth) {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
@Override
@Bean
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resources/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests()
.antMatchers("/account/**").hasRole("user")
.anyRequest().permitAll().and()
.csrf().disable();
}
@Bean
@Override
@ConditionalOnMissingBean(HttpSessionManager.class)
protected HttpSessionManager httpSessionManager() {
return new HttpSessionManager();
}
}
i need to write integration test using mockmvc only, which will test that whenever a secured resource is accessed, the authentication is triggered to keycloak and after a successful authentication the resource is returned.
can anyone suggest how to achieve that.
Solution
As already mentioned in this answer, I wrote a lib to ease unit tests with a KeycloakAuthenticationToken
in the SecurityContext
.
You can browse a few sample apps with unit tests from here: https://github.com/ch4mpy/spring-addons/tree/master/samples.
Please note all samples run against a Keycloak server and that using keycloak spring-boot adapter libs might not be the best option:
- not spring-boot 2.7+ complient (still extends WebSecurityConfigurerAdapter)
- not WebFlux complient
- very Keycloak adherent (you can hardly switch to another OIDC authorization server like Auth0, Microsoft Identity server, etc.)
KeycloakMessageServiceTest
:
@ExtendWith(SpringExtension.class)
@Import(MessageServiceTest.TestConfig.class)
class MessageServiceTest {
@Autowired
MessageService service;
@WithMockKeycloakAuth(authorities = "USER", claims = @OpenIdClaims(preferredUsername = "ch4mpy"))
void whenAuthenticatedWithoutAuthorizedPersonnelThenCanNotGetSecret() {
assertThrows(AccessDeniedException.class, () -> service.getSecret());
}
@Test()
@WithMockKeycloakAuth(authorities = "AUTHORIZED_PERSONNEL", claims = @OpenIdClaims(preferredUsername = "ch4mpy"))
void whenAuthenticatedWitAuthorizedPersonnelThenGetSecret() {
final String actual = service.getSecret();
assertEquals("Secret message", actual);
}
@Test
void whenNotAuthenticatedThenCanNotGetGreeting() {
assertThrows(Exception.class, () -> service.greet(null));
}
@Test()
@WithMockKeycloakAuth(authorities = "AUTHORIZED_PERSONNEL", claims = @OpenIdClaims(preferredUsername = "ch4mpy"))
void whenAuthenticatedThenGetGreeting() {
final String actual = service.greet((KeycloakAuthenticationToken) SecurityContextHolder.getContext().getAuthentication());
assertEquals("Hello ch4mpy! You are granted with [AUTHORIZED_PERSONNEL].", actual);
}
@TestConfiguration(proxyBeanMethods = false)
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Import({ MessageService.class })
static class TestConfig {
@Bean
GrantedAuthoritiesMapper authoritiesMapper() {
return new NullAuthoritiesMapper();
}
}
}
Controllers tests look like that:
@WebMvcTest(controllers = GreetingController.class)
class GreetingControllerAnnotatedTest {
private static final String GREETING = "Hello %s! You are granted with %s.";
@MockBean
MessageService messageService;
@MockBean
JwtDecoder jwtDecoder;
@Autowired
MockMvc api;
@BeforeEach
void setUp() {
when(messageService.greet(any())).thenAnswer(invocation -> {
final var auth = invocation.getArgument(0, Authentication.class);
return String.format(GREETING, auth.getName(), auth.getAuthorities());
});
}
// @formatter:off
@Test
@WithMockKeycloakAuth(
authorities = {"USER", "AUTHORIZED_PERSONNEL" },
claims = @OpenIdClaims(
sub = "42",
jti = "123-456-789",
nbf = "2020-11-18T20:38:00Z",
sessionState = "987-654-321",
email = "[email protected]",
emailVerified = true,
nickName = "Tonton-Pirate",
preferredUsername = "ch4mpy",
otherClaims = @Claims(jsonObjectClaims = @JsonObjectClaim(name = "foo", value = OTHER_CLAIMS))),
accessToken = @KeycloakAccessToken(
realmAccess = @KeycloakAccess(roles = { "TESTER" }),
authorization = @KeycloakAuthorization(permissions = @KeycloakPermission(rsid = "toto", rsname = "truc", scopes = "abracadabra")),
resourceAccess = {
@KeycloakResourceAccess(resourceId = "resourceA", access = @KeycloakAccess(roles = {"A_TESTER"})),
@KeycloakResourceAccess(resourceId = "resourceB", access = @KeycloakAccess(roles = {"B_TESTER"}))}))
// @formatter:on
void whenAuthenticatedWithKeycloakAuthenticationTokenThenCanGreet() throws Exception {
api
.perform(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")))
.andExpect(content().string(containsString("TESTER")))
.andExpect(content().string(containsString("A_TESTER")))
.andExpect(content().string(containsString("B_TESTER")));
}
@Test
@WithMockKeycloakAuth
void testAuthentication() throws Exception {
api.perform(get("/authentication")).andExpect(status().isOk()).andExpect(content().string("Hello user"));
}
@Test
@WithMockKeycloakAuth
void testPrincipal() throws Exception {
api.perform(get("/principal")).andExpect(status().isOk()).andExpect(content().string("Hello user"));
}
static final String OTHER_CLAIMS = "{\"bar\":\"bad\", \"nested\":{\"deep\":\"her\"}, \"arr\":[1,2,3]}";
}
Answered By - ch4mp
Answer Checked By - Cary Denson (JavaFixing Admin)