diff --git a/spring-boot-modules/spring-boot-keycloak/pom.xml b/spring-boot-modules/spring-boot-keycloak/pom.xml index ebba1f7f67..250ddb73b4 100644 --- a/spring-boot-modules/spring-boot-keycloak/pom.xml +++ b/spring-boot-modules/spring-boot-keycloak/pom.xml @@ -107,6 +107,7 @@ 4.0.0 1.6.3 3.1.0 + 17 \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-keycloak/src/main/java/com/baeldung/keycloak/SecurityConfig.java b/spring-boot-modules/spring-boot-keycloak/src/main/java/com/baeldung/keycloak/SecurityConfig.java index 5b9ce9677a..2bf3c12397 100644 --- a/spring-boot-modules/spring-boot-keycloak/src/main/java/com/baeldung/keycloak/SecurityConfig.java +++ b/spring-boot-modules/spring-boot-keycloak/src/main/java/com/baeldung/keycloak/SecurityConfig.java @@ -1,15 +1,21 @@ package com.baeldung.keycloak; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.Order; -import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.core.session.SessionRegistryImpl; +import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; +import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy; import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; @@ -19,6 +25,10 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @EnableWebSecurity class SecurityConfig { + private static final String GROUPS = "groups"; + private static final String REALM_ACCESS_CLAIM = "realm_access"; + private static final String ROLES_CLAIM = "roles"; + private final KeycloakLogoutHandler keycloakLogoutHandler; SecurityConfig(KeycloakLogoutHandler keycloakLogoutHandler) { @@ -30,38 +40,63 @@ class SecurityConfig { return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl()); } - @Order(1) + @Bean - public SecurityFilterChain clientFilterChain(HttpSecurity http) throws Exception { - http.authorizeRequests() + public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception { + http.authorizeHttpRequests(auth -> auth + .requestMatchers(new AntPathRequestMatcher("/customers*")) + .hasRole("user") .requestMatchers(new AntPathRequestMatcher("/")) .permitAll() .anyRequest() - .authenticated(); - http.oauth2Login() - .and() - .logout() - .addLogoutHandler(keycloakLogoutHandler) - .logoutSuccessUrl("/"); - return http.build(); - } - - @Order(2) - @Bean - public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception { - http.authorizeRequests() - .requestMatchers(new AntPathRequestMatcher("/customers*")) - .hasRole("USER") - .anyRequest() - .authenticated(); + .authenticated()); http.oauth2ResourceServer((oauth2) -> oauth2 .jwt(Customizer.withDefaults())); + http.oauth2Login(Customizer.withDefaults()) + .logout(logout -> logout.addLogoutHandler(keycloakLogoutHandler).logoutSuccessUrl("/")); return http.build(); } + @Bean - public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception { - return http.getSharedObject(AuthenticationManagerBuilder.class) - .build(); + public GrantedAuthoritiesMapper userAuthoritiesMapperForKeycloak() { + return authorities -> { + Set mappedAuthorities = new HashSet<>(); + var authority = authorities.iterator().next(); + boolean isOidc = authority instanceof OidcUserAuthority; + + if (isOidc) { + var oidcUserAuthority = (OidcUserAuthority) authority; + var userInfo = oidcUserAuthority.getUserInfo(); + + // Tokens can be configured to return roles under + // Groups or REALM ACCESS hence have to check both + if (userInfo.hasClaim(REALM_ACCESS_CLAIM)) { + var realmAccess = userInfo.getClaimAsMap(REALM_ACCESS_CLAIM); + var roles = (Collection) realmAccess.get(ROLES_CLAIM); + mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles)); + } else if (userInfo.hasClaim(GROUPS)) { + Collection roles = (Collection) userInfo.getClaim( + GROUPS); + mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles)); + } + } else { + var oauth2UserAuthority = (OAuth2UserAuthority) authority; + Map userAttributes = oauth2UserAuthority.getAttributes(); + + if (userAttributes.containsKey(REALM_ACCESS_CLAIM)) { + Map realmAccess = (Map) userAttributes.get( + REALM_ACCESS_CLAIM); + Collection roles = (Collection) realmAccess.get(ROLES_CLAIM); + mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles)); + } + } + return mappedAuthorities; + }; + } + + Collection generateAuthoritiesFromClaim(Collection roles) { + return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).collect( + Collectors.toList()); } } diff --git a/spring-boot-modules/spring-boot-keycloak/src/test/java/com/baeldung/keycloaksoap/KeycloakSoapLiveTest.java b/spring-boot-modules/spring-boot-keycloak/src/test/java/com/baeldung/keycloaksoap/KeycloakSoapLiveTest.java index 508061396f..8851d706c0 100644 --- a/spring-boot-modules/spring-boot-keycloak/src/test/java/com/baeldung/keycloaksoap/KeycloakSoapLiveTest.java +++ b/spring-boot-modules/spring-boot-keycloak/src/test/java/com/baeldung/keycloaksoap/KeycloakSoapLiveTest.java @@ -73,7 +73,7 @@ class KeycloakSoapLiveTest { ResponseEntity responseEntity = restTemplate.postForEntity("http://localhost:" + port + "/ws/api/v1/", request, String.class); assertThat(responseEntity).isNotNull(); - assertThat(responseEntity.getStatusCodeValue()).isEqualTo(HttpStatus.OK.value()); + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK.value()); assertThat(responseEntity.getBody()).isNotBlank(); assertThat(responseEntity.getBody()).containsIgnoringCase(":id>1 request = new HttpEntity<>(Utility.getGetProductDetailsRequest(), headers); ResponseEntity responseEntity = restTemplate.postForEntity("http://localhost:" + port + "/ws/api/v1/", request, String.class); assertThat(responseEntity).isNotNull(); - assertThat(responseEntity.getStatusCodeValue()).isEqualTo(HttpStatus.UNAUTHORIZED.value()); + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED.value()); assertThat(responseEntity.getBody()).isBlank(); } @@ -110,7 +110,7 @@ class KeycloakSoapLiveTest { ResponseEntity responseEntity = restTemplate.postForEntity("http://localhost:" + port + "/ws/api/v1/", request, String.class); assertThat(responseEntity).isNotNull(); - assertThat(responseEntity.getStatusCodeValue()).isEqualTo(HttpStatus.OK.value()); + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK.value()); assertThat(responseEntity.getBody()).isNotBlank(); assertThat(responseEntity.getBody()).containsIgnoringCase("Deleted the product with the id"); } @@ -130,7 +130,7 @@ class KeycloakSoapLiveTest { ResponseEntity responseEntity = restTemplate.postForEntity("http://localhost:" + port + "/ws/api/v1/", request, String.class); assertThat(responseEntity).isNotNull(); - assertThat(responseEntity.getStatusCodeValue()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value()); + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value()); assertThat(responseEntity.getBody()).isNotBlank(); assertThat(responseEntity.getBody()).containsIgnoringCase("Access is denied"); }