keycloak-uncached
Changes
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java 41(+40 -1)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/BearerTokenPolicyEnforcer.java 81(+0 -81)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProvider.java 76(+76 -0)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProviderFactory.java 43(+43 -0)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProvider.java 210(+210 -0)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProviderFactory.java 45(+45 -0)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/ClaimInformationPointProvider.java 30(+30 -0)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/ClaimInformationPointProviderFactory.java 33(+33 -0)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java 145(+124 -21)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java 44(+31 -13)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/JsonUtils.java 58(+58 -0)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/KeycloakSecurityContextPlaceHolderResolver.java 67(+67 -0)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolderResolver.java 30(+30 -0)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolders.java 107(+107 -0)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/RequestPlaceHolderResolver.java 175(+175 -0)
adapters/oidc/adapter-core/src/main/resources/META-INF/services/org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory 19(+19 -0)
adapters/oidc/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java 25(+23 -2)
adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletRequest.java 22(+21 -1)
adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronHttpFacade.java 19(+18 -1)
adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java 16(+16 -0)
adapters/spi/jetty-adapter-spi/src/main/java/org/keycloak/adapters/jetty/spi/JettyHttpFacade.java 22(+22 -0)
adapters/spi/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/ServletHttpFacade.java 22(+22 -0)
adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/CatalinaHttpFacade.java 22(+22 -0)
adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/UndertowHttpFacade.java 54(+53 -1)
core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java 12(+12 -0)
core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationRequest.java 10(+10 -0)
core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketToken.java 14(+14 -0)
examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/OfflineAccessPortalServlet.java 25(+23 -2)
services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java 49(+40 -9)
services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java 74(+35 -39)
testsuite/integration-arquillian/test-apps/servlet-authz/keycloak-claim-information-point-authz-service.json 25(+25 -0)
testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-app-authz-service.json 24(+24 -0)
testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/protected/context/context.jsp 14(+14 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzCIPAdapterTest.java 59(+59 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ClaimInformationPointProviderTest.java 457(+457 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/EnforcerConfigTest.java 28(+27 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/PolicyEnforcerTest.java 512(+512 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/EntitlementAPITest.java 71(+71 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionManagementTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UmaPermissionTicketPushedClaimsTest.java 100(+100 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/default-keycloak-public-client.json 5(+5 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/enforcer-config-claims-provider.json 89(+89 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/enforcer-config-path-cip.json 33(+33 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/enforcer-entitlement-claims-test.json 29(+29 -0)
Details
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java
index 402ed48..c54191f 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java
@@ -18,7 +18,10 @@
package org.keycloak.adapters.authorization;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
import java.util.Set;
import org.jboss.logging.Logger;
@@ -161,7 +164,8 @@ public abstract class AbstractPolicyEnforcer {
if (HTTP_METHOD_DELETE.equalsIgnoreCase(request.getMethod()) && actualPathConfig.isInstance()) {
policyEnforcer.getPathMatcher().removeFromCache(getPath(request));
}
- return true;
+
+ return hasValidClaims(actualPathConfig, httpFacade, authorization);
}
}
} else {
@@ -183,6 +187,41 @@ public abstract class AbstractPolicyEnforcer {
return false;
}
+ private boolean hasValidClaims(PathConfig actualPathConfig, OIDCHttpFacade httpFacade, Authorization authorization) {
+ Map<String, Map<String, Object>> claimInformationPointConfig = actualPathConfig.getClaimInformationPointConfig();
+
+ if (claimInformationPointConfig != null) {
+ Map<String, List<String>> claims = new HashMap<>();
+
+ for (Entry<String, Map<String, Object>> entry : claimInformationPointConfig.entrySet()) {
+ ClaimInformationPointProviderFactory factory = policyEnforcer.getClaimInformationPointProviderFactories().get(entry.getKey());
+
+ if (factory == null) {
+ throw new RuntimeException("Could not find claim information provider with name [" + entry.getKey() + "]");
+ }
+
+ claims.putAll(factory.create(entry.getValue()).resolve(httpFacade));
+ }
+
+ Map<String, List<String>> grantedClaims = authorization.getClaims();
+
+ if (grantedClaims != null) {
+ if (claims.isEmpty()) {
+ return false;
+ }
+ for (Entry<String, List<String>> entry : grantedClaims.entrySet()) {
+ List<String> requestClaims = claims.get(entry.getKey());
+
+ if (requestClaims == null || requestClaims.isEmpty() || !entry.getValue().containsAll(requestClaims)) {
+ return false;
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
protected void handleAccessDenied(OIDCHttpFacade httpFacade) {
httpFacade.getResponse().sendError(403);
}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProvider.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProvider.java
new file mode 100644
index 0000000..0221c8d
--- /dev/null
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProvider.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2018 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.cip;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.keycloak.adapters.authorization.ClaimInformationPointProvider;
+import org.keycloak.adapters.authorization.util.PlaceHolders;
+import org.keycloak.adapters.spi.HttpFacade;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ClaimsInformationPointProvider implements ClaimInformationPointProvider {
+
+ private final Map<String, Object> config;
+
+ public ClaimsInformationPointProvider(Map<String, Object> config) {
+ this.config = config;
+ }
+
+ @Override
+ public Map<String, List<String>> resolve(HttpFacade httpFacade) {
+ Map<String, List<String>> claims = new HashMap<>();
+
+ for (Entry<String, Object> configEntry : config.entrySet()) {
+ String claimName = configEntry.getKey();
+ Object claimValue = configEntry.getValue();
+ List<String> values = new ArrayList<>();
+
+ if (claimValue instanceof String) {
+ values = getValues(claimValue.toString(), httpFacade);
+ } else if (claimValue instanceof Collection) {
+ Iterator iterator = Collection.class.cast(claimValue).iterator();
+
+ while (iterator.hasNext()) {
+ List<String> resolvedValues = getValues(iterator.next().toString(), httpFacade);
+
+ if (!resolvedValues.isEmpty()) {
+ values.addAll(resolvedValues);
+ }
+ }
+ }
+
+ if (!values.isEmpty()) {
+ claims.put(claimName, values);
+ }
+ }
+
+ return claims;
+ }
+
+ private List<String> getValues(String value, HttpFacade httpFacade) {
+ return PlaceHolders.resolve(value, httpFacade);
+ }
+}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProviderFactory.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProviderFactory.java
new file mode 100644
index 0000000..c86c201
--- /dev/null
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProviderFactory.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2018 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.cip;
+
+import java.util.Map;
+
+import org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory;
+import org.keycloak.adapters.authorization.PolicyEnforcer;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ClaimsInformationPointProviderFactory implements ClaimInformationPointProviderFactory<ClaimsInformationPointProvider> {
+
+ @Override
+ public String getName() {
+ return "claims";
+ }
+
+ @Override
+ public void init(PolicyEnforcer policyEnforcer) {
+
+ }
+
+ @Override
+ public ClaimsInformationPointProvider create(Map<String, Object> config) {
+ return new ClaimsInformationPointProvider(config);
+ }
+}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProvider.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProvider.java
new file mode 100644
index 0000000..51efb90
--- /dev/null
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProvider.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2018 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.cip;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+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.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;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class HttpClaimInformationPointProvider implements ClaimInformationPointProvider {
+
+ private final Map<String, Object> config;
+ private final HttpClient httpClient;
+
+ public HttpClaimInformationPointProvider(Map<String, Object> config, PolicyEnforcer policyEnforcer) {
+ this.config = config;
+ this.httpClient = policyEnforcer.getDeployment().getClient();
+ }
+
+ @Override
+ public Map<String, List<String>> resolve(HttpFacade httpFacade) {
+ try {
+ InputStream responseStream = executeRequest(httpFacade);
+
+ try (InputStream inputStream = new BufferedInputStream(responseStream)) {
+ JsonNode jsonNode = JsonSerialization.mapper.readTree(inputStream);
+ Map<String, List<String>> claims = new HashMap<>();
+ Map<String, Object> claimsDef = (Map<String, Object>) config.get("claims");
+
+ if (claimsDef == null) {
+ Iterator<String> nodeNames = jsonNode.fieldNames();
+
+ while (nodeNames.hasNext()) {
+ String nodeName = nodeNames.next();
+ claims.put(nodeName, JsonUtils.getValues(jsonNode.get(nodeName)));
+ }
+ } else {
+ for (Entry<String, Object> claimDef : claimsDef.entrySet()) {
+ List<String> jsonPaths = new ArrayList<>();
+
+ if (claimDef.getValue() instanceof Collection) {
+ jsonPaths.addAll(Collection.class.cast(claimDef.getValue()));
+ } else {
+ jsonPaths.add(claimDef.getValue().toString());
+ }
+
+ List<String> claimValues = new ArrayList<>();
+
+ for (String path : jsonPaths) {
+ claimValues.addAll(JsonUtils.getValues(jsonNode, path));
+ }
+
+ claims.put(claimDef.getKey(), claimValues);
+ }
+ }
+
+ return claims;
+ }
+ } catch (IOException cause) {
+ throw new RuntimeException("Could not obtain claims from http claim information point [" + config.get("url") + "] response", cause);
+ }
+ }
+
+ private InputStream executeRequest(HttpFacade httpFacade) {
+ String method = config.get("method").toString();
+
+ if (method == null) {
+ method = "GET";
+ }
+
+ RequestBuilder builder = null;
+
+ if ("GET".equalsIgnoreCase(method)) {
+ builder = RequestBuilder.get();
+ } else {
+ builder = RequestBuilder.post();
+ }
+
+ builder.setUri(config.get("url").toString());
+
+ byte[] bytes = new byte[0];
+
+ try {
+ setParameters(builder, httpFacade);
+
+ if (config.containsKey("headers")) {
+ setHeaders(builder, httpFacade);
+ }
+
+ HttpResponse response = httpClient.execute(builder.build());
+ HttpEntity entity = response.getEntity();
+
+ if (entity != null) {
+ bytes = EntityUtils.toByteArray(entity);
+ }
+
+ StatusLine statusLine = response.getStatusLine();
+ int statusCode = statusLine.getStatusCode();
+
+ if (statusCode < 200 || statusCode >= 300) {
+ throw new HttpResponseException("Unexpected response from server: " + statusCode + " / " + statusLine.getReasonPhrase(), statusCode, statusLine.getReasonPhrase(), bytes);
+ }
+
+ return new ByteArrayInputStream(bytes);
+ } catch (Exception cause) {
+ try {
+ throw new RuntimeException("Error executing http method [" + builder + "]. Response : " + StreamUtil.readString(new ByteArrayInputStream(bytes), Charset.forName("UTF-8")), cause);
+ } catch (Exception e) {
+ throw new RuntimeException("Error executing http method [" + builder + "]", cause);
+ }
+ }
+ }
+
+ private void setHeaders(RequestBuilder builder, HttpFacade httpFacade) {
+ Object headersDef = config.get("headers");
+
+ if (headersDef != null) {
+ Map<String, Object> headers = Map.class.cast(headersDef);
+
+ for (Entry<String, Object> header : headers.entrySet()) {
+ Object value = header.getValue();
+ List<String> headerValues = new ArrayList<>();
+
+ if (value instanceof Collection) {
+ Collection values = Collection.class.cast(value);
+ Iterator iterator = values.iterator();
+
+ while (iterator.hasNext()) {
+ headerValues.addAll(PlaceHolders.resolve(iterator.next().toString(), httpFacade));
+ }
+ } else {
+ headerValues.addAll(PlaceHolders.resolve(value.toString(), httpFacade));
+ }
+
+ for (String headerValue : headerValues) {
+ builder.addHeader(header.getKey(), headerValue);
+ }
+ }
+ }
+ }
+
+ private void setParameters(RequestBuilder builder, HttpFacade httpFacade) {
+ Object config = this.config.get("parameters");
+
+ if (config != null) {
+ Map<String, Object> paramsDef = Map.class.cast(config);
+
+ for (Entry<String, Object> paramDef : paramsDef.entrySet()) {
+ Object value = paramDef.getValue();
+ List<String> paramValues = new ArrayList<>();
+
+ if (value instanceof Collection) {
+ Collection values = Collection.class.cast(value);
+ Iterator iterator = values.iterator();
+
+ while (iterator.hasNext()) {
+ paramValues.addAll(PlaceHolders.resolve(iterator.next().toString(), httpFacade));
+ }
+ } else {
+ paramValues.addAll(PlaceHolders.resolve(value.toString(), httpFacade));
+ }
+
+ for (String paramValue : paramValues) {
+ builder.addParameter(paramDef.getKey(), paramValue);
+ }
+ }
+ }
+ }
+}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProviderFactory.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProviderFactory.java
new file mode 100644
index 0000000..6e36562
--- /dev/null
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProviderFactory.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2018 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.cip;
+
+import java.util.Map;
+
+import org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory;
+import org.keycloak.adapters.authorization.PolicyEnforcer;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class HttpClaimInformationPointProviderFactory implements ClaimInformationPointProviderFactory<HttpClaimInformationPointProvider> {
+
+ private PolicyEnforcer policyEnforcer;
+
+ @Override
+ public String getName() {
+ return "http";
+ }
+
+ @Override
+ public void init(PolicyEnforcer policyEnforcer) {
+ this.policyEnforcer = policyEnforcer;
+ }
+
+ @Override
+ public HttpClaimInformationPointProvider create(Map<String, Object> config) {
+ return new HttpClaimInformationPointProvider(config, policyEnforcer);
+ }
+}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/ClaimInformationPointProvider.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/ClaimInformationPointProvider.java
new file mode 100644
index 0000000..fb594ef
--- /dev/null
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/ClaimInformationPointProvider.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2018 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.List;
+import java.util.Map;
+
+import org.keycloak.adapters.spi.HttpFacade;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface ClaimInformationPointProvider {
+
+ Map<String, List<String>> resolve(HttpFacade httpFacade);
+}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/ClaimInformationPointProviderFactory.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/ClaimInformationPointProviderFactory.java
new file mode 100644
index 0000000..894debc
--- /dev/null
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/ClaimInformationPointProviderFactory.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2018 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.Map;
+
+import org.keycloak.adapters.spi.HttpFacade;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface ClaimInformationPointProviderFactory<C extends ClaimInformationPointProvider> {
+
+ String getName();
+
+ void init(PolicyEnforcer policyEnforcer);
+
+ C create(Map<String, Object> config);
+}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java
index 0732ee9..4e87c90 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java
@@ -18,7 +18,11 @@
package org.keycloak.adapters.authorization;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
import org.jboss.logging.Logger;
import org.keycloak.KeycloakSecurityContext;
@@ -28,6 +32,9 @@ import org.keycloak.adapters.rotation.AdapterRSATokenVerifier;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.authorization.client.AuthorizationDeniedException;
import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.authorization.client.resource.PermissionResource;
+import org.keycloak.authorization.client.resource.ProtectionResource;
+import org.keycloak.common.util.Base64;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
@@ -35,7 +42,7 @@ 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.representations.idm.authorization.PermissionResponse;
+import org.keycloak.util.JsonSerialization;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@@ -72,7 +79,27 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
AccessToken.Authorization newAuthorization = accessToken.getAuthorization();
if (newAuthorization != null) {
- authorization.getPermissions().addAll(newAuthorization.getPermissions());
+ List<Permission> grantedPermissions = authorization.getPermissions();
+ List<Permission> newPermissions = newAuthorization.getPermissions();
+
+ for (Permission newPermission : newPermissions) {
+ if (!grantedPermissions.contains(newPermission)) {
+ grantedPermissions.add(newPermission);
+ }
+ }
+
+ Map<String, List<String>> newClaims = newAuthorization.getClaims();
+
+ if (newClaims != null) {
+ Map<String, List<String>> claims = authorization.getClaims();
+
+ if (claims == null) {
+ claims = new HashMap<>();
+ authorization.setClaims(claims);
+ }
+
+ claims.putAll(newClaims);
+ }
}
original.setAuthorization(authorization);
@@ -81,8 +108,29 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
}
@Override
- protected boolean challenge(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade facade) {
- handleAccessDenied(facade);
+ protected boolean challenge(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade httpFacade) {
+ if (isBearerAuthorization(httpFacade)) {
+ HttpFacade.Response response = httpFacade.getResponse();
+ AuthzClient authzClient = getAuthzClient();
+ String ticket = getPermissionTicket(pathConfig, methodConfig, authzClient, httpFacade);
+
+ if (ticket != null) {
+ response.setStatus(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.setStatus(403);
+ }
+
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("Sending challenge");
+ }
+
+ return true;
+ }
+
+ handleAccessDenied(httpFacade);
+
return true;
}
@@ -106,28 +154,31 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
}
private AccessToken requestAuthorizationToken(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade httpFacade) {
+ if (getPolicyEnforcer().getDeployment().isBearerOnly() || (isBearerAuthorization(httpFacade) && getEnforcerConfig().getUserManagedAccess() != null)) {
+ return null;
+ }
+
try {
KeycloakSecurityContext securityContext = httpFacade.getSecurityContext();
String accessTokenString = securityContext.getTokenString();
- AuthzClient authzClient = getAuthzClient();
KeycloakDeployment deployment = getPolicyEnforcer().getDeployment();
- PermissionRequest permissionRequest = new PermissionRequest();
-
- permissionRequest.setResourceId(pathConfig.getId());
- permissionRequest.setScopes(new HashSet<>(methodConfig.getScopes()));
-
AccessToken accessToken = securityContext.getToken();
- AuthorizationRequest authzRequest;
+ AuthorizationRequest authzRequest = new AuthorizationRequest();
if (getEnforcerConfig().getUserManagedAccess() != null) {
- PermissionResponse permissionResponse = authzClient.protection().permission().create(permissionRequest);
- authzRequest = new AuthorizationRequest();
- authzRequest.setTicket(permissionResponse.getTicket());
+ String ticket = getPermissionTicket(pathConfig, methodConfig, getAuthzClient(), httpFacade);
+ authzRequest.setTicket(ticket);
} else {
- authzRequest = new AuthorizationRequest();
if (accessToken.getAuthorization() != null) {
authzRequest.addPermission(pathConfig.getId(), methodConfig.getScopes());
}
+
+ Map<String, List<String>> claims = getClaims(pathConfig, httpFacade);
+
+ if (!claims.isEmpty()) {
+ authzRequest.setClaimTokenFormat("urn:ietf:params:oauth:token-type:jwt");
+ authzRequest.setClaimToken(Base64.encodeBytes(JsonSerialization.writeValueAsBytes(claims)));
+ }
}
if (accessToken.getAuthorization() != null) {
@@ -135,18 +186,70 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
}
LOGGER.debug("Obtaining authorization for authenticated user.");
- AuthorizationResponse authzResponse = authzClient.authorization(accessTokenString).authorize(authzRequest);
+ AuthorizationResponse authzResponse = getAuthzClient().authorization(accessTokenString).authorize(authzRequest);
if (authzResponse != null) {
return AdapterRSATokenVerifier.verifyToken(authzResponse.getToken(), deployment);
}
-
- return null;
- } catch (AuthorizationDeniedException e) {
- LOGGER.debug("Authorization denied", e);
- return null;
+ } catch (AuthorizationDeniedException ignore) {
+ LOGGER.debug("Authorization denied", ignore);
} catch (Exception e) {
throw new RuntimeException("Unexpected error during authorization request.", e);
}
+
+ return null;
+ }
+
+ private String getPermissionTicket(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, AuthzClient authzClient, HttpFacade httpFacade) {
+ if (getEnforcerConfig().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<String, List<String>> claims = getClaims(pathConfig, httpFacade);
+
+ if (!claims.isEmpty()) {
+ permissionRequest.setClaims(claims);
+ }
+
+ return permission.create(permissionRequest).getTicket();
+ }
+
+ return null;
+ }
+
+ private Map<String, List<String>> getClaims(PathConfig pathConfig, HttpFacade httpFacade) {
+ Map<String, List<String>> claims = new HashMap<>();
+ Map<String, Map<String, Object>> claimInformationPointConfig = pathConfig.getClaimInformationPointConfig();
+
+ if (claimInformationPointConfig != null) {
+ for (Entry<String, Map<String, Object>> claimDef : claimInformationPointConfig.entrySet()) {
+ ClaimInformationPointProviderFactory factory = getPolicyEnforcer().getClaimInformationPointProviderFactories().get(claimDef.getKey());
+
+ if (factory != null) {
+ claims.putAll(factory.create(claimDef.getValue()).resolve(httpFacade));
+ }
+ }
+ }
+ return claims;
+ }
+
+ private boolean isBearerAuthorization(OIDCHttpFacade httpFacade) {
+ List<String> authHeaders = httpFacade.getRequest().getHeaders("Authorization");
+ if (authHeaders == null || authHeaders.size() == 0) {
+ return false;
+ }
+
+ 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 false;
}
}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
index 2ccdf28..50278b9 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
@@ -21,10 +21,12 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
+import java.util.ServiceLoader;
import org.jboss.logging.Logger;
import org.keycloak.AuthorizationContext;
@@ -55,6 +57,7 @@ public class PolicyEnforcer {
private final PolicyEnforcerConfig enforcerConfig;
private final PathConfigMatcher pathMatcher;
private final Map<String, PathConfig> paths;
+ private final Map<String, ClaimInformationPointProviderFactory> claimInformationPointProviderFactories = new HashMap<>();
public PolicyEnforcer(KeycloakDeployment deployment, AdapterConfig adapterConfig) {
this.deployment = deployment;
@@ -80,20 +83,17 @@ public class PolicyEnforcer {
LOGGER.debug(pathConfig);
}
}
+
+ loadClaimInformationPointProviders(ServiceLoader.load(ClaimInformationPointProviderFactory.class, ClaimInformationPointProviderFactory.class.getClassLoader()));
+ loadClaimInformationPointProviders(ServiceLoader.load(ClaimInformationPointProviderFactory.class, Thread.currentThread().getContextClassLoader()));
}
public AuthorizationContext enforce(OIDCHttpFacade facade) {
if (LOGGER.isDebugEnabled()) {
- LOGGER.debugv("Policy enforcement is enable. Enforcing policy decisions for path [{0}].", facade.getRequest().getURI());
+ LOGGER.debugv("Policy enforcement is enabled. Enforcing policy decisions for path [{0}].", facade.getRequest().getURI());
}
- AuthorizationContext context;
-
- if (deployment.isBearerOnly()) {
- context = new BearerTokenPolicyEnforcer(this).authorize(facade);
- } else {
- context = new KeycloakAdapterPolicyEnforcer(this).authorize(facade);
- }
+ AuthorizationContext context = new KeycloakAdapterPolicyEnforcer(this).authorize(facade);
if (LOGGER.isDebugEnabled()) {
LOGGER.debugv("Policy enforcement result for path [{0}] is : {1}", facade.getRequest().getURI(), context.isGranted() ? "GRANTED" : "DENIED");
@@ -126,6 +126,22 @@ public class PolicyEnforcer {
return deployment;
}
+ public Map<String, ClaimInformationPointProviderFactory> getClaimInformationPointProviderFactories() {
+ return claimInformationPointProviderFactories;
+ }
+
+ private void loadClaimInformationPointProviders(ServiceLoader<ClaimInformationPointProviderFactory> loader) {
+ Iterator<ClaimInformationPointProviderFactory> iterator = loader.iterator();
+
+ while (iterator.hasNext()) {
+ ClaimInformationPointProviderFactory factory = iterator.next();
+
+ factory.init(this);
+
+ claimInformationPointProviderFactories.put(factory.getName(), factory);
+ }
+ }
+
private Map<String, PathConfig> configurePaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
boolean loadPathsFromServer = true;
@@ -164,6 +180,10 @@ public class PolicyEnforcer {
LOGGER.debugf("Trying to find resource with uri [%s] for path [%s].", path, path);
List<ResourceRepresentation> resources = protectedResource.findByUri(path);
+ if (resources.isEmpty()) {
+ resources = protectedResource.findByMatchingUri(path);
+ }
+
if (resources.size() == 1) {
resource = resources.get(0);
} else if (resources.size() > 1) {
@@ -173,16 +193,14 @@ public class PolicyEnforcer {
}
}
- if (resource == null) {
- throw new RuntimeException("Could not find matching resource on server with uri [" + path + "] or name [" + resourceName + "]. Make sure you have created a resource on the server that matches with the path configuration.");
+ if (resource != null) {
+ pathConfig.setId(resource.getId());
}
- pathConfig.setId(resource.getId());
-
PathConfig existingPath = null;
for (PathConfig current : paths.values()) {
- if (current.getId().equals(pathConfig.getId()) && current.getPath().equals(pathConfig.getPath())) {
+ if (current.getPath().equals(pathConfig.getPath())) {
existingPath = current;
break;
}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/JsonUtils.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/JsonUtils.java
new file mode 100644
index 0000000..36c6d9b
--- /dev/null
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/JsonUtils.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2018 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.util;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class JsonUtils {
+
+ public static List<String> getValues(JsonNode jsonNode, String path) {
+ return getValues(jsonNode.at(path));
+ }
+
+ public static List<String> getValues(JsonNode jsonNode) {
+ List<String> values = new ArrayList<>();
+
+ if (jsonNode.isArray()) {
+ Iterator<JsonNode> iterator = jsonNode.iterator();
+
+ while (iterator.hasNext()) {
+ String value = iterator.next().textValue();
+
+ if (value != null) {
+ values.add(value);
+ }
+ }
+ } else {
+ String value = jsonNode.textValue();
+
+ if (value != null) {
+ values.add(value);
+ }
+ }
+
+ return values;
+ }
+
+}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/KeycloakSecurityContextPlaceHolderResolver.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/KeycloakSecurityContextPlaceHolderResolver.java
new file mode 100644
index 0000000..d59cfd7
--- /dev/null
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/KeycloakSecurityContextPlaceHolderResolver.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2018 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.util;
+
+import static org.keycloak.adapters.authorization.util.PlaceHolders.getParameter;
+
+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.util.JsonSerialization;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class KeycloakSecurityContextPlaceHolderResolver implements PlaceHolderResolver {
+
+ public static final String NAME = "keycloak";
+
+ @Override
+ public List<String> resolve(String placeHolder, HttpFacade httpFacade) {
+ String source = placeHolder.substring(placeHolder.indexOf('.') + 1);
+ OIDCHttpFacade oidcHttpFacade = OIDCHttpFacade.class.cast(httpFacade);
+ KeycloakSecurityContext securityContext = oidcHttpFacade.getSecurityContext();
+
+ if (securityContext == null) {
+ return null;
+ }
+
+ if (source.endsWith("access_token")) {
+ return Arrays.asList(securityContext.getTokenString());
+ }
+
+ if (source.endsWith("id_token")) {
+ return Arrays.asList(securityContext.getIdTokenString());
+ }
+
+ 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());
+ } else {
+ throw new RuntimeException("Invalid placeholder [" + placeHolder + "]");
+ }
+
+ return JsonUtils.getValues(jsonNode, getParameter(source, "Invalid placeholder [" + placeHolder + "]"));
+ }
+}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolderResolver.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolderResolver.java
new file mode 100644
index 0000000..62d6e1e
--- /dev/null
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolderResolver.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2018 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.util;
+
+import java.util.List;
+
+import org.keycloak.adapters.spi.HttpFacade;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface PlaceHolderResolver {
+
+ List<String> resolve(String placeHolder, HttpFacade httpFacade);
+
+}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolders.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolders.java
new file mode 100644
index 0000000..7a39dbf
--- /dev/null
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolders.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2018 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.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.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.keycloak.adapters.spi.HttpFacade;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class PlaceHolders {
+
+ private static Map<String, PlaceHolderResolver> resolvers = new HashMap<>();
+
+ static {
+ resolvers.put(RequestPlaceHolderResolver.NAME, new RequestPlaceHolderResolver());
+ resolvers.put(KeycloakSecurityContextPlaceHolderResolver.NAME, new KeycloakSecurityContextPlaceHolderResolver());
+ }
+
+ private static Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{(.+?)\\}");
+ private static Pattern PLACEHOLDER_PARAM_PATTERN = Pattern.compile("\\[(.+?)\\]");
+
+ public static List<String> resolve(String value, HttpFacade httpFacade) {
+ Map<String, List<String>> placeHolders = parsePlaceHolders(value, httpFacade);
+
+ if (!placeHolders.isEmpty()) {
+ value = formatPlaceHolder(value);
+
+ for (Entry<String, List<String>> entry : placeHolders.entrySet()) {
+ List<String> values = entry.getValue();
+
+ if (values.isEmpty() || values.size() > 1) {
+ return values;
+ }
+
+ value = value.replaceAll(entry.getKey(), values.get(0)).trim();
+ }
+ }
+
+ return Arrays.asList(value);
+ }
+
+ static String getParameter(String source, String messageIfNotFound) {
+ Matcher matcher = PLACEHOLDER_PARAM_PATTERN.matcher(source);
+
+ while (matcher.find()) {
+ return matcher.group(1).replaceAll("'", "");
+ }
+
+ if (messageIfNotFound != null) {
+ throw new RuntimeException(messageIfNotFound);
+ }
+
+ return null;
+ }
+
+ private static Map<String, List<String>> parsePlaceHolders(String value, HttpFacade httpFacade) {
+ Map<String, List<String>> placeHolders = new HashMap<>();
+ Matcher matcher = PLACEHOLDER_PATTERN.matcher(value);
+
+ while (matcher.find()) {
+ 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<String> resolved = resolver.resolve(placeHolder, httpFacade);
+
+ if (resolved != null) {
+ placeHolders.put(formatPlaceHolder(placeHolder), resolved);
+ }
+ }
+ }
+
+ return placeHolders;
+ }
+
+ private static String formatPlaceHolder(String placeHolder) {
+ return placeHolder.replaceAll("\\{", "").replace("}", "").replace("[", "").replace("]", "").replace("[", "").replace("]", "");
+ }
+}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/RequestPlaceHolderResolver.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/RequestPlaceHolderResolver.java
new file mode 100644
index 0000000..cdd362d
--- /dev/null
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/RequestPlaceHolderResolver.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2018 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.util;
+
+import static org.keycloak.adapters.authorization.util.PlaceHolders.getParameter;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+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.util.JsonSerialization;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class RequestPlaceHolderResolver implements PlaceHolderResolver {
+
+ static String NAME = "request";
+
+ @Override
+ public List<String> resolve(String placeHolder, HttpFacade httpFacade) {
+ 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);
+ }
+
+ if (parameterValue != null) {
+ return Arrays.asList(parameterValue);
+ }
+ } else if (source.startsWith("header")) {
+ String headerName = getParameter(source, "Could not obtain header name from placeholder [" + source + "]");
+ List<String> headerValue = request.getHeaders(headerName);
+
+ if (headerValue != null) {
+ return headerValue;
+ }
+ } else if (source.startsWith("cookie")) {
+ String cookieName = getParameter(source, "Could not obtain cookie name from placeholder [" + source + "]");
+ Cookie cookieValue = request.getCookie(cookieName);
+
+ if (cookieValue != null) {
+ return Arrays.asList(cookieValue.getValue());
+ }
+ } else if (source.startsWith("remoteAddr")) {
+ String value = request.getRemoteAddr();
+
+ if (value != null) {
+ return Arrays.asList(value);
+ }
+ } else if (source.startsWith("method")) {
+ String value = request.getMethod();
+
+ if (value != null) {
+ return Arrays.asList(value);
+ }
+ } else if (source.startsWith("uri")) {
+ String value = request.getURI();
+
+ if (value != null) {
+ return Arrays.asList(value);
+ }
+ } else if (source.startsWith("relativePath")) {
+ String value = request.getRelativePath();
+
+ if (value != null) {
+ return Arrays.asList(value);
+ }
+ } else if (source.startsWith("secure")) {
+ return Arrays.asList(String.valueOf(request.isSecure()));
+ } else if (source.startsWith("body")) {
+ String contentType = request.getHeader("Content-Type");
+
+ if (contentType == null) {
+ contentType = "";
+ }
+
+ InputStream body = request.getInputStream(true);
+
+ try {
+ if (body == null || body.available() == 0) {
+ return Collections.emptyList();
+ }
+ } catch (IOException cause) {
+ throw new RuntimeException("Failed to check available bytes in request input stream", cause);
+ }
+
+ if (body.markSupported()) {
+ body.mark(0);
+ }
+
+ List<String> values = new ArrayList<>();
+
+ try {
+ switch (contentType) {
+ case "application/json":
+ try {
+ JsonNode jsonNode = JsonSerialization.mapper.readTree(new BufferedInputStream(body) {
+ @Override
+ public void close() {
+ // we can't close the stream because it may be used later by the application
+ }
+ });
+ String path = getParameter(source, null);
+
+ if (path == null) {
+ values.addAll(JsonUtils.getValues(jsonNode));
+ } else {
+ values.addAll(JsonUtils.getValues(jsonNode, path));
+ }
+ } catch (IOException cause) {
+ throw new RuntimeException("Could not extract claim from request JSON body", cause);
+ }
+ break;
+ default:
+ StringBuilder value = new StringBuilder();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(body));
+
+ try {
+ int ch;
+
+ while ((ch = reader.read()) != -1) {
+ value.append((char) ch);
+ }
+ } catch (IOException cause) {
+ throw new RuntimeException("Could not extract claim from request body", cause);
+ }
+
+ values.add(value.toString());
+ }
+ } finally {
+ if (body.markSupported()) {
+ try {
+ body.reset();
+ } catch (IOException cause) {
+ throw new RuntimeException("Failed to reset request input stream", cause);
+ }
+ }
+ }
+
+ return values;
+ }
+
+ return Collections.emptyList();
+ }
+}
diff --git a/adapters/oidc/adapter-core/src/main/resources/META-INF/services/org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory b/adapters/oidc/adapter-core/src/main/resources/META-INF/services/org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory
new file mode 100644
index 0000000..f40afed
--- /dev/null
+++ b/adapters/oidc/adapter-core/src/main/resources/META-INF/services/org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory
@@ -0,0 +1,19 @@
+#
+# * Copyright 2018 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.
+#
+
+org.keycloak.adapters.authorization.cip.ClaimsInformationPointProviderFactory
+org.keycloak.adapters.authorization.cip.HttpClaimInformationPointProviderFactory
\ No newline at end of file
diff --git a/adapters/oidc/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsHttpFacade.java b/adapters/oidc/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsHttpFacade.java
index 1a0eb9c..0d984eb 100755
--- a/adapters/oidc/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsHttpFacade.java
+++ b/adapters/oidc/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsHttpFacade.java
@@ -27,6 +27,9 @@ import javax.security.cert.X509Certificate;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.SecurityContext;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
@@ -51,6 +54,8 @@ public class JaxrsHttpFacade implements OIDCHttpFacade {
protected class RequestFacade implements OIDCHttpFacade.Request {
+ private InputStream inputStream;
+
@Override
public String getFirstParam(String param) {
throw new RuntimeException("NOT IMPLEMENTED");
@@ -108,6 +113,19 @@ public class JaxrsHttpFacade implements OIDCHttpFacade {
@Override
public InputStream getInputStream() {
+ return getInputStream(false);
+ }
+
+ @Override
+ public InputStream getInputStream(boolean buffered) {
+ if (inputStream != null) {
+ return inputStream;
+ }
+
+ if (buffered) {
+ return inputStream = new BufferedInputStream(requestContext.getEntityStream());
+ }
+
return requestContext.getEntityStream();
}
diff --git a/adapters/oidc/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java b/adapters/oidc/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java
index 67c9f08..47c9a93 100755
--- a/adapters/oidc/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java
+++ b/adapters/oidc/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java
@@ -36,6 +36,8 @@ import javax.security.cert.X509Certificate;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+
+import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
@@ -265,6 +267,8 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
public Request getRequest() {
return new Request() {
+ private InputStream inputStream;
+
@Override
public String getFirstParam(String param) {
return servletRequest.getParameter(param);
@@ -314,10 +318,27 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
@Override
public InputStream getInputStream() {
+ return getInputStream(false);
+ }
+
+ @Override
+ public InputStream getInputStream(boolean buffered) {
+ if (inputStream != null) {
+ return inputStream;
+ }
+
+ if (buffered) {
+ try {
+ return inputStream = new BufferedInputStream(servletRequest.getInputStream());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
try {
return servletRequest.getInputStream();
- } catch (IOException ioe) {
- throw new RuntimeException(ioe);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
}
}
diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletRequest.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletRequest.java
index 848ca45..f4fa074 100755
--- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletRequest.java
+++ b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletRequest.java
@@ -24,6 +24,8 @@ import org.keycloak.adapters.spi.LogoutError;
import org.springframework.util.Assert;
import javax.servlet.http.HttpServletRequest;
+
+import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
@@ -40,6 +42,7 @@ import java.util.List;
class WrappedHttpServletRequest implements Request {
private final HttpServletRequest request;
+ private InputStream inputStream;
/**
* Creates a new request for the given <code>HttpServletRequest</code>
@@ -122,10 +125,27 @@ class WrappedHttpServletRequest implements Request {
@Override
public InputStream getInputStream() {
+ return getInputStream(false);
+ }
+
+ @Override
+ public InputStream getInputStream(boolean buffered) {
+ if (inputStream != null) {
+ return inputStream;
+ }
+
+ if (buffered) {
+ try {
+ return inputStream = new BufferedInputStream(request.getInputStream());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
try {
return request.getInputStream();
} catch (IOException e) {
- throw new RuntimeException("Unable to get request input stream", e);
+ throw new RuntimeException(e);
}
}
diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronHttpFacade.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronHttpFacade.java
index 543e848..bda24bb 100644
--- a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronHttpFacade.java
+++ b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronHttpFacade.java
@@ -40,6 +40,8 @@ import org.wildfly.security.http.Scope;
import javax.security.auth.callback.CallbackHandler;
import javax.security.cert.X509Certificate;
+
+import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -158,6 +160,8 @@ class ElytronHttpFacade implements OIDCHttpFacade {
@Override
public Request getRequest() {
return new Request() {
+ private InputStream inputStream;
+
@Override
public String getMethod() {
return request.getRequestMethod();
@@ -184,7 +188,7 @@ class ElytronHttpFacade implements OIDCHttpFacade {
@Override
public String getFirstParam(String param) {
- throw new RuntimeException("Not implemented.");
+ return request.getFirstParameterValue(param);
}
@Override
@@ -230,6 +234,19 @@ class ElytronHttpFacade implements OIDCHttpFacade {
@Override
public InputStream getInputStream() {
+ return getInputStream(false);
+ }
+
+ @Override
+ public InputStream getInputStream(boolean buffered) {
+ if (inputStream != null) {
+ return inputStream;
+ }
+
+ if (buffered) {
+ return inputStream = new BufferedInputStream(request.getInputStream());
+ }
+
return request.getInputStream();
}
diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java
index 1458dd9..8b31a31 100644
--- a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java
+++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java
@@ -17,6 +17,7 @@
package org.keycloak.adapters.saml.elytron;
+import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -146,6 +147,8 @@ class ElytronHttpFacade implements HttpFacade {
@Override
public Request getRequest() {
return new Request() {
+ private InputStream inputStream;
+
@Override
public String getMethod() {
return request.getRequestMethod();
@@ -207,6 +210,19 @@ class ElytronHttpFacade implements HttpFacade {
@Override
public InputStream getInputStream() {
+ return getInputStream(false);
+ }
+
+ @Override
+ public InputStream getInputStream(boolean buffered) {
+ if (inputStream != null) {
+ return inputStream;
+ }
+
+ if (buffered) {
+ return inputStream = new BufferedInputStream(request.getInputStream());
+ }
+
return request.getInputStream();
}
diff --git a/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/HttpFacade.java b/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/HttpFacade.java
index 2429286..0636ebc 100755
--- a/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/HttpFacade.java
+++ b/adapters/spi/adapter-spi/src/main/java/org/keycloak/adapters/spi/HttpFacade.java
@@ -69,6 +69,7 @@ public interface HttpFacade {
String getHeader(String name);
List<String> getHeaders(String name);
InputStream getInputStream();
+ InputStream getInputStream(boolean buffered);
String getRemoteAddr();
void setError(AuthenticationError error);
diff --git a/adapters/spi/jetty-adapter-spi/src/main/java/org/keycloak/adapters/jetty/spi/JettyHttpFacade.java b/adapters/spi/jetty-adapter-spi/src/main/java/org/keycloak/adapters/jetty/spi/JettyHttpFacade.java
index dac7973..d097ee4 100755
--- a/adapters/spi/jetty-adapter-spi/src/main/java/org/keycloak/adapters/jetty/spi/JettyHttpFacade.java
+++ b/adapters/spi/jetty-adapter-spi/src/main/java/org/keycloak/adapters/jetty/spi/JettyHttpFacade.java
@@ -25,6 +25,8 @@ import org.keycloak.common.util.UriUtils;
import javax.security.cert.X509Certificate;
import javax.servlet.http.HttpServletResponse;
+
+import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -69,6 +71,9 @@ public class JettyHttpFacade implements HttpFacade {
}
protected class RequestFacade implements Request {
+
+ private InputStream inputStream;
+
@Override
public String getURI() {
StringBuffer buf = request.getRequestURL();
@@ -128,6 +133,23 @@ public class JettyHttpFacade implements HttpFacade {
@Override
public InputStream getInputStream() {
+ return getInputStream(false);
+ }
+
+ @Override
+ public InputStream getInputStream(boolean buffered) {
+ if (inputStream != null) {
+ return inputStream;
+ }
+
+ if (buffered) {
+ try {
+ return inputStream = new BufferedInputStream(request.getInputStream());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
try {
return request.getInputStream();
} catch (IOException e) {
diff --git a/adapters/spi/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/ServletHttpFacade.java b/adapters/spi/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/ServletHttpFacade.java
index 11e4f93..f77b1e3 100755
--- a/adapters/spi/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/ServletHttpFacade.java
+++ b/adapters/spi/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/ServletHttpFacade.java
@@ -27,6 +27,8 @@ import org.keycloak.common.util.UriUtils;
import javax.security.cert.X509Certificate;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+
+import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -51,6 +53,9 @@ public class ServletHttpFacade implements HttpFacade {
}
protected class RequestFacade implements Request {
+
+ private InputStream inputStream;
+
@Override
public String getMethod() {
return request.getMethod();
@@ -132,6 +137,23 @@ public class ServletHttpFacade implements HttpFacade {
@Override
public InputStream getInputStream() {
+ return getInputStream(false);
+ }
+
+ @Override
+ public InputStream getInputStream(boolean buffered) {
+ if (inputStream != null) {
+ return inputStream;
+ }
+
+ if (buffered) {
+ try {
+ return inputStream = new BufferedInputStream(request.getInputStream());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
try {
return request.getInputStream();
} catch (IOException e) {
diff --git a/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/CatalinaHttpFacade.java b/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/CatalinaHttpFacade.java
index 631474e..c2813b2 100755
--- a/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/CatalinaHttpFacade.java
+++ b/adapters/spi/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/CatalinaHttpFacade.java
@@ -26,6 +26,8 @@ import org.keycloak.common.util.UriUtils;
import javax.security.cert.X509Certificate;
import javax.servlet.http.HttpServletResponse;
+
+import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -69,6 +71,9 @@ public class CatalinaHttpFacade implements HttpFacade {
}
protected class RequestFacade implements Request {
+
+ private InputStream inputStream;
+
@Override
public String getURI() {
StringBuffer buf = request.getRequestURL();
@@ -136,6 +141,23 @@ public class CatalinaHttpFacade implements HttpFacade {
@Override
public InputStream getInputStream() {
+ return getInputStream(false);
+ }
+
+ @Override
+ public InputStream getInputStream(boolean buffered) {
+ if (inputStream != null) {
+ return inputStream;
+ }
+
+ if (buffered) {
+ try {
+ return inputStream = new BufferedInputStream(request.getInputStream());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
try {
return request.getInputStream();
} catch (IOException e) {
diff --git a/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/UndertowHttpFacade.java b/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/UndertowHttpFacade.java
index 21102f1..d47b363 100755
--- a/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/UndertowHttpFacade.java
+++ b/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/UndertowHttpFacade.java
@@ -19,6 +19,10 @@ package org.keycloak.adapters.undertow;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.CookieImpl;
+import io.undertow.server.handlers.form.FormData;
+import io.undertow.server.handlers.form.FormData.FormValue;
+import io.undertow.server.handlers.form.FormDataParser;
+import io.undertow.server.handlers.form.FormParserFactory;
import io.undertow.util.AttachmentKey;
import io.undertow.util.Headers;
import io.undertow.util.HttpString;
@@ -28,6 +32,8 @@ import org.keycloak.adapters.spi.LogoutError;
import org.keycloak.common.util.KeycloakUriBuilder;
import javax.security.cert.X509Certificate;
+
+import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -75,6 +81,11 @@ public class UndertowHttpFacade implements HttpFacade {
}
protected class RequestFacade implements Request {
+
+ private InputStream inputStream;
+ private final FormParserFactory formParserFactory = FormParserFactory.builder().build();
+ private FormData formData;
+
@Override
public String getURI() {
KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(exchange.getRequestURI())
@@ -96,7 +107,34 @@ public class UndertowHttpFacade implements HttpFacade {
@Override
public String getFirstParam(String param) {
- throw new RuntimeException("Not implemented yet");
+ Deque<String> values = exchange.getQueryParameters().get(param);
+
+ if (values != null && !values.isEmpty()) {
+ return values.getFirst();
+ }
+
+ if (formData == null && "post".equalsIgnoreCase(getMethod())) {
+ FormDataParser parser = formParserFactory.createParser(exchange);
+ try {
+ formData = parser.parseBlocking();
+ } catch (IOException cause) {
+ throw new RuntimeException("Failed to parse form parameters", cause);
+ }
+ }
+
+ if (formData != null) {
+ Deque<FormValue> formValues = formData.get(param);
+
+ if (formValues != null && !formValues.isEmpty()) {
+ FormValue firstValue = formValues.getFirst();
+
+ if (!firstValue.isFile()) {
+ return firstValue.getValue();
+ }
+ }
+ }
+
+ return null;
}
@Override
@@ -136,7 +174,21 @@ public class UndertowHttpFacade implements HttpFacade {
@Override
public InputStream getInputStream() {
+ return getInputStream(false);
+ }
+
+ @Override
+ public InputStream getInputStream(boolean buffered) {
if (!exchange.isBlocking()) exchange.startBlocking();
+
+ if (inputStream != null) {
+ return inputStream;
+ }
+
+ if (buffered) {
+ return inputStream = new BufferedInputStream(exchange.getInputStream());
+ }
+
return exchange.getInputStream();
}
diff --git a/core/src/main/java/org/keycloak/representations/AccessToken.java b/core/src/main/java/org/keycloak/representations/AccessToken.java
index 36778e1..542712d 100755
--- a/core/src/main/java/org/keycloak/representations/AccessToken.java
+++ b/core/src/main/java/org/keycloak/representations/AccessToken.java
@@ -88,6 +88,9 @@ public class AccessToken extends IDToken {
@JsonProperty("permissions")
private List<Permission> permissions;
+ @JsonProperty("claims")
+ private Map<String, List<String>> claims;
+
public List<Permission> getPermissions() {
return permissions;
}
@@ -95,6 +98,14 @@ public class AccessToken extends IDToken {
public void setPermissions(List<Permission> permissions) {
this.permissions = permissions;
}
+
+ public void setClaims(Map<String, List<String>> claims) {
+ this.claims = claims;
+ }
+
+ public Map<String, List<String>> getClaims() {
+ return claims;
+ }
}
@JsonProperty("trusted-certs")
diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
index d01d7c5..81dc506 100644
--- a/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
+++ b/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
@@ -19,6 +19,7 @@ package org.keycloak.representations.adapters.config;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
@@ -139,6 +140,9 @@ public class PolicyEnforcerConfig {
@JsonProperty("enforcement-mode")
private EnforcementMode enforcementMode = EnforcementMode.ENFORCING;
+ @JsonProperty("claim-information-point")
+ private Map<String, Map<String, Object>> claimInformationPointConfig;
+
@JsonIgnore
private PathConfig parentConfig;
@@ -198,6 +202,14 @@ public class PolicyEnforcerConfig {
this.enforcementMode = enforcementMode;
}
+ public Map<String, Map<String, Object>> getClaimInformationPointConfig() {
+ return claimInformationPointConfig;
+ }
+
+ public void setClaimInformationPointConfig(Map<String, Map<String, Object>> claimInformationPointConfig) {
+ this.claimInformationPointConfig = claimInformationPointConfig;
+ }
+
@Override
public String toString() {
return "PathConfig{" +
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationRequest.java b/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationRequest.java
index 764ae02..14f1f3d 100644
--- a/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationRequest.java
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationRequest.java
@@ -21,6 +21,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import org.keycloak.representations.idm.authorization.PermissionTicketToken.ResourcePermission;
@@ -40,6 +41,7 @@ public class AuthorizationRequest {
private String audience;
private String accessToken;
private boolean submitRequest;
+ private Map<String, List<String>> claims;
public AuthorizationRequest(String ticket) {
this.ticket = ticket;
@@ -129,6 +131,14 @@ public class AuthorizationRequest {
return accessToken;
}
+ public Map<String, List<String>> getClaims() {
+ return claims;
+ }
+
+ public void setClaims(Map<String, List<String>> claims) {
+ this.claims = claims;
+ }
+
public void addPermission(String resourceId, List<String> scopes) {
addPermission(resourceId, scopes.toArray(new String[scopes.size()]));
}
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/Permission.java b/core/src/main/java/org/keycloak/representations/idm/authorization/Permission.java
index ed392f0..53760c3 100644
--- a/core/src/main/java/org/keycloak/representations/idm/authorization/Permission.java
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/Permission.java
@@ -16,13 +16,14 @@
*/
package org.keycloak.representations.idm.authorization;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
import java.util.HashSet;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@@ -72,6 +73,21 @@ public class Permission {
}
@Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Permission that = (Permission) o;
+
+ return getResourceId().equals(that.resourceId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(resourceId);
+ }
+
+ @Override
public String toString() {
StringBuilder builder = new StringBuilder();
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionRequest.java b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionRequest.java
index 5830e16..bd2a94b 100644
--- a/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionRequest.java
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionRequest.java
@@ -18,10 +18,15 @@
package org.keycloak.representations.idm.authorization;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import org.keycloak.json.StringListMapDeserializer;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@@ -32,6 +37,9 @@ public class PermissionRequest {
private Set<String> scopes;
private String resourceServerId;
+ @JsonDeserialize(using = StringListMapDeserializer.class)
+ private Map<String, List<String>> claims;
+
public PermissionRequest(String resourceId, String... scopes) {
this.resourceId = resourceId;
if (scopes != null) {
@@ -69,4 +77,28 @@ public class PermissionRequest {
public String getResourceServerId() {
return resourceServerId;
}
+
+ public Map<String, List<String>> getClaims() {
+ return claims;
+ }
+
+ public void setClaims(Map<String, List<String>> claims) {
+ this.claims = claims;
+ }
+
+ public void setClaim(String name, String... value) {
+ if (claims == null) {
+ claims = new HashMap<>();
+ }
+
+ claims.put(name, Arrays.asList(value));
+ }
+
+ public void addScope(String... name) {
+ if (scopes == null) {
+ scopes = new HashSet<>();
+ }
+
+ scopes.addAll(Arrays.asList(name));
+ }
}
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketToken.java b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketToken.java
index ff4a927..a9f6ba5 100644
--- a/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketToken.java
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketToken.java
@@ -18,10 +18,13 @@ package org.keycloak.representations.idm.authorization;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import org.keycloak.TokenIdGenerator;
+import org.keycloak.json.StringListMapDeserializer;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.JsonWebToken;
@@ -32,6 +35,9 @@ public class PermissionTicketToken extends JsonWebToken {
private final List<ResourcePermission> resources;
+ @JsonDeserialize(using = StringListMapDeserializer.class)
+ private Map<String, List<String>> claims;
+
public PermissionTicketToken() {
this(new ArrayList<ResourcePermission>());
}
@@ -59,6 +65,14 @@ public class PermissionTicketToken extends JsonWebToken {
return this.resources;
}
+ public Map<String, List<String>> getClaims() {
+ return claims;
+ }
+
+ public void setClaims(Map<String, List<String>> claims) {
+ this.claims = claims;
+ }
+
public static class ResourcePermission {
@JsonProperty("id")
diff --git a/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/OfflineAccessPortalServlet.java b/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/OfflineAccessPortalServlet.java
index cd912d7..ef84de1 100755
--- a/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/OfflineAccessPortalServlet.java
+++ b/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/OfflineAccessPortalServlet.java
@@ -43,6 +43,8 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+
+import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
@@ -171,6 +173,8 @@ public class OfflineAccessPortalServlet extends HttpServlet {
public Request getRequest() {
return new Request() {
+ private InputStream inputStream;
+
@Override
public String getMethod() {
return servletRequest.getMethod();
@@ -220,10 +224,27 @@ public class OfflineAccessPortalServlet extends HttpServlet {
@Override
public InputStream getInputStream() {
+ return getInputStream(false);
+ }
+
+ @Override
+ public InputStream getInputStream(boolean buffered) {
+ if (inputStream != null) {
+ return inputStream;
+ }
+
+ if (buffered) {
+ try {
+ return inputStream = new BufferedInputStream(servletRequest.getInputStream());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
try {
return servletRequest.getInputStream();
- } catch (IOException ioe) {
- throw new RuntimeException(ioe);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
}
}
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/attribute/Attributes.java b/server-spi-private/src/main/java/org/keycloak/authorization/attribute/Attributes.java
index 0719bab..8f33fcf 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/attribute/Attributes.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/attribute/Attributes.java
@@ -142,5 +142,9 @@ public interface Attributes {
public long asLong(int idx) {
return Long.parseLong(asString(idx));
}
+
+ public double asDouble(int idx) {
+ return Double.parseDouble(asString(idx));
+ }
}
}
diff --git a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java
index 989d228..b791f7f 100644
--- a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java
+++ b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java
@@ -16,6 +16,7 @@
*/
package org.keycloak.authorization.authorization;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -54,6 +55,7 @@ import org.keycloak.authorization.store.ScopeStore;
import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.authorization.util.Permissions;
import org.keycloak.authorization.util.Tokens;
+import org.keycloak.common.util.Base64Url;
import org.keycloak.events.EventBuilder;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
@@ -77,12 +79,15 @@ import org.keycloak.representations.idm.authorization.PermissionTicketToken;
import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.resources.Cors;
+import org.keycloak.util.JsonSerialization;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class AuthorizationTokenService {
+ public static final String CLAIM_TOKEN_FORMAT_ID_TOKEN = "http://openid.net/specs/openid-connect-core-1_0.html#IDToken";
+
private static final Logger logger = Logger.getLogger(AuthorizationTokenService.class);
private static Map<String, BiFunction<AuthorizationRequest, AuthorizationProvider, KeycloakEvaluationContext>> SUPPORTED_CLAIM_TOKEN_FORMATS;
@@ -91,17 +96,30 @@ public class AuthorizationTokenService {
SUPPORTED_CLAIM_TOKEN_FORMATS.put("urn:ietf:params:oauth:token-type:jwt", (authorizationRequest, authorization) -> {
String claimToken = authorizationRequest.getClaimToken();
- if (claimToken == null) {
- claimToken = authorizationRequest.getAccessToken();
+ if (claimToken != null) {
+ try {
+ Map claims = JsonSerialization.readValue(Base64Url.decode(authorizationRequest.getClaimToken()), Map.class);
+ authorizationRequest.setClaims(claims);
+ return new KeycloakEvaluationContext(new KeycloakIdentity(authorization.getKeycloakSession(), Tokens.getAccessToken(authorizationRequest.getAccessToken(), authorization.getKeycloakSession())), claims, authorization.getKeycloakSession());
+ } catch (IOException cause) {
+ throw new RuntimeException("Failed to map claims from claim token [" + claimToken + "]", cause);
+ }
}
- return new KeycloakEvaluationContext(new KeycloakIdentity(authorization.getKeycloakSession(), Tokens.getAccessToken(claimToken, authorization.getKeycloakSession())), authorization.getKeycloakSession());
+ throw new RuntimeException("Claim token can not be null");
});
- SUPPORTED_CLAIM_TOKEN_FORMATS.put("http://openid.net/specs/openid-connect-core-1_0.html#IDToken", (authorizationRequest, authorization) -> {
+ SUPPORTED_CLAIM_TOKEN_FORMATS.put(CLAIM_TOKEN_FORMAT_ID_TOKEN, (authorizationRequest, authorization) -> {
try {
KeycloakSession keycloakSession = authorization.getKeycloakSession();
- IDToken idToken = new TokenManager().verifyIDTokenSignature(keycloakSession, authorization.getRealm(), authorizationRequest.getClaimToken());
- return new KeycloakEvaluationContext(new KeycloakIdentity(keycloakSession, idToken), keycloakSession);
+ RealmModel realm = authorization.getRealm();
+ String accessToken = authorizationRequest.getAccessToken();
+
+ if (accessToken == null) {
+ throw new RuntimeException("Claim token can not be null and must be a valid IDToken");
+ }
+
+ IDToken idToken = new TokenManager().verifyIDTokenSignature(keycloakSession, realm, accessToken);
+ return new KeycloakEvaluationContext(new KeycloakIdentity(keycloakSession, idToken), authorizationRequest.getClaims(), keycloakSession);
} catch (OAuthErrorException cause) {
throw new RuntimeException("Failed to verify ID token", cause);
}
@@ -127,8 +145,16 @@ public class AuthorizationTokenService {
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Invalid authorization request.", Status.BAD_REQUEST);
}
+ // it is not secure to allow public clients to push arbitrary claims because message can be tampered
+ if (isPublicClientRequestingEntitlemesWithClaims(request)) {
+ throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Public clients are not allowed to send claims", Status.FORBIDDEN);
+ }
+
try {
PermissionTicketToken ticket = getPermissionTicket(request);
+
+ request.setClaims(ticket.getClaims());
+
ResourceServer resourceServer = getResourceServer(ticket);
KeycloakEvaluationContext evaluationContext = createEvaluationContext(request);
KeycloakIdentity identity = KeycloakIdentity.class.cast(evaluationContext.getIdentity());
@@ -153,7 +179,7 @@ public class AuthorizationTokenService {
}
ClientModel targetClient = this.authorization.getRealm().getClientById(resourceServer.getId());
- AuthorizationResponse response = new AuthorizationResponse(createRequestingPartyToken(identity, permissions, targetClient), request.getRpt() != null);
+ AuthorizationResponse response = new AuthorizationResponse(createRequestingPartyToken(identity, permissions, request, targetClient), request.getRpt() != null);
return Cors.add(httpRequest, Response.status(Status.OK).type(MediaType.APPLICATION_JSON_TYPE).entity(response))
.allowedOrigins(getKeycloakSession().getContext().getUri(), targetClient)
@@ -170,6 +196,10 @@ public class AuthorizationTokenService {
}
}
+ private boolean isPublicClientRequestingEntitlemesWithClaims(AuthorizationRequest request) {
+ return request.getClaimToken() != null && getKeycloakSession().getContext().getClient().isPublicClient() && request.getTicket() == null;
+ }
+
private List<Result> evaluatePermissions(AuthorizationRequest authorizationRequest, PermissionTicketToken ticket, ResourceServer resourceServer, KeycloakEvaluationContext evaluationContext, KeycloakIdentity identity) {
return authorization.evaluators()
.from(createPermissions(ticket, authorizationRequest, resourceServer, identity, authorization), evaluationContext)
@@ -188,7 +218,7 @@ public class AuthorizationTokenService {
.evaluate();
}
- private AccessTokenResponse createRequestingPartyToken(KeycloakIdentity identity, List<Permission> entitlements, ClientModel targetClient) {
+ private AccessTokenResponse createRequestingPartyToken(KeycloakIdentity identity, List<Permission> entitlements, AuthorizationRequest request, ClientModel targetClient) {
KeycloakSession keycloakSession = getKeycloakSession();
AccessToken accessToken = identity.getAccessToken();
UserSessionModel userSessionModel = keycloakSession.sessions().getUserSession(getRealm(), accessToken.getSessionState());
@@ -205,6 +235,7 @@ public class AuthorizationTokenService {
Authorization authorization = new Authorization();
authorization.setPermissions(entitlements);
+ authorization.setClaims(request.getClaims());
rpt.setAuthorization(authorization);
@@ -264,7 +295,7 @@ public class AuthorizationTokenService {
String claimTokenFormat = authorizationRequest.getClaimTokenFormat();
if (claimTokenFormat == null) {
- claimTokenFormat = "urn:ietf:params:oauth:token-type:jwt";
+ claimTokenFormat = CLAIM_TOKEN_FORMAT_ID_TOKEN;
}
BiFunction<AuthorizationRequest, AuthorizationProvider, KeycloakEvaluationContext> evaluationContextProvider = SUPPORTED_CLAIM_TOKEN_FORMATS.get(claimTokenFormat);
diff --git a/services/src/main/java/org/keycloak/authorization/common/DefaultEvaluationContext.java b/services/src/main/java/org/keycloak/authorization/common/DefaultEvaluationContext.java
index dec33cd..e740cdf 100644
--- a/services/src/main/java/org/keycloak/authorization/common/DefaultEvaluationContext.java
+++ b/services/src/main/java/org/keycloak/authorization/common/DefaultEvaluationContext.java
@@ -22,7 +22,6 @@ import org.keycloak.authorization.attribute.Attributes;
import org.keycloak.authorization.identity.Identity;
import org.keycloak.authorization.policy.evaluation.EvaluationContext;
import org.keycloak.models.KeycloakSession;
-import org.keycloak.representations.AccessToken;
import java.text.SimpleDateFormat;
import java.util.Arrays;
@@ -31,6 +30,7 @@ import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Map.Entry;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@@ -39,10 +39,16 @@ public class DefaultEvaluationContext implements EvaluationContext {
protected final KeycloakSession keycloakSession;
protected final Identity identity;
+ private final Map<String, List<String>> claims;
public DefaultEvaluationContext(Identity identity, KeycloakSession keycloakSession) {
- this.keycloakSession = keycloakSession;
+ this(identity, null, keycloakSession);
+ }
+
+ public DefaultEvaluationContext(Identity identity, Map<String, List<String>> claims, KeycloakSession keycloakSession) {
this.identity = identity;
+ this.claims = claims;
+ this.keycloakSession = keycloakSession;
}
@Override
@@ -51,7 +57,7 @@ public class DefaultEvaluationContext implements EvaluationContext {
}
public Map<String, Collection<String>> getBaseAttributes() {
- HashMap<String, Collection<String>> attributes = new HashMap<>();
+ Map<String, Collection<String>> attributes = new HashMap<>();
attributes.put("kc.time.date_time", Arrays.asList(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())));
attributes.put("kc.client.network.ip_address", Arrays.asList(this.keycloakSession.getContext().getConnection().getRemoteAddr()));
@@ -65,6 +71,12 @@ public class DefaultEvaluationContext implements EvaluationContext {
attributes.put("kc.realm.name", Arrays.asList(this.keycloakSession.getContext().getRealm().getName()));
+ if (claims != null) {
+ for (Entry<String, List<String>> entry : claims.entrySet()) {
+ attributes.put(entry.getKey(), entry.getValue());
+ }
+ }
+
return attributes;
}
diff --git a/services/src/main/java/org/keycloak/authorization/common/KeycloakEvaluationContext.java b/services/src/main/java/org/keycloak/authorization/common/KeycloakEvaluationContext.java
index 047ff5a..bbb4218 100644
--- a/services/src/main/java/org/keycloak/authorization/common/KeycloakEvaluationContext.java
+++ b/services/src/main/java/org/keycloak/authorization/common/KeycloakEvaluationContext.java
@@ -20,6 +20,7 @@ package org.keycloak.authorization.common;
import java.util.Arrays;
import java.util.Collection;
+import java.util.List;
import java.util.Map;
import org.keycloak.authorization.identity.Identity;
@@ -34,7 +35,11 @@ public class KeycloakEvaluationContext extends DefaultEvaluationContext {
private final KeycloakIdentity identity;
public KeycloakEvaluationContext(KeycloakIdentity identity, KeycloakSession keycloakSession) {
- super(identity, keycloakSession);
+ this(identity, null, keycloakSession);
+ }
+
+ public KeycloakEvaluationContext(KeycloakIdentity identity, Map<String, List<String>> claims, KeycloakSession keycloakSession) {
+ super(identity, claims, keycloakSession);
this.identity = identity;
}
diff --git a/services/src/main/java/org/keycloak/authorization/common/KeycloakIdentity.java b/services/src/main/java/org/keycloak/authorization/common/KeycloakIdentity.java
index b2ea5d4..c02ecb5 100644
--- a/services/src/main/java/org/keycloak/authorization/common/KeycloakIdentity.java
+++ b/services/src/main/java/org/keycloak/authorization/common/KeycloakIdentity.java
@@ -112,7 +112,6 @@ public class KeycloakIdentity implements Identity {
if (token instanceof AccessToken) {
this.accessToken = AccessToken.class.cast(token);
} else {
- UserModel userById = keycloakSession.users().getUserById(token.getSubject(), realm);
UserSessionModel userSession = keycloakSession.sessions().getUserSession(realm, token.getSessionState());
ClientModel client = realm.getClientByClientId(token.getIssuedFor());
AuthenticatedClientSessionModel clientSessionModel = userSession.getAuthenticatedClientSessions().get(client.getId());
@@ -123,7 +122,7 @@ public class KeycloakIdentity implements Identity {
requestedRoles.add(role);
}
}
- this.accessToken = new TokenManager().createClientAccessToken(keycloakSession, requestedRoles, realm, client, userById, userSession, clientSessionModel);
+ this.accessToken = new TokenManager().createClientAccessToken(keycloakSession, requestedRoles, realm, client, userSession.getUser(), userSession, clientSessionModel);
}
AccessToken.Access realmAccess = this.accessToken.getRealmAccess();
diff --git a/services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java b/services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java
index 7dd3496..e045d31 100644
--- a/services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java
+++ b/services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java
@@ -16,32 +16,30 @@
*/
package org.keycloak.authorization.protection.permission;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.ws.rs.core.Response;
+
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.common.KeycloakIdentity;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
-import org.keycloak.models.ClientModel;
-import org.keycloak.representations.idm.authorization.PermissionRequest;
-import org.keycloak.representations.idm.authorization.PermissionResponse;
import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.jose.jws.JWSBuilder;
+import org.keycloak.models.ClientModel;
import org.keycloak.models.KeyManager;
-import org.keycloak.models.utils.ModelToRepresentation;
+import org.keycloak.representations.idm.authorization.PermissionRequest;
+import org.keycloak.representations.idm.authorization.PermissionResponse;
import org.keycloak.representations.idm.authorization.PermissionTicketToken;
-import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentation;
-import org.keycloak.representations.idm.authorization.ResourceRepresentation;
-import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.services.ErrorResponseException;
-import javax.ws.rs.core.Response;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Set;
-import java.util.stream.Collectors;
-
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@@ -65,9 +63,9 @@ public class AbstractPermissionService {
return Response.status(Response.Status.CREATED).entity(new PermissionResponse(createPermissionTicket(request))).build();
}
- private List<ResourceRepresentation> verifyRequestedResource(List<PermissionRequest> request) {
+ private List<PermissionTicketToken.ResourcePermission> verifyRequestedResource(List<PermissionRequest> request) {
ResourceStore resourceStore = authorization.getStoreFactory().getResourceStore();
- List<ResourceRepresentation> requestedResources = new ArrayList<>();
+ List<PermissionTicketToken.ResourcePermission> requestedResources = new ArrayList<>();
for (PermissionRequest permissionRequest : request) {
String resourceSetId = permissionRequest.getResourceId();
@@ -104,19 +102,10 @@ public class AbstractPermissionService {
}
if (resources.isEmpty()) {
- requestedResources.add(new ResourceRepresentation(null, verifyRequestedScopes(permissionRequest, null)));
-
+ requestedResources.add(new PermissionTicketToken.ResourcePermission(null, verifyRequestedScopes(permissionRequest, null)));
} else {
for (Resource resource : resources) {
- Set<ScopeRepresentation> scopes = verifyRequestedScopes(permissionRequest, resource);
-
- ResourceRepresentation representation = new ResourceRepresentation(resource.getName(), scopes);
-
- representation.setId(resource.getId());
- representation.setOwnerManagedAccess(resource.isOwnerManagedAccess());
- representation.setOwner(new ResourceOwnerRepresentation(resource.getOwner()));
-
- requestedResources.add(representation);
+ requestedResources.add(new PermissionTicketToken.ResourcePermission(resource.getId(), verifyRequestedScopes(permissionRequest, resource)));
}
}
}
@@ -124,7 +113,7 @@ public class AbstractPermissionService {
return requestedResources;
}
- private Set<ScopeRepresentation> verifyRequestedScopes(PermissionRequest request, Resource resource) {
+ private Set<String> verifyRequestedScopes(PermissionRequest request, Resource resource) {
Set<String> requestScopes = request.getScopes();
if (requestScopes == null) {
@@ -153,24 +142,31 @@ public class AbstractPermissionService {
throw new ErrorResponseException("invalid_scope", "Scope [" + scopeName + "] is invalid", Response.Status.BAD_REQUEST);
}
- return ModelToRepresentation.toRepresentation(scope);
+ return scope.getName();
}).collect(Collectors.toSet());
}
private String createPermissionTicket(List<PermissionRequest> request) {
- List<PermissionTicketToken.ResourcePermission> permissions = verifyRequestedResource(request).stream().flatMap(resource -> {
- List<PermissionTicketToken.ResourcePermission> perms = new ArrayList<>();
- Set<ScopeRepresentation> scopes = resource.getScopes();
-
- perms.add(new PermissionTicketToken.ResourcePermission(resource.getId(), scopes.stream().map(ScopeRepresentation::getName).collect(Collectors.toSet())));
-
- return perms.stream();
- }).collect(Collectors.toList());
+ List<PermissionTicketToken.ResourcePermission> permissions = verifyRequestedResource(request);
KeyManager.ActiveRsaKey keys = this.authorization.getKeycloakSession().keys().getActiveRsaKey(this.authorization.getRealm());
ClientModel targetClient = authorization.getRealm().getClientById(resourceServer.getId());
+ PermissionTicketToken token = new PermissionTicketToken(permissions, targetClient.getClientId(), this.identity.getAccessToken());
+ Map<String, List<String>> claims = new HashMap<>();
+
+ for (PermissionRequest permissionRequest : request) {
+ Map<String, List<String>> requestClaims = permissionRequest.getClaims();
+
+ if (requestClaims != null) {
+ claims.putAll(requestClaims);
+ }
+ }
+
+ if (!claims.isEmpty()) {
+ token.setClaims(claims);
+ }
- return new JWSBuilder().kid(keys.getKid()).jsonContent(new PermissionTicketToken(permissions, targetClient.getClientId(), this.identity.getAccessToken()))
+ return new JWSBuilder().kid(keys.getKid()).jsonContent(token)
.rsa256(keys.getPrivateKey());
}
}
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
index 38d2489..b6e1050 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -997,6 +997,8 @@ public class TokenEndpoint {
accessTokenString = new AppAuthManager().extractAuthorizationHeaderToken(headers);
}
+ // we allow public clients to authenticate using a bearer token, where the token should be a valid access token.
+ // public clients don't have secret and should be able to obtain a RPT by providing an access token previously issued by the server
if (accessTokenString != null) {
AccessToken accessToken = Tokens.getAccessToken(session);
@@ -1004,7 +1006,11 @@ public class TokenEndpoint {
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Invalid bearer token", Status.UNAUTHORIZED);
}
- cors.allowedOrigins(uriInfo, realm.getClientByClientId(accessToken.getIssuedFor()));
+ ClientModel client = realm.getClientByClientId(accessToken.getIssuedFor());
+
+ session.getContext().setClient(client);
+
+ cors.allowedOrigins(uriInfo, client);
}
String claimToken = null;
@@ -1014,18 +1020,30 @@ public class TokenEndpoint {
claimToken = formParams.get("claim_token").get(0);
}
+ String claimTokenFormat = formParams.getFirst("claim_token_format");
+
+ if (claimToken != null && claimTokenFormat == null) {
+ claimTokenFormat = AuthorizationTokenService.CLAIM_TOKEN_FORMAT_ID_TOKEN;
+ }
+
if (accessTokenString == null) {
// in case no bearer token is provided, we force client authentication
checkClient();
- // Clients need to authenticate in order to obtain a RPT from the server.
- // In order to support cases where the client is obtaining permissions on its on behalf, we issue a temporary access token
- accessTokenString = AccessTokenResponse.class.cast(clientCredentialsGrant().getEntity()).getToken();
+
+ // if a claim token is provided, we check if the format is a OpenID Connect IDToken and assume the token represents the identity asking for permissions
+ if (AuthorizationTokenService.CLAIM_TOKEN_FORMAT_ID_TOKEN.equalsIgnoreCase(claimTokenFormat)) {
+ accessTokenString = claimToken;
+ } else {
+ // Clients need to authenticate in order to obtain a RPT from the server.
+ // In order to support cases where the client is obtaining permissions on its on behalf, we issue a temporary access token
+ accessTokenString = AccessTokenResponse.class.cast(clientCredentialsGrant().getEntity()).getToken();
+ }
}
AuthorizationRequest authorizationRequest = new AuthorizationRequest(formParams.getFirst("ticket"));
authorizationRequest.setClaimToken(claimToken);
- authorizationRequest.setClaimTokenFormat(formParams.getFirst("claim_token_format"));
+ authorizationRequest.setClaimTokenFormat(claimTokenFormat);
authorizationRequest.setPct(formParams.getFirst("pct"));
authorizationRequest.setRpt(formParams.getFirst("rpt"));
authorizationRequest.setScope(formParams.getFirst("scope"));
diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/keycloak-claim-information-point-authz-service.json b/testsuite/integration-arquillian/test-apps/servlet-authz/keycloak-claim-information-point-authz-service.json
new file mode 100644
index 0000000..04f0906
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/servlet-authz/keycloak-claim-information-point-authz-service.json
@@ -0,0 +1,25 @@
+{
+ "realm": "servlet-authz",
+ "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "auth-server-url" : "http://localhost:8180/auth",
+ "ssl-required" : "external",
+ "resource" : "servlet-authz-app",
+ "public-client" : false,
+ "credentials": {
+ "secret": "secret"
+ },
+ "policy-enforcer": {
+ "on-deny-redirect-to" : "/servlet-authz-app/accessDenied.jsp",
+ "lazy-load-paths": true,
+ "paths": [
+ {
+ "path": "/protected/context/context.jsp",
+ "claim-information-point": {
+ "claims": {
+ "request-claim": "{request.parameter['request-claim']}"
+ }
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-app-authz-service.json b/testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-app-authz-service.json
index b074ebc..b986bb6 100644
--- a/testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-app-authz-service.json
+++ b/testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-app-authz-service.json
@@ -56,6 +56,10 @@
"name": "write"
}
]
+ },
+ {
+ "name": "Resource Protected With Claim",
+ "uri": "/protected/context/context.jsp"
}
],
"policies": [
@@ -183,6 +187,26 @@
"scopes": "[\"write\"]",
"applyPolicies": "[\"Deny Policy\"]"
}
+ },
+ {
+ "name": "Resource Protected With Claim Permission",
+ "type": "resource",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Resource Protected With Claim\"]",
+ "applyPolicies": "[\"Request Claim Policy\"]"
+ }
+ },
+ {
+ "name": "Request Claim Policy",
+ "description": "A policy that grants access based on claims from an http request",
+ "type": "js",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "code": "var context = $evaluation.getContext();\nvar attributes = context.getAttributes();\nvar claim = attributes.getValue('request-claim');\n\nif (claim && claim.asString(0) == 'expected-value') {\n $evaluation.grant();\n}"
+ }
}
]
}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/protected/context/context.jsp b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/protected/context/context.jsp
new file mode 100644
index 0000000..e01c5da
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/protected/context/context.jsp
@@ -0,0 +1,14 @@
+<%@page import="org.keycloak.AuthorizationContext" %>
+<%@ page import="org.keycloak.KeycloakSecurityContext" %>
+
+<%
+ KeycloakSecurityContext keycloakSecurityContext = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
+ AuthorizationContext authzContext = keycloakSecurityContext.getAuthorizationContext();
+%>
+
+<html>
+<body>
+<h2>Access granted: <%= authzContext.isGranted() %></h2>
+<%@include file="../../logout-include.jsp"%>
+</body>
+</html>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzCIPAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzCIPAdapterTest.java
new file mode 100644
index 0000000..8ab56da
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzCIPAdapterTest.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2018 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.testsuite.adapter.example.authorization;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Test;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public abstract class AbstractServletAuthzCIPAdapterTest extends AbstractServletAuthzFunctionalAdapterTest {
+
+ @Deployment(name = RESOURCE_SERVER_ID, managed = false)
+ public static WebArchive deployment() throws IOException {
+ return exampleDeployment(RESOURCE_SERVER_ID)
+ .addAsWebInfResource(new File(TEST_APPS_HOME_DIR + "/servlet-authz-app/keycloak-claim-information-point-authz-service.json"), "keycloak.json");
+ }
+
+ @Test
+ public void testClaimInformationPoint() {
+ performTests(() -> {
+ login("alice", "alice");
+ assertWasNotDenied();
+
+ this.driver.navigate().to(getResourceServerUrl() + "/protected/context/context.jsp?request-claim=unexpected-value");
+
+ assertWasDenied();
+
+ this.driver.navigate().to(getResourceServerUrl() + "/protected/context/context.jsp?request-claim=expected-value");
+ assertWasNotDenied();
+ hasText("Access granted: true");
+
+ this.driver.navigate().to(getResourceServerUrl() + "/protected/context/context.jsp");
+
+ assertWasDenied();
+ });
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ClaimInformationPointProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ClaimInformationPointProviderTest.java
new file mode 100644
index 0000000..87c4382
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ClaimInformationPointProviderTest.java
@@ -0,0 +1,457 @@
+/*
+ * Copyright 2018 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.testsuite.admin.client.authorization;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.keycloak.testsuite.util.IOUtil.loadRealm;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.security.cert.X509Certificate;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.TreeNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.undertow.Undertow;
+import io.undertow.server.handlers.form.FormData;
+import io.undertow.server.handlers.form.FormDataParser;
+import io.undertow.server.handlers.form.FormParserFactory;
+import org.apache.http.impl.client.HttpClients;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.keycloak.KeycloakSecurityContext;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.KeycloakDeploymentBuilder;
+import org.keycloak.adapters.OIDCHttpFacade;
+import org.keycloak.adapters.authorization.ClaimInformationPointProvider;
+import org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory;
+import org.keycloak.adapters.authorization.PolicyEnforcer;
+import org.keycloak.adapters.spi.AuthenticationError;
+import org.keycloak.adapters.spi.HttpFacade;
+import org.keycloak.adapters.spi.HttpFacade.Cookie;
+import org.keycloak.adapters.spi.HttpFacade.Request;
+import org.keycloak.adapters.spi.HttpFacade.Response;
+import org.keycloak.adapters.spi.LogoutError;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.IDToken;
+import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.ProfileAssume;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ClaimInformationPointProviderTest extends AbstractKeycloakTest {
+
+ private static Undertow httpService;
+
+ @BeforeClass
+ public static void onBeforeClass() {
+ ProfileAssume.assumePreview();
+ httpService = Undertow.builder().addHttpListener(8989, "localhost").setHandler(exchange -> {
+ if (exchange.isInIoThread()) {
+ try {
+ if (exchange.getRelativePath().equals("/post-claim-information-provider")) {
+ FormParserFactory parserFactory = FormParserFactory.builder().build();
+ FormDataParser parser = parserFactory.createParser(exchange);
+ FormData formData = parser.parseBlocking();
+
+ if (!"Bearer tokenString".equals(exchange.getRequestHeaders().getFirst("Authorization"))
+ || !"post".equalsIgnoreCase(exchange.getRequestMethod().toString())
+ || !"application/x-www-form-urlencoded".equals(exchange.getRequestHeaders().getFirst("Content-Type"))
+ || !exchange.getRequestHeaders().get("header-b").contains("header-b-value1")
+ || !exchange.getRequestHeaders().get("header-b").contains("header-b-value2")
+ || !formData.get("param-a").getFirst().getValue().equals("param-a-value1")
+ || !formData.get("param-a").getLast().getValue().equals("param-a-value2")
+ || !formData.get("param-subject").getFirst().getValue().equals("sub")
+ || !formData.get("param-user-name").getFirst().getValue().equals("username")
+ || !formData.get("param-other-claims").getFirst().getValue().equals("param-other-claims-value1")
+ || !formData.get("param-other-claims").getLast().getValue().equals("param-other-claims-value2")) {
+ exchange.setStatusCode(400);
+ return;
+ }
+
+ exchange.setStatusCode(200);
+ } else if (exchange.getRelativePath().equals("/get-claim-information-provider")) {
+ if (!"Bearer idTokenString".equals(exchange.getRequestHeaders().getFirst("Authorization"))
+ || !"get".equalsIgnoreCase(exchange.getRequestMethod().toString())
+ || !exchange.getRequestHeaders().get("header-b").contains("header-b-value1")
+ || !exchange.getRequestHeaders().get("header-b").contains("header-b-value2")
+ || !exchange.getQueryParameters().get("param-a").contains("param-a-value1")
+ || !exchange.getQueryParameters().get("param-a").contains("param-a-value2")
+ || !exchange.getQueryParameters().get("param-subject").contains("sub")
+ || !exchange.getQueryParameters().get("param-user-name").contains("username")) {
+ exchange.setStatusCode(400);
+ return;
+ }
+
+ exchange.setStatusCode(200);
+ } else {
+ exchange.setStatusCode(404);
+ }
+ } finally {
+ if (exchange.getStatusCode() == 200) {
+ try {
+ ObjectMapper mapper = JsonSerialization.mapper;
+ JsonParser jsonParser = mapper.getFactory().createParser("{\"a\": \"a-value1\", \"b\": \"b-value1\", \"d\": [\"d-value1\", \"d-value2\"]}");
+ TreeNode treeNode = mapper.readTree(jsonParser);
+ exchange.getResponseSender().send(treeNode.toString());
+ } catch (Exception ignore) {
+ ignore.printStackTrace();
+ }
+ }
+ exchange.endExchange();
+ }
+ }
+ }).build();
+
+ httpService.start();
+ }
+
+ @AfterClass
+ public static void onAfterClass() {
+ httpService.stop();
+ }
+
+ @Override
+ public void addTestRealms(List<RealmRepresentation> testRealms) {
+ RealmRepresentation realm = loadRealm(getClass().getResourceAsStream("/authorization-test/test-authz-realm.json"));
+ testRealms.add(realm);
+ }
+
+ private ClaimInformationPointProvider getClaimInformationProviderForPath(String path, String providerName) {
+ KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getClass().getResourceAsStream("/authorization-test/enforcer-config-claims-provider.json"));
+ deployment.setClient(HttpClients.createDefault());
+ PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer();
+ Map<String, ClaimInformationPointProviderFactory> providers = policyEnforcer.getClaimInformationPointProviderFactories();
+
+ PathConfig pathConfig = policyEnforcer.getPaths().get(path);
+
+ assertNotNull(pathConfig);
+
+ Map<String, Map<String, Object>> cipConfig = pathConfig.getClaimInformationPointConfig();
+
+ assertNotNull(cipConfig);
+
+ ClaimInformationPointProviderFactory factory = providers.get(providerName);
+
+ assertNotNull(factory);
+
+ Map<String, Object> claimsConfig = cipConfig.get(providerName);
+
+ return factory.create(claimsConfig);
+ }
+
+ @Test
+ public void testBasicClaimsInformationPoint() {
+ HttpFacade httpFacade = createHttpFacade();
+ Map<String, List<String>> claims = getClaimInformationProviderForPath("/claims-provider", "claims").resolve(httpFacade);
+
+ assertEquals("parameter-a", claims.get("claim-from-request-parameter").get(0));
+ assertEquals("header-b", claims.get("claim-from-header").get(0));
+ assertEquals("cookie-c", claims.get("claim-from-cookie").get(0));
+ assertEquals("user-remote-addr", claims.get("claim-from-remoteAddr").get(0));
+ assertEquals("GET", claims.get("claim-from-method").get(0));
+ assertEquals("/app/request-uri", claims.get("claim-from-uri").get(0));
+ assertEquals("/request-relative-path", claims.get("claim-from-relativePath").get(0));
+ assertEquals("true", claims.get("claim-from-secure").get(0));
+ assertEquals("static value", claims.get("claim-from-static-value").get(0));
+ assertEquals("static", claims.get("claim-from-multiple-static-value").get(0));
+ assertEquals("value", claims.get("claim-from-multiple-static-value").get(1));
+ assertEquals("Test param-other-claims-value1 and parameter-a", claims.get("param-replace-multiple-placeholder").get(0));
+ }
+
+ @Test
+ public void testBodyJsonClaimsInformationPoint() throws Exception {
+ Map<String, List<String>> headers = new HashMap<>();
+
+ headers.put("Content-Type", Arrays.asList("application/json"));
+
+ ObjectMapper mapper = JsonSerialization.mapper;
+ JsonParser parser = mapper.getFactory().createParser("{\"a\": {\"b\": {\"c\": \"c-value\"}}, \"d\": [\"d-value1\", \"d-value2\"]}");
+ TreeNode treeNode = mapper.readTree(parser);
+ HttpFacade httpFacade = createHttpFacade(headers, new ByteArrayInputStream(treeNode.toString().getBytes()));
+
+ Map<String, List<String>> claims = getClaimInformationProviderForPath("/claims-provider", "claims").resolve(httpFacade);
+
+ assertEquals("c-value", claims.get("claim-from-json-body-object").get(0));
+ assertEquals("d-value2", claims.get("claim-from-json-body-array").get(0));
+ }
+
+ @Test
+ public void testBodyClaimsInformationPoint() {
+ HttpFacade httpFacade = createHttpFacade(new HashMap<>(), new ByteArrayInputStream("raw-body-text".getBytes()));
+
+ Map<String, List<String>> claims = getClaimInformationProviderForPath("/claims-provider", "claims").resolve(httpFacade);
+
+ assertEquals("raw-body-text", claims.get("claim-from-body").get(0));
+ }
+
+ @Test
+ public void testHttpClaimInformationPointProviderWithoutClaims() {
+ HttpFacade httpFacade = createHttpFacade();
+
+ Map<String, List<String>> claims = getClaimInformationProviderForPath("/http-get-claim-provider", "http").resolve(httpFacade);
+
+ assertEquals("a-value1", claims.get("a").get(0));
+ assertEquals("b-value1", claims.get("b").get(0));
+ assertEquals("d-value1", claims.get("d").get(0));
+ assertEquals("d-value2", claims.get("d").get(1));
+
+ assertNull(claims.get("claim-a"));
+ assertNull(claims.get("claim-d"));
+ assertNull(claims.get("claim-d0"));
+ assertNull(claims.get("claim-d-all"));
+ }
+
+ @Test
+ public void testHttpClaimInformationPointProviderWithClaims() {
+ HttpFacade httpFacade = createHttpFacade();
+
+ Map<String, List<String>> claims = getClaimInformationProviderForPath("/http-post-claim-provider", "http").resolve(httpFacade);
+
+ assertEquals("a-value1", claims.get("claim-a").get(0));
+ assertEquals("d-value1", claims.get("claim-d").get(0));
+ assertEquals("d-value2", claims.get("claim-d").get(1));
+ assertEquals("d-value1", claims.get("claim-d0").get(0));
+ assertEquals("d-value1", claims.get("claim-d-all").get(0));
+ assertEquals("d-value2", claims.get("claim-d-all").get(1));
+
+ assertNull(claims.get("a"));
+ assertNull(claims.get("b"));
+ assertNull(claims.get("d"));
+ }
+
+ private HttpFacade createHttpFacade(Map<String, List<String>> headers, InputStream requestBody) {
+ return new OIDCHttpFacade() {
+ private Request request;
+
+ @Override
+ public KeycloakSecurityContext getSecurityContext() {
+ AccessToken token = new AccessToken();
+
+ token.subject("sub");
+ token.setPreferredUsername("username");
+ token.getOtherClaims().put("custom_claim", Arrays.asList("param-other-claims-value1", "param-other-claims-value2"));
+
+ IDToken idToken = new IDToken();
+
+ idToken.subject("sub");
+ idToken.setPreferredUsername("username");
+ idToken.getOtherClaims().put("custom_claim", Arrays.asList("param-other-claims-value1", "param-other-claims-value2"));
+
+ return new KeycloakSecurityContext("tokenString", token, "idTokenString", idToken);
+ }
+
+ @Override
+ public Request getRequest() {
+ if (request == null) {
+ request = createHttpRequest(headers, requestBody);
+ }
+ return request;
+ }
+
+ @Override
+ public Response getResponse() {
+ return createHttpResponse();
+ }
+
+ @Override
+ public X509Certificate[] getCertificateChain() {
+ return new X509Certificate[0];
+ }
+ };
+ }
+
+ private HttpFacade createHttpFacade() {
+ return createHttpFacade(new HashMap<>(), null);
+ }
+
+ private Response createHttpResponse() {
+ return new Response() {
+ @Override
+ public void setStatus(int status) {
+
+ }
+
+ @Override
+ public void addHeader(String name, String value) {
+
+ }
+
+ @Override
+ public void setHeader(String name, String value) {
+
+ }
+
+ @Override
+ public void resetCookie(String name, String path) {
+
+ }
+
+ @Override
+ public void setCookie(String name, String value, String path, String domain, int maxAge, boolean secure, boolean httpOnly) {
+
+ }
+
+ @Override
+ public OutputStream getOutputStream() {
+ return null;
+ }
+
+ @Override
+ public void sendError(int code) {
+
+ }
+
+ @Override
+ public void sendError(int code, String message) {
+
+ }
+
+ @Override
+ public void end() {
+
+ }
+ };
+ }
+
+ private Request createHttpRequest(Map<String, List<String>> headers, InputStream requestBody) {
+ Map<String, List<String>> queryParameter = new HashMap<>();
+
+ queryParameter.put("a", Arrays.asList("parameter-a"));
+
+ headers.put("b", Arrays.asList("header-b"));
+
+ Map<String, Cookie> cookies = new HashMap<>();
+
+ cookies.put("c", new Cookie("c", "cookie-c", 1, "localhost", "/"));
+
+ return new Request() {
+
+ private InputStream inputStream;
+
+ @Override
+ public String getMethod() {
+ return "GET";
+ }
+
+ @Override
+ public String getURI() {
+ return "/app/request-uri";
+ }
+
+ @Override
+ public String getRelativePath() {
+ return "/request-relative-path";
+ }
+
+ @Override
+ public boolean isSecure() {
+ return true;
+ }
+
+ @Override
+ public String getFirstParam(String param) {
+ List<String> values = queryParameter.getOrDefault(param, Collections.emptyList());
+
+ if (!values.isEmpty()) {
+ return values.get(0);
+ }
+
+ return null;
+ }
+
+ @Override
+ public String getQueryParamValue(String param) {
+ return getFirstParam(param);
+ }
+
+ @Override
+ public Cookie getCookie(String cookieName) {
+ return cookies.get(cookieName);
+ }
+
+ @Override
+ public String getHeader(String name) {
+ List<String> headers = getHeaders(name);
+
+ if (!headers.isEmpty()) {
+ return headers.get(0);
+ }
+
+ return null;
+ }
+
+ @Override
+ public List<String> getHeaders(String name) {
+ return headers.getOrDefault(name, Collections.emptyList());
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ return getInputStream(false);
+ }
+
+ @Override
+ public InputStream getInputStream(boolean buffer) {
+ if (requestBody == null) {
+ return new ByteArrayInputStream(new byte[] {});
+ }
+
+ if (inputStream != null) {
+ return inputStream;
+ }
+
+ if (buffer) {
+ return inputStream = new BufferedInputStream(requestBody);
+ }
+
+ return requestBody;
+ }
+
+ @Override
+ public String getRemoteAddr() {
+ return "user-remote-addr";
+ }
+
+ @Override
+ public void setError(AuthenticationError error) {
+
+ }
+
+ @Override
+ public void setError(LogoutError error) {
+
+ }
+ };
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/EnforcerConfigTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/EnforcerConfigTest.java
index 081bb79..327b733 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/EnforcerConfigTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/EnforcerConfigTest.java
@@ -22,6 +22,7 @@ import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.authorization.PolicyEnforcer;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
+import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.ProfileAssume;
@@ -30,6 +31,8 @@ import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
import static org.keycloak.testsuite.util.IOUtil.loadRealm;
/**
@@ -47,11 +50,34 @@ public class EnforcerConfigTest extends AbstractKeycloakTest {
}
@Test
- public void testMultiplePathsWithSameName() throws Exception{
+ public void testMultiplePathsWithSameName() {
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getClass().getResourceAsStream("/authorization-test/enforcer-config-paths-same-name.json"));
PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer();
Map<String, PolicyEnforcerConfig.PathConfig> paths = policyEnforcer.getPaths();
assertEquals(1, paths.size());
assertEquals(4, paths.values().iterator().next().getMethods().size());
}
+
+ @Test
+ public void testPathConfigClaimInformationPoint() {
+ KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getClass().getResourceAsStream("/authorization-test/enforcer-config-path-cip.json"));
+ PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer();
+ Map<String, PolicyEnforcerConfig.PathConfig> paths = policyEnforcer.getPaths();
+
+ assertEquals(1, paths.size());
+
+ PathConfig pathConfig = paths.values().iterator().next();
+ Map<String, Map<String, Object>> cipConfig = pathConfig.getClaimInformationPointConfig();
+
+ assertEquals(1, cipConfig.size());
+
+ Map<String, Object> claims = cipConfig.get("claims");
+
+ assertNotNull(claims);
+
+ assertEquals(3, claims.size());
+ assertEquals("{request.parameter['a']}", claims.get("claim-a"));
+ assertEquals("{request.header['b']}", claims.get("claim-b"));
+ assertEquals("{request.cookie['c']}", claims.get("claim-c"));
+ }
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/PolicyEnforcerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/PolicyEnforcerTest.java
new file mode 100644
index 0000000..81f2ffd
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/PolicyEnforcerTest.java
@@ -0,0 +1,512 @@
+/*
+ * Copyright 2018 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.testsuite.admin.client.authorization;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import javax.security.cert.X509Certificate;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.keycloak.AuthorizationContext;
+import org.keycloak.KeycloakSecurityContext;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.KeycloakDeploymentBuilder;
+import org.keycloak.adapters.OIDCHttpFacade;
+import org.keycloak.adapters.authorization.PolicyEnforcer;
+import org.keycloak.adapters.spi.AuthenticationError;
+import org.keycloak.adapters.spi.HttpFacade.Cookie;
+import org.keycloak.adapters.spi.HttpFacade.Request;
+import org.keycloak.adapters.spi.HttpFacade.Response;
+import org.keycloak.adapters.spi.LogoutError;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.ClientsResource;
+import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.authorization.client.Configuration;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.JWSInputException;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.authorization.AuthorizationRequest;
+import org.keycloak.representations.idm.authorization.AuthorizationResponse;
+import org.keycloak.representations.idm.authorization.JSPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.ProfileAssume;
+import org.keycloak.testsuite.util.ClientBuilder;
+import org.keycloak.testsuite.util.RealmBuilder;
+import org.keycloak.testsuite.util.RoleBuilder;
+import org.keycloak.testsuite.util.RolesBuilder;
+import org.keycloak.testsuite.util.UserBuilder;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class PolicyEnforcerTest extends AbstractKeycloakTest {
+
+ protected static final String REALM_NAME = "authz-test";
+
+ @BeforeClass
+ public static void onBeforeClass() {
+ ProfileAssume.assumePreview();
+ }
+
+ @Override
+ public void addTestRealms(List<RealmRepresentation> testRealms) {
+ testRealms.add(RealmBuilder.create().name(REALM_NAME)
+ .roles(RolesBuilder.create()
+ .realmRole(RoleBuilder.create().name("uma_authorization").build())
+ .realmRole(RoleBuilder.create().name("uma_protection").build())
+ )
+ .user(UserBuilder.create().username("marta").password("password")
+ .addRoles("uma_authorization", "uma_protection")
+ .role("resource-server-test", "uma_protection"))
+ .user(UserBuilder.create().username("kolo").password("password"))
+ .client(ClientBuilder.create().clientId("resource-server-uma-test")
+ .secret("secret")
+ .authorizationServicesEnabled(true)
+ .redirectUris("http://localhost/resource-server-uma-test")
+ .defaultRoles("uma_protection")
+ .directAccessGrants())
+ .client(ClientBuilder.create().clientId("resource-server-test")
+ .secret("secret")
+ .authorizationServicesEnabled(true)
+ .redirectUris("http://localhost/resource-server-test")
+ .defaultRoles("uma_protection")
+ .directAccessGrants())
+ .build());
+ }
+
+ @Test
+ public void testEnforceUMAAccessWithClaimsUsingBearerToken() {
+ initAuthorizationSettings(getClientResource("resource-server-uma-test"));
+
+ KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getAdapterConfiguration("enforcer-uma-claims-test.json"));
+ PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer();
+ HashMap<String, List<String>> headers = new HashMap<>();
+ HashMap<String, List<String>> parameters = new HashMap<>();
+
+ parameters.put("withdrawal.amount", Arrays.asList("50"));
+
+ AuthzClient authzClient = getAuthzClient("enforcer-uma-claims-test.json");
+ String token = authzClient.obtainAccessToken("marta", "password").getToken();
+
+ headers.put("Authorization", Arrays.asList("Bearer " + token));
+
+ AuthorizationContext context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
+ assertFalse(context.isGranted());
+
+ AuthorizationRequest request = new AuthorizationRequest();
+
+ request.setTicket(extractTicket(headers));
+
+ AuthorizationResponse response = authzClient.authorization("marta", "password").authorize(request);
+ token = response.getToken();
+
+ assertNotNull(token);
+
+ context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
+ assertTrue(context.isGranted());
+
+ parameters.put("withdrawal.amount", Arrays.asList("200"));
+
+ context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
+ assertFalse(context.isGranted());
+
+ parameters.put("withdrawal.amount", Arrays.asList("50"));
+
+ context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
+ assertTrue(context.isGranted());
+
+ parameters.put("withdrawal.amount", Arrays.asList("10"));
+
+ context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
+
+ request = new AuthorizationRequest();
+
+ request.setTicket(extractTicket(headers));
+
+ response = authzClient.authorization("marta", "password").authorize(request);
+ token = response.getToken();
+
+ context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
+ assertTrue(context.isGranted());
+ }
+
+ @Test
+ public void testEnforceEntitlementAccessWithClaimsWithoutBearerToken() {
+ initAuthorizationSettings(getClientResource("resource-server-test"));
+
+ KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getAdapterConfiguration("enforcer-entitlement-claims-test.json"));
+ PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer();
+ HashMap<String, List<String>> headers = new HashMap<>();
+ HashMap<String, List<String>> parameters = new HashMap<>();
+
+ AuthzClient authzClient = getAuthzClient("enforcer-entitlement-claims-test.json");
+ String token = authzClient.obtainAccessToken("marta", "password").getToken();
+
+ AuthorizationContext context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
+ assertFalse(context.isGranted());
+
+ parameters.put("withdrawal.amount", Arrays.asList("50"));
+
+ context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
+ assertTrue(context.isGranted());
+
+ parameters.put("withdrawal.amount", Arrays.asList("200"));
+
+ context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
+ assertFalse(context.isGranted());
+
+ parameters.put("withdrawal.amount", Arrays.asList("50"));
+
+ context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
+ assertTrue(context.isGranted());
+
+ parameters.put("withdrawal.amount", Arrays.asList("10"));
+
+ context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
+
+ assertTrue(context.isGranted());
+ }
+
+ @Test
+ public void testEnforceEntitlementAccessWithClaimsWithBearerToken() {
+ initAuthorizationSettings(getClientResource("resource-server-test"));
+
+ KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getAdapterConfiguration("enforcer-entitlement-claims-test.json"));
+ PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer();
+ HashMap<String, List<String>> headers = new HashMap<>();
+ HashMap<String, List<String>> parameters = new HashMap<>();
+
+ AuthzClient authzClient = getAuthzClient("enforcer-entitlement-claims-test.json");
+ String token = authzClient.obtainAccessToken("marta", "password").getToken();
+
+ headers.put("Authorization", Arrays.asList("Bearer " + token));
+
+ AuthorizationContext context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
+ assertFalse(context.isGranted());
+
+ parameters.put("withdrawal.amount", Arrays.asList("50"));
+
+ context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
+ assertTrue(context.isGranted());
+
+ parameters.put("withdrawal.amount", Arrays.asList("200"));
+
+ context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
+ assertFalse(context.isGranted());
+
+ parameters.put("withdrawal.amount", Arrays.asList("50"));
+
+ context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
+ assertTrue(context.isGranted());
+
+ parameters.put("withdrawal.amount", Arrays.asList("10"));
+
+ context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
+
+ assertTrue(context.isGranted());
+ }
+
+ private String extractTicket(HashMap<String, List<String>> headers) {
+ List<String> wwwAuthenticateHeader = headers.get("WWW-Authenticate");
+
+ assertNotNull(wwwAuthenticateHeader);
+ assertFalse(wwwAuthenticateHeader.isEmpty());
+
+ String wwwAuthenticate = wwwAuthenticateHeader.get(0);
+ return wwwAuthenticate.substring(wwwAuthenticate.indexOf("ticket=") + "ticket=\"".length(), wwwAuthenticate.lastIndexOf('"'));
+ }
+
+ private void initAuthorizationSettings(ClientResource clientResource) {
+ if (clientResource.authorization().resources().findByName("Bank Account").isEmpty()) {
+ JSPolicyRepresentation policy = new JSPolicyRepresentation();
+
+ policy.setName("Withdrawal Limit Policy");
+
+ StringBuilder code = new StringBuilder();
+
+ code.append("var context = $evaluation.getContext();");
+ code.append("var attributes = context.getAttributes();");
+ code.append("var withdrawalAmount = attributes.getValue('withdrawal.amount');");
+ code.append("if (withdrawalAmount && withdrawalAmount.asDouble(0) <= 100) {");
+ code.append(" $evaluation.grant();");
+ code.append("}");
+
+ policy.setCode(code.toString());
+
+ clientResource.authorization().policies().js().create(policy);
+
+ createResource(clientResource, "Bank Account", "/api/bank/account/{id}/withdrawal", "withdrawal");
+
+ ScopePermissionRepresentation permission = new ScopePermissionRepresentation();
+
+ permission.setName("Withdrawal Permission");
+ permission.addScope("withdrawal");
+ permission.addPolicy(policy.getName());
+
+ clientResource.authorization().permissions().scope().create(permission);
+ }
+ }
+
+ private InputStream getAdapterConfiguration(String fileName) {
+ return getClass().getResourceAsStream("/authorization-test/" + fileName);
+ }
+
+ private ResourceRepresentation createResource(ClientResource clientResource, String name, String uri, String... scopes) {
+ ResourceRepresentation representation = new ResourceRepresentation();
+
+ representation.setName(name);
+ representation.setUri(uri);
+ representation.setScopes(Arrays.asList(scopes).stream().map(ScopeRepresentation::new).collect(Collectors.toSet()));
+
+ javax.ws.rs.core.Response response = clientResource.authorization().resources().create(representation);
+
+ representation.setId(response.readEntity(ResourceRepresentation.class).getId());
+
+ return representation;
+ }
+
+ private ClientResource getClientResource(String name) {
+ ClientsResource clients = realmsResouce().realm(REALM_NAME).clients();
+ ClientRepresentation representation = clients.findByClientId(name).get(0);
+ return clients.get(representation.getId());
+ }
+
+ private OIDCHttpFacade createHttpFacade(String path, String token, Map<String, List<String>> headers, Map<String, List<String>> parameters, InputStream requestBody) {
+ return new OIDCHttpFacade() {
+ Request request;
+ Response response;
+
+ @Override
+ public KeycloakSecurityContext getSecurityContext() {
+ AccessToken accessToken;
+ try {
+ accessToken = new JWSInput(token).readJsonContent(AccessToken.class);
+ } catch (JWSInputException cause) {
+ throw new RuntimeException(cause);
+ }
+ return new KeycloakSecurityContext(token, accessToken, null, null);
+ }
+
+ @Override
+ public Request getRequest() {
+ if (request == null) {
+ request = createHttpRequest(path, headers, parameters, requestBody);
+ }
+ return request;
+ }
+
+ @Override
+ public Response getResponse() {
+ if (response == null) {
+ response = createHttpResponse(headers);
+ }
+ return response;
+ }
+
+ @Override
+ public X509Certificate[] getCertificateChain() {
+ return new X509Certificate[0];
+ }
+ };
+ }
+
+ private OIDCHttpFacade createHttpFacade(String path, String token, Map<String, List<String>> headers, Map<String, List<String>> parameters) {
+ return createHttpFacade(path, token, headers, parameters, null);
+ }
+
+ private Response createHttpResponse(Map<String, List<String>> headers) {
+ return new Response() {
+
+ private int status;
+
+ @Override
+ public void setStatus(int status) {
+ this.status = status;
+ }
+
+ @Override
+ public void addHeader(String name, String value) {
+ setHeader(name, value);
+ }
+
+ @Override
+ public void setHeader(String name, String value) {
+ headers.put(name, Arrays.asList(value));
+ }
+
+ @Override
+ public void resetCookie(String name, String path) {
+
+ }
+
+ @Override
+ public void setCookie(String name, String value, String path, String domain, int maxAge, boolean secure, boolean httpOnly) {
+
+ }
+
+ @Override
+ public OutputStream getOutputStream() {
+ return null;
+ }
+
+ @Override
+ public void sendError(int code) {
+
+ }
+
+ @Override
+ public void sendError(int code, String message) {
+
+ }
+
+ @Override
+ public void end() {
+
+ }
+ };
+ }
+
+ private Request createHttpRequest(String path, Map<String, List<String>> headers, Map<String, List<String>> parameters, InputStream requestBody) {
+ return new Request() {
+
+ private InputStream inputStream;
+
+ @Override
+ public String getMethod() {
+ return "GET";
+ }
+
+ @Override
+ public String getURI() {
+ return path;
+ }
+
+ @Override
+ public String getRelativePath() {
+ return path;
+ }
+
+ @Override
+ public boolean isSecure() {
+ return true;
+ }
+
+ @Override
+ public String getFirstParam(String param) {
+ List<String> values = parameters.getOrDefault(param, Collections.emptyList());
+
+ if (!values.isEmpty()) {
+ return values.get(0);
+ }
+
+ return null;
+ }
+
+ @Override
+ public String getQueryParamValue(String param) {
+ return getFirstParam(param);
+ }
+
+ @Override
+ public Cookie getCookie(String cookieName) {
+ return null;
+ }
+
+ @Override
+ public String getHeader(String name) {
+ List<String> headers = getHeaders(name);
+
+ if (!headers.isEmpty()) {
+ return headers.get(0);
+ }
+
+ return null;
+ }
+
+ @Override
+ public List<String> getHeaders(String name) {
+ return headers.getOrDefault(name, Collections.emptyList());
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ return getInputStream(false);
+ }
+
+ @Override
+ public InputStream getInputStream(boolean buffer) {
+ if (requestBody == null) {
+ return new ByteArrayInputStream(new byte[] {});
+ }
+
+ if (inputStream != null) {
+ return inputStream;
+ }
+
+ if (buffer) {
+ return inputStream = new BufferedInputStream(requestBody);
+ }
+
+ return requestBody;
+ }
+
+ @Override
+ public String getRemoteAddr() {
+ return "user-remote-addr";
+ }
+
+ @Override
+ public void setError(AuthenticationError error) {
+
+ }
+
+ @Override
+ public void setError(LogoutError error) {
+
+ }
+ };
+ }
+
+ protected AuthzClient getAuthzClient(String fileName) {
+ try {
+ return AuthzClient.create(JsonSerialization.readValue(getAdapterConfiguration(fileName), Configuration.class));
+ } catch (IOException cause) {
+ throw new RuntimeException("Failed to create authz client", cause);
+ }
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/EntitlementAPITest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/EntitlementAPITest.java
index a7fab87..167761f 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/EntitlementAPITest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/EntitlementAPITest.java
@@ -20,19 +20,25 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
import java.io.IOException;
+import java.util.HashMap;
import java.util.List;
import java.util.function.Supplier;
import org.junit.Before;
import org.junit.Test;
+import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.AuthorizationResource;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.authorization.client.AuthorizationDeniedException;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.Configuration;
+import org.keycloak.authorization.client.util.HttpResponseException;
+import org.keycloak.common.util.Base64Url;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.authorization.AuthorizationRequest;
@@ -61,6 +67,8 @@ public class EntitlementAPITest extends AbstractAuthzTest {
private static final String PAIRWISE_RESOURCE_SERVER_TEST = "pairwise-resource-server-test";
private static final String PAIRWISE_TEST_CLIENT = "test-client-pairwise";
private static final String PAIRWISE_AUTHZ_CLIENT_CONFIG = "default-keycloak-pairwise.json";
+ private static final String PUBLIC_TEST_CLIENT = "test-public-client";
+ private static final String PUBLIC_TEST_CLIENT_CONFIG = "default-keycloak-public-client.json";
private AuthzClient authzClient;
@@ -94,6 +102,10 @@ public class EntitlementAPITest extends AbstractAuthzTest {
.redirectUris("http://localhost/test-client")
.pairwise("http://pairwise.com")
.directAccessGrants())
+ .client(ClientBuilder.create().clientId(PUBLIC_TEST_CLIENT)
+ .secret("secret")
+ .redirectUris("http://localhost:8180/auth/realms/master/app/auth/*")
+ .publicClient())
.build());
}
@@ -164,6 +176,65 @@ public class EntitlementAPITest extends AbstractAuthzTest {
testRptRequestWithResourceName(PAIRWISE_AUTHZ_CLIENT_CONFIG);
}
+ @Test
+ public void testInvalidRequestWithClaimsFromConfidentialClient() throws IOException {
+ AuthorizationRequest request = new AuthorizationRequest();
+
+ request.addPermission("Resource 13");
+ HashMap<Object, Object> obj = new HashMap<>();
+
+ obj.put("claim-a", "claim-a");
+
+ request.setClaimToken(Base64Url.encode(JsonSerialization.writeValueAsBytes(obj)));
+
+ assertResponse(new Metadata(), () -> getAuthzClient(AUTHZ_CLIENT_CONFIG).authorization("marta", "password").authorize(request));
+ }
+
+ @Test
+ public void testInvalidRequestWithClaimsFromPublicClient() throws IOException {
+ oauth.realm("authz-test");
+ oauth.clientId(PUBLIC_TEST_CLIENT);
+
+ oauth.doLogin("marta", "password");
+
+ // Token request
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, null);
+
+ AuthorizationRequest request = new AuthorizationRequest();
+
+ request.addPermission("Resource 13");
+ HashMap<Object, Object> obj = new HashMap<>();
+
+ obj.put("claim-a", "claim-a");
+
+ request.setClaimToken(Base64Url.encode(JsonSerialization.writeValueAsBytes(obj)));
+
+ try {
+ getAuthzClient(AUTHZ_CLIENT_CONFIG).authorization(response.getAccessToken()).authorize(request);
+ } catch (AuthorizationDeniedException expected) {
+ assertEquals(403, HttpResponseException.class.cast(expected.getCause()).getStatusCode());
+ assertTrue(HttpResponseException.class.cast(expected.getCause()).toString().contains("Public clients are not allowed to send claims"));
+ }
+ }
+
+ @Test
+ public void testRequestWithoutClaimsFromPublicClient() {
+ oauth.realm("authz-test");
+ oauth.clientId(PUBLIC_TEST_CLIENT);
+
+ oauth.doLogin("marta", "password");
+
+ // Token request
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, null);
+
+ AuthorizationRequest request = new AuthorizationRequest();
+
+ request.addPermission("Resource 13");
+
+ assertResponse(new Metadata(), () -> getAuthzClient(AUTHZ_CLIENT_CONFIG).authorization(response.getAccessToken()).authorize(request));
+ }
public void testRptRequestWithResourceName(String configFile) {
Metadata metadata = new Metadata();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionManagementTest.java
index f488d32..9d47f8d 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionManagementTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionManagementTest.java
@@ -219,7 +219,7 @@ public class PermissionManagementTest extends AbstractResourceServerTest {
try {
authzClient.authorization().authorize(request);
} catch (Exception e) {
-
+ e.printStackTrace();
}
List permissions = authzClient.protection().permission().findByResource(resource.getId());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UmaPermissionTicketPushedClaimsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UmaPermissionTicketPushedClaimsTest.java
new file mode 100644
index 0000000..0d36913
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UmaPermissionTicketPushedClaimsTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2018 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.testsuite.authz;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+import org.keycloak.admin.client.resource.AuthorizationResource;
+import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.representations.idm.authorization.AuthorizationRequest;
+import org.keycloak.representations.idm.authorization.AuthorizationResponse;
+import org.keycloak.representations.idm.authorization.JSPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.PermissionRequest;
+import org.keycloak.representations.idm.authorization.PermissionResponse;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class UmaPermissionTicketPushedClaimsTest extends AbstractResourceServerTest {
+
+ @Test
+ public void testEvaluatePermissionsWithPushedClaims() throws Exception {
+ ResourceRepresentation resource = addResource("Bank Account", "withdraw");
+ JSPolicyRepresentation policy = new JSPolicyRepresentation();
+
+ policy.setName("Withdraw Limit Policy");
+
+ StringBuilder code = new StringBuilder();
+
+ code.append("var context = $evaluation.getContext();");
+ code.append("var attributes = context.getAttributes();");
+ code.append("var withdrawValue = attributes.getValue('my.bank.account.withdraw.value');");
+ code.append("if (withdrawValue && withdrawValue.asDouble(0) <= 100) {");
+ code.append(" $evaluation.grant();");
+ code.append("}");
+
+ policy.setCode(code.toString());
+
+ AuthorizationResource authorization = getClient(getRealm()).authorization();
+
+ authorization.policies().js().create(policy);
+
+ ScopePermissionRepresentation representation = new ScopePermissionRepresentation();
+
+ representation.setName("Withdraw Permission");
+ representation.addScope("withdraw");
+ representation.addPolicy(policy.getName());
+
+ authorization.permissions().scope().create(representation);
+
+ AuthzClient authzClient = getAuthzClient();
+ PermissionRequest permissionRequest = new PermissionRequest(resource.getId());
+
+ permissionRequest.addScope("withdraw");
+ permissionRequest.setClaim("my.bank.account.withdraw.value", "50.5");
+
+ PermissionResponse response = authzClient.protection("marta", "password").permission().create(permissionRequest);
+ AuthorizationRequest request = new AuthorizationRequest();
+
+ request.setTicket(response.getTicket());
+ request.setClaimToken(authzClient.obtainAccessToken("marta", "password").getToken());
+
+ AuthorizationResponse authorizationResponse = authzClient.authorization().authorize(request);
+
+ assertNotNull(authorizationResponse);
+ assertNotNull(authorizationResponse.getToken());
+
+ permissionRequest.setClaim("my.bank.account.withdraw.value", "100.5");
+
+ response = authzClient.protection("marta", "password").permission().create(permissionRequest);
+ request = new AuthorizationRequest();
+
+ request.setTicket(response.getTicket());
+ request.setClaimToken(authzClient.obtainAccessToken("marta", "password").getToken());
+
+ try {
+ authorizationResponse = authzClient.authorization().authorize(request);
+ fail("Access should be denied");
+ } catch (Exception ignore) {
+
+ }
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/default-keycloak-public-client.json b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/default-keycloak-public-client.json
new file mode 100644
index 0000000..b04b57f
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/default-keycloak-public-client.json
@@ -0,0 +1,5 @@
+{
+ "realm": "authz-test",
+ "auth-server-url" : "http://localhost:8180/auth",
+ "resource" : "test-public-client"
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/enforcer-config-claims-provider.json b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/enforcer-config-claims-provider.json
new file mode 100644
index 0000000..9da5dae
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/enforcer-config-claims-provider.json
@@ -0,0 +1,89 @@
+{
+ "realm": "test-realm-authz",
+ "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "auth-server-url": "http://localhost:8180/auth",
+ "ssl-required": "external",
+ "resource": "test-app-authz",
+ "bearer-only": true,
+ "credentials": {
+ "secret": "secret"
+ },
+ "policy-enforcer": {
+ "paths": [
+ {
+ "path": "/claims-provider",
+ "methods": [
+ {
+ "method": "POST",
+ "scopes": [
+ "create"
+ ]
+ }
+ ],
+ "claim-information-point": {
+ "claims": {
+ "claim-from-request-parameter": "{request.parameter['a']}",
+ "claim-from-header": "{request.header['b']}",
+ "claim-from-cookie": "{request.cookie['c']}",
+ "claim-from-remoteAddr": "{request.remoteAddr}",
+ "claim-from-method": "{request.method}",
+ "claim-from-uri": "{request.uri}",
+ "claim-from-relativePath": "{request.relativePath}",
+ "claim-from-secure": "{request.secure}",
+ "claim-from-json-body-object": "{request.body['/a/b/c']}",
+ "claim-from-json-body-array": "{request.body['/d/1']}",
+ "claim-from-body": "{request.body}",
+ "claim-from-static-value": "static value",
+ "claim-from-multiple-static-value": ["static", "value"],
+ "param-replace-multiple-placeholder": "Test {keycloak.access_token['/custom_claim/0']} and {request.parameter['a']} "
+ }
+ }
+ },
+ {
+ "path": "/http-post-claim-provider",
+ "claim-information-point": {
+ "http": {
+ "claims": {
+ "claim-a": "/a",
+ "claim-d": "/d",
+ "claim-d0": "/d/0",
+ "claim-d-all": ["/d/0", "/d/1"]
+ },
+ "url": "http://localhost:8989/post-claim-information-provider",
+ "method": "POST",
+ "headers": {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "header-b": ["header-b-value1", "header-b-value2"],
+ "Authorization": "Bearer {keycloak.access_token}"
+ },
+ "parameters": {
+ "param-a": ["param-a-value1", "param-a-value2"],
+ "param-subject": "{keycloak.access_token['/sub']}",
+ "param-user-name": "{keycloak.access_token['/preferred_username']}",
+ "param-other-claims": "{keycloak.access_token['/custom_claim']}"
+ }
+ }
+ }
+ },
+ {
+ "path": "/http-get-claim-provider",
+ "claim-information-point": {
+ "http": {
+ "url": "http://localhost:8989/get-claim-information-provider",
+ "method": "get",
+ "headers": {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "header-b": ["header-b-value1", "header-b-value2"],
+ "Authorization": "Bearer {keycloak.id_token}"
+ },
+ "parameters": {
+ "param-a": ["param-a-value1", "param-a-value2"],
+ "param-subject": "{keycloak.id_token['/sub']}",
+ "param-user-name": "{keycloak.id_token['/preferred_username']}"
+ }
+ }
+ }
+ }
+ ]
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/enforcer-config-path-cip.json b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/enforcer-config-path-cip.json
new file mode 100644
index 0000000..78fd7c7
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/enforcer-config-path-cip.json
@@ -0,0 +1,33 @@
+{
+ "realm": "test-realm-authz",
+ "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "auth-server-url": "http://localhost:8180/auth",
+ "ssl-required": "external",
+ "resource": "test-app-authz",
+ "bearer-only": true,
+ "credentials": {
+ "secret": "secret"
+ },
+ "policy-enforcer": {
+ "paths": [
+ {
+ "path": "/v1/product/*",
+ "methods": [
+ {
+ "method": "POST",
+ "scopes": [
+ "create"
+ ]
+ }
+ ],
+ "claim-information-point": {
+ "claims": {
+ "claim-a": "{request.parameter['a']}",
+ "claim-b": "{request.header['b']}",
+ "claim-c": "{request.cookie['c']}"
+ }
+ }
+ }
+ ]
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/enforcer-entitlement-claims-test.json b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/enforcer-entitlement-claims-test.json
new file mode 100644
index 0000000..b01909d
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/enforcer-entitlement-claims-test.json
@@ -0,0 +1,29 @@
+{
+ "realm": "authz-test",
+ "auth-server-url": "http://localhost:8180/auth",
+ "ssl-required": "external",
+ "resource": "resource-server-test",
+ "credentials": {
+ "secret": "secret"
+ },
+ "policy-enforcer": {
+ "paths": [
+ {
+ "path": "/api/bank/account/{id}/withdrawal",
+ "methods": [
+ {
+ "method": "POST",
+ "scopes": [
+ "withdrawal"
+ ]
+ }
+ ],
+ "claim-information-point": {
+ "claims": {
+ "withdrawal.amount": "{request.parameter['withdrawal.amount']}"
+ }
+ }
+ }
+ ]
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/enforcer-uma-claims-test.json b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/enforcer-uma-claims-test.json
new file mode 100644
index 0000000..9729103
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/enforcer-uma-claims-test.json
@@ -0,0 +1,31 @@
+{
+ "realm": "authz-test",
+ "auth-server-url": "http://localhost:8180/auth",
+ "ssl-required": "external",
+ "resource": "resource-server-uma-test",
+ "bearer-only": true,
+ "credentials": {
+ "secret": "secret"
+ },
+ "policy-enforcer": {
+ "user-managed-access": {},
+ "paths": [
+ {
+ "path": "/api/bank/account/{id}/withdrawal",
+ "methods": [
+ {
+ "method": "POST",
+ "scopes": [
+ "withdrawal"
+ ]
+ }
+ ],
+ "claim-information-point": {
+ "claims": {
+ "withdrawal.amount": "{request.parameter['withdrawal.amount']}"
+ }
+ }
+ }
+ ]
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyServletAuthzCIPAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyServletAuthzCIPAdapterTest.java
new file mode 100644
index 0000000..d59955d
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyServletAuthzCIPAdapterTest.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018 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.testsuite.adapter.example.authorization;
+
+import org.jboss.arquillian.container.test.api.RunAsClient;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+/**
+ *
+ * @author tkyjovsk
+ */
+@RunAsClient
+@AppServerContainer("app-server-wildfly")
+//@AdapterLibsLocationProperty("adapter.libs.wildfly")
+public class WildflyServletAuthzCIPAdapterTest extends AbstractServletAuthzCIPAdapterTest {
+
+}