diff --git a/jpo-conflictvisualizer-api/pom.xml b/jpo-conflictvisualizer-api/pom.xml index 03739c3b7..c536b1df7 100644 --- a/jpo-conflictvisualizer-api/pom.xml +++ b/jpo-conflictvisualizer-api/pom.xml @@ -141,10 +141,6 @@ org.springframework.boot spring-boot-starter-security - - org.springframework.boot - spring-boot-starter-oauth2-client - org.springframework.boot spring-boot-starter-oauth2-resource-server 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/CorsFilter.java b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/CorsFilter.java index 7782d272e..8a62f2cab 100644 --- a/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/CorsFilter.java +++ b/jpo-conflictvisualizer-api/src/main/java/us/dot/its/jpo/ode/api/CorsFilter.java @@ -1,33 +1,33 @@ -package us.dot.its.jpo.ode.api; - -import jakarta.servlet.*; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import java.io.IOException; - -/** - * Custom servlet filter to add CORS header - */ -public class CorsFilter implements Filter { - @Override - public void init(FilterConfig filterConfig) throws ServletException { - // Nothing to initialize - } - - @Override - public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { - var response = (HttpServletResponse)servletResponse; - var request = (HttpServletRequest)servletRequest; - response.setHeader("Access-Control-Allow-Origin", "*"); - response.setHeader("Access-Control-Allow-Methods", "OPTIONS,GET,POST,DELETE"); - response.setHeader("Access-Control-Allow-Headers", "authorization"); - response.setIntHeader("Access-Control-Max-Age", 1800); - filterChain.doFilter(servletRequest, servletResponse); - } - - @Override - public void destroy() { - // Nothing to destroy - } -} +//package us.dot.its.jpo.ode.api; +// +//import jakarta.servlet.*; +//import jakarta.servlet.http.HttpServletRequest; +//import jakarta.servlet.http.HttpServletResponse; +// +//import java.io.IOException; +// +///** +// * Custom servlet filter to add CORS header +// */ +//public class CorsFilter implements Filter { +// @Override +// public void init(FilterConfig filterConfig) throws ServletException { +// // Nothing to initialize +// } +// +// @Override +// public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { +// var response = (HttpServletResponse)servletResponse; +// var request = (HttpServletRequest)servletRequest; +// response.setHeader("Access-Control-Allow-Origin", "*"); +// response.setHeader("Access-Control-Allow-Methods", "OPTIONS,GET,POST,DELETE"); +// response.setHeader("Access-Control-Allow-Headers", "authorization"); +// response.setIntHeader("Access-Control-Max-Age", 1800); +// filterChain.doFilter(servletRequest, servletResponse); +// } +// +// @Override +// public void destroy() { +// // Nothing to destroy +// } +//} 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 index ab7e3532b..f9e1c466c 100644 --- 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 @@ -2,6 +2,7 @@ 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; @@ -12,20 +13,35 @@ 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.annotation.web.configurers.CorsConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +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.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; +import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.session.SessionManagementFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.*; +import java.util.stream.Collectors; import static org.springframework.security.config.Customizer.withDefaults; -// provides keycloak based spring security configuration -// annotation covers 2 annotations - @Configuration and @EnableWebSecurity -//@KeycloakConfiguration +/** + * provides keycloak based spring security configuration + * annotation covers 2 annotations - @Configuration and @EnableWebSecurity + * + * @see Keycloak Spring Boot 3 API Example App + */ @Configuration @EnableWebSecurity public class KeycloakConfig { @@ -54,32 +70,30 @@ public class KeycloakConfig { @Value("${keycloak.client-secret}") private String clientSecret; - // This condition allows for disabling security -// @ConditionalOnProperty(prefix = "security", -// name = "enabled", -// havingValue = "true") -// @EnableMethodSecurity(prePostEnabled = true, jsr250Enabled = true) // Allow @PreAuthorize and @RoleAllowed annotations -// static class Dummy { -// public Dummy(){ -// System.out.println("Initializing Security"); -// } -// -// } + private ConflictMonitorApiProperties properties; - @Bean - CorsFilter corsFilter() { - return new CorsFilter(); + @Autowired + public KeycloakConfig(ConflictMonitorApiProperties properties) { + this.properties = properties; } +// @Bean +// CorsFilter corsFilter() { +// return new CorsFilter(); +// } + + @Bean public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { if(securityEnabled){ System.out.println("Running with KeyCloak Authentication"); return httpSecurity - .addFilterBefore(corsFilter(), SessionManagementFilter.class) + //.addFilterBefore(corsFilter(), SessionManagementFilter.class) + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .cors(this::configureCors) .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(request -> request .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // Allow CORS preflight @@ -92,7 +106,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws }else{ System.out.println("Running without KeyCloak Authentication"); return httpSecurity - .addFilterBefore(corsFilter(), SessionManagementFilter.class) + //.addFilterBefore(corsFilter(), SessionManagementFilter.class) + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .cors(this::configureCors) .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests( request -> request.anyRequest().permitAll() @@ -120,5 +136,97 @@ public Keycloak keyCloakBuilder() { .build(); } + /** + * Configures CORS + * + * @param cors mutable cors configuration + * + * + */ + protected void configureCors(CorsConfigurer cors) { + + 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; + }); + } + +// // This condition allows for disabling security +// @ConditionalOnProperty(prefix = "security", +// name = "enabled", +// havingValue = "true") +// @EnableMethodSecurity(prePostEnabled = true, jsr250Enabled = true) // Allow @PreAuthorize and @RoleAllowed annotations +// static class Dummy { +// public Dummy(){ +// System.out.println("Initializing Security"); +// } +// +// } + +// private static final String GROUPS = "groups"; +// private static final String REALM_ACCESS_CLAIM = "realm_access"; +// private static final String ROLES_CLAIM = "roles"; +// +// /** +// * Needed to get role-based authorization to work. +// * @see https://www.baeldung.com/spring-boot-keycloak +// */ +// @Bean +// public GrantedAuthoritiesMapper userAuthoritiesMapperForKeycloak() { +// return authorities -> { +// System.out.printf("Authorities: %s%n", authorities); +// Set mappedAuthorities = new HashSet<>(); +// var authority = authorities.iterator().next(); +// boolean isOidc = authority instanceof OidcUserAuthority; +// +// if (isOidc) { +// var oidcUserAuthority = (OidcUserAuthority) authority; +// var userInfo = oidcUserAuthority.getUserInfo(); +// +// // Tokens can be configured to return roles under +// // Groups or REALM ACCESS hence have to check both +// if (userInfo.hasClaim(REALM_ACCESS_CLAIM)) { +// var realmAccess = userInfo.getClaimAsMap(REALM_ACCESS_CLAIM); +// var roles = (Collection) realmAccess.get(ROLES_CLAIM); +// mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles)); +// } else if (userInfo.hasClaim(GROUPS)) { +// Collection roles = (Collection) userInfo.getClaim( +// GROUPS); +// mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles)); +// } +// } else { +// var oauth2UserAuthority = (OAuth2UserAuthority) authority; +// Map userAttributes = oauth2UserAuthority.getAttributes(); +// +// if (userAttributes.containsKey(REALM_ACCESS_CLAIM)) { +// Map realmAccess = (Map) userAttributes.get( +// REALM_ACCESS_CLAIM); +// Collection roles = (Collection) realmAccess.get(ROLES_CLAIM); +// mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles)); +// } +// } +// System.out.printf("Mapped Authorities: %s%n", mappedAuthorities); +// return mappedAuthorities; +// }; +// } +// +// Collection generateAuthoritiesFromClaim(Collection roles) { +// return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).collect( +// Collectors.toList()); +// } + + } \ No newline at end of file