From 80408dab3161d6e3416b23e5837ad4320e6f0a3b Mon Sep 17 00:00:00 2001 From: Jim Martens Date: Sun, 23 Jul 2023 10:31:08 +0200 Subject: [PATCH] Allow anonymous requests for OPTIONS --- .../configuration/WebConfiguration.java | 11 +- .../WebSecurityConfiguration.java | 18 +++- .../security/SpringPolicyEnforcer.java | 86 +++++++++++++++ .../security/SpringPolicyEnforcerFilter.java | 102 ++++++++++++++++++ 4 files changed, 210 insertions(+), 7 deletions(-) create mode 100644 module-server/src/main/java/de/twomartens/wahlrecht/security/SpringPolicyEnforcer.java create mode 100644 module-server/src/main/java/de/twomartens/wahlrecht/security/SpringPolicyEnforcerFilter.java diff --git a/module-server/src/main/java/de/twomartens/wahlrecht/configuration/WebConfiguration.java b/module-server/src/main/java/de/twomartens/wahlrecht/configuration/WebConfiguration.java index 653b4a7..2becedf 100644 --- a/module-server/src/main/java/de/twomartens/wahlrecht/configuration/WebConfiguration.java +++ b/module-server/src/main/java/de/twomartens/wahlrecht/configuration/WebConfiguration.java @@ -3,6 +3,8 @@ package de.twomartens.wahlrecht.configuration; import de.twomartens.wahlrecht.interceptor.HeaderInterceptorRest; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.lang.NonNull; import org.springframework.web.servlet.config.annotation.CorsRegistration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; @@ -15,14 +17,17 @@ public class WebConfiguration implements WebMvcConfigurer { private final HeaderInterceptorRest headerInterceptorRest; @Override - public void addInterceptors(InterceptorRegistry registry) { + public void addInterceptors(@NonNull InterceptorRegistry registry) { registry.addInterceptor(headerInterceptorRest); } @Override - public void addCorsMappings(CorsRegistry registry) { + public void addCorsMappings(@NonNull CorsRegistry registry) { CorsRegistration registration = registry.addMapping("/**"); - registration.allowedOrigins("*"); + registration.allowedMethods(HttpMethod.GET.name(), HttpMethod.POST.name(), + HttpMethod.PUT.name(), HttpMethod.OPTIONS.name()); + registration.allowCredentials(true); + registration.allowedOrigins("http://localhost:4200"); } } diff --git a/module-server/src/main/java/de/twomartens/wahlrecht/configuration/WebSecurityConfiguration.java b/module-server/src/main/java/de/twomartens/wahlrecht/configuration/WebSecurityConfiguration.java index e2ca501..d82f41d 100644 --- a/module-server/src/main/java/de/twomartens/wahlrecht/configuration/WebSecurityConfiguration.java +++ b/module-server/src/main/java/de/twomartens/wahlrecht/configuration/WebSecurityConfiguration.java @@ -1,12 +1,12 @@ package de.twomartens.wahlrecht.configuration; +import de.twomartens.wahlrecht.security.SpringPolicyEnforcerFilter; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; -import org.keycloak.adapters.authorization.integration.jakarta.ServletPolicyEnforcerFilter; import org.keycloak.adapters.authorization.spi.ConfigurationResolver; import org.keycloak.adapters.authorization.spi.HttpRequest; import org.keycloak.representations.adapters.config.PolicyEnforcerConfig; @@ -16,6 +16,7 @@ import org.keycloak.util.JsonSerialization; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.lang.NonNull; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -53,6 +54,10 @@ public class WebSecurityConfiguration { .requestMatchers(PERMITTED_PATHS.toArray(new String[0])) .permitAll() .and() + .authorizeHttpRequests() + .requestMatchers(HttpMethod.OPTIONS) + .permitAll() + .and() .authorizeHttpRequests() .anyRequest() .authenticated() @@ -67,15 +72,20 @@ public class WebSecurityConfiguration { return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri).build(); } - private ServletPolicyEnforcerFilter createPolicyEnforcerFilter() { - return new ServletPolicyEnforcerFilter(new ConfigurationResolver() { + private SpringPolicyEnforcerFilter createPolicyEnforcerFilter() { + return new SpringPolicyEnforcerFilter(new ConfigurationResolver() { @Override public PolicyEnforcerConfig resolve(HttpRequest request) { try { PolicyEnforcerConfig policyEnforcerConfig = JsonSerialization.readValue( getClass().getResourceAsStream("/policy-enforcer.json"), PolicyEnforcerConfig.class); policyEnforcerConfig.setCredentials(Map.of("secret", clientSecret)); - policyEnforcerConfig.setPaths(PATHS); + if (request.getMethod().equals(HttpMethod.OPTIONS.name())) { + // always allow options request + policyEnforcerConfig.setEnforcementMode(EnforcementMode.DISABLED); + } else { + policyEnforcerConfig.setPaths(PATHS); + } return policyEnforcerConfig; } catch (IOException e) { diff --git a/module-server/src/main/java/de/twomartens/wahlrecht/security/SpringPolicyEnforcer.java b/module-server/src/main/java/de/twomartens/wahlrecht/security/SpringPolicyEnforcer.java new file mode 100644 index 0000000..77b5172 --- /dev/null +++ b/module-server/src/main/java/de/twomartens/wahlrecht/security/SpringPolicyEnforcer.java @@ -0,0 +1,86 @@ +package de.twomartens.wahlrecht.security; + +import java.util.Collections; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.keycloak.AuthorizationContext; +import org.keycloak.adapters.authorization.PolicyEnforcer; +import org.keycloak.adapters.authorization.spi.HttpRequest; +import org.keycloak.adapters.authorization.spi.HttpResponse; +import org.keycloak.authorization.client.ClientAuthorizationContext; +import org.keycloak.representations.adapters.config.PolicyEnforcerConfig; +import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode; +import org.keycloak.representations.idm.authorization.Permission; + +/** + * Allows any request, even from anonymous, if enforcement mode is disabled. For remaining requests, + * it refers to the original PolicyEnforcer. + */ +@Slf4j +public class SpringPolicyEnforcer { + + private final PolicyEnforcer policyEnforcer; + private final PolicyEnforcerConfig policyEnforcerConfig; + + public SpringPolicyEnforcer(PolicyEnforcer policyEnforcer, PolicyEnforcerConfig policyEnforcerConfig) { + this.policyEnforcer = policyEnforcer; + this.policyEnforcerConfig = policyEnforcerConfig; + } + + public AuthorizationContext enforce(HttpRequest request, HttpResponse response) { + if (log.isDebugEnabled()) { + log.debug("Policy enforcement is enabled. Enforcing policy decisions for path [{}].", request.getURI()); + } + + AuthorizationContext context = authorize(request, response); + + if (log.isDebugEnabled()) { + log.debug("Policy enforcement result for path [{}] is : {}", request.getURI(), context.isGranted() ? "GRANTED" : "DENIED"); + log.debug("Returning authorization context with permissions:"); + for (Permission permission : context.getPermissions()) { + log.debug(permission.toString()); + } + } + + return context; + } + + private AuthorizationContext authorize(HttpRequest request, HttpResponse response) { + EnforcementMode enforcementMode = policyEnforcerConfig.getEnforcementMode(); + + if (EnforcementMode.DISABLED.equals(enforcementMode)) { + return createAuthorizedContext(); + } + + return policyEnforcer.enforce(request, response); + } + + private AuthorizationContext createAuthorizedContext() { + return new ClientAuthorizationContext(policyEnforcer.getAuthzClient()) { + @Override + public boolean hasPermission(String resourceName, String scopeName) { + return true; + } + + @Override + public boolean hasResourcePermission(String resourceName) { + return true; + } + + @Override + public boolean hasScopePermission(String scopeName) { + return true; + } + + @Override + public List getPermissions() { + return Collections.emptyList(); + } + + @Override + public boolean isGranted() { + return true; + } + }; + } +} diff --git a/module-server/src/main/java/de/twomartens/wahlrecht/security/SpringPolicyEnforcerFilter.java b/module-server/src/main/java/de/twomartens/wahlrecht/security/SpringPolicyEnforcerFilter.java new file mode 100644 index 0000000..bbc84c7 --- /dev/null +++ b/module-server/src/main/java/de/twomartens/wahlrecht/security/SpringPolicyEnforcerFilter.java @@ -0,0 +1,102 @@ +package de.twomartens.wahlrecht.security; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.keycloak.AuthorizationContext; +import org.keycloak.adapters.authorization.PolicyEnforcer; +import org.keycloak.adapters.authorization.integration.elytron.ServletHttpRequest; +import org.keycloak.adapters.authorization.integration.elytron.ServletHttpResponse; +import org.keycloak.adapters.authorization.spi.ConfigurationResolver; +import org.keycloak.adapters.authorization.spi.HttpRequest; +import org.keycloak.representations.adapters.config.PolicyEnforcerConfig; +import org.springframework.lang.NonNull; + +/** + * A Jakarta Servlet {@link Filter} acting as a policy enforcer. This filter does not enforce access for anonymous subjects.

+ * + * For authenticated subjects, this filter delegates the access decision to the {@link PolicyEnforcer} and decide if + * the request should continue.

+ * + * If access is not granted, this filter aborts the request and relies on the {@link PolicyEnforcer} to properly + * respond to client. + * + * @author Pedro Igor + */ +@Slf4j +public class SpringPolicyEnforcerFilter implements Filter { + private final Map policyEnforcer; + private final ConfigurationResolver configResolver; + + public SpringPolicyEnforcerFilter(ConfigurationResolver configResolver) { + this.configResolver = configResolver; + this.policyEnforcer = Collections.synchronizedMap(new HashMap<>()); + } + + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + ServletHttpRequest httpRequest = new ServletHttpRequest(request, + () -> extractBearerToken(request)); + + SpringPolicyEnforcer policyEnforcer = getOrCreatePolicyEnforcer(request, httpRequest); + AuthorizationContext authzContext = policyEnforcer.enforce(httpRequest, new ServletHttpResponse(response)); + + request.setAttribute(AuthorizationContext.class.getName(), authzContext); + + if (authzContext.isGranted()) { + log.debug("Request authorized, continuing the filter chain"); + filterChain.doFilter(servletRequest, servletResponse); + } else { + log.debug("Unauthorized request to path [{}], aborting the filter chain", request.getRequestURI()); + } + } + + protected String extractBearerToken(@NonNull HttpServletRequest request) { + Enumeration authorizationHeaderValues = request.getHeaders("Authorization"); + + while (authorizationHeaderValues.hasMoreElements()) { + String value = authorizationHeaderValues.nextElement(); + String[] parts = value.trim().split("\\s+"); + + if (parts.length != 2) { + continue; + } + + String bearer = parts[0]; + + if (bearer.equalsIgnoreCase("Bearer")) { + return parts[1]; + } + } + + return null; + } + + private SpringPolicyEnforcer getOrCreatePolicyEnforcer(HttpServletRequest servletRequest, HttpRequest request) { + return policyEnforcer.computeIfAbsent(configResolver.resolve(request), + enforcerConfig -> createPolicyEnforcer(servletRequest, enforcerConfig)); + } + + protected SpringPolicyEnforcer createPolicyEnforcer(HttpServletRequest servletRequest, @NonNull PolicyEnforcerConfig enforcerConfig) { + String authServerUrl = enforcerConfig.getAuthServerUrl(); + + return new SpringPolicyEnforcer(PolicyEnforcer.builder() + .authServerUrl(authServerUrl) + .realm(enforcerConfig.getRealm()) + .clientId(enforcerConfig.getResource()) + .credentials(enforcerConfig.getCredentials()) + .bearerOnly(false) + .enforcerConfig(enforcerConfig).build(), enforcerConfig); + } +}