diff --git a/Dockerfile b/Dockerfile index ee3a4f589..3c2b7c7b9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM maven:3.8.1-openjdk-11 as builder +FROM maven:3.8-eclipse-temurin-21-alpine as builder WORKDIR /home @@ -38,7 +38,7 @@ WORKDIR /home/jpo-conflictvisualizer-api RUN mvn clean package -DskipTests # ENTRYPOINT ["tail", "-f", "/dev/null"] -FROM openjdk:11-jre +FROM eclipse-temurin:21-jre-alpine WORKDIR /home diff --git a/jpo-conflictmonitor b/jpo-conflictmonitor index e1087f81d..3b6020183 160000 --- a/jpo-conflictmonitor +++ b/jpo-conflictmonitor @@ -1 +1 @@ -Subproject commit e1087f81d1341efd9dc15b9f8721deb9035f53d7 +Subproject commit 3b602018373d7ef75ee2ddd3f58cd244c6449f25 diff --git a/jpo-conflictvisualizer-api/pom.xml b/jpo-conflictvisualizer-api/pom.xml index 633586d2a..ad16fc560 100644 --- a/jpo-conflictvisualizer-api/pom.xml +++ b/jpo-conflictvisualizer-api/pom.xml @@ -6,7 +6,7 @@ <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> - <version>2.7.8</version> + <version>3.1.3</version> <relativePath /> <!-- lookup parent from repository --> </parent> <groupId>usdot.jpo.ode</groupId> @@ -15,7 +15,7 @@ <name>jpo-conflictvisualizer-api</name> <description>Conflict Visualizer</description> <properties> - <java.version>11</java.version> + <java.version>21</java.version> <!-- <thymeleaf.version>3.0.3.RELEASE</thymeleaf.version> <thymeleaf-layout-dialect.version>2.2.1</thymeleaf-layout-dialect.version> --> </properties> @@ -48,7 +48,7 @@ <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> - <version>2.14.2</version> + <version>2.15.2</version> <exclusions> <exclusion> <groupId>javax.ws.rs</groupId> @@ -67,7 +67,7 @@ <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> - <version>1.18.24</version> + <version>1.18.30</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> @@ -87,7 +87,7 @@ <dependency> <groupId>usdot.jpo.ode</groupId> <artifactId>jpo-ode-core</artifactId> - <version>1.3.0</version> + <version>1.6.0-SNAPSHOT</version> <!-- required exclusion to preserve keycloak versions --> <exclusions> <exclusion> @@ -99,29 +99,29 @@ <dependency> <groupId>usdot.jpo.ode</groupId> <artifactId>jpo-ode-plugins</artifactId> - <version>1.3.0</version> + <version>1.6.0-SNAPSHOT</version> </dependency> <dependency> <groupId>usdot.jpo.ode</groupId> <artifactId>jpo-geojsonconverter</artifactId> - <version>1.0.0</version> + <version>1.2.0-SNAPSHOT</version> <classifier>jpo-geojsonconverter</classifier> </dependency> <dependency> <groupId>usdot.jpo.ode</groupId> <artifactId>jpo-conflictmonitor</artifactId> - <version>1.0.0</version> + <version>1.2.0-SNAPSHOT</version> <classifier>jpo-conflictmonitor</classifier> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> - <version>3.12.0</version> + <version>3.14.0</version> </dependency> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-ui</artifactId> - <version>1.6.14</version> + <version>1.7.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> @@ -142,15 +142,23 @@ <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> - <groupId>org.keycloak</groupId> - <artifactId>keycloak-spring-boot-starter</artifactId> - <version>21.0.2</version> - <!-- <version>20.0.3</version> --> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-oauth2-client</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> +<!-- <dependency>--> +<!-- <groupId>org.keycloak</groupId>--> +<!-- <artifactId>keycloak-spring-boot-starter</artifactId>--> +<!-- <version>21.0.2</version>--> +<!-- <!– <version>20.0.3</version> –>--> +<!-- </dependency>--> <dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-admin-client</artifactId> - <version>21.0.2</version> + <version>23.0.6</version> <!-- <version>20.0.3</version> --> </dependency> <dependency> @@ -169,17 +177,17 @@ <dependency> <groupId>org.xhtmlrenderer</groupId> <artifactId>flying-saucer-pdf</artifactId> - <version>9.1.20</version> + <version>9.5.1</version> </dependency> <dependency> <groupId>com.itextpdf</groupId> <artifactId>itextpdf</artifactId> - <version>5.5.13</version> + <version>5.5.13.3</version> </dependency> <dependency> <groupId>org.knowm.xchart</groupId> <artifactId>xchart</artifactId> - <version>3.8.3</version> + <version>3.8.7</version> </dependency> <dependency> <groupId>org.awaitility</groupId> @@ -190,13 +198,13 @@ <dependency> <groupId>com.sendgrid</groupId> <artifactId>sendgrid-java</artifactId> - <version>4.6.0</version> + <version>4.10.1</version> </dependency> <!-- https://mvnrepository.com/artifact/com.postmarkapp/postmark --> <dependency> <groupId>com.postmarkapp</groupId> <artifactId>postmark</artifactId> - <version>1.10.21</version> + <version>1.11.1</version> </dependency> </dependencies> <build> @@ -211,23 +219,27 @@ <artifactId>lombok</artifactId> </exclude> </excludes> - <fork>false</fork> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> - <version>3.0.0-M7</version> + <version>3.2.5</version> <configuration> <enableProcessChecker>all</enableProcessChecker> <shutdown>exit</shutdown> + <!-- Prevent JDK 21 warnings related to Mockito during tests. --> + <!-- See: https://github.com/mockito/mockito/issues/3037 --> + <argLine> + -XX:+EnableDynamicAgentLoading + </argLine> </configuration> <dependencies> <dependency> <!-- Force test to use junit4.7 --> <groupId>org.apache.maven.surefire</groupId> <artifactId>surefire-junit47</artifactId> - <version>3.0.0-M7</version> + <version>3.2.5</version> </dependency> </dependencies> </plugin> diff --git a/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/ConflictApiApplication.java b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/ConflictApiApplication.java index 1399ff231..cd8e82440 100644 --- a/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/ConflictApiApplication.java +++ b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/ConflictApiApplication.java @@ -30,17 +30,17 @@ public static void main(String[] args) { } - @Bean - public WebMvcConfigurer corsConfigurer() { - return new WebMvcConfigurer() { - @Override - public void addCorsMappings(CorsRegistry registry) { - ConflictMonitorApiProperties props = new ConflictMonitorApiProperties(); - registry.addMapping("/**").allowedOrigins(props.getCors()); - // registry.addMapping("/**").allowedMethods("*"); - } - }; - } +// @Bean +// public WebMvcConfigurer corsConfigurer() { +// return new WebMvcConfigurer() { +// @Override +// public void addCorsMappings(CorsRegistry registry) { +// ConflictMonitorApiProperties props = new ConflictMonitorApiProperties(); +// registry.addMapping("/**").allowedOrigins(props.getCors()); +// // registry.addMapping("/**").allowedMethods("*"); +// } +// }; +// } diff --git a/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/ConflictMonitorApiProperties.java b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/ConflictMonitorApiProperties.java index d68b77383..7ebc10712 100644 --- a/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/ConflictMonitorApiProperties.java +++ b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/ConflictMonitorApiProperties.java @@ -21,7 +21,7 @@ import java.util.Arrays; import java.util.Properties; -import javax.annotation.PostConstruct; +import jakarta.annotation.PostConstruct; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/KeycloakConfig.java b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/KeycloakConfig.java deleted file mode 100644 index cd6f0fbc5..000000000 --- a/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/KeycloakConfig.java +++ /dev/null @@ -1,124 +0,0 @@ -package us.dot.its.jpo.ode.api; - -import org.keycloak.adapters.KeycloakConfigResolver; -import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver; -import org.keycloak.adapters.springsecurity.KeycloakConfiguration; -import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider; -import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter; -import org.keycloak.admin.client.Keycloak; -import org.keycloak.admin.client.KeycloakBuilder; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper; -import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy; -import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; - -// provides keycloak based spring security configuration -// annotation covers 2 annotations - @Configuration and @EnableWebSecurity -@KeycloakConfiguration -@EnableWebSecurity -public class KeycloakConfig extends KeycloakWebSecurityConfigurerAdapter { - - @Value("${security.enabled:true}") - private boolean securityEnabled; - - @Value("${keycloak.realm}") - private String realm; - - @Value("${keycloak.resource}") - private String resource; - - @Value("${keycloak.auth-server-url}") - private String authServer; - - @Value("${keycloak_username}") - private String username; - - @Value("${keycloak_password}") - private String password; - - - - - - // sets KeycloakAuthenticationProvider as an authentication provider - // sets SimpleAuthorityMapper as the authority mapper - @Autowired - protected void configureGlobal(final AuthenticationManagerBuilder auth) { - final KeycloakAuthenticationProvider provider = super.keycloakAuthenticationProvider(); - provider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper()); - auth.authenticationProvider(provider); - } - - - @Bean - @Override - protected SessionAuthenticationStrategy sessionAuthenticationStrategy() { - - return new NullAuthenticatedSessionStrategy(); - } - - // ensure that spring boot will resolve the keycloak configuration - // from application.yml (or application.properties) - @Bean - public KeycloakConfigResolver keycloakConfigResolver() { - return new KeycloakSpringBootConfigResolver(); - } - - @Bean - public Keycloak keyCloakBuilder() { - System.out.println("Auth Server: " + authServer); - System.out.println("Realm: " + realm); - System.out.println("Resource: " + resource); - Keycloak keycloak = KeycloakBuilder.builder() - .serverUrl(authServer) - .grantType("password") - .realm("master") - .clientId("admin-cli") - .username(username) - .password(password) - .build(); - return keycloak; - } - - @Override - protected void configure(final HttpSecurity httpSecurity) throws Exception { - super.configure(httpSecurity); - - if(securityEnabled){ - System.out.println("Running with KeyCloak Authentication"); - httpSecurity - .cors() - .and() - .csrf().disable() - .authorizeRequests() - .antMatchers("/**").permitAll() - .anyRequest().fullyAuthenticated(); - }else{ - System.out.println("Running without KeyCloak Authentication"); - httpSecurity - .cors() - .and() - .csrf().disable() - .authorizeRequests().anyRequest().permitAll(); - } - } - - // This is condition allows for disabling securit - @ConditionalOnProperty(prefix = "security", - name = "enabled", - havingValue = "true") - @EnableGlobalMethodSecurity(prePostEnabled = true) - static class Dummy { - public Dummy(){ - System.out.println("Initializing Security"); - } - - } -} \ No newline at end of file diff --git a/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/auth/StompHandshakeInterceptor.java b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/auth/StompHandshakeInterceptor.java index b06816c70..d1bab20f4 100644 --- a/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/auth/StompHandshakeInterceptor.java +++ b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/auth/StompHandshakeInterceptor.java @@ -1,27 +1,26 @@ package us.dot.its.jpo.ode.api.auth; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.keycloak.adapters.KeycloakDeploymentBuilder; -import org.keycloak.adapters.rotation.AdapterTokenVerifier; -import org.keycloak.adapters.springboot.KeycloakSpringBootProperties; -import org.keycloak.common.VerificationException; +import lombok.RequiredArgsConstructor; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.lang.Nullable; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeInterceptor; -import lombok.RequiredArgsConstructor; +import java.util.List; +import java.util.Map; @RequiredArgsConstructor public class StompHandshakeInterceptor implements HandshakeInterceptor { - private final KeycloakSpringBootProperties configuration; + private final OAuth2TokenValidator<Jwt> defaultTokenValidator; + private final JwtDecoder jwtDecoder; @Override public boolean beforeHandshake(ServerHttpRequest req, ServerHttpResponse resp, WebSocketHandler h, Map<String, Object> atts) { @@ -30,28 +29,31 @@ public boolean beforeHandshake(ServerHttpRequest req, ServerHttpResponse resp, W for(String key: atts.keySet()){ System.out.println("Attribute Key" + key); - } - System.out.println(configuration.getRealm()); + System.out.println("JwtDecoder" + jwtDecoder); + System.out.println("DefaultTokenValidator" + defaultTokenValidator); String token = getToken(req); - System.out.println("Token: " + token); - - AdapterTokenVerifier.verifyToken(token, KeycloakDeploymentBuilder.build(configuration)); + + var decodedToken = jwtDecoder.decode(token); + OAuth2TokenValidatorResult result = defaultTokenValidator.validate(decodedToken); + if (result.hasErrors()) { + resp.setStatusCode(HttpStatus.FORBIDDEN); + System.out.println("Token is not valid:"); + for (var tokenError : result.getErrors()) { + System.out.printf("Oauth2Error: %s%n", tokenError); + } + return false; + } resp.setStatusCode(HttpStatus.SWITCHING_PROTOCOLS); System.out.println("token valid"); - } catch (IndexOutOfBoundsException e) { + } catch (Exception e) { resp.setStatusCode(HttpStatus.UNAUTHORIZED); return false; } - catch (VerificationException e) { - resp.setStatusCode(HttpStatus.FORBIDDEN); - System.out.println(e.getMessage()); - System.out.println(); - return false; - } + return true; } diff --git a/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/config/StompConfig.java b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/config/StompConfig.java index d8e57ef6a..870e03287 100644 --- a/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/config/StompConfig.java +++ b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/config/StompConfig.java @@ -1,13 +1,16 @@ package us.dot.its.jpo.ode.api.config; -import org.keycloak.adapters.springboot.KeycloakSpringBootProperties; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; import lombok.RequiredArgsConstructor; +import us.dot.its.jpo.ode.api.ConflictMonitorApiProperties; import us.dot.its.jpo.ode.api.auth.StompHandshakeInterceptor; @Configuration @@ -15,7 +18,10 @@ @RequiredArgsConstructor public class StompConfig implements WebSocketMessageBrokerConfigurer { - private final KeycloakSpringBootProperties configuration; + + private final OAuth2TokenValidator<Jwt> defaultTokenValidator; + private final JwtDecoder jwtDecoder; + private final ConflictMonitorApiProperties properties; @Override public void configureMessageBroker(MessageBrokerRegistry config) { @@ -26,7 +32,7 @@ public void configureMessageBroker(MessageBrokerRegistry config) { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/stomp") - .addInterceptors(new StompHandshakeInterceptor(configuration)) - .setAllowedOrigins("*"); + .addInterceptors(new StompHandshakeInterceptor(defaultTokenValidator, jwtDecoder)) + .setAllowedOrigins(properties.getCors()); } } \ No newline at end of file diff --git a/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/controllers/UserController.java b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/controllers/UserController.java index a984314e5..5b9e1b7b7 100644 --- a/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/controllers/UserController.java +++ b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/controllers/UserController.java @@ -5,7 +5,7 @@ import java.util.List; import java.util.Map; -import javax.ws.rs.core.Response; +import jakarta.ws.rs.core.Response; import org.apache.commons.lang3.exception.ExceptionUtils; import org.keycloak.KeycloakPrincipal; diff --git a/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/config/JwtSecurityConfig.java b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/config/JwtSecurityConfig.java new file mode 100644 index 000000000..efac71840 --- /dev/null +++ b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/config/JwtSecurityConfig.java @@ -0,0 +1,85 @@ +package us.dot.its.jpo.ode.api.keycloak.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.jwt.*; +import us.dot.its.jpo.ode.api.keycloak.support.KeycloakGrantedAuthoritiesConverter; +import us.dot.its.jpo.ode.api.keycloak.support.KeycloakJwtAuthenticationConverter; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +/** + * Configures JWT handling (decoder and validator) + */ +@Configuration +@ConditionalOnProperty(prefix = "security", + name = "enabled", + havingValue = "true") // Allow disabling security +class JwtSecurityConfig { + + /** + * Configures a decoder with the specified validators (validation key fetched from JWKS endpoint) + * + * @param validators validators for the given key + * @param properties key properties (provides JWK location) + * @return the decoder bean + */ + @Bean + JwtDecoder jwtDecoder(List<OAuth2TokenValidator<Jwt>> validators, OAuth2ResourceServerProperties properties) { + + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder // + .withJwkSetUri(properties.getJwt().getJwkSetUri()) // + .jwsAlgorithms(algs -> algs.addAll(Set.of(SignatureAlgorithm.RS256, SignatureAlgorithm.ES256))) // + .build(); + + jwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(validators)); + + return jwtDecoder; + } + + /** + * Configures the token validator. Specifies two additional validation constraints: + * <p> + * * Timestamp on the token is still valid + * * The issuer is the expected entity + * + * @param properties JWT resource specification + * @return token validator + */ + @Bean + OAuth2TokenValidator<Jwt> defaultTokenValidator(OAuth2ResourceServerProperties properties) { + + List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>(); + validators.add(new JwtTimestampValidator()); + validators.add(new JwtIssuerValidator(properties.getJwt().getIssuerUri())); + + return new DelegatingOAuth2TokenValidator<>(validators); + } + + @Bean + KeycloakJwtAuthenticationConverter keycloakJwtAuthenticationConverter(Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter) { + return new KeycloakJwtAuthenticationConverter(authoritiesConverter); + } + + + + @Bean + Converter<Jwt, Collection<GrantedAuthority>> keycloakGrantedAuthoritiesConverter( + GrantedAuthoritiesMapper authoritiesMapper, + @Value("${keycloak.resource}") String clientId) { + return new KeycloakGrantedAuthoritiesConverter(clientId, authoritiesMapper); + } + +} diff --git a/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/config/KeycloakAdminConfig.java b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/config/KeycloakAdminConfig.java new file mode 100644 index 000000000..60efb25d1 --- /dev/null +++ b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/config/KeycloakAdminConfig.java @@ -0,0 +1,45 @@ +package us.dot.its.jpo.ode.api.keycloak.config; + +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.KeycloakBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class KeycloakAdminConfig { + + @Value("${security.enabled:true}") + private boolean securityEnabled; + + @Value("${keycloak.realm}") + private String realm; + + @Value("${keycloak.resource}") + private String resource; + + @Value("${keycloak.auth-server-url}") + private String authServer; + + @Value("${keycloak_username}") + private String username; + + @Value("${keycloak_password}") + private String password; + + // Keycloak admin client used for email + @Bean + public Keycloak keyCloakBuilder() { + System.out.println("Auth Server: " + authServer); + System.out.println("Realm: " + realm); + System.out.println("Resource: " + resource); + return KeycloakBuilder.builder() + .serverUrl(authServer) + .grantType("password") + .realm("master") + .clientId("admin-cli") + .username(username) + .password(password) + .build(); + } +} diff --git a/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/config/KeycloakNoSecurityConfig.java b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/config/KeycloakNoSecurityConfig.java new file mode 100644 index 000000000..802d98e47 --- /dev/null +++ b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/config/KeycloakNoSecurityConfig.java @@ -0,0 +1,48 @@ +package us.dot.its.jpo.ode.api.keycloak.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import us.dot.its.jpo.ode.api.ConflictMonitorApiProperties; +import us.dot.its.jpo.ode.api.keycloak.support.CorsUtil; + +import static org.springframework.security.config.Customizer.withDefaults; + +/** + * Alternative keycloack configuration for when security is disabled + */ +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +@ConditionalOnProperty(prefix = "security", + name = "enabled", + havingValue = "false") // Allow disabling security +public class KeycloakNoSecurityConfig { + + final ConflictMonitorApiProperties properties; + + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { + + System.out.println("Running without KeyCloak Authentication"); + return httpSecurity + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .cors(corsConfigurer -> CorsUtil.configureCors(corsConfigurer, properties)) + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests( + request -> request.anyRequest().permitAll() + ) + .anonymous(withDefaults()) + .build(); + + } + + +} diff --git a/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/config/KeycloakSecurityConfig.java b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/config/KeycloakSecurityConfig.java new file mode 100644 index 000000000..897caa729 --- /dev/null +++ b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/config/KeycloakSecurityConfig.java @@ -0,0 +1,65 @@ +package us.dot.its.jpo.ode.api.keycloak.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +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.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import us.dot.its.jpo.ode.api.ConflictMonitorApiProperties; +import us.dot.its.jpo.ode.api.keycloak.support.AccessController; +import us.dot.its.jpo.ode.api.keycloak.support.CorsUtil; +import us.dot.its.jpo.ode.api.keycloak.support.KeycloakJwtAuthenticationConverter; + + +/** + * Provides keycloak based spring security configuration. + */ +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +@ConditionalOnProperty(prefix = "security", + name = "enabled", + havingValue = "true") // Allow disabling security +public class KeycloakSecurityConfig { + + final ConflictMonitorApiProperties properties; + + final KeycloakJwtAuthenticationConverter keycloakJwtAuthenticationConverter; + + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { + + System.out.println("Running with KeyCloak Authentication"); + + return httpSecurity + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .cors(corsConfigurer -> CorsUtil.configureCors(corsConfigurer, properties)) + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(request -> request + .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // Allow CORS preflight + .requestMatchers("/**").access(AccessController::checkAccess) + .anyRequest().authenticated() + ) + .oauth2ResourceServer(resourceServerConfigurer -> resourceServerConfigurer.jwt( + jwtConfigurer -> jwtConfigurer.jwtAuthenticationConverter(keycloakJwtAuthenticationConverter) + + )) + .build(); + + } + + + @Bean + AccessController accessController() { + return new AccessController(); + } + + + +} \ No newline at end of file diff --git a/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/config/MethodSecurityConfig.java b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/config/MethodSecurityConfig.java new file mode 100644 index 000000000..d49bf0a8a --- /dev/null +++ b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/config/MethodSecurityConfig.java @@ -0,0 +1,57 @@ +package us.dot.its.jpo.ode.api.keycloak.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.access.PermissionEvaluator; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper; + + +/** + * Enables security annotations via like {@link org.springframework.security.access.prepost.PreAuthorize} and + * {@link org.springframework.security.access.prepost.PostAuthorize} annotations per-method. + */ +@Configuration +@EnableMethodSecurity(prePostEnabled = true, jsr250Enabled = true) // jsr250 = @RolesAllowed +@ConditionalOnProperty(prefix = "security", + name = "enabled", + havingValue = "true") // Allow disabling security +class MethodSecurityConfig { + + + + private final ApplicationContext applicationContext; + + private final PermissionEvaluator permissionEvaluator; + + @Autowired + public MethodSecurityConfig(PermissionEvaluator permissionEvaluator, ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + this.permissionEvaluator = permissionEvaluator; + System.out.println("Method-level security annotations are enabled"); + } + + @Bean + MethodSecurityExpressionHandler customMethodSecurityExpressionHandler() { + + var expressionHandler = new DefaultMethodSecurityExpressionHandler(); + expressionHandler.setApplicationContext(applicationContext); + expressionHandler.setPermissionEvaluator(permissionEvaluator); + return expressionHandler; + } + + @Bean + GrantedAuthoritiesMapper keycloakAuthoritiesMapper() { + + var mapper = new SimpleAuthorityMapper(); + mapper.setConvertToUpperCase(true); + return mapper; + } + +} diff --git a/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/package-info.java b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/package-info.java new file mode 100644 index 000000000..ab2f000ad --- /dev/null +++ b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/package-info.java @@ -0,0 +1,6 @@ +/** + * <p>Classes to support using Keycloak with Spring Boot 3, adapted from the example here: + * <a href="https://github.com/thomasdarimont/keycloak-project-example/tree/main/apps/backend-api-springboot3"> + * Keycloak Spring Boot 3 Backend API Example App</a> + */ +package us.dot.its.jpo.ode.api.keycloak; \ No newline at end of file diff --git a/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/support/AccessController.java b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/support/AccessController.java new file mode 100644 index 000000000..52a8e3f2b --- /dev/null +++ b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/support/AccessController.java @@ -0,0 +1,31 @@ +package us.dot.its.jpo.ode.api.keycloak.support; + +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.access.intercept.RequestAuthorizationContext; + +import java.util.function.Supplier; + +/** + * Example for generic custom access checks on request level. + * + * Logs access attempts and checks whether authenticated. + */ +public class AccessController { + + private static final AuthorizationDecision GRANTED = new AuthorizationDecision(true); + private static final AuthorizationDecision DENIED = new AuthorizationDecision(false); + + public static AuthorizationDecision checkAccess(Supplier<Authentication> authentication, RequestAuthorizationContext requestContext) { + + var auth = authentication.get(); + + System.out.printf("Check access for username=%s path=%s%n", auth.getName(), requestContext.getRequest().getRequestURI()); + System.out.printf("Authorities: %s%n", auth.getAuthorities()); + System.out.printf("Is authenticated: %s%n", auth.isAuthenticated()); + System.out.printf("Details: %s%n", auth.getDetails()); + + return auth.isAuthenticated() ? GRANTED : DENIED; + } +} + diff --git a/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/support/CorsUtil.java b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/support/CorsUtil.java new file mode 100644 index 000000000..3faa3db99 --- /dev/null +++ b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/support/CorsUtil.java @@ -0,0 +1,42 @@ +package us.dot.its.jpo.ode.api.keycloak.support; + +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.CorsConfigurer; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import us.dot.its.jpo.ode.api.ConflictMonitorApiProperties; + +import java.util.List; + +/** + * Provides utility methods for configuring CORS + */ +public class CorsUtil { + + /** + * Configures CORS + * + * @param cors mutable cors configuration + * + * + */ + public static void configureCors(CorsConfigurer<HttpSecurity> cors, ConflictMonitorApiProperties properties) { + + UrlBasedCorsConfigurationSource defaultUrlBasedCorsConfigSource = new UrlBasedCorsConfigurationSource(); + CorsConfiguration corsConfiguration = new CorsConfiguration().applyPermitDefaultValues(); + corsConfiguration.addAllowedOrigin(properties.getCors()); + List.of("GET", "POST", "PUT", "DELETE", "OPTIONS").forEach(corsConfiguration::addAllowedMethod); + defaultUrlBasedCorsConfigSource.registerCorsConfiguration("/**", corsConfiguration); + + cors.configurationSource(req -> { + + CorsConfiguration config = new CorsConfiguration(); + + config = config.combine(defaultUrlBasedCorsConfigSource.getCorsConfiguration(req)); + + // check if request Header "origin" is in white-list -> dynamically generate cors config + + return config; + }); + } +} diff --git a/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/support/DefaultPermissionEvaluator.java b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/support/DefaultPermissionEvaluator.java new file mode 100644 index 000000000..5bee1f52f --- /dev/null +++ b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/support/DefaultPermissionEvaluator.java @@ -0,0 +1,29 @@ +package us.dot.its.jpo.ode.api.keycloak.support; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.PermissionEvaluator; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import java.io.Serializable; + +/** + * Custom {@link PermissionEvaluator} for method level permission checks. + */ +@Component +@RequiredArgsConstructor +class DefaultPermissionEvaluator implements PermissionEvaluator { + + @Override + public boolean hasPermission(Authentication auth, Object targetDomainObject, Object permission) { + System.out.printf("check permission user=%s target=%s permission=%s%n", auth.getName(), targetDomainObject, permission); + + return true; + } + + @Override + public boolean hasPermission(Authentication auth, Serializable targetId, String targetType, Object permission) { + DomainObjectReference dor = new DomainObjectReference(targetType, targetId.toString()); + return hasPermission(auth, dor, permission); + } +} diff --git a/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/support/DomainObjectReference.java b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/support/DomainObjectReference.java new file mode 100644 index 000000000..e1e409601 --- /dev/null +++ b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/support/DomainObjectReference.java @@ -0,0 +1,14 @@ +package us.dot.its.jpo.ode.api.keycloak.support; + +import lombok.Data; + +/** + * Defines a single domain object by a type and name to look up + */ +@Data +public class DomainObjectReference { + + private final String type; + + private final String id; +} \ No newline at end of file diff --git a/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/support/KeycloakGrantedAuthoritiesConverter.java b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/support/KeycloakGrantedAuthoritiesConverter.java new file mode 100644 index 000000000..32910aee9 --- /dev/null +++ b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/support/KeycloakGrantedAuthoritiesConverter.java @@ -0,0 +1,110 @@ +package us.dot.its.jpo.ode.api.keycloak.support; + +import org.springframework.core.convert.converter.Converter; +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.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Allows to extract granted authorities from a given JWT. The authorities + * are determined by combining the realm (overarching) and client (application-specific) + * roles, and normalizing them (configure them to the default format). + */ +public class KeycloakGrantedAuthoritiesConverter implements Converter<Jwt, Collection<GrantedAuthority>> { + + private static final Converter<Jwt, Collection<GrantedAuthority>> JWT_SCOPE_GRANTED_AUTHORITIES_CONVERTER = new JwtGrantedAuthoritiesConverter(); + + private final String clientId; + + private final GrantedAuthoritiesMapper authoritiesMapper; + + public KeycloakGrantedAuthoritiesConverter(String clientId, GrantedAuthoritiesMapper authoritiesMapper) { + this.clientId = clientId; + this.authoritiesMapper = authoritiesMapper; + } + + @Override + public Collection<GrantedAuthority> convert(Jwt jwt) { + + Collection<GrantedAuthority> authorities = mapKeycloakRolesToAuthorities( // + getRealmRolesFrom(jwt), // + getClientRolesFrom(jwt, clientId) // + ); + + Collection<GrantedAuthority> scopeAuthorities = JWT_SCOPE_GRANTED_AUTHORITIES_CONVERTER.convert(jwt); + if(!CollectionUtils.isEmpty(scopeAuthorities)) { + authorities.addAll(scopeAuthorities); + } + + return authorities; + } + + protected Collection<GrantedAuthority> mapKeycloakRolesToAuthorities(Set<String> realmRoles, Set<String> clientRoles) { + + List<GrantedAuthority> combinedAuthorities = new ArrayList<>(); + + combinedAuthorities.addAll(authoritiesMapper.mapAuthorities(realmRoles.stream() // + .map(SimpleGrantedAuthority::new) // + .collect(Collectors.toList()))); + + combinedAuthorities.addAll(authoritiesMapper.mapAuthorities(clientRoles.stream() // + .map(SimpleGrantedAuthority::new) // + .collect(Collectors.toList()))); + + return combinedAuthorities; + } + + protected Set<String> getRealmRolesFrom(Jwt jwt) { + + Map<String, Object> realmAccess = jwt.getClaimAsMap("realm_access"); + + if (CollectionUtils.isEmpty(realmAccess)) { + return Collections.emptySet(); + } + + @SuppressWarnings("unchecked") + Collection<String> realmRoles = (Collection<String>) realmAccess.get("roles"); + if (CollectionUtils.isEmpty(realmRoles)) { + return Collections.emptySet(); + } + + return realmRoles.stream().map(this::normalizeRole).collect(Collectors.toSet()); + } + + protected Set<String> getClientRolesFrom(Jwt jwt, String clientId) { + + Map<String, Object> resourceAccess = jwt.getClaimAsMap("resource_access"); + + if (CollectionUtils.isEmpty(resourceAccess)) { + return Collections.emptySet(); + } + + @SuppressWarnings("unchecked") + Map<String, List<String>> clientAccess = (Map<String, List<String>>) resourceAccess.get(clientId); + if (CollectionUtils.isEmpty(clientAccess)) { + return Collections.emptySet(); + } + + List<String> clientRoles = clientAccess.get("roles"); + if (CollectionUtils.isEmpty(clientRoles)) { + return Collections.emptySet(); + } + + return clientRoles.stream().map(this::normalizeRole).collect(Collectors.toSet()); + } + + private String normalizeRole(String role) { + return role.replace('-', '_'); + } +} diff --git a/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/support/KeycloakJwtAuthenticationConverter.java b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/support/KeycloakJwtAuthenticationConverter.java new file mode 100644 index 000000000..16dec044d --- /dev/null +++ b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/keycloak/support/KeycloakJwtAuthenticationConverter.java @@ -0,0 +1,45 @@ +package us.dot.its.jpo.ode.api.keycloak.support; + +import lombok.RequiredArgsConstructor; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +import java.util.Collection; + +/** + * Converts a JWT into a Spring authentication token (by extracting + * the username and roles from the claims of the token, delegating + * to the {@link KeycloakGrantedAuthoritiesConverter}) + */ +@RequiredArgsConstructor +public class KeycloakJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> { + + private Converter<Jwt, Collection<GrantedAuthority>> grantedAuthoritiesConverter; + + public KeycloakJwtAuthenticationConverter(Converter<Jwt, Collection<GrantedAuthority>> grantedAuthoritiesConverter) { + this.grantedAuthoritiesConverter = grantedAuthoritiesConverter; + } + + @Override + public JwtAuthenticationToken convert(Jwt jwt) { + + Collection<GrantedAuthority> authorities = grantedAuthoritiesConverter.convert(jwt); + String username = getUsernameFrom(jwt); + + var token = new JwtAuthenticationToken(jwt, authorities, username); + System.out.printf("KeycloakJwtAuthenticationConverter: Converted token: %s%n Authorities: %s%n Username: %s%n", token, authorities, username); + return token; + } + + protected String getUsernameFrom(Jwt jwt) { + + if (jwt.hasClaim("preferred_username")) { + return jwt.getClaimAsString("preferred_username"); + } + + return jwt.getSubject(); + } +} diff --git a/jpo-conflictvisualizer-api/src/main/resources/application.yaml b/jpo-conflictvisualizer-api/src/main/resources/application.yaml index a94fbb40d..2168f18df 100644 --- a/jpo-conflictvisualizer-api/src/main/resources/application.yaml +++ b/jpo-conflictvisualizer-api/src/main/resources/application.yaml @@ -1,21 +1,17 @@ # application configuration # server: # port: 8081 -# keycloak configuration + +### keycloak configuration keycloak: - # name of the created realm realm: conflictvisualizer - # name of the created client - resource: conflictvisualizer-api - # indicates that our service has been created as a bearer-only (by default it is false) - bearer-only: true - # url of our Keycloak server - # auth-server-url: 'http://172.250.250.181:8084' - ssl-required: none - + resource: conflictvisualizer-gui # client id auth-server-url: ${AUTH_SERVER_URL:http://localhost:8084} keycloak_username: ${KEYCLOAK_ADMIN:admin} keycloak_password: ${KEYCLOAK_ADMIN_PASSWORD:admin} +spring.security.oauth2.resourceserver.jwt: + issuer-uri: ${AUTH_SERVER_URL:http://localhost:8084}/realms/conflictvisualizer + jwk-set-uri: ${AUTH_SERVER_URL:http://localhost:8084}/realms/conflictvisualizer/protocol/openid-connect/certs spring.kafka.bootstrap-servers: localhost:9092 logging.level.org.apache.kafka: INFO diff --git a/jpo-conflictvisualizer-api/src/test/java/us/dot/its/jpo/ode/api/MockKeyCloakAuth.java b/jpo-conflictvisualizer-api/src/test/java/us/dot/its/jpo/ode/api/MockKeyCloakAuth.java index fa2bb41d7..ad88cfdf4 100644 --- a/jpo-conflictvisualizer-api/src/test/java/us/dot/its/jpo/ode/api/MockKeyCloakAuth.java +++ b/jpo-conflictvisualizer-api/src/test/java/us/dot/its/jpo/ode/api/MockKeyCloakAuth.java @@ -1,26 +1,32 @@ package us.dot.its.jpo.ode.api; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - import java.security.Principal; +import java.util.ArrayList; +import java.util.Collection; import java.util.Set; -import org.keycloak.adapters.OidcKeycloakAccount; -import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; + +import org.mockito.Mockito; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; +import static org.mockito.Mockito.*; + public class MockKeyCloakAuth { public static void setSecurityContextHolder(String user, Set<String> roles){ Principal principal = mock(Principal.class); when(principal.getName()).thenReturn(user); - OidcKeycloakAccount account = mock(OidcKeycloakAccount.class); - when(account.getRoles()).thenReturn(roles); - when(account.getPrincipal()).thenReturn(principal); - - KeycloakAuthenticationToken authentication = mock(KeycloakAuthenticationToken.class); - when(authentication.getAccount()).thenReturn(account); + Authentication authentication = mock(Authentication.class); + when(authentication.isAuthenticated()).thenReturn(true); + Collection<GrantedAuthority> authorities = new ArrayList<>(); + for (String role : roles) { + authorities.add(new SimpleGrantedAuthority("ROLE_" + role)); + } + // type safe "when" doesn't work here + doReturn(authorities).when(authentication).getAuthorities(); SecurityContextHolder.getContext().setAuthentication(authentication); }