diff --git a/alfa-service/pom.xml b/alfa-service/pom.xml index 0f4827be47ecc8c9af69e268a612b133bf61ecfd..afa097c22d300291d6de9d645039bc4f1f465371 100644 --- a/alfa-service/pom.xml +++ b/alfa-service/pom.xml @@ -57,6 +57,7 @@ <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.7.16</version> + </dependency> <dependency> <groupId>org.springframework.boot</groupId> @@ -84,37 +85,20 @@ <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>2.7.16</version> - <exclusions> - <exclusion> - <groupId>org.springframework.security</groupId> - <artifactId>spring-security-config</artifactId> - </exclusion> - <exclusion> - <groupId>org.springframework.security</groupId> - <artifactId>spring-security-web</artifactId> - </exclusion> - </exclusions> </dependency> <dependency> - <groupId>org.springframework.security</groupId> - <artifactId>spring-security-config</artifactId> - <version>5.8.7</version> - </dependency> - <dependency> - <groupId>org.springframework.security</groupId> - <artifactId>spring-security-web</artifactId> - <version>5.8.7</version> - </dependency> - + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-oauth2-client</artifactId> + <version>2.7.16</version> + </dependency> <dependency> - <groupId>org.keycloak</groupId> - <artifactId>keycloak-spring-boot-starter</artifactId> - <version>20.0.3</version> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> + <version>2.7.16</version> </dependency> <dependency> - <groupId>org.keycloak</groupId> - <artifactId>keycloak-admin-client</artifactId> - <version>20.0.3</version> + <groupId>com.jayway.jsonpath</groupId> + <artifactId>json-path</artifactId> </dependency> <!-- jwt --> diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/EnvironmentController.java b/alfa-service/src/main/java/de/ozgcloud/alfa/EnvironmentController.java index 0103a54760c7db79f34c0ab0980a0b569383108b..6312425734f9c38f93c530272881183560d9f5ed 100644 --- a/alfa-service/src/main/java/de/ozgcloud/alfa/EnvironmentController.java +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/EnvironmentController.java @@ -25,7 +25,6 @@ package de.ozgcloud.alfa; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; -import org.keycloak.adapters.springboot.KeycloakSpringBootProperties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; @@ -37,7 +36,7 @@ import org.springframework.web.bind.annotation.RestController; public class EnvironmentController { @Autowired - private KeycloakSpringBootProperties kcProperties; + private KeycloakProperties keycloakProperties; @Value("${goofy.production}") private boolean production = true; @@ -46,14 +45,10 @@ public class EnvironmentController { public FrontendEnvironment getFrontendEnvironment() { return FrontendEnvironment.builder()// .production(production)// - .remoteHost(apiRoot())// - .authServer(kcProperties.getAuthServerUrl())// - .clientId(kcProperties.getResource())// - .realm(kcProperties.getRealm()) + .remoteHost(linkTo(RootController.class).toUri().toString())// + .authServer(keycloakProperties.getAuthServerUrl())// + .clientId(keycloakProperties.getResource())// + .realm(keycloakProperties.getRealm()) .build(); } - - private String apiRoot() { - return linkTo(RootController.class).toUri().toString(); - } } \ No newline at end of file diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/JwtAuthConverter.java b/alfa-service/src/main/java/de/ozgcloud/alfa/JwtAuthConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..a2579c5e464af4f38f095b2eb6f139398362bbe2 --- /dev/null +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/JwtAuthConverter.java @@ -0,0 +1,83 @@ +package de.ozgcloud.alfa; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.collections.MapUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.convert.converter.Converter; +import org.springframework.lang.NonNull; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; +import org.springframework.stereotype.Component; + +@Component +public class JwtAuthConverter implements Converter<Jwt, AbstractAuthenticationToken> { + + private final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + + @Value("${jwt.auth.converter.principle-attribute}") + private String principleAttribute; + @Value("${jwt.auth.converter.resource-id}") + private String resourceId; + + private static final String SIMPLE_GRANT_AUTHORITY_PREFIX = "ROLE_"; + + @Override + public AbstractAuthenticationToken convert(@NonNull Jwt jwt) { + return new JwtAuthenticationToken(jwt, getAuthorities(jwt), getPrincipleClaimName(jwt)); + } + + private String getPrincipleClaimName(Jwt jwt) { + return MapUtils.getString(jwt.getClaims(), getClaimName()); + } + + private String getClaimName() { + return Optional.ofNullable(principleAttribute).orElse(JwtClaimNames.SUB); + } + + private Set<GrantedAuthority> getAuthorities(Jwt jwt) { + return Stream.concat(jwtGrantedAuthoritiesConverter.convert(jwt).stream(), extractResourceRoles(jwt).stream()).collect(Collectors.toSet()); + } + + private Collection<? extends GrantedAuthority> extractResourceRoles(Jwt jwt) { + if (Objects.isNull(getResourceAccess(jwt))) { + return Collections.emptySet(); + } + + var resourceAccess = getResourceAccess(jwt); + if (Objects.isNull(resourceAccess.get(resourceId))) { + return Collections.emptySet(); + } + return extractRoles(getClaimMapFromMap(resourceAccess, resourceId)); + } + + private Map<String, Object> getResourceAccess(Jwt jwt) { + return jwt.getClaimAsMap("resource_access"); + } + + @SuppressWarnings("unchecked") + private Map<String, Object> getClaimMapFromMap(Map<String, Object> claimMap, String claimName) { + return MapUtils.getMap(claimMap, claimName); + } + + private Set<SimpleGrantedAuthority> extractRoles(Map<String, Object> resource) { + return getRoles(resource).stream().map(role -> new SimpleGrantedAuthority(SIMPLE_GRANT_AUTHORITY_PREFIX + role)).collect(Collectors.toSet()); + } + + @SuppressWarnings("unchecked") + private Collection<String> getRoles(Map<String, Object> resource) { + return (Collection<String>) resource.get("roles"); + } +} \ No newline at end of file diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/KeycloakProperties.java b/alfa-service/src/main/java/de/ozgcloud/alfa/KeycloakProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..c29d67d881715958dfdfe003c3981292dc8f55e1 --- /dev/null +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/KeycloakProperties.java @@ -0,0 +1,31 @@ +package de.ozgcloud.alfa; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Configuration +@ConfigurationProperties(prefix = KeycloakProperties.PREFIX) +public class KeycloakProperties { + + static final String PREFIX = "ozgcloud.keycloak"; + + /** + * Keycloak auth server url + */ + private String authServerUrl; + + /** + * Keycloak realm + */ + private String realm; + + /** + * Keycloak client + */ + private String resource; +} \ No newline at end of file diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/SecurityConfiguration.java b/alfa-service/src/main/java/de/ozgcloud/alfa/SecurityConfiguration.java index b4b58515539b57b624f699cded4194c2e1ef4263..c8de75605a3a632175628325b50f122df47879ee 100644 --- a/alfa-service/src/main/java/de/ozgcloud/alfa/SecurityConfiguration.java +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/SecurityConfiguration.java @@ -23,16 +23,21 @@ */ package de.ozgcloud.alfa; -import org.keycloak.adapters.springsecurity.KeycloakConfiguration; -import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider; -import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter; +import java.util.Objects; + import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 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.authority.mapping.SimpleAuthorityMapper; +import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy; import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; @@ -42,47 +47,65 @@ import org.springframework.security.web.header.writers.frameoptions.XFrameOption import de.ozgcloud.alfa.common.downloadtoken.DownloadTokenAuthenticationFilter; +@Configuration +@EnableWebSecurity @EnableMethodSecurity(securedEnabled = true, prePostEnabled = true) -@KeycloakConfiguration -public class SecurityConfiguration extends KeycloakWebSecurityConfigurerAdapter { +public class SecurityConfiguration { @Autowired private DownloadTokenAuthenticationFilter downloadTokenFilter; - @Override - protected void configure(HttpSecurity http) throws Exception { - super.configure(http); + @Autowired + private JwtAuthConverter jwtAuthConverter; + + @Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") + private String jwkSetUri; + + @Order(2) + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())); - http.sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS)).authorizeHttpRequests()// - .requestMatchers(HttpMethod.GET, "/api/environment").permitAll()// - .requestMatchers(HttpMethod.GET, "/assets/**").permitAll()// - .requestMatchers(HttpMethod.GET, "/vorgang/**").permitAll()// - .requestMatchers(HttpMethod.GET, "/meine/**").permitAll()// - .requestMatchers(HttpMethod.GET, "/alle/**").permitAll()// - .requestMatchers(HttpMethod.GET, "/unassigned/**").permitAll()// - .requestMatchers("/api").authenticated()// - .requestMatchers("/api/**").authenticated()// - .requestMatchers("/actuator").permitAll()// - .requestMatchers("/actuator/**").permitAll()// - .requestMatchers("/").permitAll()// - .requestMatchers("/*").permitAll()// - .anyRequest().denyAll(); + http.sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + + http.authorizeHttpRequests()// + .antMatchers(HttpMethod.GET, "/api/environment").permitAll()// + .antMatchers(HttpMethod.GET, "/assets/**").permitAll()// + .antMatchers(HttpMethod.GET, "/vorgang/**").permitAll()// + .antMatchers(HttpMethod.GET, "/meine/**").permitAll()// + .antMatchers(HttpMethod.GET, "/alle/**").permitAll()// + .antMatchers(HttpMethod.GET, "/unassigned/**").permitAll()// + .antMatchers(HttpMethod.GET, "/oauth2/**").permitAll()// + .antMatchers(HttpMethod.GET, "/login/**").permitAll()// + .antMatchers("/api").authenticated()// + .antMatchers("/api/**").authenticated()// + .antMatchers("/actuator").permitAll()// + .antMatchers("/actuator/**").permitAll()// + .antMatchers("/").permitAll()// + .antMatchers("/*").permitAll()// + .anyRequest().denyAll().and(); + + http.oauth2ResourceServer(server -> { + server.jwt().jwtAuthenticationConverter(jwtAuthConverter); + + if (Objects.nonNull(jwkSetUri)) { + server.jwt().jwkSetUri(jwkSetUri); + } + }); http.headers(headers -> headers.addHeaderWriter(new XFrameOptionsHeaderWriter(XFrameOptionsMode.SAMEORIGIN))); http.addFilterBefore(downloadTokenFilter, UsernamePasswordAuthenticationFilter.class); - } - @Autowired - public void configureGlobal(AuthenticationManagerBuilder auth) { - KeycloakAuthenticationProvider keyCloakAuthProvider = keycloakAuthenticationProvider(); - keyCloakAuthProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper()); - auth.authenticationProvider(keyCloakAuthProvider); + return http.build(); } - @Override + @Bean protected SessionAuthenticationStrategy sessionAuthenticationStrategy() { return new NullAuthenticatedSessionStrategy(); } + @Bean + AuthenticationManager authenticationManager(HttpSecurity http) throws Exception { + return http.getSharedObject(AuthenticationManagerBuilder.class).build(); + } } diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/WebConfig.java b/alfa-service/src/main/java/de/ozgcloud/alfa/WebConfig.java index 2074b2e67f74c77f5b00ff6c9a8f6b6f8e0bd7ed..a0669a9539fecfa3fb4bb46020f5c85f9f133dde 100644 --- a/alfa-service/src/main/java/de/ozgcloud/alfa/WebConfig.java +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/WebConfig.java @@ -26,9 +26,6 @@ package de.ozgcloud.alfa; import java.io.IOException; import java.util.concurrent.TimeUnit; -import org.keycloak.adapters.KeycloakConfigResolver; -import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.Resource; import org.springframework.http.CacheControl; @@ -36,6 +33,8 @@ import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.resource.PathResourceResolver; +import lombok.NoArgsConstructor; + @Configuration public class WebConfig implements WebMvcConfigurer { @@ -43,7 +42,6 @@ public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { - registry.addResourceHandler("/*.js", "/*.css", "/*.ttf", "/*.woff", "/*.woff2", "/*.eot", "/**/*.svg", "/*.svf", "/*.otf", "/*.ico", "/**/*.png") .addResourceLocations(RESOURCE_LOCATION) @@ -56,18 +54,20 @@ public class WebConfig implements WebMvcConfigurer { .setCacheControl(CacheControl.noStore()) .setUseLastModified(false) .resourceChain(true) - .addResolver(new PathResourceResolver() { - @Override - protected Resource getResource(String resourcePath, Resource location) throws IOException { - Resource requestedResource = location.createRelative(resourcePath); - return requestedResource.exists() && requestedResource.isReadable() ? requestedResource - : super.getResource("index.html", location); - } - }); + .addResolver(new OzgCloudPathResourceResolver()); } - @Bean - public KeycloakConfigResolver keyCloakConfigResolver() { - return new KeycloakSpringBootConfigResolver(); + @NoArgsConstructor + static class OzgCloudPathResourceResolver extends PathResourceResolver { + + @Override + protected Resource getResource(String resourcePath, Resource location) throws IOException { + var requestedResource = location.createRelative(resourcePath); + + if (requestedResource.exists() && requestedResource.isReadable()) { + return requestedResource; + } + return super.getResource("index.html", location); + } } -} +} \ No newline at end of file diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/common/binaryfile/DownloadAuthenticationHandler.java b/alfa-service/src/main/java/de/ozgcloud/alfa/common/binaryfile/DownloadAuthenticationHandler.java index ad7639749de0b76fa04e88485ae985ba110f6718..ee46850a3953a45a759529e3ba3513766bfc62a8 100644 --- a/alfa-service/src/main/java/de/ozgcloud/alfa/common/binaryfile/DownloadAuthenticationHandler.java +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/common/binaryfile/DownloadAuthenticationHandler.java @@ -39,7 +39,7 @@ public class DownloadAuthenticationHandler { boolean check(FileId fileId, Authentication auth) { if (auth instanceof UsernamePasswordAuthenticationToken userPasswordToken) { - GoofyUserWithFileId user = (GoofyUserWithFileId) userPasswordToken.getPrincipal(); + var user = (GoofyUserWithFileId) userPasswordToken.getPrincipal(); return Objects.nonNull(fileId) && fileId.equals(user.getFileId()) && auth.isAuthenticated(); } diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/common/downloadtoken/DownloadTokenAuthenticationFilter.java b/alfa-service/src/main/java/de/ozgcloud/alfa/common/downloadtoken/DownloadTokenAuthenticationFilter.java index 297aa8e7a22308f54ca8a3781c92e3742eeb6adf..b8f3f00358092931c6b8a96bb8b1680879539ad9 100644 --- a/alfa-service/src/main/java/de/ozgcloud/alfa/common/downloadtoken/DownloadTokenAuthenticationFilter.java +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/common/downloadtoken/DownloadTokenAuthenticationFilter.java @@ -31,7 +31,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; -import org.apache.http.HttpStatus; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; @@ -52,7 +51,7 @@ public class DownloadTokenAuthenticationFilter extends OncePerRequestFilter { try { downloadTokenService.handleToken(request, getDownloadToken(request)); } catch (TechnicalException e) { - response.setStatus(HttpStatus.SC_UNAUTHORIZED); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/common/user/CurrentUserService.java b/alfa-service/src/main/java/de/ozgcloud/alfa/common/user/CurrentUserService.java index 94b9c3900b2f7decee285d35e220fd28463a5fe5..8425d6052b7dde99dc4ba6839d4d3dff2ed5989b 100644 --- a/alfa-service/src/main/java/de/ozgcloud/alfa/common/user/CurrentUserService.java +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/common/user/CurrentUserService.java @@ -27,15 +27,13 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Optional; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.representations.AccessToken; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.stereotype.Service; import de.itvsh.kop.common.errorhandling.TechnicalException; @@ -50,6 +48,10 @@ public class CurrentUserService { static final String USER_ATTRIBUTE_ORGANISATIONSEINHEIT_ID = "organisationseinheitId"; + static final String KEYCLOAK_USER_PREFERRED_USERNAME = "preferred_username"; + static final String KEYCLOAK_USER_GIVEN_NAME = "given_name"; + static final String KEYCLOAK_USER_FAMILY_NAME = "family_name"; + @Autowired private UserService userService; @Autowired @@ -65,30 +67,28 @@ public class CurrentUserService { return CurrentUserHelper.containsRole(reachableRoles, role); } - public Collection<GrantedAuthority> getAuthorities() { - return Collections.unmodifiableCollection(new HashSet<GrantedAuthority>(CurrentUserHelper.getAuthentication().getAuthorities())); - } - public UserProfile getUser() { - var dlUser = getDownloadUser(); - if (dlUser.isPresent()) { - return dlUser.get(); - } - - Optional<AccessToken> token = getCurrentSecurityToken(); + return getDownloadUser().orElseGet(this::buildUserProfile); + } + private UserProfile buildUserProfile() { var userBuilder = UserProfile.builder() .id(getUserId()) .authorities(getAuthorities()); - token.ifPresent(t -> userBuilder.userName(t.getPreferredUsername()) - .firstName(t.getGivenName()) - .lastName(t.getFamilyName()) - .organisationseinheitIds(getOrganisationseinheitId(t.getOtherClaims()))); + getCurrentSecurityToken().ifPresent(token -> userBuilder + .userName(token.getClaimAsString(KEYCLOAK_USER_PREFERRED_USERNAME)) + .firstName(token.getClaimAsString(KEYCLOAK_USER_GIVEN_NAME)) + .lastName(token.getClaimAsString(KEYCLOAK_USER_FAMILY_NAME)) + .organisationseinheitIds(getOrganisationsEinheitIds(token))); return userBuilder.build(); } + public Collection<GrantedAuthority> getAuthorities() { + return Collections.unmodifiableCollection(new HashSet<GrantedAuthority>(CurrentUserHelper.getAuthentication().getAuthorities())); + } + public UserId getUserId() { return findUserId() .orElseThrow(() -> new TechnicalException("Cannot find internal UserId. Check sync with UserManager or Token Mapper in keycloak.")); @@ -96,13 +96,14 @@ public class CurrentUserService { public Optional<UserId> findUserId() { return Optional.ofNullable( - getSingleClaimValue(ATTRIBUTE_NAME_USER_ID).map(UserId::from) - .orElseGet(() -> userService.getInternalId(CurrentUserHelper.getCurrentUserId()).orElse(null))) + getSingleClaimValue(ATTRIBUTE_NAME_USER_ID).map(UserId::from) + .orElseGet(() -> userService.getInternalId(CurrentUserHelper.getCurrentUserId()).orElse(null))) .filter(Objects::nonNull); } - List<String> getOrganisationseinheitId(Map<String, Object> claims) { - return Optional.ofNullable(claims.get(USER_ATTRIBUTE_ORGANISATIONSEINHEIT_ID)) + List<String> getOrganisationsEinheitIds(Jwt jwt) { + return Optional.ofNullable(jwt) + .map(token -> token.getClaim(USER_ATTRIBUTE_ORGANISATIONSEINHEIT_ID)) .map(col -> (Collection<?>) col).orElse(Collections.emptyList()) // NOSONAR - Collection.class::cast has type-safty issue .stream().map(Object::toString).toList(); } @@ -114,19 +115,19 @@ public class CurrentUserService { .map(GoofyUserWithFileId::getUser); } - Optional<AccessToken> getCurrentSecurityToken() { - Object principal = CurrentUserHelper.getAuthentication().getPrincipal(); + Optional<String> getSingleClaimValue(String attributeName) { + return getCurrentSecurityToken() + .map(token -> token.getClaim(attributeName)) + .map(String.class::cast); + } + + Optional<Jwt> getCurrentSecurityToken() { + var principal = CurrentUserHelper.getAuthentication().getPrincipal(); - if (principal instanceof KeycloakPrincipal<?> kcPrincipal) { - return Optional.of(kcPrincipal.getKeycloakSecurityContext().getToken()); + if (principal instanceof Jwt kcPrincipal) { + return Optional.of(kcPrincipal); } return Optional.empty(); } - - Optional<String> getSingleClaimValue(String attributeName) { - return getCurrentSecurityToken() - .map(token -> token.getOtherClaims().get(attributeName)) - .map(String.class::cast); - } } \ No newline at end of file diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/historie/HistorieCommandHandler.java b/alfa-service/src/main/java/de/ozgcloud/alfa/historie/HistorieCommandHandler.java index dc735be8bf8f3da848fd631fa28612ff23a444fb..fe54b77f7a60a44ac005f05864c7819587701b36 100644 --- a/alfa-service/src/main/java/de/ozgcloud/alfa/historie/HistorieCommandHandler.java +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/historie/HistorieCommandHandler.java @@ -27,8 +27,8 @@ import java.util.Map; import java.util.Optional; import java.util.function.Predicate; -import org.apache.commons.codec.binary.StringUtils; import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/EnvironmentControllerTest.java b/alfa-service/src/test/java/de/ozgcloud/alfa/EnvironmentControllerTest.java index 145d1e512f80ce0538074b74fde6272c7269e12a..bb29e2e75e420ce3d6745bd83998973566af9d70 100644 --- a/alfa-service/src/test/java/de/ozgcloud/alfa/EnvironmentControllerTest.java +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/EnvironmentControllerTest.java @@ -29,12 +29,14 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.keycloak.adapters.springboot.KeycloakSpringBootProperties; import org.mockito.InjectMocks; import org.mockito.Mock; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import lombok.SneakyThrows; + class EnvironmentControllerTest { private final String PATH = "/api/environment"; @@ -42,7 +44,7 @@ class EnvironmentControllerTest { @InjectMocks private EnvironmentController controller; @Mock - private KeycloakSpringBootProperties kcProperties; + private KeycloakProperties keycloakProperties; private MockMvc mockMvc; @@ -52,22 +54,30 @@ class EnvironmentControllerTest { } @Test - void loadEnvironment() throws Exception { - mockMvc.perform(get(PATH)).andExpect(status().isOk()); + void shouldReturnOk() throws Exception { + var response = doRequest(); + + response.andExpect(status().isOk()); } @Test void shouldHaveProductionTrueAsDefault() throws Exception { - mockMvc.perform(get(PATH)).andExpect(status().is2xxSuccessful())// - .andExpect(jsonPath("$.production").value(true)); + var response = doRequest(); + + response.andExpect(jsonPath("$.production").value(true)); } @Test void shouldHaveClientId() throws Exception { var client = "goofy"; + when(keycloakProperties.getResource()).thenReturn(client); + var response = doRequest(); - when(kcProperties.getResource()).thenReturn(client); + response.andExpect(jsonPath("$.clientId").value(client)); + } - mockMvc.perform(get(PATH)).andExpect(jsonPath("$.clientId").value(client)); + @SneakyThrows + private ResultActions doRequest() { + return mockMvc.perform(get(PATH)); } } \ No newline at end of file diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/common/binaryfile/DownloadAuthenticationHandlerTest.java b/alfa-service/src/test/java/de/ozgcloud/alfa/common/binaryfile/DownloadAuthenticationHandlerTest.java index 155ac009c8fc0b9604f993dd188a71fde2ed1355..d9cc748ca8211b95706ca0ad8af73fea013a1693 100644 --- a/alfa-service/src/test/java/de/ozgcloud/alfa/common/binaryfile/DownloadAuthenticationHandlerTest.java +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/common/binaryfile/DownloadAuthenticationHandlerTest.java @@ -29,9 +29,10 @@ import static org.mockito.Mockito.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; +import org.mockito.Mock; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.jwt.Jwt; class DownloadAuthenticationHandlerTest { @@ -40,18 +41,22 @@ class DownloadAuthenticationHandlerTest { @Nested class TestAuthorizationKeycloak { + @Mock + private Jwt jwt; + private Authentication authentication = mock(Authentication.class); @BeforeEach void init() { when(authentication.isAuthenticated()).thenReturn(Boolean.TRUE); - KeycloakAuthenticationToken keycloakToken = mock(KeycloakAuthenticationToken.class); - when(authentication.getPrincipal()).thenReturn(keycloakToken); + when(authentication.getPrincipal()).thenReturn(jwt); } @Test void shouldAuthenticate() { - assertThat(downloadAuthorizationHandler.check(FileId.createNew(), authentication)).isTrue(); + var check = downloadAuthorizationHandler.check(FileId.createNew(), authentication); + + assertThat(check).isTrue(); } } @@ -69,23 +74,32 @@ class DownloadAuthenticationHandlerTest { @Test void shouldAuthenticate() { - assertThat(downloadAuthorizationHandler.check(fileId, authentication)).isTrue(); + var check = downloadAuthorizationHandler.check(fileId, authentication); + + assertThat(check).isTrue(); } @Test void shouldNotAuthenticateWrongFileId() { - assertThat(downloadAuthorizationHandler.check(FileId.createNew(), authentication)).isFalse(); + var check = downloadAuthorizationHandler.check(FileId.createNew(), authentication); + + assertThat(check).isFalse(); } @Test void shouldNotAuthenticateNoFileId() { - assertThat(downloadAuthorizationHandler.check(null, authentication)).isFalse(); + var check = downloadAuthorizationHandler.check(null, authentication); + + assertThat(check).isFalse(); } @Test void shouldNotAuthenticate() { when(authentication.isAuthenticated()).thenReturn(Boolean.FALSE); - assertThat(downloadAuthorizationHandler.check(fileId, authentication)).isFalse(); + + var check = downloadAuthorizationHandler.check(fileId, authentication); + + assertThat(check).isFalse(); } } } diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/common/user/CurrentUserServiceTest.java b/alfa-service/src/test/java/de/ozgcloud/alfa/common/user/CurrentUserServiceTest.java index f22c42b6cb253d3b2a1956e9f741d1aaa7c6a076..d1f47b8ac1203f0f06f9841ea7562783987d9f8d 100644 --- a/alfa-service/src/test/java/de/ozgcloud/alfa/common/user/CurrentUserServiceTest.java +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/common/user/CurrentUserServiceTest.java @@ -34,8 +34,9 @@ import java.util.Optional; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.keycloak.representations.AccessToken; +import org.mockito.Mock; import org.mockito.Spy; +import org.springframework.security.oauth2.jwt.Jwt; class CurrentUserServiceTest { @@ -45,34 +46,39 @@ class CurrentUserServiceTest { @Nested class TestGetOrganisationseinheit { + @Mock + private Jwt jwt; + @Test void shouldReturnOrganisationseinheitIdsFromMap() { - Map<String, Object> claims = Map.of(CurrentUserService.USER_ATTRIBUTE_ORGANISATIONSEINHEIT_ID, List.of("1", "2")); + when(jwt.getClaim(CurrentUserService.USER_ATTRIBUTE_ORGANISATIONSEINHEIT_ID)).thenReturn(List.of("1", "2")); - var result = service.getOrganisationseinheitId(claims); + var result = service.getOrganisationsEinheitIds(jwt); assertThat(result).contains("1").contains("2"); } @Test void shouldReturnOrgaIdAsString() { - var result = service.getOrganisationseinheitId(Map.of(CurrentUserService.USER_ATTRIBUTE_ORGANISATIONSEINHEIT_ID, List.of("1", 2))); + when(jwt.getClaim(CurrentUserService.USER_ATTRIBUTE_ORGANISATIONSEINHEIT_ID)).thenReturn(List.of("1", 2)); + + var result = service.getOrganisationsEinheitIds(jwt); assertThat(result).contains("1").contains("2"); } @Test void shouldReturnEmptyList() { - Map<String, Object> claims = Map.of(CurrentUserService.USER_ATTRIBUTE_ORGANISATIONSEINHEIT_ID, Collections.emptyList()); + when(jwt.getClaim(CurrentUserService.USER_ATTRIBUTE_ORGANISATIONSEINHEIT_ID)).thenReturn(Collections.emptyList()); - var result = service.getOrganisationseinheitId(claims); + var result = service.getOrganisationsEinheitIds(jwt); assertThat(result).isEmpty(); } @Test void shouldReturnEmptyListIfNotExists() { - var result = service.getOrganisationseinheitId(Collections.emptyMap()); + var result = service.getOrganisationsEinheitIds(null); assertThat(result).isEmpty(); } @@ -98,8 +104,8 @@ class CurrentUserServiceTest { @Test void shouldReturnUserAttribute() { - var token = new AccessToken(); - token.setOtherClaims(ATTRIBUTE_NAME, ATTRIBUTE_VALUE); + var claims = Map.<String, Object>of(ATTRIBUTE_NAME, ATTRIBUTE_VALUE); + var token = new Jwt("dummyTokenValue", null, null, Collections.singletonMap("dummyHeader", "DummyHeaderValue"), claims); doReturn(Optional.of(token)).when(service).getCurrentSecurityToken(); var attribute = service.getSingleClaimValue(ATTRIBUTE_NAME); @@ -109,7 +115,8 @@ class CurrentUserServiceTest { @Test void shouldReturnEmpty() { - var token = new AccessToken(); + var token = new Jwt("dummyTokenValue", null, null, Collections.singletonMap("dummyHeader", "DummyHeaderValue"), + Collections.singletonMap("dummyClaim", "DummyClaimValue")); doReturn(Optional.of(token)).when(service).getCurrentSecurityToken(); var attribute = service.getSingleClaimValue(ATTRIBUTE_NAME); diff --git a/goofy-server/pom.xml b/goofy-server/pom.xml index b05c91b03da1dd016cc49bdc0ddbb33ef2844cce..58579ddc04e285c4a014834c83ed9160472438ef 100644 --- a/goofy-server/pom.xml +++ b/goofy-server/pom.xml @@ -40,50 +40,7 @@ <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>2.7.16</version> - <exclusions> - <exclusion> - <groupId>org.springframework.security</groupId> - <artifactId>spring-security-config</artifactId> - </exclusion> - <exclusion> - <groupId>org.springframework.security</groupId> - <artifactId>spring-security-web</artifactId> - </exclusion> - </exclusions> </dependency> - <dependency> - <groupId>org.springframework.security</groupId> - <artifactId>spring-security-config</artifactId> - <exclusions> - <exclusion> - <groupId>org.springframework.security</groupId> - <artifactId>spring-security-core</artifactId> - </exclusion> - </exclusions> - <version>5.8.7</version> - </dependency> - <dependency> - <groupId>org.springframework.security</groupId> - <artifactId>spring-security-core</artifactId> - <exclusions> - <exclusion> - <groupId>org.springframework.security</groupId> - <artifactId>spring-security-crypto</artifactId> - </exclusion> - </exclusions> - <version>5.8.7</version> - </dependency> - <dependency> - <groupId>org.springframework.security</groupId> - <artifactId>spring-security-crypto</artifactId> - <version>5.8.7</version> - </dependency> - <dependency> - <groupId>org.springframework.security</groupId> - <artifactId>spring-security-web</artifactId> - <version>5.8.7</version> - </dependency> - </dependencies> <build> diff --git a/goofy-server/src/main/resources/application-dev.yml b/goofy-server/src/main/resources/application-dev.yml index be7dc4d1adb0c218ef1d2c908b35b8fbd2142632..f40a7a1ac1d60f9273e9d670b0696a8b9fd5dc50 100644 --- a/goofy-server/src/main/resources/application-dev.yml +++ b/goofy-server/src/main/resources/application-dev.yml @@ -1,11 +1,6 @@ goofy: production: false -keycloak: - auth-server-url: https://sso.dev.by.ozg-cloud.de - realm: by-kiel-dev - resource: alfa - server: error: include-stacktrace: always @@ -14,4 +9,7 @@ ozgcloud: feature: vorgang-export: true stage: - production: false \ No newline at end of file + production: false + keycloak: + auth-server-url: https://sso.dev.by.ozg-cloud.de + realm: by-kiel-dev \ No newline at end of file diff --git a/goofy-server/src/main/resources/application-e2e.yml b/goofy-server/src/main/resources/application-e2e.yml index 6b4c84428d26ff921ab8c233062ebd1b8837dce4..285c643223b91da42101e8ff5ab2327b8bd06720 100644 --- a/goofy-server/src/main/resources/application-e2e.yml +++ b/goofy-server/src/main/resources/application-e2e.yml @@ -15,4 +15,4 @@ ozgcloud: createBescheid: true user-assistance: documentation: - url: /assets/benutzerleitfaden/Benutzerleitfaden_2.5.pdf \ No newline at end of file + url: /assets/benutzerleitfaden/benutzerleitfaden.pdf \ No newline at end of file diff --git a/goofy-server/src/main/resources/application-local.yml b/goofy-server/src/main/resources/application-local.yml index 4f0e0f12aab2ad4c7fd4c89d4263a8a03ccaa8e2..11b20f5d0b48ca03e09809d0c83c4af9db1d08ad 100644 --- a/goofy-server/src/main/resources/application-local.yml +++ b/goofy-server/src/main/resources/application-local.yml @@ -7,11 +7,6 @@ logging: goofy: production: false -keycloak: - auth-server-url: http://localhost:8088 - realm: sh-kiel-dev #TODO adjust - resource: sh-kiel-dev-goofy #TODO adjust - server: error: include-stacktrace: always @@ -30,4 +25,9 @@ grpc: ozgcloud: feature: - vorgang-export: true \ No newline at end of file + vorgang-export: true + user-assistance: + documentation: + url: /assets/benutzerleitfaden/benutzerleitfaden.pdf + keycloak: + auth-server-url: http://localhost:8088 \ No newline at end of file diff --git a/goofy-server/src/main/resources/application-remotekc.yml b/goofy-server/src/main/resources/application-remotekc.yml index f27d8bf81a26dd58d2b10d44b8b01eb89add3c8f..20d8a33c22dfe39d946824c8a39230d95478f223 100644 --- a/goofy-server/src/main/resources/application-remotekc.yml +++ b/goofy-server/src/main/resources/application-remotekc.yml @@ -1,6 +1,4 @@ -keycloak: - realm: by-kiel-dev - resource: alfa - public-client: true - use-resource-role-mappings: true - auth-server-url: https://sso.dev.by.ozg-cloud.de +ozgcloud: + keycloak: + auth-server-url: https://sso.dev.by.ozg-cloud.de + realm: by-kiel-dev \ No newline at end of file diff --git a/goofy-server/src/main/resources/application.yml b/goofy-server/src/main/resources/application.yml index a84e4da06ffa475b8240b43d5301fe68a85cbbda..a3d23ad996201e37050106a4eebcbcb26bc1edaa 100644 --- a/goofy-server/src/main/resources/application.yml +++ b/goofy-server/src/main/resources/application.yml @@ -4,7 +4,19 @@ logging: '[de.itvsh]': INFO '[de.ozgcloud]': INFO, '[org.springframework.security]': WARN - '[org.keycloak.adapters]': WARN + + +ozgcloud: + keycloak: + auth-server-url: https://sso.dev.by.ozg-cloud.de + realm: by-kiel-dev + resource: ${jwt.auth.converter.resource-id} + +jwt: + auth: + converter: + resource-id: alfa + principle-attribute: preferred_username spring: mvc: @@ -19,7 +31,13 @@ spring: multipart: max-file-size: 2GB max-request-size: 2GB - + security: + oauth2: + resourceserver: + jwt: + issuer-uri: ${ozgcloud.keycloak.auth-server-url}/realms/${ozgcloud.keycloak.realm} + jwk-set-uri: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}/protocol/openid-connect/certs + server: http2: enabled: true @@ -52,13 +70,6 @@ management: goofy: production: true -keycloak: - auth-server-url: http://localhost:8088 - realm: sh-kiel-dev - resource: sh-kiel-dev-goofy - public-client: true - use-resource-role-mappings: true - grpc: client: pluto: