diff --git a/authz/policy-enforcer/pom.xml b/authz/policy-enforcer/pom.xml
index f1706319b6..5c00fa4468 100755
--- a/authz/policy-enforcer/pom.xml
+++ b/authz/policy-enforcer/pom.xml
@@ -30,35 +30,29 @@
Keycloak Authz: Policy Enforcer
jar
+
+ 6.0.0
+ 2.0.0.Final
+
+
-
- org.jboss.logging
- jboss-logging
-
org.keycloak
keycloak-authz-client
+
+
- com.fasterxml.jackson.core
- jackson-core
+ jakarta.servlet
+ jakarta.servlet-api
+ ${jakarta.servlet.version}
+ true
- com.fasterxml.jackson.core
- jackson-databind
-
-
- com.fasterxml.jackson.core
- jackson-annotations
-
-
- junit
- junit
- test
-
-
- org.apache.httpcomponents
- httpclient
+ org.wildfly.security
+ wildfly-elytron-http-oidc
+ ${wildfly-elytron.version}
+ true
diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
index 3c25e8d217..eb7754759b 100644
--- a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
+++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
@@ -82,7 +82,25 @@ public class PolicyEnforcer {
protected PolicyEnforcer(Builder builder) {
enforcerConfig = builder.getEnforcerConfig();
- authzClient = AuthzClient.create(builder.authzClientConfig);
+ Configuration authzClientConfig = builder.authzClientConfig;
+
+ if (authzClientConfig.getRealm() == null) {
+ authzClientConfig.setRealm(enforcerConfig.getRealm());
+ }
+
+ if (authzClientConfig.getAuthServerUrl() == null) {
+ authzClientConfig.setAuthServerUrl(enforcerConfig.getAuthServerUrl());
+ }
+
+ if (authzClientConfig.getCredentials() == null || authzClientConfig.getCredentials().isEmpty()) {
+ authzClientConfig.setCredentials(enforcerConfig.getCredentials());
+ }
+
+ if (authzClientConfig.getResource() == null) {
+ authzClientConfig.setResource(enforcerConfig.getResource());
+ }
+
+ authzClient = AuthzClient.create(authzClientConfig);
httpClient = authzClient.getConfiguration().getHttpClient();
pathMatcher = new PathConfigMatcher(builder.getEnforcerConfig(), authzClient);
paths = pathMatcher.getPathConfig();
diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/integration/elytron/PolicyEnforcerFilter.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/integration/elytron/PolicyEnforcerFilter.java
new file mode 100644
index 0000000000..9ca6164490
--- /dev/null
+++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/integration/elytron/PolicyEnforcerFilter.java
@@ -0,0 +1,110 @@
+package org.keycloak.adapters.authorization.integration.elytron;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.ServletContextAttributeListener;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+import org.jboss.logging.Logger;
+import org.keycloak.AuthorizationContext;
+import org.keycloak.adapters.authorization.PolicyEnforcer;
+import org.keycloak.adapters.authorization.TokenPrincipal;
+import org.keycloak.adapters.authorization.spi.ConfigurationResolver;
+import org.keycloak.adapters.authorization.spi.HttpRequest;
+import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
+import org.wildfly.security.http.oidc.OidcClientConfiguration;
+import org.wildfly.security.http.oidc.OidcPrincipal;
+import org.wildfly.security.http.oidc.RefreshableOidcSecurityContext;
+
+/**
+ * A {@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
+ */
+public class PolicyEnforcerFilter implements Filter, ServletContextAttributeListener {
+
+ private final Logger logger = Logger.getLogger(getClass());
+ private final Map policyEnforcer;
+ private final ConfigurationResolver configResolver;
+
+ public PolicyEnforcerFilter(ConfigurationResolver configResolver) {
+ this.configResolver = configResolver;
+ this.policyEnforcer = Collections.synchronizedMap(new HashMap<>());
+ }
+
+ @Override
+ public void init(FilterConfig filterConfig) {
+ // no-init
+ }
+
+ @Override
+ public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
+ HttpServletRequest request = (HttpServletRequest) servletRequest;
+ HttpSession session = request.getSession(false);
+
+ if (session == null) {
+ logger.debug("Anonymous request, continuing the filter chain");
+ filterChain.doFilter(servletRequest, servletResponse);
+ return;
+ }
+
+ RefreshableOidcSecurityContext securityContext = (RefreshableOidcSecurityContext) ((OidcPrincipal) request.getUserPrincipal()).getOidcSecurityContext();
+ HttpServletResponse response = (HttpServletResponse) servletResponse;
+ String accessToken = securityContext.getTokenString();
+ ServletHttpRequest httpRequest = new ServletHttpRequest(request, new TokenPrincipal() {
+ @Override
+ public String getRawToken() {
+ return accessToken;
+ }
+ });
+
+ PolicyEnforcer policyEnforcer = getOrCreatePolicyEnforcer(httpRequest, securityContext);
+ AuthorizationContext authzContext = policyEnforcer.enforce(httpRequest, new ServletHttpResponse(response));
+
+ request.setAttribute(AuthorizationContext.class.getName(), authzContext);
+
+ if (authzContext.isGranted()) {
+ logger.debug("Request authorized, continuing the filter chain");
+ filterChain.doFilter(servletRequest, servletResponse);
+ } else {
+ logger.debugf("Unauthorized request to path [%s], aborting the filter chain", request.getRequestURI());
+ }
+ }
+
+ private PolicyEnforcer getOrCreatePolicyEnforcer(HttpRequest request, RefreshableOidcSecurityContext securityContext) {
+ return policyEnforcer.computeIfAbsent(configResolver.resolve(request), new Function() {
+ @Override
+ public PolicyEnforcer apply(PolicyEnforcerConfig enforcerConfig) {
+ OidcClientConfiguration configuration = securityContext.getOidcClientConfiguration();
+ String authServerUrl = configuration.getAuthServerBaseUrl();
+
+ return PolicyEnforcer.builder()
+ .authServerUrl(authServerUrl)
+ .realm(configuration.getRealm())
+ .clientId(configuration.getClientId())
+ .credentials(configuration.getResourceCredentials())
+ .bearerOnly(false)
+ .enforcerConfig(enforcerConfig)
+ .httpClient(configuration.getClient()).build();
+ }
+ });
+ }
+}
diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/integration/elytron/PolicyEnforcerServletContextListener.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/integration/elytron/PolicyEnforcerServletContextListener.java
new file mode 100644
index 0000000000..496937966d
--- /dev/null
+++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/integration/elytron/PolicyEnforcerServletContextListener.java
@@ -0,0 +1,81 @@
+package org.keycloak.adapters.authorization.integration.elytron;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.EnumSet;
+import java.util.Iterator;
+import java.util.ServiceLoader;
+
+import jakarta.servlet.DispatcherType;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletContextEvent;
+import jakarta.servlet.ServletContextListener;
+import jakarta.servlet.annotation.WebListener;
+import org.jboss.logging.Logger;
+import org.keycloak.adapters.authorization.spi.ConfigurationResolver;
+import org.keycloak.adapters.authorization.spi.HttpRequest;
+import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ * A {@link ServletContextListener} to programmatically configure the {@link ServletContext} in order to
+ * enable the policy enforcer.
+ *
+ * By default, the policy enforcer configuration is loaded from a file at {@code WEB-INF/policy-enforcer.json}.
+ *
+ * Applications can also dynamically resolve the configuration by implementing the {@link ConfigurationResolver} SPI. For that,
+ * make sure to create a {@link META-INF/services/org.keycloak.adapters.authorization.spi.ConfigurationResolver} to register
+ * the implementation.
+ *
+ * @author Pedro Igor
+ */
+@WebListener
+public class PolicyEnforcerServletContextListener implements ServletContextListener {
+
+ private final Logger logger = Logger.getLogger(getClass());
+
+ @Override
+ public void contextInitialized(ServletContextEvent sce) {
+ ServletContext servletContext = sce.getServletContext();
+ Iterator configResolvers = ServiceLoader.load(ConfigurationResolver.class).iterator();
+ ConfigurationResolver configResolver;
+
+ if (configResolvers.hasNext()) {
+ configResolver = configResolvers.next();
+
+ if (configResolvers.hasNext()) {
+ throw new IllegalStateException("Multiple " + ConfigurationResolver.class.getName() + " implementations found");
+ }
+
+ logger.debugf("Configuration resolver found from classpath: %s", configResolver);
+ } else {
+ String enforcerConfigLocation = "WEB-INF/policy-enforcer.json";
+ InputStream config = servletContext.getResourceAsStream(enforcerConfigLocation);
+
+ if (config == null) {
+ logger.debugf("Could not find the policy enforcer configuration file: %s", enforcerConfigLocation);
+ return;
+ }
+
+ try {
+ configResolver = createDefaultConfigurationResolver(JsonSerialization.readValue(config, PolicyEnforcerConfig.class));
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to parse policy enforcer configuration: " + enforcerConfigLocation);
+ }
+ }
+
+ logger.debug("Policy enforcement filter is enabled.");
+
+ servletContext.addFilter("keycloak-policy-enforcer", new PolicyEnforcerFilter(configResolver))
+ .addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
+ }
+
+ private ConfigurationResolver createDefaultConfigurationResolver(PolicyEnforcerConfig enforcerConfig) {
+ return new ConfigurationResolver() {
+ @Override
+ public PolicyEnforcerConfig resolve(HttpRequest request) {
+ return enforcerConfig;
+ }
+ };
+ }
+}
diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/integration/elytron/ServletHttpRequest.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/integration/elytron/ServletHttpRequest.java
new file mode 100644
index 0000000000..ed4892980c
--- /dev/null
+++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/integration/elytron/ServletHttpRequest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.adapters.authorization.integration.elytron;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import org.keycloak.adapters.authorization.TokenPrincipal;
+import org.keycloak.adapters.authorization.spi.HttpRequest;
+
+/**
+ * @author Pedro Igor
+ */
+public class ServletHttpRequest implements HttpRequest {
+
+ private final HttpServletRequest request;
+ private final TokenPrincipal tokenPrincipal;
+ private InputStream inputStream;
+
+ public ServletHttpRequest(HttpServletRequest request, TokenPrincipal tokenPrincipal) {
+ this.request = request;
+ this.tokenPrincipal = tokenPrincipal;
+ }
+
+ @Override
+ public String getRelativePath() {
+ return request.getServletPath();
+ }
+
+ @Override
+ public String getMethod() {
+ return request.getMethod();
+ }
+
+ @Override
+ public String getURI() {
+ return request.getRequestURI();
+ }
+
+ @Override
+ public List getHeaders(String name) {
+ return Collections.list(request.getHeaders(name));
+ }
+
+ @Override
+ public String getFirstParam(String name) {
+ Map parameters = request.getParameterMap();
+ String[] values = parameters.get(name);
+
+ if (values == null || values.length == 0) {
+ return null;
+ }
+
+ return values[0];
+ }
+
+ @Override
+ public String getCookieValue(String name) {
+ Cookie[] cookies = request.getCookies();
+
+ for (Cookie cookie : cookies) {
+ if (cookie.getName().equals(name)) {
+ return cookie.getValue();
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public String getRemoteAddr() {
+ return request.getRemoteAddr();
+ }
+
+ @Override
+ public boolean isSecure() {
+ return request.isSecure();
+ }
+
+ @Override
+ public String getHeader(String name) {
+ return request.getHeader(name);
+ }
+
+ @Override
+ public InputStream getInputStream(boolean buffered) {
+ if (inputStream != null) {
+ return inputStream;
+ }
+
+ if (buffered) {
+ try {
+ return inputStream = new BufferedInputStream(request.getInputStream());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ try {
+ return request.getInputStream();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public TokenPrincipal getPrincipal() {
+ return tokenPrincipal;
+ }
+}
diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/integration/elytron/ServletHttpResponse.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/integration/elytron/ServletHttpResponse.java
new file mode 100644
index 0000000000..9528224790
--- /dev/null
+++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/integration/elytron/ServletHttpResponse.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.adapters.authorization.integration.elytron;
+
+import java.io.IOException;
+
+import jakarta.servlet.http.HttpServletResponse;
+import org.keycloak.adapters.authorization.spi.HttpResponse;
+
+/**
+ * @author Pedro Igor
+ */
+public class ServletHttpResponse implements HttpResponse {
+
+ private HttpServletResponse response;
+
+ public ServletHttpResponse(HttpServletResponse response) {
+ this.response = response;
+ }
+
+ @Override
+ public void sendError(int status) {
+ try {
+ response.sendError(status);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void sendError(int status, String reason) {
+ try {
+ response.sendError(status, reason);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void setHeader(String name, String value) {
+ response.setHeader(name, value);
+ }
+}
diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/spi/ConfigurationResolver.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/spi/ConfigurationResolver.java
new file mode 100644
index 0000000000..158d3f0922
--- /dev/null
+++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/spi/ConfigurationResolver.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.adapters.authorization.spi;
+
+import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
+
+/**
+ * Resolves a {@link PolicyEnforcerConfig} based on the information from the {@link HttpRequest}.
+ *
+ * @author Pedro Igor
+ */
+public interface ConfigurationResolver {
+
+ /**
+ * Resolves a {@link PolicyEnforcerConfig} based on the information from the {@link HttpRequest}.
+ *
+ * @param request the request
+ * @return the policy enforcer configuration for the given request
+ */
+ PolicyEnforcerConfig resolve(HttpRequest request);
+}
diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
index 8fae65ba4e..ec2016577e 100644
--- a/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
+++ b/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
@@ -23,12 +23,14 @@ import java.util.Map;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import java.util.HashSet;
import java.util.Set;
+import java.util.TreeMap;
/**
* @author Pedro Igor
@@ -39,31 +41,42 @@ public class PolicyEnforcerConfig {
private EnforcementMode enforcementMode = EnforcementMode.ENFORCING;
@JsonProperty("paths")
- @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ @JsonInclude(Include.NON_EMPTY)
private List paths = new ArrayList<>();
@JsonProperty("path-cache")
- @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ @JsonInclude(Include.NON_EMPTY)
private PathCacheConfig pathCacheConfig;
@JsonProperty("lazy-load-paths")
private Boolean lazyLoadPaths = Boolean.FALSE;
@JsonProperty("on-deny-redirect-to")
- @JsonInclude(JsonInclude.Include.NON_NULL)
+ @JsonInclude(Include.NON_NULL)
private String onDenyRedirectTo;
@JsonProperty("user-managed-access")
- @JsonInclude(JsonInclude.Include.NON_NULL)
+ @JsonInclude(Include.NON_NULL)
private UserManagedAccessConfig userManagedAccess;
@JsonProperty("claim-information-point")
- @JsonInclude(JsonInclude.Include.NON_NULL)
+ @JsonInclude(Include.NON_NULL)
private Map> claimInformationPointConfig;
@JsonProperty("http-method-as-scope")
private Boolean httpMethodAsScope;
+ private String realm;
+
+ @JsonProperty("auth-server-url")
+ private String authServerUrl;
+
+ @JsonProperty("credentials")
+ protected Map credentials = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+
+ @JsonProperty("resource")
+ private String resource;
+
public List getPaths() {
return this.paths;
}
@@ -128,6 +141,38 @@ public class PolicyEnforcerConfig {
this.httpMethodAsScope = httpMethodAsScope;
}
+ public String getRealm() {
+ return realm;
+ }
+
+ public void setRealm(String realm) {
+ this.realm = realm;
+ }
+
+ public String getAuthServerUrl() {
+ return authServerUrl;
+ }
+
+ public void setAuthServerUrl(String authServerUrl) {
+ this.authServerUrl = authServerUrl;
+ }
+
+ public Map getCredentials() {
+ return credentials;
+ }
+
+ public void setCredentials(Map credentials) {
+ this.credentials = credentials;
+ }
+
+ public String getResource() {
+ return resource;
+ }
+
+ public void setResource(String resource) {
+ this.resource = resource;
+ }
+
public static class PathConfig {
public static Set createPathConfigs(ResourceRepresentation resourceDescription) {