[KEYCLOAK-4903] - Claim Information point Provider SPI and configuration
This commit is contained in:
parent
e813fcd9c8
commit
035ebc881a
43 changed files with 2622 additions and 161 deletions
|
@ -18,7 +18,10 @@
|
||||||
package org.keycloak.adapters.authorization;
|
package org.keycloak.adapters.authorization;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
@ -161,7 +164,8 @@ public abstract class AbstractPolicyEnforcer {
|
||||||
if (HTTP_METHOD_DELETE.equalsIgnoreCase(request.getMethod()) && actualPathConfig.isInstance()) {
|
if (HTTP_METHOD_DELETE.equalsIgnoreCase(request.getMethod()) && actualPathConfig.isInstance()) {
|
||||||
policyEnforcer.getPathMatcher().removeFromCache(getPath(request));
|
policyEnforcer.getPathMatcher().removeFromCache(getPath(request));
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
|
return hasValidClaims(actualPathConfig, httpFacade, authorization);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -183,6 +187,41 @@ public abstract class AbstractPolicyEnforcer {
|
||||||
return false;
|
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) {
|
protected void handleAccessDenied(OIDCHttpFacade httpFacade) {
|
||||||
httpFacade.getResponse().sendError(403);
|
httpFacade.getResponse().sendError(403);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,81 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
|
||||||
* and other contributors as indicated by the @author tags.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package org.keycloak.adapters.authorization;
|
|
||||||
|
|
||||||
import java.util.HashSet;
|
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
|
||||||
import org.keycloak.adapters.OIDCHttpFacade;
|
|
||||||
import org.keycloak.adapters.spi.HttpFacade;
|
|
||||||
import org.keycloak.authorization.client.AuthzClient;
|
|
||||||
import org.keycloak.authorization.client.resource.PermissionResource;
|
|
||||||
import org.keycloak.authorization.client.resource.ProtectionResource;
|
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
|
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
|
||||||
import org.keycloak.representations.idm.authorization.PermissionRequest;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
|
||||||
*/
|
|
||||||
public class BearerTokenPolicyEnforcer extends AbstractPolicyEnforcer {
|
|
||||||
|
|
||||||
private static Logger LOGGER = Logger.getLogger(BearerTokenPolicyEnforcer.class);
|
|
||||||
|
|
||||||
public BearerTokenPolicyEnforcer(PolicyEnforcer enforcer) {
|
|
||||||
super(enforcer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean challenge(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade facade) {
|
|
||||||
HttpFacade.Response response = facade.getResponse();
|
|
||||||
AuthzClient authzClient = getAuthzClient();
|
|
||||||
String ticket = getPermissionTicket(pathConfig, methodConfig, authzClient);
|
|
||||||
|
|
||||||
if (ticket == null) {
|
|
||||||
response.setStatus(403);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
String realm = authzClient.getConfiguration().getRealm();
|
|
||||||
String authorizationServerUri = authzClient.getServerConfiguration().getIssuer().toString();
|
|
||||||
response.setStatus(401);
|
|
||||||
StringBuilder wwwAuthenticate = new StringBuilder("UMA realm=\"").append(realm).append("\"").append(",as_uri=\"").append(authorizationServerUri).append("\"");
|
|
||||||
|
|
||||||
if (ticket != null) {
|
|
||||||
wwwAuthenticate.append(",ticket=\"").append(ticket).append("\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
response.setHeader("WWW-Authenticate", wwwAuthenticate.toString());
|
|
||||||
if (LOGGER.isDebugEnabled()) {
|
|
||||||
LOGGER.debug("Sending UMA challenge");
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getPermissionTicket(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, AuthzClient authzClient) {
|
|
||||||
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()));
|
|
||||||
return permission.create(permissionRequest).getTicket();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -18,7 +18,11 @@
|
||||||
package org.keycloak.adapters.authorization;
|
package org.keycloak.adapters.authorization;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.KeycloakSecurityContext;
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
|
@ -28,6 +32,9 @@ import org.keycloak.adapters.rotation.AdapterRSATokenVerifier;
|
||||||
import org.keycloak.adapters.spi.HttpFacade;
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
import org.keycloak.authorization.client.AuthorizationDeniedException;
|
import org.keycloak.authorization.client.AuthorizationDeniedException;
|
||||||
import org.keycloak.authorization.client.AuthzClient;
|
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.AccessToken;
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
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.AuthorizationResponse;
|
||||||
import org.keycloak.representations.idm.authorization.Permission;
|
import org.keycloak.representations.idm.authorization.Permission;
|
||||||
import org.keycloak.representations.idm.authorization.PermissionRequest;
|
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>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
@ -72,7 +79,27 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
|
||||||
AccessToken.Authorization newAuthorization = accessToken.getAuthorization();
|
AccessToken.Authorization newAuthorization = accessToken.getAuthorization();
|
||||||
|
|
||||||
if (newAuthorization != null) {
|
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);
|
original.setAuthorization(authorization);
|
||||||
|
@ -81,8 +108,29 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean challenge(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade facade) {
|
protected boolean challenge(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade httpFacade) {
|
||||||
handleAccessDenied(facade);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,28 +154,31 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
|
||||||
}
|
}
|
||||||
|
|
||||||
private AccessToken requestAuthorizationToken(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade httpFacade) {
|
private AccessToken requestAuthorizationToken(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade httpFacade) {
|
||||||
|
if (getPolicyEnforcer().getDeployment().isBearerOnly() || (isBearerAuthorization(httpFacade) && getEnforcerConfig().getUserManagedAccess() != null)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
KeycloakSecurityContext securityContext = httpFacade.getSecurityContext();
|
KeycloakSecurityContext securityContext = httpFacade.getSecurityContext();
|
||||||
String accessTokenString = securityContext.getTokenString();
|
String accessTokenString = securityContext.getTokenString();
|
||||||
AuthzClient authzClient = getAuthzClient();
|
|
||||||
KeycloakDeployment deployment = getPolicyEnforcer().getDeployment();
|
KeycloakDeployment deployment = getPolicyEnforcer().getDeployment();
|
||||||
PermissionRequest permissionRequest = new PermissionRequest();
|
|
||||||
|
|
||||||
permissionRequest.setResourceId(pathConfig.getId());
|
|
||||||
permissionRequest.setScopes(new HashSet<>(methodConfig.getScopes()));
|
|
||||||
|
|
||||||
AccessToken accessToken = securityContext.getToken();
|
AccessToken accessToken = securityContext.getToken();
|
||||||
AuthorizationRequest authzRequest;
|
AuthorizationRequest authzRequest = new AuthorizationRequest();
|
||||||
|
|
||||||
if (getEnforcerConfig().getUserManagedAccess() != null) {
|
if (getEnforcerConfig().getUserManagedAccess() != null) {
|
||||||
PermissionResponse permissionResponse = authzClient.protection().permission().create(permissionRequest);
|
String ticket = getPermissionTicket(pathConfig, methodConfig, getAuthzClient(), httpFacade);
|
||||||
authzRequest = new AuthorizationRequest();
|
authzRequest.setTicket(ticket);
|
||||||
authzRequest.setTicket(permissionResponse.getTicket());
|
|
||||||
} else {
|
} else {
|
||||||
authzRequest = new AuthorizationRequest();
|
|
||||||
if (accessToken.getAuthorization() != null) {
|
if (accessToken.getAuthorization() != null) {
|
||||||
authzRequest.addPermission(pathConfig.getId(), methodConfig.getScopes());
|
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) {
|
if (accessToken.getAuthorization() != null) {
|
||||||
|
@ -135,18 +186,70 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGGER.debug("Obtaining authorization for authenticated user.");
|
LOGGER.debug("Obtaining authorization for authenticated user.");
|
||||||
AuthorizationResponse authzResponse = authzClient.authorization(accessTokenString).authorize(authzRequest);
|
AuthorizationResponse authzResponse = getAuthzClient().authorization(accessTokenString).authorize(authzRequest);
|
||||||
|
|
||||||
if (authzResponse != null) {
|
if (authzResponse != null) {
|
||||||
return AdapterRSATokenVerifier.verifyToken(authzResponse.getToken(), deployment);
|
return AdapterRSATokenVerifier.verifyToken(authzResponse.getToken(), deployment);
|
||||||
}
|
}
|
||||||
|
} catch (AuthorizationDeniedException ignore) {
|
||||||
return null;
|
LOGGER.debug("Authorization denied", ignore);
|
||||||
} catch (AuthorizationDeniedException e) {
|
|
||||||
LOGGER.debug("Authorization denied", e);
|
|
||||||
return null;
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Unexpected error during authorization request.", 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,10 +21,12 @@ import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
import java.util.ServiceLoader;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.AuthorizationContext;
|
import org.keycloak.AuthorizationContext;
|
||||||
|
@ -55,6 +57,7 @@ public class PolicyEnforcer {
|
||||||
private final PolicyEnforcerConfig enforcerConfig;
|
private final PolicyEnforcerConfig enforcerConfig;
|
||||||
private final PathConfigMatcher pathMatcher;
|
private final PathConfigMatcher pathMatcher;
|
||||||
private final Map<String, PathConfig> paths;
|
private final Map<String, PathConfig> paths;
|
||||||
|
private final Map<String, ClaimInformationPointProviderFactory> claimInformationPointProviderFactories = new HashMap<>();
|
||||||
|
|
||||||
public PolicyEnforcer(KeycloakDeployment deployment, AdapterConfig adapterConfig) {
|
public PolicyEnforcer(KeycloakDeployment deployment, AdapterConfig adapterConfig) {
|
||||||
this.deployment = deployment;
|
this.deployment = deployment;
|
||||||
|
@ -80,20 +83,17 @@ public class PolicyEnforcer {
|
||||||
LOGGER.debug(pathConfig);
|
LOGGER.debug(pathConfig);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadClaimInformationPointProviders(ServiceLoader.load(ClaimInformationPointProviderFactory.class, ClaimInformationPointProviderFactory.class.getClassLoader()));
|
||||||
|
loadClaimInformationPointProviders(ServiceLoader.load(ClaimInformationPointProviderFactory.class, Thread.currentThread().getContextClassLoader()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public AuthorizationContext enforce(OIDCHttpFacade facade) {
|
public AuthorizationContext enforce(OIDCHttpFacade facade) {
|
||||||
if (LOGGER.isDebugEnabled()) {
|
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;
|
AuthorizationContext context = new KeycloakAdapterPolicyEnforcer(this).authorize(facade);
|
||||||
|
|
||||||
if (deployment.isBearerOnly()) {
|
|
||||||
context = new BearerTokenPolicyEnforcer(this).authorize(facade);
|
|
||||||
} else {
|
|
||||||
context = new KeycloakAdapterPolicyEnforcer(this).authorize(facade);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (LOGGER.isDebugEnabled()) {
|
if (LOGGER.isDebugEnabled()) {
|
||||||
LOGGER.debugv("Policy enforcement result for path [{0}] is : {1}", facade.getRequest().getURI(), context.isGranted() ? "GRANTED" : "DENIED");
|
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;
|
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) {
|
private Map<String, PathConfig> configurePaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
|
||||||
boolean loadPathsFromServer = true;
|
boolean loadPathsFromServer = true;
|
||||||
|
|
||||||
|
@ -164,6 +180,10 @@ public class PolicyEnforcer {
|
||||||
LOGGER.debugf("Trying to find resource with uri [%s] for path [%s].", path, path);
|
LOGGER.debugf("Trying to find resource with uri [%s] for path [%s].", path, path);
|
||||||
List<ResourceRepresentation> resources = protectedResource.findByUri(path);
|
List<ResourceRepresentation> resources = protectedResource.findByUri(path);
|
||||||
|
|
||||||
|
if (resources.isEmpty()) {
|
||||||
|
resources = protectedResource.findByMatchingUri(path);
|
||||||
|
}
|
||||||
|
|
||||||
if (resources.size() == 1) {
|
if (resources.size() == 1) {
|
||||||
resource = resources.get(0);
|
resource = resources.get(0);
|
||||||
} else if (resources.size() > 1) {
|
} else if (resources.size() > 1) {
|
||||||
|
@ -173,16 +193,14 @@ public class PolicyEnforcer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resource == null) {
|
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.");
|
pathConfig.setId(resource.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
pathConfig.setId(resource.getId());
|
|
||||||
|
|
||||||
PathConfig existingPath = null;
|
PathConfig existingPath = null;
|
||||||
|
|
||||||
for (PathConfig current : paths.values()) {
|
for (PathConfig current : paths.values()) {
|
||||||
if (current.getId().equals(pathConfig.getId()) && current.getPath().equals(pathConfig.getPath())) {
|
if (current.getPath().equals(pathConfig.getPath())) {
|
||||||
existingPath = current;
|
existingPath = current;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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 + "]"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
||||||
|
}
|
|
@ -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("]", "");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -184,7 +184,7 @@ class ElytronHttpFacade implements OIDCHttpFacade {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getFirstParam(String param) {
|
public String getFirstParam(String param) {
|
||||||
throw new RuntimeException("Not implemented.");
|
return request.getFirstParameterValue(param);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -88,6 +88,9 @@ public class AccessToken extends IDToken {
|
||||||
@JsonProperty("permissions")
|
@JsonProperty("permissions")
|
||||||
private List<Permission> permissions;
|
private List<Permission> permissions;
|
||||||
|
|
||||||
|
@JsonProperty("claims")
|
||||||
|
private Map<String, List<String>> claims;
|
||||||
|
|
||||||
public List<Permission> getPermissions() {
|
public List<Permission> getPermissions() {
|
||||||
return permissions;
|
return permissions;
|
||||||
}
|
}
|
||||||
|
@ -95,6 +98,14 @@ public class AccessToken extends IDToken {
|
||||||
public void setPermissions(List<Permission> permissions) {
|
public void setPermissions(List<Permission> permissions) {
|
||||||
this.permissions = 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")
|
@JsonProperty("trusted-certs")
|
||||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.representations.adapters.config;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
@ -139,6 +140,9 @@ public class PolicyEnforcerConfig {
|
||||||
@JsonProperty("enforcement-mode")
|
@JsonProperty("enforcement-mode")
|
||||||
private EnforcementMode enforcementMode = EnforcementMode.ENFORCING;
|
private EnforcementMode enforcementMode = EnforcementMode.ENFORCING;
|
||||||
|
|
||||||
|
@JsonProperty("claim-information-point")
|
||||||
|
private Map<String, Map<String, Object>> claimInformationPointConfig;
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
private PathConfig parentConfig;
|
private PathConfig parentConfig;
|
||||||
|
|
||||||
|
@ -198,6 +202,14 @@ public class PolicyEnforcerConfig {
|
||||||
this.enforcementMode = enforcementMode;
|
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
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "PathConfig{" +
|
return "PathConfig{" +
|
||||||
|
|
|
@ -41,7 +41,7 @@ public class AuthorizationRequest {
|
||||||
private String audience;
|
private String audience;
|
||||||
private String accessToken;
|
private String accessToken;
|
||||||
private boolean submitRequest;
|
private boolean submitRequest;
|
||||||
private Map<String, Object> claims;
|
private Map<String, List<String>> claims;
|
||||||
|
|
||||||
public AuthorizationRequest(String ticket) {
|
public AuthorizationRequest(String ticket) {
|
||||||
this.ticket = ticket;
|
this.ticket = ticket;
|
||||||
|
@ -131,11 +131,11 @@ public class AuthorizationRequest {
|
||||||
return accessToken;
|
return accessToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, Object> getClaims() {
|
public Map<String, List<String>> getClaims() {
|
||||||
return claims;
|
return claims;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setClaims(Map<String, Object> claims) {
|
public void setClaims(Map<String, List<String>> claims) {
|
||||||
this.claims = claims;
|
this.claims = claims;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,13 +16,14 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.representations.idm.authorization;
|
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.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
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>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
*/
|
*/
|
||||||
|
@ -71,6 +72,21 @@ public class Permission {
|
||||||
return claims;
|
return claims;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
|
|
|
@ -69,6 +69,10 @@ public class PermissionTicketToken extends JsonWebToken {
|
||||||
return claims;
|
return claims;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setClaims(Map<String, List<String>> claims) {
|
||||||
|
this.claims = claims;
|
||||||
|
}
|
||||||
|
|
||||||
public static class ResourcePermission {
|
public static class ResourcePermission {
|
||||||
|
|
||||||
@JsonProperty("id")
|
@JsonProperty("id")
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.authorization.authorization;
|
package org.keycloak.authorization.authorization;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -54,6 +55,7 @@ import org.keycloak.authorization.store.ScopeStore;
|
||||||
import org.keycloak.authorization.store.StoreFactory;
|
import org.keycloak.authorization.store.StoreFactory;
|
||||||
import org.keycloak.authorization.util.Permissions;
|
import org.keycloak.authorization.util.Permissions;
|
||||||
import org.keycloak.authorization.util.Tokens;
|
import org.keycloak.authorization.util.Tokens;
|
||||||
|
import org.keycloak.common.util.Base64Url;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.jose.jws.JWSInput;
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
import org.keycloak.jose.jws.JWSInputException;
|
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.CorsErrorResponseException;
|
||||||
import org.keycloak.services.ErrorResponseException;
|
import org.keycloak.services.ErrorResponseException;
|
||||||
import org.keycloak.services.resources.Cors;
|
import org.keycloak.services.resources.Cors;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
*/
|
*/
|
||||||
public class AuthorizationTokenService {
|
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 final Logger logger = Logger.getLogger(AuthorizationTokenService.class);
|
||||||
private static Map<String, BiFunction<AuthorizationRequest, AuthorizationProvider, KeycloakEvaluationContext>> SUPPORTED_CLAIM_TOKEN_FORMATS;
|
private static Map<String, BiFunction<AuthorizationRequest, AuthorizationProvider, KeycloakEvaluationContext>> SUPPORTED_CLAIM_TOKEN_FORMATS;
|
||||||
|
|
||||||
|
@ -91,16 +96,29 @@ public class AuthorizationTokenService {
|
||||||
SUPPORTED_CLAIM_TOKEN_FORMATS.put("urn:ietf:params:oauth:token-type:jwt", (authorizationRequest, authorization) -> {
|
SUPPORTED_CLAIM_TOKEN_FORMATS.put("urn:ietf:params:oauth:token-type:jwt", (authorizationRequest, authorization) -> {
|
||||||
String claimToken = authorizationRequest.getClaimToken();
|
String claimToken = authorizationRequest.getClaimToken();
|
||||||
|
|
||||||
if (claimToken == null) {
|
if (claimToken != null) {
|
||||||
claimToken = authorizationRequest.getAccessToken();
|
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())), authorizationRequest.getClaims(), 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 {
|
try {
|
||||||
KeycloakSession keycloakSession = authorization.getKeycloakSession();
|
KeycloakSession keycloakSession = authorization.getKeycloakSession();
|
||||||
IDToken idToken = new TokenManager().verifyIDTokenSignature(keycloakSession, authorization.getRealm(), authorizationRequest.getClaimToken());
|
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);
|
return new KeycloakEvaluationContext(new KeycloakIdentity(keycloakSession, idToken), authorizationRequest.getClaims(), keycloakSession);
|
||||||
} catch (OAuthErrorException cause) {
|
} catch (OAuthErrorException cause) {
|
||||||
throw new RuntimeException("Failed to verify ID token", cause);
|
throw new RuntimeException("Failed to verify ID token", cause);
|
||||||
|
@ -127,10 +145,15 @@ public class AuthorizationTokenService {
|
||||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Invalid authorization request.", Status.BAD_REQUEST);
|
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 {
|
try {
|
||||||
PermissionTicketToken ticket = getPermissionTicket(request);
|
PermissionTicketToken ticket = getPermissionTicket(request);
|
||||||
|
|
||||||
request.setClaims(ticket.getOtherClaims());
|
request.setClaims(ticket.getClaims());
|
||||||
|
|
||||||
ResourceServer resourceServer = getResourceServer(ticket);
|
ResourceServer resourceServer = getResourceServer(ticket);
|
||||||
KeycloakEvaluationContext evaluationContext = createEvaluationContext(request);
|
KeycloakEvaluationContext evaluationContext = createEvaluationContext(request);
|
||||||
|
@ -156,7 +179,7 @@ public class AuthorizationTokenService {
|
||||||
}
|
}
|
||||||
|
|
||||||
ClientModel targetClient = this.authorization.getRealm().getClientById(resourceServer.getId());
|
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))
|
return Cors.add(httpRequest, Response.status(Status.OK).type(MediaType.APPLICATION_JSON_TYPE).entity(response))
|
||||||
.allowedOrigins(getKeycloakSession().getContext().getUri(), targetClient)
|
.allowedOrigins(getKeycloakSession().getContext().getUri(), targetClient)
|
||||||
|
@ -173,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) {
|
private List<Result> evaluatePermissions(AuthorizationRequest authorizationRequest, PermissionTicketToken ticket, ResourceServer resourceServer, KeycloakEvaluationContext evaluationContext, KeycloakIdentity identity) {
|
||||||
return authorization.evaluators()
|
return authorization.evaluators()
|
||||||
.from(createPermissions(ticket, authorizationRequest, resourceServer, identity, authorization), evaluationContext)
|
.from(createPermissions(ticket, authorizationRequest, resourceServer, identity, authorization), evaluationContext)
|
||||||
|
@ -191,7 +218,7 @@ public class AuthorizationTokenService {
|
||||||
.evaluate();
|
.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();
|
KeycloakSession keycloakSession = getKeycloakSession();
|
||||||
AccessToken accessToken = identity.getAccessToken();
|
AccessToken accessToken = identity.getAccessToken();
|
||||||
UserSessionModel userSessionModel = keycloakSession.sessions().getUserSession(getRealm(), accessToken.getSessionState());
|
UserSessionModel userSessionModel = keycloakSession.sessions().getUserSession(getRealm(), accessToken.getSessionState());
|
||||||
|
@ -208,6 +235,7 @@ public class AuthorizationTokenService {
|
||||||
Authorization authorization = new Authorization();
|
Authorization authorization = new Authorization();
|
||||||
|
|
||||||
authorization.setPermissions(entitlements);
|
authorization.setPermissions(entitlements);
|
||||||
|
authorization.setClaims(request.getClaims());
|
||||||
|
|
||||||
rpt.setAuthorization(authorization);
|
rpt.setAuthorization(authorization);
|
||||||
|
|
||||||
|
@ -267,7 +295,7 @@ public class AuthorizationTokenService {
|
||||||
String claimTokenFormat = authorizationRequest.getClaimTokenFormat();
|
String claimTokenFormat = authorizationRequest.getClaimTokenFormat();
|
||||||
|
|
||||||
if (claimTokenFormat == null) {
|
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);
|
BiFunction<AuthorizationRequest, AuthorizationProvider, KeycloakEvaluationContext> evaluationContextProvider = SUPPORTED_CLAIM_TOKEN_FORMATS.get(claimTokenFormat);
|
||||||
|
|
|
@ -24,7 +24,6 @@ import org.keycloak.authorization.policy.evaluation.EvaluationContext;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
@ -40,13 +39,13 @@ public class DefaultEvaluationContext implements EvaluationContext {
|
||||||
|
|
||||||
protected final KeycloakSession keycloakSession;
|
protected final KeycloakSession keycloakSession;
|
||||||
protected final Identity identity;
|
protected final Identity identity;
|
||||||
private final Map<String, Object> claims;
|
private final Map<String, List<String>> claims;
|
||||||
|
|
||||||
public DefaultEvaluationContext(Identity identity, KeycloakSession keycloakSession) {
|
public DefaultEvaluationContext(Identity identity, KeycloakSession keycloakSession) {
|
||||||
this(identity, null, keycloakSession);
|
this(identity, null, keycloakSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DefaultEvaluationContext(Identity identity, Map<String, Object> claims, KeycloakSession keycloakSession) {
|
public DefaultEvaluationContext(Identity identity, Map<String, List<String>> claims, KeycloakSession keycloakSession) {
|
||||||
this.identity = identity;
|
this.identity = identity;
|
||||||
this.claims = claims;
|
this.claims = claims;
|
||||||
this.keycloakSession = keycloakSession;
|
this.keycloakSession = keycloakSession;
|
||||||
|
@ -73,16 +72,8 @@ public class DefaultEvaluationContext implements EvaluationContext {
|
||||||
attributes.put("kc.realm.name", Arrays.asList(this.keycloakSession.getContext().getRealm().getName()));
|
attributes.put("kc.realm.name", Arrays.asList(this.keycloakSession.getContext().getRealm().getName()));
|
||||||
|
|
||||||
if (claims != null) {
|
if (claims != null) {
|
||||||
for (Entry<String, Object> entry : claims.entrySet()) {
|
for (Entry<String, List<String>> entry : claims.entrySet()) {
|
||||||
Object value = entry.getValue();
|
attributes.put(entry.getKey(), entry.getValue());
|
||||||
|
|
||||||
if (value.getClass().isArray()) {
|
|
||||||
attributes.put(entry.getKey(), Arrays.asList(String[].class.cast(value)));
|
|
||||||
} else if (value instanceof Collection) {
|
|
||||||
attributes.put(entry.getKey(), Collection.class.cast(value));
|
|
||||||
} else {
|
|
||||||
attributes.put(entry.getKey(), Arrays.asList(String.valueOf(value)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ package org.keycloak.authorization.common;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.keycloak.authorization.identity.Identity;
|
import org.keycloak.authorization.identity.Identity;
|
||||||
|
@ -37,7 +38,7 @@ public class KeycloakEvaluationContext extends DefaultEvaluationContext {
|
||||||
this(identity, null, keycloakSession);
|
this(identity, null, keycloakSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
public KeycloakEvaluationContext(KeycloakIdentity identity, Map<String, Object> claims, KeycloakSession keycloakSession) {
|
public KeycloakEvaluationContext(KeycloakIdentity identity, Map<String, List<String>> claims, KeycloakSession keycloakSession) {
|
||||||
super(identity, claims, keycloakSession);
|
super(identity, claims, keycloakSession);
|
||||||
this.identity = identity;
|
this.identity = identity;
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,7 +112,6 @@ public class KeycloakIdentity implements Identity {
|
||||||
if (token instanceof AccessToken) {
|
if (token instanceof AccessToken) {
|
||||||
this.accessToken = AccessToken.class.cast(token);
|
this.accessToken = AccessToken.class.cast(token);
|
||||||
} else {
|
} else {
|
||||||
UserModel userById = keycloakSession.users().getUserById(token.getSubject(), realm);
|
|
||||||
UserSessionModel userSession = keycloakSession.sessions().getUserSession(realm, token.getSessionState());
|
UserSessionModel userSession = keycloakSession.sessions().getUserSession(realm, token.getSessionState());
|
||||||
ClientModel client = realm.getClientByClientId(token.getIssuedFor());
|
ClientModel client = realm.getClientByClientId(token.getIssuedFor());
|
||||||
AuthenticatedClientSessionModel clientSessionModel = userSession.getAuthenticatedClientSessions().get(client.getId());
|
AuthenticatedClientSessionModel clientSessionModel = userSession.getAuthenticatedClientSessions().get(client.getId());
|
||||||
|
@ -123,7 +122,7 @@ public class KeycloakIdentity implements Identity {
|
||||||
requestedRoles.add(role);
|
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();
|
AccessToken.Access realmAccess = this.accessToken.getRealmAccess();
|
||||||
|
|
|
@ -18,9 +18,9 @@ package org.keycloak.authorization.protection.permission;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@ -152,17 +152,20 @@ public class AbstractPermissionService {
|
||||||
KeyManager.ActiveRsaKey keys = this.authorization.getKeycloakSession().keys().getActiveRsaKey(this.authorization.getRealm());
|
KeyManager.ActiveRsaKey keys = this.authorization.getKeycloakSession().keys().getActiveRsaKey(this.authorization.getRealm());
|
||||||
ClientModel targetClient = authorization.getRealm().getClientById(resourceServer.getId());
|
ClientModel targetClient = authorization.getRealm().getClientById(resourceServer.getId());
|
||||||
PermissionTicketToken token = new PermissionTicketToken(permissions, targetClient.getClientId(), this.identity.getAccessToken());
|
PermissionTicketToken token = new PermissionTicketToken(permissions, targetClient.getClientId(), this.identity.getAccessToken());
|
||||||
|
Map<String, List<String>> claims = new HashMap<>();
|
||||||
|
|
||||||
for (PermissionRequest permissionRequest : request) {
|
for (PermissionRequest permissionRequest : request) {
|
||||||
Map<String, List<String>> claims = permissionRequest.getClaims();
|
Map<String, List<String>> requestClaims = permissionRequest.getClaims();
|
||||||
|
|
||||||
if (claims != null) {
|
if (requestClaims != null) {
|
||||||
for (Entry<String, List<String>> claim : claims.entrySet()) {
|
claims.putAll(requestClaims);
|
||||||
token.setOtherClaims(claim.getKey(), claim.getValue());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!claims.isEmpty()) {
|
||||||
|
token.setClaims(claims);
|
||||||
|
}
|
||||||
|
|
||||||
return new JWSBuilder().kid(keys.getKid()).jsonContent(token)
|
return new JWSBuilder().kid(keys.getKid()).jsonContent(token)
|
||||||
.rsa256(keys.getPrivateKey());
|
.rsa256(keys.getPrivateKey());
|
||||||
}
|
}
|
||||||
|
|
|
@ -997,6 +997,8 @@ public class TokenEndpoint {
|
||||||
accessTokenString = new AppAuthManager().extractAuthorizationHeaderToken(headers);
|
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) {
|
if (accessTokenString != null) {
|
||||||
AccessToken accessToken = Tokens.getAccessToken(session);
|
AccessToken accessToken = Tokens.getAccessToken(session);
|
||||||
|
|
||||||
|
@ -1004,7 +1006,11 @@ public class TokenEndpoint {
|
||||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Invalid bearer token", Status.UNAUTHORIZED);
|
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;
|
String claimToken = null;
|
||||||
|
@ -1014,18 +1020,30 @@ public class TokenEndpoint {
|
||||||
claimToken = formParams.get("claim_token").get(0);
|
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) {
|
if (accessTokenString == null) {
|
||||||
// in case no bearer token is provided, we force client authentication
|
// in case no bearer token is provided, we force client authentication
|
||||||
checkClient();
|
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
|
// 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
|
||||||
accessTokenString = AccessTokenResponse.class.cast(clientCredentialsGrant().getEntity()).getToken();
|
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 authorizationRequest = new AuthorizationRequest(formParams.getFirst("ticket"));
|
||||||
|
|
||||||
authorizationRequest.setClaimToken(claimToken);
|
authorizationRequest.setClaimToken(claimToken);
|
||||||
authorizationRequest.setClaimTokenFormat(formParams.getFirst("claim_token_format"));
|
authorizationRequest.setClaimTokenFormat(claimTokenFormat);
|
||||||
authorizationRequest.setPct(formParams.getFirst("pct"));
|
authorizationRequest.setPct(formParams.getFirst("pct"));
|
||||||
authorizationRequest.setRpt(formParams.getFirst("rpt"));
|
authorizationRequest.setRpt(formParams.getFirst("rpt"));
|
||||||
authorizationRequest.setScope(formParams.getFirst("scope"));
|
authorizationRequest.setScope(formParams.getFirst("scope"));
|
||||||
|
|
|
@ -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']}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -56,6 +56,10 @@
|
||||||
"name": "write"
|
"name": "write"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Resource Protected With Claim",
|
||||||
|
"uri": "/protected/context/context.jsp"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"policies": [
|
"policies": [
|
||||||
|
@ -183,6 +187,26 @@
|
||||||
"scopes": "[\"write\"]",
|
"scopes": "[\"write\"]",
|
||||||
"applyPolicies": "[\"Deny Policy\"]"
|
"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}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -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>
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ import org.keycloak.adapters.KeycloakDeployment;
|
||||||
import org.keycloak.adapters.KeycloakDeploymentBuilder;
|
import org.keycloak.adapters.KeycloakDeploymentBuilder;
|
||||||
import org.keycloak.adapters.authorization.PolicyEnforcer;
|
import org.keycloak.adapters.authorization.PolicyEnforcer;
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
|
||||||
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.testsuite.AbstractKeycloakTest;
|
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||||
import org.keycloak.testsuite.ProfileAssume;
|
import org.keycloak.testsuite.ProfileAssume;
|
||||||
|
@ -30,6 +31,8 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
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;
|
import static org.keycloak.testsuite.util.IOUtil.loadRealm;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -47,11 +50,34 @@ public class EnforcerConfigTest extends AbstractKeycloakTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testMultiplePathsWithSameName() throws Exception{
|
public void testMultiplePathsWithSameName() {
|
||||||
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getClass().getResourceAsStream("/authorization-test/enforcer-config-paths-same-name.json"));
|
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getClass().getResourceAsStream("/authorization-test/enforcer-config-paths-same-name.json"));
|
||||||
PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer();
|
PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer();
|
||||||
Map<String, PolicyEnforcerConfig.PathConfig> paths = policyEnforcer.getPaths();
|
Map<String, PolicyEnforcerConfig.PathConfig> paths = policyEnforcer.getPaths();
|
||||||
assertEquals(1, paths.size());
|
assertEquals(1, paths.size());
|
||||||
assertEquals(4, paths.values().iterator().next().getMethods().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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,19 +20,25 @@ import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.admin.client.resource.AuthorizationResource;
|
import org.keycloak.admin.client.resource.AuthorizationResource;
|
||||||
import org.keycloak.admin.client.resource.ClientResource;
|
import org.keycloak.admin.client.resource.ClientResource;
|
||||||
import org.keycloak.admin.client.resource.ClientsResource;
|
import org.keycloak.admin.client.resource.ClientsResource;
|
||||||
import org.keycloak.admin.client.resource.RealmResource;
|
import org.keycloak.admin.client.resource.RealmResource;
|
||||||
|
import org.keycloak.authorization.client.AuthorizationDeniedException;
|
||||||
import org.keycloak.authorization.client.AuthzClient;
|
import org.keycloak.authorization.client.AuthzClient;
|
||||||
import org.keycloak.authorization.client.Configuration;
|
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.AccessToken;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.representations.idm.authorization.AuthorizationRequest;
|
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_RESOURCE_SERVER_TEST = "pairwise-resource-server-test";
|
||||||
private static final String PAIRWISE_TEST_CLIENT = "test-client-pairwise";
|
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 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;
|
private AuthzClient authzClient;
|
||||||
|
|
||||||
|
@ -94,6 +102,10 @@ public class EntitlementAPITest extends AbstractAuthzTest {
|
||||||
.redirectUris("http://localhost/test-client")
|
.redirectUris("http://localhost/test-client")
|
||||||
.pairwise("http://pairwise.com")
|
.pairwise("http://pairwise.com")
|
||||||
.directAccessGrants())
|
.directAccessGrants())
|
||||||
|
.client(ClientBuilder.create().clientId(PUBLIC_TEST_CLIENT)
|
||||||
|
.secret("secret")
|
||||||
|
.redirectUris("http://localhost:8180/auth/realms/master/app/auth/*")
|
||||||
|
.publicClient())
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,6 +176,65 @@ public class EntitlementAPITest extends AbstractAuthzTest {
|
||||||
testRptRequestWithResourceName(PAIRWISE_AUTHZ_CLIENT_CONFIG);
|
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) {
|
public void testRptRequestWithResourceName(String configFile) {
|
||||||
Metadata metadata = new Metadata();
|
Metadata metadata = new Metadata();
|
||||||
|
|
|
@ -219,7 +219,7 @@ public class PermissionManagementTest extends AbstractResourceServerTest {
|
||||||
try {
|
try {
|
||||||
authzClient.authorization().authorize(request);
|
authzClient.authorization().authorize(request);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
List permissions = authzClient.protection().permission().findByResource(resource.getId());
|
List permissions = authzClient.protection().permission().findByResource(resource.getId());
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"realm": "authz-test",
|
||||||
|
"auth-server-url" : "http://localhost:8180/auth",
|
||||||
|
"resource" : "test-public-client"
|
||||||
|
}
|
|
@ -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']}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -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']}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -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']}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -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']}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue