Allow anonymous requests for OPTIONS

This commit is contained in:
2023-07-23 10:31:08 +02:00
parent c5c0411135
commit 80408dab31
4 changed files with 210 additions and 7 deletions
@@ -3,6 +3,8 @@ package de.twomartens.wahlrecht.configuration;
import de.twomartens.wahlrecht.interceptor.HeaderInterceptorRest; import de.twomartens.wahlrecht.interceptor.HeaderInterceptorRest;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration; 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.CorsRegistration;
import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
@@ -15,14 +17,17 @@ public class WebConfiguration implements WebMvcConfigurer {
private final HeaderInterceptorRest headerInterceptorRest; private final HeaderInterceptorRest headerInterceptorRest;
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(@NonNull InterceptorRegistry registry) {
registry.addInterceptor(headerInterceptorRest); registry.addInterceptor(headerInterceptorRest);
} }
@Override @Override
public void addCorsMappings(CorsRegistry registry) { public void addCorsMappings(@NonNull CorsRegistry registry) {
CorsRegistration registration = registry.addMapping("/**"); 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");
} }
} }
@@ -1,12 +1,12 @@
package de.twomartens.wahlrecht.configuration; package de.twomartens.wahlrecht.configuration;
import de.twomartens.wahlrecht.security.SpringPolicyEnforcerFilter;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; 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.ConfigurationResolver;
import org.keycloak.adapters.authorization.spi.HttpRequest; import org.keycloak.adapters.authorization.spi.HttpRequest;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig; 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.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; 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.configuration.EnableWebSecurity;
@@ -53,6 +54,10 @@ public class WebSecurityConfiguration {
.requestMatchers(PERMITTED_PATHS.toArray(new String[0])) .requestMatchers(PERMITTED_PATHS.toArray(new String[0]))
.permitAll() .permitAll()
.and() .and()
.authorizeHttpRequests()
.requestMatchers(HttpMethod.OPTIONS)
.permitAll()
.and()
.authorizeHttpRequests() .authorizeHttpRequests()
.anyRequest() .anyRequest()
.authenticated() .authenticated()
@@ -67,15 +72,20 @@ public class WebSecurityConfiguration {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri).build(); return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri).build();
} }
private ServletPolicyEnforcerFilter createPolicyEnforcerFilter() { private SpringPolicyEnforcerFilter createPolicyEnforcerFilter() {
return new ServletPolicyEnforcerFilter(new ConfigurationResolver() { return new SpringPolicyEnforcerFilter(new ConfigurationResolver() {
@Override @Override
public PolicyEnforcerConfig resolve(HttpRequest request) { public PolicyEnforcerConfig resolve(HttpRequest request) {
try { try {
PolicyEnforcerConfig policyEnforcerConfig = JsonSerialization.readValue( PolicyEnforcerConfig policyEnforcerConfig = JsonSerialization.readValue(
getClass().getResourceAsStream("/policy-enforcer.json"), PolicyEnforcerConfig.class); getClass().getResourceAsStream("/policy-enforcer.json"), PolicyEnforcerConfig.class);
policyEnforcerConfig.setCredentials(Map.of("secret", clientSecret)); 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; return policyEnforcerConfig;
} catch (IOException e) { } catch (IOException e) {
@@ -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<Permission> getPermissions() {
return Collections.emptyList();
}
@Override
public boolean isGranted() {
return true;
}
};
}
}
@@ -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.</p>
*
* For authenticated subjects, this filter delegates the access decision to the {@link PolicyEnforcer} and decide if
* the request should continue.</p>
*
* If access is not granted, this filter aborts the request and relies on the {@link PolicyEnforcer} to properly
* respond to client.
*
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@Slf4j
public class SpringPolicyEnforcerFilter implements Filter {
private final Map<PolicyEnforcerConfig, SpringPolicyEnforcer> 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<String> 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);
}
}