Allow anonymous requests for OPTIONS
This commit is contained in:
+8
-3
@@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-4
@@ -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) {
|
||||||
|
|||||||
+86
@@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+102
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user