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>-->
+<!--			&lt;!&ndash; <version>20.0.3</version> &ndash;&gt;-->
+<!--		</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);
     }