diff --git a/src/main/java/de/ozgcloud/admin/security/SecurityConfiguration.java b/src/main/java/de/ozgcloud/admin/security/SecurityConfiguration.java index 1318b9030d6829dd74f5c8cae32076ff78ff2696..4a87241bbec518fea5af2bbbd95f25c1aa56a39d 100644 --- a/src/main/java/de/ozgcloud/admin/security/SecurityConfiguration.java +++ b/src/main/java/de/ozgcloud/admin/security/SecurityConfiguration.java @@ -19,10 +19,13 @@ */ package de.ozgcloud.admin.security; +import static java.util.stream.Collectors.*; + import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -32,6 +35,8 @@ import org.springframework.security.config.annotation.method.configuration.Enabl import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.core.oidc.StandardClaimNames; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; @@ -53,6 +58,8 @@ public class SecurityConfiguration { static final String RESOURCE_ACCESS_KEY = "resource_access"; + static final String SIMPLE_GRANT_AUTHORITY_PREFIX = "ROLE_"; + static final String ROLES_KEY = "roles"; @Bean @@ -81,11 +88,23 @@ public class SecurityConfiguration { @Bean JwtAuthenticationConverter jwtAuthenticationConverter() { var jwtConverter = new JwtAuthenticationConverter(); - jwtConverter.setJwtGrantedAuthoritiesConverter(jwt -> List.of(() -> "ROLE_USER")); + jwtConverter.setJwtGrantedAuthoritiesConverter( + this::convertJwtToGrantedAuthorities); jwtConverter.setPrincipalClaimName(StandardClaimNames.PREFERRED_USERNAME); return jwtConverter; } + Set<GrantedAuthority> convertJwtToGrantedAuthorities(Jwt jwt) { + return getKeycloakRolesFromJwt(jwt) + .stream() + .map(this::mapRoleStringToGrantedAuthority) + .collect(toSet()); + } + + private GrantedAuthority mapRoleStringToGrantedAuthority(String role) { + return new SimpleGrantedAuthority(SIMPLE_GRANT_AUTHORITY_PREFIX + role); + } + List<String> getKeycloakRolesFromJwt(Jwt jwt) { return Optional.ofNullable(jwt.getClaimAsMap(RESOURCE_ACCESS_KEY)) .flatMap(resourceAccessMap -> getMap(resourceAccessMap, oAuth2Properties.getResource())) diff --git a/src/test/java/de/ozgcloud/admin/security/SecurityConfigurationTest.java b/src/test/java/de/ozgcloud/admin/security/SecurityConfigurationTest.java index 0707f7712ad10737b023a7709aeac1565fd1bc02..5fa89431acaa6676227f61a25c6e6840bc713952 100644 --- a/src/test/java/de/ozgcloud/admin/security/SecurityConfigurationTest.java +++ b/src/test/java/de/ozgcloud/admin/security/SecurityConfigurationTest.java @@ -21,6 +21,7 @@ */ package de.ozgcloud.admin.security; +import static de.ozgcloud.admin.security.JwtTestFactory.*; import static de.ozgcloud.admin.security.SecurityConfiguration.*; import static java.util.Collections.*; import static org.assertj.core.api.Assertions.*; @@ -28,6 +29,7 @@ import static org.mockito.Mockito.*; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; @@ -40,8 +42,13 @@ import org.junit.jupiter.params.provider.MethodSource; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Spy; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.core.oidc.StandardClaimNames; import org.springframework.security.oauth2.jwt.Jwt; +import com.thedeanda.lorem.LoremIpsum; + import de.ozgcloud.admin.environment.OAuth2Properties; class SecurityConfigurationTest { @@ -49,12 +56,86 @@ class SecurityConfigurationTest { @Spy @InjectMocks private SecurityConfiguration securityConfiguration; - @Mock - private AdminAuthenticationEntryPoint authenticationEntryPoint; @Mock private OAuth2Properties oAuth2Properties; + @DisplayName("jwt authentication converter") + @Nested + class TestJwtAuthenticationConverter { + + private final String roleString = "ROLE_Test"; + + @BeforeEach + void mock() { + doReturn(Set.of(new SimpleGrantedAuthority(roleString))).when(securityConfiguration).convertJwtToGrantedAuthorities(any()); + } + + @DisplayName("should use preferred_username") + @Test + void shouldUsePreferredUsername() { + var preferredName = LoremIpsum.getInstance().getName(); + var jwtWithPreferredName = JwtTestFactory.createBuilder() + .claim(StandardClaimNames.PREFERRED_USERNAME, preferredName) + .build(); + + var jwtAuthenticationConverter = securityConfiguration.jwtAuthenticationConverter(); + + var abstractAuthenticationToken = jwtAuthenticationConverter.convert(jwtWithPreferredName); + assertThat(abstractAuthenticationToken.getName()).isEqualTo(preferredName); + } + + @DisplayName("should use granted authorities converter") + @Test + void shouldUseGrantedAuthoritiesConverter() { + var jwtWithRoles = JwtTestFactory.create(); + + var jwtAuthenticationConverter = securityConfiguration.jwtAuthenticationConverter(); + + var abstractAuthenticationToken = jwtAuthenticationConverter.convert(jwtWithRoles); + var securityRoleStrings = abstractAuthenticationToken.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList(); + + assertThat(securityRoleStrings).isEqualTo(List.of(roleString)); + } + } + + @DisplayName("convert jwt to granted authorities") + @Nested + class TestConvertJwtToGrantedAuthorities { + + private List<String> expectedSecurityRoleStrings; + + @BeforeEach + void mock() { + var keycloakRoles = List.of(ROLE_1, JwtTestFactory.ROLE_2, JwtTestFactory.ROLE_3); + expectedSecurityRoleStrings = keycloakRoles.stream().map(role -> SIMPLE_GRANT_AUTHORITY_PREFIX + role).toList(); + doReturn(keycloakRoles).when(securityConfiguration).getKeycloakRolesFromJwt(any()); + } + + @DisplayName("should call get keycloak roles from jwt") + @Test + void shouldCallGetKeycloakRolesFromJwt() { + var jwt = JwtTestFactory.create(); + + securityConfiguration.convertJwtToGrantedAuthorities(jwt); + + verify(securityConfiguration).getKeycloakRolesFromJwt(jwt); + } + + @DisplayName("should return granted authorities with ROLE_ prefix") + @Test + void shouldReturnGrantedAuthoritiesWithRolePrefix() { + var jwt = JwtTestFactory.create(); + + var grantedAuthorities = securityConfiguration.convertJwtToGrantedAuthorities(jwt); + + var securityRoles = grantedAuthorities + .stream() + .map(GrantedAuthority::getAuthority).toList(); + assertThat(securityRoles).containsAll(expectedSecurityRoleStrings); + } + } + @DisplayName("get keycloak roles from jwt") @Nested class TestGetKeycloakRolesFromJwt { @@ -84,7 +165,7 @@ class SecurityConfigurationTest { @DisplayName("should return resource_access.admin.roles list") @Test void shouldReturnResourceAccessAdminRolesList() { - var expectedRoles = List.of(JwtTestFactory.ROLE_1, JwtTestFactory.ROLE_2, JwtTestFactory.ROLE_3); + var expectedRoles = List.of(ROLE_1, JwtTestFactory.ROLE_2, JwtTestFactory.ROLE_3); var jwtWithRoles = JwtTestFactory.createWithRoles(expectedRoles).build(); var roleStrings = securityConfiguration.getKeycloakRolesFromJwt(jwtWithRoles);