diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/AuthzClient.java b/authz/client/src/main/java/org/keycloak/authorization/client/AuthzClient.java
index 7844052b1f..cd06d2a4b8 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/AuthzClient.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/AuthzClient.java
@@ -91,18 +91,7 @@ public class AuthzClient {
* @return a new instance
*/
public static AuthzClient create(Configuration configuration) {
- return new AuthzClient(configuration, configuration.getClientAuthenticator());
- }
-
- /**
- * Creates a new instance.
- *
- * @param configuration the client configuration
- * @param authenticator the client authenticator
- * @return a new instance
- */
- public static AuthzClient create(Configuration configuration, ClientAuthenticator authenticator) {
- return new AuthzClient(configuration, authenticator);
+ return new AuthzClient(configuration);
}
private final ServerConfiguration serverConfiguration;
@@ -242,7 +231,7 @@ public class AuthzClient {
return this.configuration;
}
- private AuthzClient(Configuration configuration, ClientAuthenticator authenticator) {
+ private AuthzClient(Configuration configuration) {
if (configuration == null) {
throw new IllegalArgumentException("Client configuration can not be null.");
}
@@ -256,7 +245,7 @@ public class AuthzClient {
configurationUrl = KeycloakUriBuilder.fromUri(configurationUrl).clone().path(AUTHZ_DISCOVERY_URL).build(configuration.getRealm()).toString();
this.configuration = configuration;
- this.http = new Http(configuration, authenticator != null ? authenticator : configuration.getClientAuthenticator());
+ this.http = new Http(configuration, configuration.getClientCredentialsProvider());
try {
this.serverConfiguration = this.http.get(configurationUrl)
@@ -265,8 +254,6 @@ public class AuthzClient {
} catch (Exception e) {
throw new RuntimeException("Could not obtain configuration from server [" + configurationUrl + "].", e);
}
-
- this.http.setServerConfiguration(this.serverConfiguration);
}
private TokenCallable createPatSupplier(String userName, String password) {
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/ClientAuthenticator.java b/authz/client/src/main/java/org/keycloak/authorization/client/ClientAuthenticator.java
deleted file mode 100644
index d9077e5c92..0000000000
--- a/authz/client/src/main/java/org/keycloak/authorization/client/ClientAuthenticator.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright 2016 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.authorization.client;
-
-import java.util.List;
-import java.util.Map;
-
-/**
- * @author Pedro Igor
- */
-public interface ClientAuthenticator {
- void configureClientCredentials(Map> requestParams, Map requestHeaders);
-}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/Configuration.java b/authz/client/src/main/java/org/keycloak/authorization/client/Configuration.java
index 5bfe1826cd..3186435cf7 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/Configuration.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/Configuration.java
@@ -17,14 +17,14 @@
*/
package org.keycloak.authorization.client;
-import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.HttpClients;
+import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider;
+import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProviderUtils;
import org.keycloak.representations.adapters.config.AdapterConfig;
-import org.keycloak.util.BasicAuthHelper;
/**
* @author Pedro Igor
@@ -35,7 +35,7 @@ public class Configuration extends AdapterConfig {
private HttpClient httpClient;
@JsonIgnore
- private ClientAuthenticator clientAuthenticator = createDefaultClientAuthenticator();
+ private ClientCredentialsProvider clientCredentialsProvider;
public Configuration() {
@@ -66,27 +66,18 @@ public class Configuration extends AdapterConfig {
return httpClient;
}
- ClientAuthenticator getClientAuthenticator() {
- return this.clientAuthenticator;
+ public void setHttpClient(HttpClient httpClient) {
+ this.httpClient = httpClient;
}
- /**
- * Creates a default client authenticator which uses HTTP BASIC and client id and secret to authenticate the client.
- *
- * @return the default client authenticator
- */
- private ClientAuthenticator createDefaultClientAuthenticator() {
- return new ClientAuthenticator() {
- @Override
- public void configureClientCredentials(Map> requestParams, Map requestHeaders) {
- String secret = (String) getCredentials().get("secret");
+ public void setClientCredentialsProvider(ClientCredentialsProvider clientCredentialsProvider) {
+ this.clientCredentialsProvider = clientCredentialsProvider;
+ }
- if (secret == null) {
- throw new RuntimeException("Client secret not provided.");
- }
-
- requestHeaders.put("Authorization", BasicAuthHelper.RFC6749.createHeader(getResource(), secret));
- }
- };
+ public ClientCredentialsProvider getClientCredentialsProvider() {
+ if (clientCredentialsProvider == null) {
+ clientCredentialsProvider = ClientCredentialsProviderUtils.bootstrapClientAuthenticator(this);
+ }
+ return clientCredentialsProvider;
}
}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/Http.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/Http.java
index eecb7e30af..2f84d5e3ad 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/util/Http.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/Http.java
@@ -18,9 +18,9 @@
package org.keycloak.authorization.client.util;
import org.apache.http.client.methods.RequestBuilder;
-import org.keycloak.authorization.client.ClientAuthenticator;
import org.keycloak.authorization.client.Configuration;
import org.keycloak.authorization.client.representation.ServerConfiguration;
+import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider;
/**
* @author Pedro Igor
@@ -28,10 +28,9 @@ import org.keycloak.authorization.client.representation.ServerConfiguration;
public class Http {
private final Configuration configuration;
- private final ClientAuthenticator authenticator;
- private ServerConfiguration serverConfiguration;
+ private final ClientCredentialsProvider authenticator;
- public Http(Configuration configuration, ClientAuthenticator authenticator) {
+ public Http(Configuration configuration, ClientCredentialsProvider authenticator) {
this.configuration = configuration;
this.authenticator = authenticator;
}
@@ -55,8 +54,4 @@ public class Http {
private HttpMethod method(RequestBuilder builder) {
return new HttpMethod(this.configuration, authenticator, builder);
}
-
- public void setServerConfiguration(ServerConfiguration serverConfiguration) {
- this.serverConfiguration = serverConfiguration;
- }
}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java
index b301e38c77..8f0f3c0d2f 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java
@@ -35,28 +35,29 @@ import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
-import org.keycloak.authorization.client.ClientAuthenticator;
import org.keycloak.authorization.client.Configuration;
+import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider;
/**
* @author Pedro Igor
*/
public class HttpMethod {
- private final HttpClient httpClient;
- private final ClientAuthenticator authenticator;
- protected final RequestBuilder builder;
- protected final Configuration configuration;
- protected final Map headers;
- protected final Map> params;
private static final Logger logger = Logger.getLogger(HttpMethod.class.getName());
+
+ private final HttpClient httpClient;
+ final RequestBuilder builder;
+ final Configuration configuration;
+ final Map headers;
+ final Map> params;
+ private final ClientCredentialsProvider authenticator;
private HttpMethodResponse response;
- public HttpMethod(Configuration configuration, ClientAuthenticator authenticator, RequestBuilder builder) {
+ public HttpMethod(Configuration configuration, ClientCredentialsProvider authenticator, RequestBuilder builder) {
this(configuration, authenticator, builder, new HashMap>(), new HashMap());
}
- public HttpMethod(Configuration configuration, ClientAuthenticator authenticator, RequestBuilder builder, Map> params, Map headers) {
+ public HttpMethod(Configuration configuration, ClientCredentialsProvider authenticator, RequestBuilder builder, Map> params, Map headers) {
this.configuration = configuration;
this.httpClient = configuration.getHttpClient();
this.authenticator = authenticator;
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodAuthenticator.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodAuthenticator.java
index 9f467228ee..9c9978bfa9 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodAuthenticator.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodAuthenticator.java
@@ -18,11 +18,16 @@
package org.keycloak.authorization.client.util;
import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
import java.util.Set;
import org.apache.http.Header;
import org.keycloak.OAuth2Constants;
-import org.keycloak.authorization.client.ClientAuthenticator;
+import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider;
+import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProviderUtils;
import org.keycloak.representations.idm.authorization.AuthorizationRequest;
import org.keycloak.representations.idm.authorization.AuthorizationRequest.Metadata;
import org.keycloak.representations.idm.authorization.Permission;
@@ -34,16 +39,16 @@ import org.keycloak.representations.idm.authorization.PermissionTicketToken;
public class HttpMethodAuthenticator {
private final HttpMethod method;
- private final ClientAuthenticator authenticator;
+ private ClientCredentialsProvider clientCredentialProvider;
- public HttpMethodAuthenticator(HttpMethod method, ClientAuthenticator authenticator) {
+ public HttpMethodAuthenticator(HttpMethod method, ClientCredentialsProvider clientCredentialsProvider) {
this.method = method;
- this.authenticator = authenticator;
+ this.clientCredentialProvider = clientCredentialsProvider;
}
public HttpMethod client() {
this.method.params.put(OAuth2Constants.GRANT_TYPE, Arrays.asList(OAuth2Constants.CLIENT_CREDENTIALS));
- authenticator.configureClientCredentials(this.method.params, this.method.headers);
+ configureClientCredentials(this.method.params, this.method.headers);
return this.method;
}
@@ -133,4 +138,12 @@ public class HttpMethodAuthenticator {
return method;
}
+
+ private void configureClientCredentials(Map> requestParams, Map requestHeaders) {
+ Map formparams = new HashMap<>();
+ ClientCredentialsProviderUtils.setClientCredentials(method.configuration, clientCredentialProvider, requestHeaders, formparams);
+ for (Entry param : formparams.entrySet()) {
+ requestParams.put(param.getKey(), Arrays.asList(param.getValue()));
+ }
+ }
}
diff --git a/authz/policy-enforcer/pom.xml b/authz/policy-enforcer/pom.xml
new file mode 100755
index 0000000000..f1706319b6
--- /dev/null
+++ b/authz/policy-enforcer/pom.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+ org.keycloak
+ keycloak-authz-parent
+ 999.0.0-SNAPSHOT
+ ../pom.xml
+
+ 4.0.0
+
+ keycloak-policy-enforcer
+ Keycloak Authz: Policy Enforcer
+ jar
+
+
+
+ org.jboss.logging
+ jboss-logging
+
+
+ org.keycloak
+ keycloak-authz-client
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+
+
+ junit
+ junit
+ test
+
+
+ org.apache.httpcomponents
+ httpclient
+
+
+
+
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathCache.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PathCache.java
similarity index 99%
rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathCache.java
rename to authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PathCache.java
index 8cb2dc4e0a..0d90887dab 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathCache.java
+++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PathCache.java
@@ -51,7 +51,7 @@ public class PathCache {
* @param maxAge the time in milliseconds that an entry can stay in the cache. If {@code -1}, entries never expire
* @param paths the pre-configured paths
*/
- public PathCache(final int maxEntries, long maxAge,
+ PathCache(final int maxEntries, long maxAge,
Map paths) {
cache = new LinkedHashMap(16, DEFAULT_LOAD_FACTOR, true) {
@Override
diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PathConfigMatcher.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PathConfigMatcher.java
new file mode 100644
index 0000000000..2e1b75a3fc
--- /dev/null
+++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PathConfigMatcher.java
@@ -0,0 +1,293 @@
+/*
+ * 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;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.jboss.logging.Logger;
+import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.authorization.client.resource.ProtectedResource;
+import org.keycloak.common.util.PathMatcher;
+import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
+import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathCacheConfig;
+import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+
+/**
+ * @author Pedro Igor
+ */
+public class PathConfigMatcher extends PathMatcher {
+
+ private static Logger LOGGER = Logger.getLogger(PolicyEnforcer.class);
+
+ private final Map paths;
+ private final PathCache pathCache;
+ private final AuthzClient authzClient;
+ private final PolicyEnforcerConfig enforcerConfig;
+
+ PathConfigMatcher(PolicyEnforcerConfig enforcerConfig, AuthzClient authzClient) {
+ this.enforcerConfig = enforcerConfig;
+ PathCacheConfig cacheConfig = enforcerConfig.getPathCacheConfig();
+
+ if (cacheConfig == null) {
+ cacheConfig = new PathCacheConfig();
+ }
+
+ this.authzClient = authzClient;
+ this.paths = configurePaths();
+ this.pathCache = new PathCache(cacheConfig.getMaxEntries(), cacheConfig.getLifespan(), paths);
+
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("Initialization complete. Path configuration:");
+ for (PathConfig pathConfig : this.paths.values()) {
+ LOGGER.debug(pathConfig);
+ }
+ }
+ }
+
+ @Override
+ public PathConfig matches(String targetUri) {
+ PathConfig pathConfig = pathCache.get(targetUri);
+
+ if (pathCache.containsKey(targetUri) || pathConfig != null) {
+ return pathConfig;
+ }
+
+ pathConfig = super.matches(targetUri);
+
+ if (enforcerConfig.getLazyLoadPaths() || enforcerConfig.getPathCacheConfig() != null) {
+ if ((pathConfig == null || pathConfig.isInvalidated() || pathConfig.getPath().contains("*"))) {
+ try {
+ List matchingResources = authzClient.protection().resource().findByMatchingUri(targetUri);
+
+ if (matchingResources.isEmpty()) {
+ // if this config is invalidated (e.g.: due to cache expiration) we remove and return null
+ if (pathConfig != null && pathConfig.isInvalidated()) {
+ paths.remove(targetUri);
+ return null;
+ }
+ } else {
+ Map> cipConfig = null;
+ PolicyEnforcerConfig.EnforcementMode enforcementMode = PolicyEnforcerConfig.EnforcementMode.ENFORCING;
+ ResourceRepresentation targetResource = matchingResources.get(0);
+ List methodConfig = null;
+ boolean isStatic = false;
+
+ if (pathConfig != null) {
+ cipConfig = pathConfig.getClaimInformationPointConfig();
+ enforcementMode = pathConfig.getEnforcementMode();
+ methodConfig = pathConfig.getMethods();
+ isStatic = pathConfig.isStatic();
+ } else {
+ for (PathConfig existingPath : paths.values()) {
+ if (targetResource.getId().equals(existingPath.getId())
+ && existingPath.isStatic()
+ && !org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode.DISABLED.equals(existingPath.getEnforcementMode())) {
+ return null;
+ }
+ }
+ }
+
+ pathConfig = PathConfig.createPathConfigs(targetResource).iterator().next();
+
+ if (cipConfig != null) {
+ pathConfig.setClaimInformationPointConfig(cipConfig);
+ }
+
+ if (methodConfig != null) {
+ pathConfig.setMethods(methodConfig);
+ }
+
+ pathConfig.setStatic(isStatic);
+ pathConfig.setEnforcementMode(enforcementMode);
+ }
+ } catch (Exception cause) {
+ LOGGER.errorf(cause, "Could not lazy load resource with path [" + targetUri + "] from server");
+ return null;
+ }
+ }
+ }
+
+ pathCache.put(targetUri, pathConfig);
+
+ return pathConfig;
+ }
+
+ @Override
+ protected String getPath(PathConfig entry) {
+ return entry.getPath();
+ }
+
+ @Override
+ protected Collection getPaths() {
+ return paths.values();
+ }
+
+ public PathCache getPathCache() {
+ return pathCache;
+ }
+
+ @Override
+ protected PathConfig resolvePathConfig(PathConfig originalConfig, String path) {
+ if (originalConfig.hasPattern()) {
+ ProtectedResource resource = authzClient.protection().resource();
+
+ // search by an exact match
+ List search = resource.findByUri(path);
+
+ // if exact match not found, try to obtain from current path the parent path.
+ // if path is /resource/1/test and pattern from pathConfig is /resource/{id}/*, parent path is /resource/1
+ // this logic allows to match sub resources of a resource instance (/resource/1) to the parent resource,
+ // so any permission granted to parent also applies to sub resources
+ if (search.isEmpty()) {
+ search = resource.findByUri(buildUriFromTemplate(originalConfig.getPath(), path, true));
+ }
+
+ if (!search.isEmpty()) {
+ ResourceRepresentation targetResource = search.get(0);
+ PathConfig config = PathConfig.createPathConfigs(targetResource).iterator().next();
+
+ config.setScopes(originalConfig.getScopes());
+ config.setMethods(originalConfig.getMethods());
+ config.setParentConfig(originalConfig);
+ config.setEnforcementMode(originalConfig.getEnforcementMode());
+ config.setClaimInformationPointConfig(originalConfig.getClaimInformationPointConfig());
+
+ return config;
+ }
+ }
+
+ return null;
+ }
+
+ public void removeFromCache(String pathConfig) {
+ pathCache.remove(pathConfig);
+ }
+
+ public Map getPathConfig() {
+ return paths;
+ }
+
+ private Map configurePaths() {
+ ProtectedResource protectedResource = this.authzClient.protection().resource();
+ boolean loadPathsFromServer = !enforcerConfig.getLazyLoadPaths();
+
+ for (PathConfig pathConfig : enforcerConfig.getPaths()) {
+ if (!org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
+ loadPathsFromServer = false;
+ break;
+ }
+ }
+
+ if (loadPathsFromServer) {
+ LOGGER.info("No path provided in configuration.");
+ Map paths = configureAllPathsForResourceServer(protectedResource);
+
+ paths.putAll(configureDefinedPaths(protectedResource, enforcerConfig));
+
+ return paths;
+ } else {
+ LOGGER.info("Paths provided in configuration.");
+ return configureDefinedPaths(protectedResource, enforcerConfig);
+ }
+ }
+
+ private Map configureDefinedPaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
+ Map paths = Collections.synchronizedMap(new LinkedHashMap());
+
+ for (PathConfig pathConfig : enforcerConfig.getPaths()) {
+ ResourceRepresentation resource;
+ String resourceName = pathConfig.getName();
+ String path = pathConfig.getPath();
+
+ if (resourceName != null) {
+ LOGGER.debugf("Trying to find resource with name [%s] for path [%s].", resourceName, path);
+ resource = protectedResource.findByName(resourceName);
+ } else {
+ LOGGER.debugf("Trying to find resource with uri [%s] for path [%s].", path, path);
+ List resources = protectedResource.findByUri(path);
+
+ if (resources.isEmpty()) {
+ resources = protectedResource.findByMatchingUri(path);
+ }
+
+ if (resources.size() == 1) {
+ resource = resources.get(0);
+ } else if (resources.size() > 1) {
+ throw new RuntimeException("Multiple resources found with the same uri");
+ } else {
+ resource = null;
+ }
+ }
+
+ if (resource != null) {
+ pathConfig.setId(resource.getId());
+ // if the resource is statically bound to a resource it means the config can not be invalidated
+ if (resourceName != null) {
+ pathConfig.setStatic(true);
+ }
+ }
+
+ if (org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
+ pathConfig.setStatic(true);
+ }
+
+ PathConfig existingPath = null;
+
+ for (PathConfig current : paths.values()) {
+ if (current.getPath().equals(pathConfig.getPath())) {
+ existingPath = current;
+ break;
+ }
+ }
+
+ if (existingPath == null) {
+ paths.put(pathConfig.getPath(), pathConfig);
+ } else {
+ existingPath.getMethods().addAll(pathConfig.getMethods());
+ existingPath.getScopes().addAll(pathConfig.getScopes());
+ }
+ }
+
+ return paths;
+ }
+
+ private Map configureAllPathsForResourceServer(ProtectedResource protectedResource) {
+ LOGGER.info("Querying the server for all resources associated with this application.");
+ Map paths = Collections.synchronizedMap(new HashMap());
+
+ if (!enforcerConfig.getLazyLoadPaths()) {
+ for (String id : protectedResource.findAll()) {
+ ResourceRepresentation resourceDescription = protectedResource.findById(id);
+
+ if (resourceDescription.getUris() != null && !resourceDescription.getUris().isEmpty()) {
+ for(PathConfig pathConfig : PathConfig.createPathConfigs(resourceDescription)) {
+ paths.put(pathConfig.getPath(), pathConfig);
+ }
+ }
+ }
+ }
+
+ return paths;
+ }
+}
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
new file mode 100644
index 0000000000..3c25e8d217
--- /dev/null
+++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
@@ -0,0 +1,647 @@
+/*
+ * Copyright 2016 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;
+
+import static org.keycloak.adapters.authorization.util.JsonUtils.asAccessToken;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.ServiceLoader;
+import java.util.Set;
+
+import org.apache.http.client.HttpClient;
+import org.jboss.logging.Logger;
+import org.keycloak.AuthorizationContext;
+import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProviderFactory;
+import org.keycloak.adapters.authorization.spi.HttpRequest;
+import org.keycloak.adapters.authorization.spi.HttpResponse;
+import org.keycloak.authorization.client.AuthorizationDeniedException;
+import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.authorization.client.ClientAuthorizationContext;
+import org.keycloak.authorization.client.Configuration;
+import org.keycloak.authorization.client.resource.PermissionResource;
+import org.keycloak.authorization.client.resource.ProtectionResource;
+import org.keycloak.common.util.Base64;
+import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.AccessToken.Authorization;
+import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
+import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode;
+import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.MethodConfig;
+import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
+import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.ScopeEnforcementMode;
+import org.keycloak.representations.idm.authorization.AuthorizationRequest;
+import org.keycloak.representations.idm.authorization.AuthorizationResponse;
+import org.keycloak.representations.idm.authorization.Permission;
+import org.keycloak.representations.idm.authorization.PermissionRequest;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ * A Policy Enforcement Point (PEP) that requests and enforces authorization decisions from Keycloak.
+ *
+ * @author Pedro Igor
+ */
+public class PolicyEnforcer {
+
+ private static Logger LOGGER = Logger.getLogger(PolicyEnforcer.class);
+ private static final String HTTP_METHOD_DELETE = "DELETE";
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ private final AuthzClient authzClient;
+ private final Map paths;
+ private final PathConfigMatcher pathMatcher;
+ private final HttpClient httpClient;
+ private final PolicyEnforcerConfig enforcerConfig;
+
+ private final Map claimInformationPointProviderFactories = new HashMap<>();
+
+ protected PolicyEnforcer(Builder builder) {
+ enforcerConfig = builder.getEnforcerConfig();
+ authzClient = AuthzClient.create(builder.authzClientConfig);
+ httpClient = authzClient.getConfiguration().getHttpClient();
+ pathMatcher = new PathConfigMatcher(builder.getEnforcerConfig(), authzClient);
+ paths = pathMatcher.getPathConfig();
+
+ loadClaimInformationPointProviders(ServiceLoader.load(ClaimInformationPointProviderFactory.class, ClaimInformationPointProviderFactory.class.getClassLoader()));
+ loadClaimInformationPointProviders(ServiceLoader.load(ClaimInformationPointProviderFactory.class, Thread.currentThread().getContextClassLoader()));
+ }
+
+ public AuthorizationContext enforce(HttpRequest request, HttpResponse response) {
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debugv("Policy enforcement is enabled. Enforcing policy decisions for path [{0}].", request.getURI());
+ }
+
+ AuthorizationContext context = authorize(request, response);
+
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debugv("Policy enforcement result for path [{0}] is : {1}", request.getURI(), context.isGranted() ? "GRANTED" : "DENIED");
+ LOGGER.debugv("Returning authorization context with permissions:");
+ for (Permission permission : context.getPermissions()) {
+ LOGGER.debug(permission);
+ }
+ }
+
+ return context;
+ }
+
+ public HttpClient getHttpClient() {
+ return httpClient;
+ }
+
+ public Map getPaths() {
+ return Collections.unmodifiableMap(paths);
+ }
+
+ public Map getClaimInformationPointProviderFactories() {
+ return claimInformationPointProviderFactories;
+ }
+
+ public PathConfigMatcher getPathMatcher() {
+ return pathMatcher;
+ }
+
+ private AuthorizationContext authorize(HttpRequest request, HttpResponse response) {
+ EnforcementMode enforcementMode = enforcerConfig.getEnforcementMode();
+ TokenPrincipal principal = request.getPrincipal();
+ boolean anonymous = principal == null || principal.getRawToken() == null;
+
+ if (EnforcementMode.DISABLED.equals(enforcementMode)) {
+ if (anonymous) {
+ response.sendError(401, "Invalid bearer");
+ }
+ return createEmptyAuthorizationContext(true);
+ }
+
+ PathConfig pathConfig = getPathConfig(request);
+
+ if (anonymous) {
+ if (!isDefaultAccessDeniedUri(request)) {
+ if (pathConfig != null) {
+ if (EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
+ return createEmptyAuthorizationContext(true);
+ } else {
+ challenge(pathConfig, getRequiredScopes(pathConfig, request), request, response);
+ }
+ } else {
+ handleAccessDenied(response);
+ }
+ }
+ return createEmptyAuthorizationContext(false);
+ }
+
+ AccessToken accessToken = principal.getToken();
+
+ if (accessToken != null) {
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig);
+ }
+
+ if (pathConfig == null) {
+ if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) {
+ return createAuthorizationContext(accessToken, null);
+ }
+
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debugf("Could not find a configuration for path [%s]", getPath(request));
+ }
+
+ if (isDefaultAccessDeniedUri(request)) {
+ return createAuthorizationContext(accessToken, null);
+ }
+
+ handleAccessDenied(response);
+
+ return createEmptyAuthorizationContext(false);
+ }
+
+ if (EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
+ return createAuthorizationContext(accessToken, pathConfig);
+ }
+
+ MethodConfig methodConfig = getRequiredScopes(pathConfig, request);
+ Map> claims = resolveClaims(pathConfig, request);
+
+ if (isAuthorized(pathConfig, methodConfig, accessToken, request, claims)) {
+ try {
+ return createAuthorizationContext(accessToken, pathConfig);
+ } catch (Exception e) {
+ throw new RuntimeException("Error processing path [" + pathConfig.getPath() + "].", e);
+ }
+ }
+
+ AccessToken original = accessToken;
+
+ accessToken = requestAuthorizationToken(pathConfig, methodConfig, request, claims);
+
+ if (accessToken != null) {
+ AccessToken.Authorization authorization = original.getAuthorization();
+
+ if (authorization == null) {
+ authorization = new AccessToken.Authorization();
+ authorization.setPermissions(new ArrayList());
+ }
+
+ AccessToken.Authorization newAuthorization = accessToken.getAuthorization();
+
+ if (newAuthorization != null) {
+ Collection grantedPermissions = authorization.getPermissions();
+ Collection newPermissions = newAuthorization.getPermissions();
+
+ for (Permission newPermission : newPermissions) {
+ if (!grantedPermissions.contains(newPermission)) {
+ grantedPermissions.add(newPermission);
+ }
+ }
+ }
+
+ original.setAuthorization(authorization);
+
+ if (isAuthorized(pathConfig, methodConfig, accessToken, request, claims)) {
+ try {
+ return createAuthorizationContext(accessToken, pathConfig);
+ } catch (Exception e) {
+ throw new RuntimeException("Error processing path [" + pathConfig.getPath() + "].", e);
+ }
+ }
+ }
+
+ if (methodConfig != null && ScopeEnforcementMode.DISABLED.equals(methodConfig.getScopesEnforcementMode())) {
+ return createEmptyAuthorizationContext(true);
+ }
+
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
+ }
+
+ if (!challenge(pathConfig, methodConfig, request, response)) {
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debugf("Challenge not sent, sending default forbidden response. Path [%s]", pathConfig);
+ }
+ handleAccessDenied(response);
+ }
+ }
+
+ return createEmptyAuthorizationContext(false);
+ }
+
+ protected boolean isAuthorized(PathConfig actualPathConfig, MethodConfig methodConfig, AccessToken accessToken, HttpRequest request, Map> claims) {
+ if (isDefaultAccessDeniedUri(request)) {
+ return true;
+ }
+
+ Authorization authorization = accessToken.getAuthorization();
+
+ if (authorization == null) {
+ return false;
+ }
+
+ boolean hasPermission = false;
+ Collection grantedPermissions = authorization.getPermissions();
+
+ for (Permission permission : grantedPermissions) {
+ if (permission.getResourceId() != null) {
+ if (isResourcePermission(actualPathConfig, permission)) {
+ hasPermission = true;
+
+ if (actualPathConfig.isInstance() && !matchResourcePermission(actualPathConfig, permission)) {
+ continue;
+ }
+
+ if (hasResourceScopePermission(methodConfig, permission)) {
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, grantedPermissions);
+ }
+ if (HTTP_METHOD_DELETE.equalsIgnoreCase(request.getMethod()) && actualPathConfig.isInstance()) {
+ pathMatcher.removeFromCache(getPath(request));
+ }
+
+ return hasValidClaims(permission, claims);
+ }
+ }
+ } else {
+ if (hasResourceScopePermission(methodConfig, permission)) {
+ return true;
+ }
+ }
+ }
+
+ if (!hasPermission && EnforcementMode.PERMISSIVE.equals(actualPathConfig.getEnforcementMode())) {
+ return true;
+ }
+
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debugf("Authorization FAILED for path [%s]. Not enough permissions [%s].", actualPathConfig, grantedPermissions);
+ }
+
+ return false;
+ }
+
+ protected Map> resolveClaims(PathConfig pathConfig, HttpRequest request) {
+ Map> claims = new HashMap<>();
+
+ resolveClaims(claims, enforcerConfig.getClaimInformationPointConfig(), request);
+ resolveClaims(claims, pathConfig.getClaimInformationPointConfig(), request);
+
+ return claims;
+ }
+
+ protected boolean challenge(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, HttpRequest request, HttpResponse response) {
+ if (isBearerAuthorization(request)) {
+ String ticket = getPermissionTicket(pathConfig, methodConfig, authzClient, request);
+
+ if (ticket != null) {
+ response.sendError(401);
+ response.setHeader("WWW-Authenticate", new StringBuilder("UMA realm=\"").append(authzClient.getConfiguration().getRealm()).append("\"").append(",as_uri=\"")
+ .append(authzClient.getServerConfiguration().getIssuer()).append("\"").append(",ticket=\"").append(ticket).append("\"").toString());
+ } else {
+ response.sendError(403);
+ }
+
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("Sending challenge");
+ }
+
+ return true;
+ }
+
+ handleAccessDenied(response);
+
+ return true;
+ }
+
+ protected void handleAccessDenied(HttpResponse response) {
+ String accessDeniedPath = enforcerConfig.getOnDenyRedirectTo();
+
+ if (accessDeniedPath != null) {
+ response.sendError(302);
+ response.setHeader("Location", accessDeniedPath);
+ } else {
+ response.sendError(403);
+ }
+ }
+
+ private boolean hasValidClaims(Permission permission, Map> claims) {
+ Map> grantedClaims = permission.getClaims();
+
+ if (grantedClaims != null) {
+ if (claims.isEmpty()) {
+ return false;
+ }
+
+ for (Entry> entry : grantedClaims.entrySet()) {
+ List requestClaims = claims.get(entry.getKey());
+
+ if (requestClaims == null || requestClaims.isEmpty() || !entry.getValue().containsAll(requestClaims)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ private boolean isDefaultAccessDeniedUri(HttpRequest request) {
+ String accessDeniedPath = enforcerConfig.getOnDenyRedirectTo();
+ return accessDeniedPath != null && request.getURI().contains(accessDeniedPath);
+ }
+
+ private boolean hasResourceScopePermission(MethodConfig methodConfig, Permission permission) {
+ List requiredScopes = methodConfig.getScopes();
+ Set allowedScopes = permission.getScopes();
+
+ if (allowedScopes.isEmpty()) {
+ return true;
+ }
+
+ PolicyEnforcerConfig.ScopeEnforcementMode enforcementMode = methodConfig.getScopesEnforcementMode();
+
+ if (PolicyEnforcerConfig.ScopeEnforcementMode.ALL.equals(enforcementMode)) {
+ return allowedScopes.containsAll(requiredScopes);
+ }
+
+ if (PolicyEnforcerConfig.ScopeEnforcementMode.ANY.equals(enforcementMode)) {
+ for (String requiredScope : requiredScopes) {
+ if (allowedScopes.contains(requiredScope)) {
+ return true;
+ }
+ }
+ }
+
+ return requiredScopes.isEmpty();
+ }
+
+ private AuthorizationContext createEmptyAuthorizationContext(final boolean granted) {
+ return new ClientAuthorizationContext(authzClient) {
+ @Override
+ public boolean hasPermission(String resourceName, String scopeName) {
+ return granted;
+ }
+
+ @Override
+ public boolean hasResourcePermission(String resourceName) {
+ return granted;
+ }
+
+ @Override
+ public boolean hasScopePermission(String scopeName) {
+ return granted;
+ }
+
+ @Override
+ public List getPermissions() {
+ return Collections.EMPTY_LIST;
+ }
+
+ @Override
+ public boolean isGranted() {
+ return granted;
+ }
+ };
+ }
+
+ private String getPath(HttpRequest request) {
+ return request.getRelativePath();
+ }
+
+ private MethodConfig getRequiredScopes(PathConfig pathConfig, HttpRequest request) {
+ String method = request.getMethod();
+
+ for (MethodConfig methodConfig : pathConfig.getMethods()) {
+ if (methodConfig.getMethod().equals(method)) {
+ return methodConfig;
+ }
+ }
+
+ MethodConfig methodConfig = new MethodConfig();
+
+ methodConfig.setMethod(request.getMethod());
+ List scopes = new ArrayList<>();
+
+ if (Boolean.TRUE.equals(enforcerConfig.getHttpMethodAsScope())) {
+ scopes.add(request.getMethod());
+ } else {
+ scopes.addAll(pathConfig.getScopes());
+ }
+
+ methodConfig.setScopes(scopes);
+ methodConfig.setScopesEnforcementMode(PolicyEnforcerConfig.ScopeEnforcementMode.ANY);
+
+ return methodConfig;
+ }
+
+ private AuthorizationContext createAuthorizationContext(AccessToken accessToken, PathConfig pathConfig) {
+ return new ClientAuthorizationContext(accessToken, pathConfig, authzClient);
+ }
+
+ private boolean isResourcePermission(PathConfig actualPathConfig, Permission permission) {
+ // first we try a match using resource id
+ boolean resourceMatch = matchResourcePermission(actualPathConfig, permission);
+
+ // as a fallback, check if the current path is an instance and if so, check if parent's id matches the permission
+ if (!resourceMatch && actualPathConfig.isInstance()) {
+ resourceMatch = matchResourcePermission(actualPathConfig.getParentConfig(), permission);
+ }
+
+ return resourceMatch;
+ }
+
+ private boolean matchResourcePermission(PathConfig actualPathConfig, Permission permission) {
+ return permission.getResourceId().equals(actualPathConfig.getId());
+ }
+
+ private PathConfig getPathConfig(HttpRequest request) {
+ return isDefaultAccessDeniedUri(request) ? null : pathMatcher.matches(getPath(request));
+ }
+
+ private AccessToken requestAuthorizationToken(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, HttpRequest request, Map> claims) {
+ if (enforcerConfig.getUserManagedAccess() != null) {
+ return null;
+ }
+
+ try {
+ TokenPrincipal principal = request.getPrincipal();
+ String accessTokenString = principal.getRawToken();
+ AccessToken accessToken = principal.getToken();
+ AuthorizationRequest authzRequest = new AuthorizationRequest();
+
+ if (isBearerAuthorization(request) || accessToken.getAuthorization() != null) {
+ authzRequest.addPermission(pathConfig.getId(), methodConfig.getScopes());
+ }
+
+ if (!claims.isEmpty()) {
+ authzRequest.setClaimTokenFormat("urn:ietf:params:oauth:token-type:jwt");
+ authzRequest.setClaimToken(Base64.encodeBytes(JsonSerialization.writeValueAsBytes(claims)));
+ }
+
+ if (accessToken.getAuthorization() != null) {
+ authzRequest.setRpt(accessTokenString);
+ }
+
+ LOGGER.debug("Obtaining authorization for authenticated user.");
+ AuthorizationResponse authzResponse;
+
+ if (isBearerAuthorization(request)) {
+ authzRequest.setSubjectToken(accessTokenString);
+ authzResponse = authzClient.authorization().authorize(authzRequest);
+ } else {
+ authzResponse = authzClient.authorization(accessTokenString).authorize(authzRequest);
+ }
+
+ if (authzResponse != null) {
+ return asAccessToken(authzResponse.getToken());
+ }
+ } catch (AuthorizationDeniedException ignore) {
+ LOGGER.debug("Authorization denied", ignore);
+ } catch (Exception e) {
+ LOGGER.debug("Authorization failed", e);
+ }
+
+ return null;
+ }
+
+ private String getPermissionTicket(PathConfig pathConfig, MethodConfig methodConfig, AuthzClient authzClient, HttpRequest httpFacade) {
+ if (enforcerConfig.getUserManagedAccess() != null) {
+ ProtectionResource protection = authzClient.protection();
+ PermissionResource permission = protection.permission();
+ PermissionRequest permissionRequest = new PermissionRequest();
+
+ permissionRequest.setResourceId(pathConfig.getId());
+ permissionRequest.setScopes(new HashSet<>(methodConfig.getScopes()));
+
+ Map> claims = resolveClaims(pathConfig, httpFacade);
+
+ if (!claims.isEmpty()) {
+ permissionRequest.setClaims(claims);
+ }
+
+ return permission.create(permissionRequest).getTicket();
+ }
+
+ return null;
+ }
+
+ private boolean isBearerAuthorization(HttpRequest request) {
+ List authHeaders = request.getHeaders("Authorization");
+
+ if (authHeaders != null) {
+ for (String authHeader : authHeaders) {
+ String[] split = authHeader.trim().split("\\s+");
+ if (split == null || split.length != 2) continue;
+ if (!split[0].equalsIgnoreCase("Bearer")) continue;
+ return true;
+ }
+ }
+
+ return authzClient.getConfiguration().isBearerOnly();
+ }
+
+ private void loadClaimInformationPointProviders(ServiceLoader loader) {
+ for (ClaimInformationPointProviderFactory factory : loader) {
+ factory.init(this);
+
+ claimInformationPointProviderFactories.put(factory.getName(), factory);
+ }
+ }
+
+ private void resolveClaims(Map> claims, Map> claimInformationPointConfig, HttpRequest request) {
+ if (claimInformationPointConfig != null) {
+ for (Entry> claimDef : claimInformationPointConfig.entrySet()) {
+ ClaimInformationPointProviderFactory factory = claimInformationPointProviderFactories.get(claimDef.getKey());
+
+ if (factory != null) {
+ claims.putAll(factory.create(claimDef.getValue()).resolve(request));
+ }
+ }
+ }
+ }
+
+ public static class Builder {
+
+ Configuration authzClientConfig = new Configuration();
+
+ private Builder() {
+ }
+
+ public Builder authServerUrl(String authServerUrl) {
+ authzClientConfig.setAuthServerUrl(authServerUrl);
+ return this;
+ }
+
+ public Builder realm(String realm) {
+ authzClientConfig.setRealm(realm);
+ return this;
+ }
+
+ public Builder clientId(String clientId) {
+ authzClientConfig.setResource(clientId);
+ return this;
+ }
+
+ public Builder bearerOnly(boolean bearerOnly) {
+ authzClientConfig.setBearerOnly(bearerOnly);
+ return this;
+ }
+
+ public Builder credentials(Map credentials) {
+ authzClientConfig.setCredentials(credentials);
+ return this;
+ }
+
+ public Builder enforcerConfig(PolicyEnforcerConfig enforcerConfig) {
+ authzClientConfig.setPolicyEnforcerConfig(enforcerConfig);
+ return this;
+ }
+
+ public Builder enforcerConfig(InputStream is) {
+ try {
+ enforcerConfig(JsonSerialization.readValue(is, PolicyEnforcerConfig.class));
+ } catch (Exception cause) {
+ throw new RuntimeException("Failed to read configuration", cause);
+ }
+ return this;
+ }
+
+ public Builder httpClient(HttpClient httpClient) {
+ authzClientConfig.setHttpClient(httpClient);
+ return this;
+ }
+
+ public Builder credentialProvider(ClientCredentialsProvider credentialsProvider) {
+ authzClientConfig.setClientCredentialsProvider(credentialsProvider);
+ return this;
+ }
+
+ public PolicyEnforcer build() {
+ return new PolicyEnforcer(this);
+ }
+
+ PolicyEnforcerConfig getEnforcerConfig() {
+ return authzClientConfig.getPolicyEnforcerConfig();
+ }
+ }
+}
diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/TokenPrincipal.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/TokenPrincipal.java
new file mode 100644
index 0000000000..d47434382b
--- /dev/null
+++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/TokenPrincipal.java
@@ -0,0 +1,56 @@
+/*
+ * 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;
+
+import java.security.Principal;
+
+import org.keycloak.adapters.authorization.util.JsonUtils;
+import org.keycloak.representations.AccessToken;
+
+/**
+ * A {@link Principal} backed by a token representing the entity requesting permissions.
+ *
+ * @author Pedro Igor
+ */
+public interface TokenPrincipal extends Principal {
+
+ /**
+ * The token in its raw format.
+ *
+ * @return the token in its raw format.
+ */
+ String getRawToken();
+
+ /**
+ * The {@link AccessToken} representation of {@link TokenPrincipal#getRawToken()}.
+ *
+ * @return the access token representation
+ */
+ default AccessToken getToken() {
+ return JsonUtils.asAccessToken(getRawToken());
+ }
+
+ /**
+ * The name of the entity represented by the token.
+ *
+ * @return the name of the principal
+ */
+ default String getName() {
+ return getToken().getPreferredUsername();
+ }
+}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProvider.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProvider.java
similarity index 85%
rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProvider.java
rename to authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProvider.java
index 93d31b6785..00eab06223 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProvider.java
+++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProvider.java
@@ -23,9 +23,9 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
-import org.keycloak.adapters.authorization.ClaimInformationPointProvider;
+import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProvider;
+import org.keycloak.adapters.authorization.spi.HttpRequest;
import org.keycloak.adapters.authorization.util.PlaceHolders;
-import org.keycloak.adapters.spi.HttpFacade;
/**
* @author Pedro Igor
@@ -39,7 +39,7 @@ public class ClaimsInformationPointProvider implements ClaimInformationPointProv
}
@Override
- public Map> resolve(HttpFacade httpFacade) {
+ public Map> resolve(HttpRequest request) {
Map> claims = new HashMap<>();
for (Entry configEntry : config.entrySet()) {
@@ -48,11 +48,11 @@ public class ClaimsInformationPointProvider implements ClaimInformationPointProv
List values = new ArrayList<>();
if (claimValue instanceof String) {
- values = getValues(claimValue.toString(), httpFacade);
+ values = getValues(claimValue.toString(), request);
} else if (claimValue instanceof Collection) {
for (Object value : Collection.class.cast(claimValue)) {
- List resolvedValues = getValues(value.toString(), httpFacade);
+ List resolvedValues = getValues(value.toString(), request);
if (!resolvedValues.isEmpty()) {
values.addAll(resolvedValues);
@@ -68,7 +68,7 @@ public class ClaimsInformationPointProvider implements ClaimInformationPointProv
return claims;
}
- private List getValues(String value, HttpFacade httpFacade) {
+ private List getValues(String value, HttpRequest httpFacade) {
return PlaceHolders.resolve(value, httpFacade);
}
}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProviderFactory.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProviderFactory.java
similarity index 84%
rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProviderFactory.java
rename to authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProviderFactory.java
index c86c2013c8..7c1d82f0c6 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProviderFactory.java
+++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProviderFactory.java
@@ -18,8 +18,7 @@ package org.keycloak.adapters.authorization.cip;
import java.util.Map;
-import org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory;
-import org.keycloak.adapters.authorization.PolicyEnforcer;
+import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProviderFactory;
/**
* @author Pedro Igor
@@ -31,11 +30,6 @@ public class ClaimsInformationPointProviderFactory implements ClaimInformationPo
return "claims";
}
- @Override
- public void init(PolicyEnforcer policyEnforcer) {
-
- }
-
@Override
public ClaimsInformationPointProvider create(Map config) {
return new ClaimsInformationPointProvider(config);
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProvider.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProvider.java
similarity index 89%
rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProvider.java
rename to authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProvider.java
index 2229cafde3..958df54779 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProvider.java
+++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProvider.java
@@ -36,11 +36,10 @@ import org.apache.http.StatusLine;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.util.EntityUtils;
-import org.keycloak.adapters.authorization.ClaimInformationPointProvider;
-import org.keycloak.adapters.authorization.PolicyEnforcer;
+import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProvider;
+import org.keycloak.adapters.authorization.spi.HttpRequest;
import org.keycloak.adapters.authorization.util.JsonUtils;
import org.keycloak.adapters.authorization.util.PlaceHolders;
-import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.authorization.client.util.HttpResponseException;
import org.keycloak.common.util.StreamUtil;
import org.keycloak.util.JsonSerialization;
@@ -53,15 +52,15 @@ public class HttpClaimInformationPointProvider implements ClaimInformationPointP
private final Map config;
private final HttpClient httpClient;
- public HttpClaimInformationPointProvider(Map config, PolicyEnforcer policyEnforcer) {
+ public HttpClaimInformationPointProvider(Map config, HttpClient httpClient) {
this.config = config;
- this.httpClient = policyEnforcer.getDeployment().getClient();
+ this.httpClient = httpClient;
}
@Override
- public Map> resolve(HttpFacade httpFacade) {
+ public Map> resolve(HttpRequest request) {
try {
- InputStream responseStream = executeRequest(httpFacade);
+ InputStream responseStream = executeRequest(request);
try (InputStream inputStream = new BufferedInputStream(responseStream)) {
JsonNode jsonNode = JsonSerialization.mapper.readTree(inputStream);
@@ -102,7 +101,7 @@ public class HttpClaimInformationPointProvider implements ClaimInformationPointP
}
}
- private InputStream executeRequest(HttpFacade httpFacade) {
+ private InputStream executeRequest(HttpRequest request) {
String method = config.get("method").toString();
if (method == null) {
@@ -122,10 +121,10 @@ public class HttpClaimInformationPointProvider implements ClaimInformationPointP
byte[] bytes = new byte[0];
try {
- setParameters(builder, httpFacade);
+ setParameters(builder, request);
if (config.containsKey("headers")) {
- setHeaders(builder, httpFacade);
+ setHeaders(builder, request);
}
HttpResponse response = httpClient.execute(builder.build());
@@ -152,7 +151,7 @@ public class HttpClaimInformationPointProvider implements ClaimInformationPointP
}
}
- private void setHeaders(RequestBuilder builder, HttpFacade httpFacade) {
+ private void setHeaders(RequestBuilder builder, HttpRequest request) {
Object headersDef = config.get("headers");
if (headersDef != null) {
@@ -166,10 +165,10 @@ public class HttpClaimInformationPointProvider implements ClaimInformationPointP
Collection values = Collection.class.cast(value);
for (Object item : values) {
- headerValues.addAll(PlaceHolders.resolve(item.toString(), httpFacade));
+ headerValues.addAll(PlaceHolders.resolve(item.toString(), request));
}
} else {
- headerValues.addAll(PlaceHolders.resolve(value.toString(), httpFacade));
+ headerValues.addAll(PlaceHolders.resolve(value.toString(), request));
}
for (String headerValue : headerValues) {
@@ -179,7 +178,7 @@ public class HttpClaimInformationPointProvider implements ClaimInformationPointP
}
}
- private void setParameters(RequestBuilder builder, HttpFacade httpFacade) {
+ private void setParameters(RequestBuilder builder, HttpRequest request) {
Object config = this.config.get("parameters");
if (config != null) {
@@ -193,10 +192,10 @@ public class HttpClaimInformationPointProvider implements ClaimInformationPointP
Collection values = Collection.class.cast(value);
for (Object item : values) {
- paramValues.addAll(PlaceHolders.resolve(item.toString(), httpFacade));
+ paramValues.addAll(PlaceHolders.resolve(item.toString(), request));
}
} else {
- paramValues.addAll(PlaceHolders.resolve(value.toString(), httpFacade));
+ paramValues.addAll(PlaceHolders.resolve(value.toString(), request));
}
for (String paramValue : paramValues) {
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProviderFactory.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProviderFactory.java
similarity index 92%
rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProviderFactory.java
rename to authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProviderFactory.java
index 6e3656203e..3c9a4ec264 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProviderFactory.java
+++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProviderFactory.java
@@ -18,8 +18,8 @@ package org.keycloak.adapters.authorization.cip;
import java.util.Map;
-import org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory;
import org.keycloak.adapters.authorization.PolicyEnforcer;
+import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProviderFactory;
/**
* @author Pedro Igor
@@ -40,6 +40,6 @@ public class HttpClaimInformationPointProviderFactory implements ClaimInformatio
@Override
public HttpClaimInformationPointProvider create(Map config) {
- return new HttpClaimInformationPointProvider(config, policyEnforcer);
+ return new HttpClaimInformationPointProvider(config, policyEnforcer.getHttpClient());
}
}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/ClaimInformationPointProvider.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/spi/ClaimInformationPointProvider.java
similarity index 77%
rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/ClaimInformationPointProvider.java
rename to authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/spi/ClaimInformationPointProvider.java
index fb594ef177..43325a8c83 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/ClaimInformationPointProvider.java
+++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/spi/ClaimInformationPointProvider.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2018 Red Hat, Inc. and/or its affiliates
+ * 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");
@@ -14,17 +14,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.keycloak.adapters.authorization;
+package org.keycloak.adapters.authorization.cip.spi;
import java.util.List;
import java.util.Map;
-import org.keycloak.adapters.spi.HttpFacade;
+import org.keycloak.adapters.authorization.spi.HttpRequest;
/**
* @author Pedro Igor
*/
public interface ClaimInformationPointProvider {
- Map> resolve(HttpFacade httpFacade);
+ Map> resolve(HttpRequest request);
}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/ClaimInformationPointProviderFactory.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/spi/ClaimInformationPointProviderFactory.java
similarity index 79%
rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/ClaimInformationPointProviderFactory.java
rename to authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/spi/ClaimInformationPointProviderFactory.java
index 894debc76a..f9e1e305a7 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/ClaimInformationPointProviderFactory.java
+++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/spi/ClaimInformationPointProviderFactory.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2018 Red Hat, Inc. and/or its affiliates
+ * 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");
@@ -14,11 +14,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.keycloak.adapters.authorization;
+package org.keycloak.adapters.authorization.cip.spi;
import java.util.Map;
-import org.keycloak.adapters.spi.HttpFacade;
+import org.keycloak.adapters.authorization.PolicyEnforcer;
/**
* @author Pedro Igor
@@ -27,7 +27,9 @@ public interface ClaimInformationPointProviderFactory config);
}
diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/spi/HttpRequest.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/spi/HttpRequest.java
new file mode 100644
index 0000000000..8fd0722488
--- /dev/null
+++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/spi/HttpRequest.java
@@ -0,0 +1,114 @@
+/*
+ * 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 java.io.InputStream;
+import java.util.List;
+
+import org.keycloak.adapters.authorization.TokenPrincipal;
+
+/**
+ * Represents an incoming HTTP request and the contract to manipulate it.
+ *
+ * @author Pedro Igor
+ */
+public interface HttpRequest {
+
+ /**
+ * Get the request path. This is the path relative to the context path.
+ * E.g.: for a HTTP GET request to http://my.appserver.com/my-application/path/sub-path this method is going to return /path/sub-path.
+
+ * @return the relative path
+ */
+ String getRelativePath();
+
+ /**
+ * Returns the name of the HTTP method with which this request was made, for example, GET, POST, or PUT.
+ *
+ * @return a {@code String} specifying the name of the method with which this request was made
+ */
+ String getMethod();
+
+ /**
+ * Get the URI representation for the current request.
+ *
+ * @return a {@code String} representation for the current request
+ */
+ String getURI();
+
+ /**
+ * Get a list of all of the values set for the specified header within the HTTP request.
+ *
+ * @param name the header name
+ * @return a list of the values set for this header, if the header is not set on the request then null should be returned
+ */
+ List getHeaders(String name);
+
+ /**
+ * Get the first value for a parameter with the given {@code name}
+ *
+ * @param name the parameter name
+ * @return the value of the parameter
+ */
+ String getFirstParam(String name);
+
+ /**
+ * Get the first value for a cookie with the given {@code name}.
+ *
+ * @param name the parameter name
+ * @return the value of the cookie
+ */
+ String getCookieValue(String name);
+
+ /**
+ * Returns the client address.
+ *
+ * @return the client address.
+ */
+ String getRemoteAddr();
+
+ /**
+ * Indicates if the request is coming from a secure channel through HTTPS.
+ *
+ * @return {@code true} if the HTTP scheme is set to 'https'. Otherwise, {@code false}
+ */
+ boolean isSecure();
+
+ /**
+ * Get the first value for a HEADER with the given {@code name}.
+ *
+ * @param name the HEADER name
+ * @return the value of the HEADER
+ */
+ String getHeader(String name);
+
+ /**
+ * Returns the request input stream
+ *
+ * @param buffered if the input stream should be buffered and support for multiple reads
+ * @return the request input stream
+ */
+ InputStream getInputStream(boolean buffered);
+
+ /**
+ * Returns a {@link TokenPrincipal} associated with the request.
+ *
+ * @return the principal
+ */
+ TokenPrincipal getPrincipal();
+}
diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/spi/HttpResponse.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/spi/HttpResponse.java
new file mode 100644
index 0000000000..dcd41a5f99
--- /dev/null
+++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/spi/HttpResponse.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+/**
+ * Represents an outgoing HTTP response and the contract to manipulate it.
+ *
+ * @author Pedro Igor
+ */
+public interface HttpResponse {
+
+ /**
+ * Send an error with the given {@code statusCode}.
+ *
+ * @param statusCode the status to set in the response
+ */
+ void sendError(int statusCode);
+
+ /**
+ * Send an error with the given {@code statusCode} and {@code reason} message.
+ *
+ * @param statusCode the status to set in the response
+ */
+ void sendError(int statusCode, String reason);
+
+ /**
+ * Set a header with the given {@code name} and {@code value}.
+ *
+ * @param name the header name
+ * @param value the header value
+ */
+ void setHeader(String name, String value);
+}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/JsonUtils.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/JsonUtils.java
similarity index 82%
rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/JsonUtils.java
rename to authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/JsonUtils.java
index c3d03659fb..5c06ca60b2 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/JsonUtils.java
+++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/JsonUtils.java
@@ -21,9 +21,13 @@ import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.databind.JsonNode;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.representations.AccessToken;
import org.keycloak.util.JsonSerialization;
/**
+ * Utility methods to manipulate JSON data
+ *
* @author Pedro Igor
*/
public class JsonUtils {
@@ -36,7 +40,6 @@ public class JsonUtils {
List values = new ArrayList<>();
if (jsonNode.isArray()) {
-
for (JsonNode node : jsonNode) {
String value;
@@ -65,4 +68,11 @@ public class JsonUtils {
return values;
}
+ public static AccessToken asAccessToken(String rawToken) {
+ try {
+ return new JWSInput(rawToken).readJsonContent(AccessToken.class);
+ } catch (Exception cause) {
+ throw new RuntimeException("Failed to decode token", cause);
+ }
+ }
}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/KeycloakSecurityContextPlaceHolderResolver.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/KeycloakSecurityContextPlaceHolderResolver.java
similarity index 64%
rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/KeycloakSecurityContextPlaceHolderResolver.java
rename to authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/KeycloakSecurityContextPlaceHolderResolver.java
index d59cfd7861..b5f824ad9a 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/KeycloakSecurityContextPlaceHolderResolver.java
+++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/KeycloakSecurityContextPlaceHolderResolver.java
@@ -22,9 +22,8 @@ import java.util.Arrays;
import java.util.List;
import com.fasterxml.jackson.databind.JsonNode;
-import org.keycloak.KeycloakSecurityContext;
-import org.keycloak.adapters.OIDCHttpFacade;
-import org.keycloak.adapters.spi.HttpFacade;
+import org.keycloak.adapters.authorization.TokenPrincipal;
+import org.keycloak.adapters.authorization.spi.HttpRequest;
import org.keycloak.util.JsonSerialization;
/**
@@ -35,29 +34,18 @@ public class KeycloakSecurityContextPlaceHolderResolver implements PlaceHolderRe
public static final String NAME = "keycloak";
@Override
- public List resolve(String placeHolder, HttpFacade httpFacade) {
+ public List resolve(String placeHolder, HttpRequest request) {
String source = placeHolder.substring(placeHolder.indexOf('.') + 1);
- OIDCHttpFacade oidcHttpFacade = OIDCHttpFacade.class.cast(httpFacade);
- KeycloakSecurityContext securityContext = oidcHttpFacade.getSecurityContext();
-
- if (securityContext == null) {
- return null;
- }
+ TokenPrincipal principal = request.getPrincipal();
if (source.endsWith("access_token")) {
- return Arrays.asList(securityContext.getTokenString());
- }
-
- if (source.endsWith("id_token")) {
- return Arrays.asList(securityContext.getIdTokenString());
+ return Arrays.asList(principal.getRawToken());
}
JsonNode jsonNode;
if (source.startsWith("access_token[")) {
- jsonNode = JsonSerialization.mapper.valueToTree(securityContext.getToken());
- } else if (source.startsWith("id_token[")) {
- jsonNode = JsonSerialization.mapper.valueToTree(securityContext.getIdToken());
+ jsonNode = JsonSerialization.mapper.valueToTree(principal.getToken());
} else {
throw new RuntimeException("Invalid placeholder [" + placeHolder + "]");
}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolderResolver.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolderResolver.java
similarity index 86%
rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolderResolver.java
rename to authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolderResolver.java
index 62d6e1e51a..935fc47db1 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolderResolver.java
+++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolderResolver.java
@@ -18,13 +18,13 @@ package org.keycloak.adapters.authorization.util;
import java.util.List;
-import org.keycloak.adapters.spi.HttpFacade;
+import org.keycloak.adapters.authorization.spi.HttpRequest;
/**
* @author Pedro Igor
*/
public interface PlaceHolderResolver {
- List resolve(String placeHolder, HttpFacade httpFacade);
+ List resolve(String placeHolder, HttpRequest httpFacade);
}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolders.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolders.java
similarity index 71%
rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolders.java
rename to authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolders.java
index 7a39dbf094..d7a4f5f479 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolders.java
+++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolders.java
@@ -17,6 +17,7 @@
package org.keycloak.adapters.authorization.util;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -24,7 +25,7 @@ import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import org.keycloak.adapters.spi.HttpFacade;
+import org.keycloak.adapters.authorization.spi.HttpRequest;
/**
* @author Pedro Igor
@@ -41,7 +42,7 @@ public class PlaceHolders {
private static Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{(.+?)\\}");
private static Pattern PLACEHOLDER_PARAM_PATTERN = Pattern.compile("\\[(.+?)\\]");
- public static List resolve(String value, HttpFacade httpFacade) {
+ public static List resolve(String value, HttpRequest httpFacade) {
Map> placeHolders = parsePlaceHolders(value, httpFacade);
if (!placeHolders.isEmpty()) {
@@ -75,27 +76,31 @@ public class PlaceHolders {
return null;
}
- private static Map> parsePlaceHolders(String value, HttpFacade httpFacade) {
- Map> placeHolders = new HashMap<>();
+ private static Map> parsePlaceHolders(String value, HttpRequest httpFacade) {
+ Map> placeHolders = Collections.emptyMap();
Matcher matcher = PLACEHOLDER_PATTERN.matcher(value);
+ boolean found = matcher.find();
- while (matcher.find()) {
- String placeHolder = matcher.group(1);
- int resolverNameIdx = placeHolder.indexOf('.');
+ if (found) {
+ placeHolders = new HashMap<>();
+ do {
+ String placeHolder = matcher.group(1);
+ int resolverNameIdx = placeHolder.indexOf('.');
- if (resolverNameIdx == -1) {
- throw new RuntimeException("Invalid placeholder [" + value + "]. Could not find resolver name.");
- }
-
- PlaceHolderResolver resolver = resolvers.get(placeHolder.substring(0, resolverNameIdx));
-
- if (resolver != null) {
- List resolved = resolver.resolve(placeHolder, httpFacade);
-
- if (resolved != null) {
- placeHolders.put(formatPlaceHolder(placeHolder), resolved);
+ if (resolverNameIdx == -1) {
+ throw new RuntimeException("Invalid placeholder [" + value + "]. Could not find resolver name.");
}
- }
+
+ PlaceHolderResolver resolver = resolvers.get(placeHolder.substring(0, resolverNameIdx));
+
+ if (resolver != null) {
+ List resolved = resolver.resolve(placeHolder, httpFacade);
+
+ if (resolved != null) {
+ placeHolders.put(formatPlaceHolder(placeHolder), resolved);
+ }
+ }
+ } while (matcher.find());
}
return placeHolders;
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/RequestPlaceHolderResolver.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/RequestPlaceHolderResolver.java
similarity index 91%
rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/RequestPlaceHolderResolver.java
rename to authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/RequestPlaceHolderResolver.java
index 607e28b188..5d49625e99 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/RequestPlaceHolderResolver.java
+++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/RequestPlaceHolderResolver.java
@@ -29,9 +29,7 @@ import java.util.Collections;
import java.util.List;
import com.fasterxml.jackson.databind.JsonNode;
-import org.keycloak.adapters.spi.HttpFacade;
-import org.keycloak.adapters.spi.HttpFacade.Cookie;
-import org.keycloak.adapters.spi.HttpFacade.Request;
+import org.keycloak.adapters.authorization.spi.HttpRequest;
import org.keycloak.util.JsonSerialization;
/**
@@ -42,17 +40,12 @@ public class RequestPlaceHolderResolver implements PlaceHolderResolver {
static String NAME = "request";
@Override
- public List resolve(String placeHolder, HttpFacade httpFacade) {
+ public List resolve(String placeHolder, HttpRequest request) {
String source = placeHolder.substring(placeHolder.indexOf('.') + 1);
- Request request = httpFacade.getRequest();
if (source.startsWith("parameter")) {
String parameterName = getParameter(source, "Could not obtain parameter name from placeholder [" + source + "]");
- String parameterValue = request.getQueryParamValue(parameterName);
-
- if (parameterValue == null) {
- parameterValue = request.getFirstParam(parameterName);
- }
+ String parameterValue = request.getFirstParam(parameterName);
if (parameterValue != null) {
return Arrays.asList(parameterValue);
@@ -66,10 +59,10 @@ public class RequestPlaceHolderResolver implements PlaceHolderResolver {
}
} else if (source.startsWith("cookie")) {
String cookieName = getParameter(source, "Could not obtain cookie name from placeholder [" + source + "]");
- Cookie cookieValue = request.getCookie(cookieName);
+ String cookieValue = request.getCookieValue(cookieName);
if (cookieValue != null) {
- return Arrays.asList(cookieValue.getValue());
+ return Arrays.asList(cookieValue);
}
} else if (source.startsWith("remoteAddr")) {
String value = request.getRemoteAddr();
diff --git a/adapters/oidc/adapter-core/src/main/resources/META-INF/services/org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory b/authz/policy-enforcer/src/main/resources/META-INF/services/org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProviderFactory
similarity index 100%
rename from adapters/oidc/adapter-core/src/main/resources/META-INF/services/org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory
rename to authz/policy-enforcer/src/main/resources/META-INF/services/org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProviderFactory
diff --git a/authz/policy/common/pom.xml b/authz/policy/common/pom.xml
index 63bebbe8d4..d5b13c0cff 100644
--- a/authz/policy/common/pom.xml
+++ b/authz/policy/common/pom.xml
@@ -32,7 +32,7 @@
keycloak-authz-policy-common
jar
- KeyCloak AuthZ: Common Policy Providers
+ Keycloak AuthZ: Common Policy Providers
KeyCloak AuthZ: Common Policy Providers
diff --git a/authz/policy/pom.xml b/authz/policy/pom.xml
index 1245fa38e5..48091592fa 100644
--- a/authz/policy/pom.xml
+++ b/authz/policy/pom.xml
@@ -14,8 +14,7 @@
keycloak-authz-provider-parent
pom
- KeyCloak AuthZ: Provider Parent
- KeyCloak AuthZ: Provider Parent
+ Keycloak AuthZ: Policy Provider Parent
common
diff --git a/authz/pom.xml b/authz/pom.xml
index 96fe528f03..31224da503 100644
--- a/authz/pom.xml
+++ b/authz/pom.xml
@@ -14,11 +14,12 @@
keycloak-authz-parent
pom
- KeyCloak Authz: Parent
+ Keycloak Authz: Parent
KeyCloak AuthZ: Parent
policy
client
+ policy-enforcer
\ No newline at end of file
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProvider.java b/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/ClientCredentialsProvider.java
similarity index 79%
rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProvider.java
rename to core/src/main/java/org/keycloak/protocol/oidc/client/authentication/ClientCredentialsProvider.java
index c84af87ce7..220ad8f146 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProvider.java
+++ b/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/ClientCredentialsProvider.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * 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");
@@ -15,12 +15,12 @@
* limitations under the License.
*/
-package org.keycloak.adapters.authentication;
-
-import org.keycloak.adapters.KeycloakDeployment;
+package org.keycloak.protocol.oidc.client.authentication;
import java.util.Map;
+import org.keycloak.representations.adapters.config.AdapterConfig;
+
/**
* The simple SPI for authenticating clients/applications . It's used by adapter during all OIDC backchannel requests to Keycloak server
* (codeToToken exchange, refresh token or backchannel logout) . You can also use it in your application during direct access grants or service account request
@@ -30,15 +30,12 @@ import java.util.Map;
* so your server is able to authenticate client
*
* You must specify a file
- * META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider in the WAR that this class is contained in (or in the JAR that is attached to the WEB-INF/lib or as jboss module
+ * META-INF/services/org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider in the WAR that this class is contained in (or in the JAR that is attached to the WEB-INF/lib or as jboss module
* if you want to share the implementation among more WARs).
*
* NOTE: The SPI is not finished and method signatures are still subject to change in future versions (for example to support
* authentication with client certificate)
*
- * @see ClientIdAndSecretCredentialsProvider
- * @see JWTClientCredentialsProvider
- *
* @author Marek Posolda
*/
public interface ClientCredentialsProvider {
@@ -62,17 +59,17 @@ public interface ClientCredentialsProvider {
/**
* Called by adapter during deployment of your application. You can for example read configuration and init your authenticator here
*
- * @param deployment the adapter configuration
+ * @param adapterConfig the adapter configuration
* @param config the configuration of your provider read from keycloak.json . For the kerberos-keytab example above, it will return map with the single key "keytab" with value "/tmp/foo"
*/
- void init(KeycloakDeployment deployment, Object config);
+ void init(AdapterConfig adapterConfig, Object config);
/**
* Called every time adapter needs to perform backchannel request
*
- * @param deployment Fully resolved deployment
+ * @param adapterConfig Fully resolved deployment
* @param requestHeaders You should put any HTTP request headers you want to use for authentication of client. These headers will be attached to the HTTP request sent to Keycloak server
* @param formParams You should put any request parameters you want to use for authentication of client. These parameters will be attached to the HTTP request sent to Keycloak server
*/
- void setClientCredentials(KeycloakDeployment deployment, Map requestHeaders, Map formParams);
+ void setClientCredentials(AdapterConfig adapterConfig, Map requestHeaders, Map formParams);
}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProviderUtils.java b/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/ClientCredentialsProviderUtils.java
similarity index 69%
rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProviderUtils.java
rename to core/src/main/java/org/keycloak/protocol/oidc/client/authentication/ClientCredentialsProviderUtils.java
index 4df727d937..dacd9aa40f 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProviderUtils.java
+++ b/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/ClientCredentialsProviderUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * 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");
@@ -15,21 +15,17 @@
* limitations under the License.
*/
-package org.keycloak.adapters.authentication;
-
-import org.apache.http.NameValuePair;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.message.BasicNameValuePair;
-import org.jboss.logging.Logger;
-import org.keycloak.adapters.KeycloakDeployment;
+package org.keycloak.protocol.oidc.client.authentication;
import java.util.HashMap;
import java.util.Iterator;
-import java.util.List;
import java.util.Map;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
+import org.jboss.logging.Logger;
+import org.keycloak.representations.adapters.config.AdapterConfig;
+
/**
* @author Marek Posolda
*/
@@ -37,9 +33,9 @@ public class ClientCredentialsProviderUtils {
private static Logger logger = Logger.getLogger(ClientCredentialsProviderUtils.class);
- public static ClientCredentialsProvider bootstrapClientAuthenticator(KeycloakDeployment deployment) {
- String clientId = deployment.getResourceName();
- Map clientCredentials = deployment.getResourceCredentials();
+ public static ClientCredentialsProvider bootstrapClientAuthenticator(AdapterConfig deployment) {
+ String clientId = deployment.getResource();
+ Map clientCredentials = deployment.getCredentials();
String authenticatorId;
if (clientCredentials == null || clientCredentials.isEmpty()) {
@@ -73,7 +69,7 @@ public class ClientCredentialsProviderUtils {
return authenticator;
}
- private static void loadAuthenticators(Map authenticators, ClassLoader classLoader) {
+ public static void loadAuthenticators(Map authenticators, ClassLoader classLoader) {
Iterator iterator = ServiceLoader.load(ClientCredentialsProvider.class, classLoader).iterator();
while (iterator.hasNext()) {
try {
@@ -91,26 +87,7 @@ public class ClientCredentialsProviderUtils {
/**
* Use this method when calling backchannel request directly from your application. See service-account example from demo for more details
*/
- public static void setClientCredentials(KeycloakDeployment deployment, Map requestHeaders, Map formparams) {
- ClientCredentialsProvider authenticator = deployment.getClientAuthenticator();
+ public static void setClientCredentials(AdapterConfig deployment, ClientCredentialsProvider authenticator, Map requestHeaders, Map formparams) {
authenticator.setClientCredentials(deployment, requestHeaders, formparams);
}
-
- /**
- * Don't use directly from your JEE apps to avoid HttpClient linkage errors! Instead use the method {@link #setClientCredentials(KeycloakDeployment, Map, Map)}
- */
- public static void setClientCredentials(KeycloakDeployment deployment, HttpPost post, List formparams) {
- Map reqHeaders = new HashMap<>();
- Map reqParams = new HashMap<>();
- setClientCredentials(deployment, reqHeaders, reqParams);
-
- for (Map.Entry header : reqHeaders.entrySet()) {
- post.setHeader(header.getKey(), header.getValue());
- }
-
- for (Map.Entry param : reqParams.entrySet()) {
- formparams.add(new BasicNameValuePair(param.getKey(), param.getValue()));
- }
- }
-
}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientIdAndSecretCredentialsProvider.java b/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/ClientIdAndSecretCredentialsProvider.java
similarity index 84%
rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientIdAndSecretCredentialsProvider.java
rename to core/src/main/java/org/keycloak/protocol/oidc/client/authentication/ClientIdAndSecretCredentialsProvider.java
index 61f1081555..b90233463d 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientIdAndSecretCredentialsProvider.java
+++ b/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/ClientIdAndSecretCredentialsProvider.java
@@ -15,16 +15,16 @@
* limitations under the License.
*/
-package org.keycloak.adapters.authentication;
+package org.keycloak.protocol.oidc.client.authentication;
+
+import java.util.Map;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
-import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.util.BasicAuthHelper;
-import java.util.Map;
-
/**
* Traditional OAuth2 authentication of clients based on client_id and client_secret
*
@@ -44,13 +44,13 @@ public class ClientIdAndSecretCredentialsProvider implements ClientCredentialsPr
}
@Override
- public void init(KeycloakDeployment deployment, Object config) {
+ public void init(AdapterConfig deployment, Object config) {
clientSecret = (config == null ? null : config.toString());
}
@Override
- public void setClientCredentials(KeycloakDeployment deployment, Map requestHeaders, Map formParams) {
- String clientId = deployment.getResourceName();
+ public void setClientCredentials(AdapterConfig deployment, Map requestHeaders, Map formParams) {
+ String clientId = deployment.getResource();
if (!deployment.isPublicClient()) {
if (clientSecret != null) {
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java b/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/JWTClientCredentialsProvider.java
similarity index 91%
rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java
rename to core/src/main/java/org/keycloak/protocol/oidc/client/authentication/JWTClientCredentialsProvider.java
index f2ed150639..9451b359e7 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java
+++ b/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/JWTClientCredentialsProvider.java
@@ -15,18 +15,15 @@
* limitations under the License.
*/
-package org.keycloak.adapters.authentication;
+package org.keycloak.protocol.oidc.client.authentication;
import java.security.KeyPair;
import java.security.PublicKey;
import java.util.Map;
+import java.util.UUID;
import org.keycloak.OAuth2Constants;
-import org.keycloak.adapters.AdapterUtils;
-import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.common.util.KeyUtils;
-import org.keycloak.jose.jws.JWSBuilder;
-import org.keycloak.representations.JsonWebToken;
import org.keycloak.common.util.KeystoreUtil;
import org.keycloak.common.util.Time;
import org.keycloak.crypto.Algorithm;
@@ -36,6 +33,9 @@ import org.keycloak.crypto.KeyType;
import org.keycloak.crypto.KeyUse;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.crypto.SignatureSignerContext;
+import org.keycloak.jose.jws.JWSBuilder;
+import org.keycloak.representations.JsonWebToken;
+import org.keycloak.representations.adapters.config.AdapterConfig;
/**
* Client authentication based on JWT signed by client private key .
@@ -102,16 +102,16 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
}
@Override
- public void init(KeycloakDeployment deployment, Object config) {
+ public void init(AdapterConfig deployment, Object config) {
if (!(config instanceof Map)) {
- throw new RuntimeException("Configuration of jwt credentials is missing or incorrect for client '" + deployment.getResourceName() + "'. Check your adapter configuration");
+ throw new RuntimeException("Configuration of jwt credentials is missing or incorrect for client '" + deployment.getResource() + "'. Check your adapter configuration");
}
Map cfg = (Map) config;
String clientKeystoreFile = (String) cfg.get("client-keystore-file");
if (clientKeystoreFile == null) {
- throw new RuntimeException("Missing parameter client-keystore-file in configuration of jwt for client " + deployment.getResourceName());
+ throw new RuntimeException("Missing parameter client-keystore-file in configuration of jwt for client " + deployment.getResource());
}
String clientKeystoreType = (String) cfg.get("client-keystore-type");
@@ -119,7 +119,7 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
String clientKeystorePassword = (String) cfg.get("client-keystore-password");
if (clientKeystorePassword == null) {
- throw new RuntimeException("Missing parameter client-keystore-password in configuration of jwt for client " + deployment.getResourceName());
+ throw new RuntimeException("Missing parameter client-keystore-password in configuration of jwt for client " + deployment.getResource());
}
String clientKeyPassword = (String) cfg.get("client-key-password");
@@ -129,7 +129,7 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
String clientKeyAlias = (String) cfg.get("client-key-alias");
if (clientKeyAlias == null) {
- clientKeyAlias = deployment.getResourceName();
+ clientKeyAlias = deployment.getResource();
}
String algorithm = (String) cfg.getOrDefault("algorithm", Algorithm.RS256);
@@ -157,8 +157,8 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
}
@Override
- public void setClientCredentials(KeycloakDeployment deployment, Map requestHeaders, Map formParams) {
- String signedToken = createSignedRequestToken(deployment.getResourceName(), deployment.getRealmInfoUrl());
+ public void setClientCredentials(AdapterConfig deployment, Map requestHeaders, Map formParams) {
+ String signedToken = createSignedRequestToken(deployment.getResource(), deployment.getRealmInfoUrl());
formParams.put(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT);
formParams.put(OAuth2Constants.CLIENT_ASSERTION, signedToken);
@@ -173,7 +173,7 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
protected JsonWebToken createRequestToken(String clientId, String realmInfoUrl) {
JsonWebToken reqToken = new JsonWebToken();
- reqToken.id(AdapterUtils.generateId());
+ reqToken.id(UUID.randomUUID().toString());
reqToken.issuer(clientId);
reqToken.subject(clientId);
reqToken.audience(realmInfoUrl);
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientSecretCredentialsProvider.java b/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/JWTClientSecretCredentialsProvider.java
similarity index 91%
rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientSecretCredentialsProvider.java
rename to core/src/main/java/org/keycloak/protocol/oidc/client/authentication/JWTClientSecretCredentialsProvider.java
index 454be93cc5..2764cb3761 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientSecretCredentialsProvider.java
+++ b/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/JWTClientSecretCredentialsProvider.java
@@ -14,23 +14,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.keycloak.adapters.authentication;
+package org.keycloak.protocol.oidc.client.authentication;
import java.nio.charset.StandardCharsets;
import java.util.Map;
+import java.util.UUID;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
-import org.keycloak.adapters.AdapterUtils;
-import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.common.util.Time;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.JavaAlgorithm;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.representations.JsonWebToken;
+import org.keycloak.representations.adapters.config.AdapterConfig;
/**
* Client authentication based on JWT signed by client secret instead of private key .
@@ -53,15 +53,15 @@ public class JWTClientSecretCredentialsProvider implements ClientCredentialsProv
}
@Override
- public void init(KeycloakDeployment deployment, Object config) {
+ public void init(AdapterConfig deployment, Object config) {
if (!(config instanceof Map)) {
- throw new RuntimeException("Configuration of jwt credentials by client secret is missing or incorrect for client '" + deployment.getResourceName() + "'. Check your adapter configuration");
+ throw new RuntimeException("Configuration of jwt credentials by client secret is missing or incorrect for client '" + deployment.getResource() + "'. Check your adapter configuration");
}
Map cfg = (Map) config;
String clientSecretString = (String) cfg.get("secret");
if (clientSecretString == null) {
- throw new RuntimeException("Missing parameter secret-jwt in configuration of jwt for client " + deployment.getResourceName());
+ throw new RuntimeException("Missing parameter secret-jwt in configuration of jwt for client " + deployment.getResource());
}
String clientSecretJwtAlg = (String) cfg.get("algorithm");
@@ -72,7 +72,7 @@ public class JWTClientSecretCredentialsProvider implements ClientCredentialsProv
setClientSecret(clientSecretString, clientSecretJwtAlg);
} else {
// invalid "algorithm" field
- throw new RuntimeException("Invalid parameter secret-jwt in configuration of jwt for client " + deployment.getResourceName());
+ throw new RuntimeException("Invalid parameter secret-jwt in configuration of jwt for client " + deployment.getResource());
}
}
@@ -84,8 +84,8 @@ public class JWTClientSecretCredentialsProvider implements ClientCredentialsProv
}
@Override
- public void setClientCredentials(KeycloakDeployment deployment, Map requestHeaders, Map formParams) {
- String signedToken = createSignedRequestToken(deployment.getResourceName(), deployment.getRealmInfoUrl());
+ public void setClientCredentials(AdapterConfig deployment, Map requestHeaders, Map formParams) {
+ String signedToken = createSignedRequestToken(deployment.getResource(), deployment.getRealmInfoUrl());
formParams.put(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT);
formParams.put(OAuth2Constants.CLIENT_ASSERTION, signedToken);
}
@@ -126,7 +126,7 @@ public class JWTClientSecretCredentialsProvider implements ClientCredentialsProv
// JWT claims is the same as one by private_key_jwt
JsonWebToken reqToken = new JsonWebToken();
- reqToken.id(AdapterUtils.generateId());
+ reqToken.id(UUID.randomUUID().toString());
reqToken.issuer(clientId);
reqToken.subject(clientId);
reqToken.audience(realmInfoUrl);
diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java
index a358eb392c..0c99011a4a 100755
--- a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java
+++ b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java
@@ -17,6 +17,7 @@
package org.keycloak.representations.adapters.config;
+import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
@@ -320,4 +321,9 @@ public class AdapterConfig extends BaseAdapterConfig implements AdapterHttpClien
public void setConnectionTTL(long connectionTTL) {
this.connectionTTL = connectionTTL;
}
+
+ @JsonIgnore
+ public String getRealmInfoUrl() {
+ return authServerUrl + "/realms/" + realm;
+ }
}
diff --git a/adapters/oidc/adapter-core/src/main/resources/META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider b/core/src/main/resources/META-INF/services/org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider
similarity index 72%
rename from adapters/oidc/adapter-core/src/main/resources/META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider
rename to core/src/main/resources/META-INF/services/org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider
index b02accf8dc..e08602e2d9 100644
--- a/adapters/oidc/adapter-core/src/main/resources/META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider
+++ b/core/src/main/resources/META-INF/services/org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider
@@ -15,6 +15,6 @@
# limitations under the License.
#
-org.keycloak.adapters.authentication.ClientIdAndSecretCredentialsProvider
-org.keycloak.adapters.authentication.JWTClientCredentialsProvider
-org.keycloak.adapters.authentication.JWTClientSecretCredentialsProvider
\ No newline at end of file
+org.keycloak.protocol.oidc.client.authentication.ClientIdAndSecretCredentialsProvider
+org.keycloak.protocol.oidc.client.authentication.JWTClientCredentialsProvider
+org.keycloak.protocol.oidc.client.authentication.JWTClientSecretCredentialsProvider
\ No newline at end of file
diff --git a/distribution/feature-packs/adapter-feature-pack/pom.xml b/distribution/feature-packs/adapter-feature-pack/pom.xml
index 28e8780a15..c76f58d7f2 100755
--- a/distribution/feature-packs/adapter-feature-pack/pom.xml
+++ b/distribution/feature-packs/adapter-feature-pack/pom.xml
@@ -154,6 +154,16 @@