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);
+ }
+}