Search code examples
spring-bootspring-securitykeycloak

spring boot keycloak integration test


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