Decouple the policy enforcer from adapters and provide a separate library

Closes keycloak#17353
This commit is contained in:
Pedro Igor 2023-03-07 13:59:19 -03:00 committed by Michal Hajas
parent 6e58f5a5d5
commit a30b6842a6
71 changed files with 1861 additions and 1387 deletions

View file

@ -82,6 +82,11 @@
<artifactId>keycloak-authz-client</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-policy-enforcer</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>

View file

@ -20,7 +20,6 @@ package org.keycloak.adapters;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.jboss.logging.Logger;
import org.keycloak.adapters.authentication.ClientCredentialsProvider;
import org.keycloak.adapters.authorization.PolicyEnforcer;
import org.keycloak.adapters.rotation.PublicKeyLocator;
import org.keycloak.adapters.spi.HttpFacade;
@ -28,6 +27,7 @@ import org.keycloak.common.enums.RelativeUrlsUsed;
import org.keycloak.common.enums.SslRequired;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.enums.TokenStore;
import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider;
import org.keycloak.representations.adapters.config.AdapterConfig;
import java.io.IOException;

View file

@ -17,11 +17,18 @@
package org.keycloak.adapters;
import org.apache.http.NameValuePair;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair;
import org.jboss.logging.Logger;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProviderUtils;
import org.keycloak.representations.AccessToken;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
@ -89,4 +96,21 @@ public class AdapterUtils {
public static KeycloakPrincipal<RefreshableKeycloakSecurityContext> createPrincipal(KeycloakDeployment deployment, RefreshableKeycloakSecurityContext securityContext) {
return new KeycloakPrincipal<>(getPrincipalName(deployment, securityContext.getToken()), securityContext);
}
/**
* Don't use directly from your JEE apps to avoid HttpClient linkage errors! Instead use the method {@link #setClientCredentials(KeycloakDeployment, Map, Map)}
*/
public static void setClientCredentials(KeycloakDeployment deployment, HttpPost post, List<NameValuePair> formparams) {
Map<String, String> reqHeaders = new HashMap<>();
Map<String, String> reqParams = new HashMap<>();
ClientCredentialsProviderUtils.setClientCredentials(deployment.getAdapterConfig(), deployment.getClientAuthenticator(), reqHeaders, reqParams);
for (Map.Entry<String, String> header : reqHeaders.entrySet()) {
post.setHeader(header.getKey(), header.getValue());
}
for (Map.Entry<String, String> param : reqParams.entrySet()) {
formparams.add(new BasicNameValuePair(param.getKey(), param.getValue()));
}
}
}

View file

@ -20,6 +20,8 @@ package org.keycloak.adapters;
import org.jboss.logging.Logger;
import org.keycloak.AuthorizationContext;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.pep.HttpAuthzRequest;
import org.keycloak.adapters.pep.HttpAuthzResponse;
import org.keycloak.adapters.authorization.PolicyEnforcer;
import org.keycloak.common.util.UriUtils;
import org.keycloak.constants.AdapterConstants;
@ -155,7 +157,7 @@ public class AuthenticatedActionsHandler {
}
try {
OIDCHttpFacade facade = (OIDCHttpFacade) this.facade;
AuthorizationContext authorizationContext = policyEnforcer.enforce(facade);
AuthorizationContext authorizationContext = policyEnforcer.enforce(new HttpAuthzRequest(facade), new HttpAuthzResponse(facade));
RefreshableKeycloakSecurityContext session = (RefreshableKeycloakSecurityContext) facade.getSecurityContext();
if (session != null) {

View file

@ -27,12 +27,9 @@ import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
import org.keycloak.adapters.spi.AuthOutcome;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.common.util.Base64;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.constants.ServiceUrlConstants;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.util.JsonSerialization;
@ -97,7 +94,7 @@ public class BasicAuthRequestAuthenticator extends BearerTokenRequestAuthenticat
formparams.add(new BasicNameValuePair("username", username));
formparams.add(new BasicNameValuePair("password", password));
ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams);
AdapterUtils.setClientCredentials(deployment, post, formparams);
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form);

View file

@ -18,6 +18,8 @@
package org.keycloak.adapters;
import org.jboss.logging.Logger;
import org.keycloak.adapters.pep.HttpAuthzRequest;
import org.keycloak.adapters.pep.HttpAuthzResponse;
import org.keycloak.adapters.rotation.AdapterTokenVerifier;
import org.keycloak.adapters.spi.AuthChallenge;
import org.keycloak.adapters.spi.AuthOutcome;
@ -180,8 +182,9 @@ public class BearerTokenRequestAuthenticator {
@Override
public boolean challenge(HttpFacade facade) {
OIDCHttpFacade oidcFacade = (OIDCHttpFacade) facade;
if (deployment.getPolicyEnforcer() != null) {
deployment.getPolicyEnforcer().enforce(OIDCHttpFacade.class.cast(facade));
deployment.getPolicyEnforcer().enforce(new HttpAuthzRequest(oidcFacade), new HttpAuthzResponse(oidcFacade));
return true;
}
OIDCAuthenticationError error = new OIDCAuthenticationError(reason, description);

View file

@ -22,7 +22,6 @@ import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.util.EntityUtils;
import org.jboss.logging.Logger;
import org.keycloak.adapters.authentication.ClientCredentialsProvider;
import org.keycloak.adapters.authorization.PolicyEnforcer;
import org.keycloak.adapters.rotation.PublicKeyLocator;
import org.keycloak.common.enums.RelativeUrlsUsed;
@ -30,6 +29,7 @@ import org.keycloak.common.enums.SslRequired;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.constants.ServiceUrlConstants;
import org.keycloak.enums.TokenStore;
import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider;
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.util.JsonSerialization;
@ -104,6 +104,7 @@ public class KeycloakDeployment {
protected boolean delegateBearerErrorResponseSending = false;
protected boolean verifyTokenAudience = false;
private AdapterConfig adapterConfig;
public KeycloakDeployment() {
}
@ -159,6 +160,8 @@ public class KeycloakDeployment {
// We have absolute URI in config
relativeUrls = RelativeUrlsUsed.NEVER;
}
this.adapterConfig = config;
}
/**
@ -598,4 +601,8 @@ public class KeycloakDeployment {
public void setClient(Callable<HttpClient> callable) {
client = callable;
}
public AdapterConfig getAdapterConfig() {
return adapterConfig;
}
}

View file

@ -17,11 +17,12 @@
package org.keycloak.adapters;
import static org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProviderUtils.bootstrapClientAuthenticator;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.client.HttpClient;
import org.jboss.logging.Logger;
import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
import org.keycloak.adapters.authorization.PolicyEnforcer;
import org.keycloak.adapters.rotation.HardcodedPublicKeyLocator;
import org.keycloak.adapters.rotation.JWKPublicKeyLocator;
@ -97,7 +98,7 @@ public class KeycloakDeploymentBuilder {
if (adapterConfig.getPrincipalAttribute() != null) deployment.setPrincipalAttribute(adapterConfig.getPrincipalAttribute());
deployment.setResourceCredentials(adapterConfig.getCredentials());
deployment.setClientAuthenticator(ClientCredentialsProviderUtils.bootstrapClientAuthenticator(deployment));
deployment.setClientAuthenticator(bootstrapClientAuthenticator(adapterConfig));
deployment.setPublicClient(adapterConfig.isPublicClient());
deployment.setUseResourceRoleMappings(adapterConfig.isUseResourceRoleMappings());
@ -152,7 +153,14 @@ public class KeycloakDeploymentBuilder {
if (policyEnforcer == null) {
synchronized (deployment) {
if (policyEnforcer == null) {
policyEnforcer = new PolicyEnforcer(deployment, adapterConfig);
policyEnforcer = PolicyEnforcer.builder()
.authServerUrl(adapterConfig.getAuthServerUrl())
.realm(adapterConfig.getRealm())
.clientId(adapterConfig.getResource())
.bearerOnly(adapterConfig.isBearerOnly())
.credentialProvider(deployment.getClientAuthenticator())
.enforcerConfig(policyEnforcerConfig)
.httpClient(deployment.getClient()).build();
}
}
}

View file

@ -21,8 +21,6 @@ import java.security.PublicKey;
import org.jboss.logging.Logger;
import org.keycloak.TokenVerifier;
import org.keycloak.adapters.authentication.ClientCredentialsProvider;
import org.keycloak.adapters.authentication.JWTClientCredentialsProvider;
import org.keycloak.adapters.rotation.AdapterTokenVerifier;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.adapters.spi.UserSessionManagement;
@ -31,6 +29,8 @@ import org.keycloak.common.util.StreamUtil;
import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKBuilder;
import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider;
import org.keycloak.protocol.oidc.client.authentication.JWTClientCredentialsProvider;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.constants.AdapterConstants;
import org.keycloak.jose.jws.JWSInput;

View file

@ -25,7 +25,6 @@ import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair;
import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
import org.keycloak.common.util.HostUtils;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.common.util.StreamUtil;
@ -75,7 +74,7 @@ public class ServerRequest {
formparams.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken));
HttpPost post = new HttpPost(uri);
ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams);
AdapterUtils.setClientCredentials(deployment, post, formparams);
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form);
@ -104,7 +103,7 @@ public class ServerRequest {
}
HttpPost post = new HttpPost(deployment.getTokenUrl());
ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams);
AdapterUtils.setClientCredentials(deployment, post, formparams);
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form);
@ -160,7 +159,7 @@ public class ServerRequest {
}
HttpPost post = new HttpPost(deployment.getTokenUrl());
ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams);
AdapterUtils.setClientCredentials(deployment, post, formparams);
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form);
@ -202,7 +201,7 @@ public class ServerRequest {
formparams.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken));
HttpPost post = new HttpPost(deployment.getTokenUrl());
ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams);
AdapterUtils.setClientCredentials(deployment, post, formparams);
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form);
@ -257,7 +256,7 @@ public class ServerRequest {
formparams.add(new BasicNameValuePair(AdapterConstants.CLIENT_CLUSTER_HOST, host));
HttpPost post = new HttpPost(endpointUrl);
ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams);
AdapterUtils.setClientCredentials(deployment, post, formparams);
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form);

View file

@ -1,374 +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.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.jboss.logging.Logger;
import org.keycloak.AuthorizationContext;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.OIDCHttpFacade;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.adapters.spi.HttpFacade.Request;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.ClientAuthorizationContext;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessToken.Authorization;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.MethodConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.ScopeEnforcementMode;
import org.keycloak.representations.idm.authorization.Permission;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public abstract class AbstractPolicyEnforcer {
private static Logger LOGGER = Logger.getLogger(AbstractPolicyEnforcer.class);
private static final String HTTP_METHOD_DELETE = "DELETE";
private final PolicyEnforcer policyEnforcer;
protected AbstractPolicyEnforcer(PolicyEnforcer policyEnforcer) {
this.policyEnforcer = policyEnforcer;
}
public AuthorizationContext authorize(OIDCHttpFacade httpFacade) {
EnforcementMode enforcementMode = getEnforcerConfig().getEnforcementMode();
KeycloakSecurityContext securityContext = httpFacade.getSecurityContext();
if (EnforcementMode.DISABLED.equals(enforcementMode)) {
if (securityContext == null) {
httpFacade.getResponse().sendError(401, "Invalid bearer");
}
return createEmptyAuthorizationContext(true);
}
Request request = httpFacade.getRequest();
PathConfig pathConfig = getPathConfig(request);
if (securityContext == null) {
if (!isDefaultAccessDeniedUri(request)) {
if (pathConfig != null) {
if (EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
return createEmptyAuthorizationContext(true);
} else {
challenge(pathConfig, getRequiredScopes(pathConfig, request), httpFacade);
}
} else {
handleAccessDenied(httpFacade);
}
}
return createEmptyAuthorizationContext(false);
}
AccessToken accessToken = securityContext.getToken();
if (accessToken != null) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig);
}
if (pathConfig == null) {
if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) {
return createAuthorizationContext(accessToken, null);
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debugf("Could not find a configuration for path [%s]", getPath(request));
}
if (isDefaultAccessDeniedUri(request)) {
return createAuthorizationContext(accessToken, null);
}
handleAccessDenied(httpFacade);
return createEmptyAuthorizationContext(false);
}
if (EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
return createAuthorizationContext(accessToken, pathConfig);
}
MethodConfig methodConfig = getRequiredScopes(pathConfig, request);
Map<String, List<String>> claims = resolveClaims(pathConfig, httpFacade);
if (isAuthorized(pathConfig, methodConfig, accessToken, httpFacade, claims)) {
try {
return createAuthorizationContext(accessToken, pathConfig);
} catch (Exception e) {
throw new RuntimeException("Error processing path [" + pathConfig.getPath() + "].", e);
}
}
if (methodConfig != null && ScopeEnforcementMode.DISABLED.equals(methodConfig.getScopesEnforcementMode())) {
return createEmptyAuthorizationContext(true);
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
}
if (!challenge(pathConfig, methodConfig, httpFacade)) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debugf("Challenge not sent, sending default forbidden response. Path [%s]", pathConfig);
}
handleAccessDenied(httpFacade);
}
}
return createEmptyAuthorizationContext(false);
}
protected abstract boolean challenge(PathConfig pathConfig, MethodConfig methodConfig, OIDCHttpFacade facade);
protected boolean isAuthorized(PathConfig actualPathConfig, MethodConfig methodConfig, AccessToken accessToken, OIDCHttpFacade httpFacade, Map<String, List<String>> claims) {
Request request = httpFacade.getRequest();
if (isDefaultAccessDeniedUri(request)) {
return true;
}
Authorization authorization = accessToken.getAuthorization();
if (authorization == null) {
return false;
}
boolean hasPermission = false;
Collection<Permission> grantedPermissions = authorization.getPermissions();
for (Permission permission : grantedPermissions) {
if (permission.getResourceId() != null) {
if (isResourcePermission(actualPathConfig, permission)) {
hasPermission = true;
if (actualPathConfig.isInstance() && !matchResourcePermission(actualPathConfig, permission)) {
continue;
}
if (hasResourceScopePermission(methodConfig, permission)) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, grantedPermissions);
}
if (HTTP_METHOD_DELETE.equalsIgnoreCase(request.getMethod()) && actualPathConfig.isInstance()) {
policyEnforcer.getPathMatcher().removeFromCache(getPath(request));
}
return hasValidClaims(permission, claims);
}
}
} else {
if (hasResourceScopePermission(methodConfig, permission)) {
hasPermission = true;
return true;
}
}
}
if (!hasPermission && EnforcementMode.PERMISSIVE.equals(actualPathConfig.getEnforcementMode())) {
return true;
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debugf("Authorization FAILED for path [%s]. Not enough permissions [%s].", actualPathConfig, grantedPermissions);
}
return false;
}
private boolean hasValidClaims(Permission permission, Map<String, List<String>> claims) {
Map<String, Set<String>> grantedClaims = permission.getClaims();
if (grantedClaims != null) {
if (claims.isEmpty()) {
return false;
}
for (Entry<String, Set<String>> entry : grantedClaims.entrySet()) {
List<String> requestClaims = claims.get(entry.getKey());
if (requestClaims == null || requestClaims.isEmpty() || !entry.getValue().containsAll(requestClaims)) {
return false;
}
}
}
return true;
}
protected void handleAccessDenied(OIDCHttpFacade httpFacade) {
httpFacade.getResponse().sendError(403);
}
protected AuthzClient getAuthzClient() {
return policyEnforcer.getClient();
}
protected PolicyEnforcerConfig getEnforcerConfig() {
return policyEnforcer.getEnforcerConfig();
}
protected PolicyEnforcer getPolicyEnforcer() {
return policyEnforcer;
}
private boolean isDefaultAccessDeniedUri(Request request) {
String accessDeniedPath = getEnforcerConfig().getOnDenyRedirectTo();
return accessDeniedPath != null && request.getURI().contains(accessDeniedPath);
}
private boolean hasResourceScopePermission(MethodConfig methodConfig, Permission permission) {
List<String> requiredScopes = methodConfig.getScopes();
Set<String> allowedScopes = permission.getScopes();
if (allowedScopes.isEmpty()) {
return true;
}
PolicyEnforcerConfig.ScopeEnforcementMode enforcementMode = methodConfig.getScopesEnforcementMode();
if (PolicyEnforcerConfig.ScopeEnforcementMode.ALL.equals(enforcementMode)) {
return allowedScopes.containsAll(requiredScopes);
}
if (PolicyEnforcerConfig.ScopeEnforcementMode.ANY.equals(enforcementMode)) {
for (String requiredScope : requiredScopes) {
if (allowedScopes.contains(requiredScope)) {
return true;
}
}
}
return requiredScopes.isEmpty();
}
private AuthorizationContext createEmptyAuthorizationContext(final boolean granted) {
return new ClientAuthorizationContext(getAuthzClient()) {
@Override
public boolean hasPermission(String resourceName, String scopeName) {
return granted;
}
@Override
public boolean hasResourcePermission(String resourceName) {
return granted;
}
@Override
public boolean hasScopePermission(String scopeName) {
return granted;
}
@Override
public List<Permission> getPermissions() {
return Collections.EMPTY_LIST;
}
@Override
public boolean isGranted() {
return granted;
}
};
}
private String getPath(Request request) {
return request.getRelativePath();
}
private MethodConfig getRequiredScopes(PathConfig pathConfig, Request request) {
String method = request.getMethod();
for (MethodConfig methodConfig : pathConfig.getMethods()) {
if (methodConfig.getMethod().equals(method)) {
return methodConfig;
}
}
MethodConfig methodConfig = new MethodConfig();
methodConfig.setMethod(request.getMethod());
List scopes = new ArrayList<>();
if (Boolean.TRUE.equals(getEnforcerConfig().getHttpMethodAsScope())) {
scopes.add(request.getMethod());
} else {
scopes.addAll(pathConfig.getScopes());
}
methodConfig.setScopes(scopes);
methodConfig.setScopesEnforcementMode(PolicyEnforcerConfig.ScopeEnforcementMode.ANY);
return methodConfig;
}
private AuthorizationContext createAuthorizationContext(AccessToken accessToken, PathConfig pathConfig) {
return new ClientAuthorizationContext(accessToken, pathConfig, getAuthzClient());
}
private boolean isResourcePermission(PathConfig actualPathConfig, Permission permission) {
// first we try a match using resource id
boolean resourceMatch = matchResourcePermission(actualPathConfig, permission);
// as a fallback, check if the current path is an instance and if so, check if parent's id matches the permission
if (!resourceMatch && actualPathConfig.isInstance()) {
resourceMatch = matchResourcePermission(actualPathConfig.getParentConfig(), permission);
}
return resourceMatch;
}
private boolean matchResourcePermission(PathConfig actualPathConfig, Permission permission) {
return permission.getResourceId().equals(actualPathConfig.getId());
}
private PathConfig getPathConfig(Request request) {
return isDefaultAccessDeniedUri(request) ? null : policyEnforcer.getPathMatcher().matches(getPath(request));
}
protected Map<String, List<String>> resolveClaims(PathConfig pathConfig, OIDCHttpFacade httpFacade) {
Map<String, List<String>> claims = new HashMap<>();
resolveClaims(claims, getEnforcerConfig().getClaimInformationPointConfig(), httpFacade);
resolveClaims(claims, pathConfig.getClaimInformationPointConfig(), httpFacade);
return claims;
}
private void resolveClaims(Map<String, List<String>> claims, Map<String, Map<String, Object>> claimInformationPointConfig, HttpFacade httpFacade) {
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));
}
}
}
}
}

View file

@ -1,219 +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.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import org.jboss.logging.Logger;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.OIDCHttpFacade;
import org.keycloak.adapters.rotation.AdapterTokenVerifier;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.authorization.client.AuthorizationDeniedException;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.resource.PermissionResource;
import org.keycloak.authorization.client.resource.ProtectionResource;
import org.keycloak.authorization.client.util.HttpResponseException;
import org.keycloak.common.util.Base64;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
import org.keycloak.representations.idm.authorization.AuthorizationRequest;
import org.keycloak.representations.idm.authorization.AuthorizationResponse;
import org.keycloak.representations.idm.authorization.Permission;
import org.keycloak.representations.idm.authorization.PermissionRequest;
import org.keycloak.util.JsonSerialization;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
private static Logger LOGGER = Logger.getLogger(KeycloakAdapterPolicyEnforcer.class);
public KeycloakAdapterPolicyEnforcer(PolicyEnforcer policyEnforcer) {
super(policyEnforcer);
}
@Override
protected boolean isAuthorized(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, AccessToken accessToken, OIDCHttpFacade httpFacade, Map<String, List<String>> claims) {
AccessToken original = accessToken;
if (super.isAuthorized(pathConfig, methodConfig, accessToken, httpFacade, claims)) {
return true;
}
accessToken = requestAuthorizationToken(pathConfig, methodConfig, httpFacade, claims);
if (accessToken == null) {
return false;
}
AccessToken.Authorization authorization = original.getAuthorization();
if (authorization == null) {
authorization = new AccessToken.Authorization();
authorization.setPermissions(new ArrayList<Permission>());
}
AccessToken.Authorization newAuthorization = accessToken.getAuthorization();
if (newAuthorization != null) {
Collection<Permission> grantedPermissions = authorization.getPermissions();
Collection<Permission> newPermissions = newAuthorization.getPermissions();
for (Permission newPermission : newPermissions) {
if (!grantedPermissions.contains(newPermission)) {
grantedPermissions.add(newPermission);
}
}
}
original.setAuthorization(authorization);
return super.isAuthorized(pathConfig, methodConfig, accessToken, httpFacade, claims);
}
@Override
protected boolean challenge(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade httpFacade) {
if (isBearerAuthorization(httpFacade)) {
HttpFacade.Response response = httpFacade.getResponse();
AuthzClient authzClient = getAuthzClient();
String ticket = getPermissionTicket(pathConfig, methodConfig, authzClient, httpFacade);
if (ticket != null) {
response.setStatus(401);
response.setHeader("WWW-Authenticate", new StringBuilder("UMA realm=\"").append(authzClient.getConfiguration().getRealm()).append("\"").append(",as_uri=\"")
.append(authzClient.getServerConfiguration().getIssuer()).append("\"").append(",ticket=\"").append(ticket).append("\"").toString());
} else {
response.setStatus(403);
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Sending challenge");
}
return true;
}
handleAccessDenied(httpFacade);
return true;
}
@Override
protected void handleAccessDenied(OIDCHttpFacade facade) {
String accessDeniedPath = getEnforcerConfig().getOnDenyRedirectTo();
HttpFacade.Response response = facade.getResponse();
if (accessDeniedPath != null) {
response.setStatus(302);
response.setHeader("Location", accessDeniedPath);
} else {
response.sendError(403);
}
}
private AccessToken requestAuthorizationToken(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade httpFacade, Map<String, List<String>> claims) {
if (getEnforcerConfig().getUserManagedAccess() != null) {
return null;
}
try {
KeycloakSecurityContext securityContext = httpFacade.getSecurityContext();
String accessTokenString = securityContext.getTokenString();
KeycloakDeployment deployment = getPolicyEnforcer().getDeployment();
AccessToken accessToken = securityContext.getToken();
AuthorizationRequest authzRequest = new AuthorizationRequest();
if (isBearerAuthorization(httpFacade) || accessToken.getAuthorization() != null) {
authzRequest.addPermission(pathConfig.getId(), methodConfig.getScopes());
}
if (!claims.isEmpty()) {
authzRequest.setClaimTokenFormat("urn:ietf:params:oauth:token-type:jwt");
authzRequest.setClaimToken(Base64.encodeBytes(JsonSerialization.writeValueAsBytes(claims)));
}
if (accessToken.getAuthorization() != null) {
authzRequest.setRpt(accessTokenString);
}
LOGGER.debug("Obtaining authorization for authenticated user.");
AuthorizationResponse authzResponse;
if (isBearerAuthorization(httpFacade)) {
authzRequest.setSubjectToken(accessTokenString);
authzResponse = getAuthzClient().authorization().authorize(authzRequest);
} else {
authzResponse = getAuthzClient().authorization(accessTokenString).authorize(authzRequest);
}
if (authzResponse != null) {
return AdapterTokenVerifier.verifyToken(authzResponse.getToken(), deployment);
}
} catch (AuthorizationDeniedException ignore) {
LOGGER.debug("Authorization denied", ignore);
} catch (Exception e) {
LOGGER.debug("Authorization failed", e);
}
return null;
}
private String getPermissionTicket(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, AuthzClient authzClient, OIDCHttpFacade 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 = resolveClaims(pathConfig, httpFacade);
if (!claims.isEmpty()) {
permissionRequest.setClaims(claims);
}
return permission.create(permissionRequest).getTicket();
}
return null;
}
private boolean isBearerAuthorization(OIDCHttpFacade httpFacade) {
List<String> authHeaders = httpFacade.getRequest().getHeaders("Authorization");
if (authHeaders != null) {
for (String authHeader : authHeaders) {
String[] split = authHeader.trim().split("\\s+");
if (split == null || split.length != 2) continue;
if (!split[0].equalsIgnoreCase("Bearer")) continue;
return true;
}
}
return getPolicyEnforcer().getDeployment().isBearerOnly();
}
}

View file

@ -1,382 +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.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.ServiceLoader;
import org.jboss.logging.Logger;
import org.keycloak.AuthorizationContext;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.OIDCHttpFacade;
import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.ClientAuthenticator;
import org.keycloak.authorization.client.Configuration;
import org.keycloak.authorization.client.resource.ProtectedResource;
import org.keycloak.common.util.PathMatcher;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathCacheConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
import org.keycloak.representations.idm.authorization.Permission;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class PolicyEnforcer {
private static Logger LOGGER = Logger.getLogger(PolicyEnforcer.class);
private final KeycloakDeployment deployment;
private final AuthzClient authzClient;
private final PolicyEnforcerConfig enforcerConfig;
private final PathConfigMatcher pathMatcher;
private final Map<String, PathConfig> paths;
private final Map<String, ClaimInformationPointProviderFactory> claimInformationPointProviderFactories = new HashMap<>();
public PolicyEnforcer(KeycloakDeployment deployment, AdapterConfig adapterConfig) {
this.deployment = deployment;
this.enforcerConfig = adapterConfig.getPolicyEnforcerConfig();
Configuration configuration = new Configuration(adapterConfig.getAuthServerUrl(), adapterConfig.getRealm(), adapterConfig.getResource(), adapterConfig.getCredentials(), deployment.getClient());
this.authzClient = AuthzClient.create(configuration, new ClientAuthenticator() {
@Override
public void configureClientCredentials(Map<String, List<String>> requestParams, Map<String, String> requestHeaders) {
Map<String, String> formparams = new HashMap<>();
ClientCredentialsProviderUtils.setClientCredentials(PolicyEnforcer.this.deployment, requestHeaders, formparams);
for (Entry<String, String> param : formparams.entrySet()) {
requestParams.put(param.getKey(), Arrays.asList(param.getValue()));
}
}
});
paths = configurePaths(this.authzClient.protection().resource(), this.enforcerConfig);
pathMatcher = new PathConfigMatcher(paths, enforcerConfig, authzClient);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Initialization complete. Path configurations:");
for (PathConfig pathConfig : this.paths.values()) {
LOGGER.debug(pathConfig);
}
}
loadClaimInformationPointProviders(ServiceLoader.load(ClaimInformationPointProviderFactory.class, ClaimInformationPointProviderFactory.class.getClassLoader()));
loadClaimInformationPointProviders(ServiceLoader.load(ClaimInformationPointProviderFactory.class, Thread.currentThread().getContextClassLoader()));
}
public AuthorizationContext enforce(OIDCHttpFacade facade) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debugv("Policy enforcement is enabled. Enforcing policy decisions for path [{0}].", facade.getRequest().getURI());
}
AuthorizationContext context = new KeycloakAdapterPolicyEnforcer(this).authorize(facade);
if (LOGGER.isDebugEnabled()) {
LOGGER.debugv("Policy enforcement result for path [{0}] is : {1}", facade.getRequest().getURI(), context.isGranted() ? "GRANTED" : "DENIED");
LOGGER.debugv("Returning authorization context with permissions:");
for (Permission permission : context.getPermissions()) {
LOGGER.debug(permission);
}
}
return context;
}
public PolicyEnforcerConfig getEnforcerConfig() {
return enforcerConfig;
}
public AuthzClient getClient() {
return authzClient;
}
public Map<String, PathConfig> getPaths() {
return paths;
}
public PathConfigMatcher getPathMatcher() {
return pathMatcher;
}
public KeycloakDeployment getDeployment() {
return deployment;
}
public Map<String, ClaimInformationPointProviderFactory> getClaimInformationPointProviderFactories() {
return claimInformationPointProviderFactories;
}
private void loadClaimInformationPointProviders(ServiceLoader<ClaimInformationPointProviderFactory> loader) {
for (ClaimInformationPointProviderFactory factory : loader) {
factory.init(this);
claimInformationPointProviderFactories.put(factory.getName(), factory);
}
}
private Map<String, PathConfig> configurePaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
boolean loadPathsFromServer = !enforcerConfig.getLazyLoadPaths();
for (PathConfig pathConfig : enforcerConfig.getPaths()) {
if (!PolicyEnforcerConfig.EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
loadPathsFromServer = false;
break;
}
}
if (loadPathsFromServer) {
LOGGER.info("No path provided in configuration.");
Map<String, PathConfig> paths = configureAllPathsForResourceServer(protectedResource);
paths.putAll(configureDefinedPaths(protectedResource, enforcerConfig));
return paths;
} else {
LOGGER.info("Paths provided in configuration.");
return configureDefinedPaths(protectedResource, enforcerConfig);
}
}
private Map<String, PathConfig> configureDefinedPaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
Map<String, PathConfig> paths = Collections.synchronizedMap(new LinkedHashMap<String, PathConfig>());
for (PathConfig pathConfig : enforcerConfig.getPaths()) {
ResourceRepresentation resource;
String resourceName = pathConfig.getName();
String path = pathConfig.getPath();
if (resourceName != null) {
LOGGER.debugf("Trying to find resource with name [%s] for path [%s].", resourceName, path);
resource = protectedResource.findByName(resourceName);
} else {
LOGGER.debugf("Trying to find resource with uri [%s] for path [%s].", path, path);
List<ResourceRepresentation> resources = protectedResource.findByUri(path);
if (resources.isEmpty()) {
resources = protectedResource.findByMatchingUri(path);
}
if (resources.size() == 1) {
resource = resources.get(0);
} else if (resources.size() > 1) {
throw new RuntimeException("Multiple resources found with the same uri");
} else {
resource = null;
}
}
if (resource != null) {
pathConfig.setId(resource.getId());
// if the resource is statically bound to a resource it means the config can not be invalidated
if (resourceName != null) {
pathConfig.setStatic(true);
}
}
if (PolicyEnforcerConfig.EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
pathConfig.setStatic(true);
}
PathConfig existingPath = null;
for (PathConfig current : paths.values()) {
if (current.getPath().equals(pathConfig.getPath())) {
existingPath = current;
break;
}
}
if (existingPath == null) {
paths.put(pathConfig.getPath(), pathConfig);
} else {
existingPath.getMethods().addAll(pathConfig.getMethods());
existingPath.getScopes().addAll(pathConfig.getScopes());
}
}
return paths;
}
private Map<String, PathConfig> configureAllPathsForResourceServer(ProtectedResource protectedResource) {
LOGGER.info("Querying the server for all resources associated with this application.");
Map<String, PathConfig> paths = Collections.synchronizedMap(new HashMap<String, PathConfig>());
if (!enforcerConfig.getLazyLoadPaths()) {
for (String id : protectedResource.findAll()) {
ResourceRepresentation resourceDescription = protectedResource.findById(id);
if (resourceDescription.getUris() != null && !resourceDescription.getUris().isEmpty()) {
for(PathConfig pathConfig : PathConfig.createPathConfigs(resourceDescription)) {
paths.put(pathConfig.getPath(), pathConfig);
}
}
}
}
return paths;
}
public static class PathConfigMatcher extends PathMatcher<PathConfig> {
private final Map<String, PathConfig> paths;
private final PathCache pathCache;
private final AuthzClient authzClient;
private final PolicyEnforcerConfig enforcerConfig;
public PathConfigMatcher(Map<String, PathConfig> paths, PolicyEnforcerConfig enforcerConfig, AuthzClient authzClient) {
this.paths = paths;
this.enforcerConfig = enforcerConfig;
PathCacheConfig cacheConfig = enforcerConfig.getPathCacheConfig();
if (cacheConfig == null) {
cacheConfig = new PathCacheConfig();
}
pathCache = new PathCache(cacheConfig.getMaxEntries(), cacheConfig.getLifespan(), paths);
this.authzClient = authzClient;
}
@Override
public PathConfig matches(String targetUri) {
PathConfig pathConfig = pathCache.get(targetUri);
if (pathCache.containsKey(targetUri) || pathConfig != null) {
return pathConfig;
}
pathConfig = super.matches(targetUri);
if (enforcerConfig.getLazyLoadPaths() || enforcerConfig.getPathCacheConfig() != null) {
if ((pathConfig == null || pathConfig.isInvalidated() || pathConfig.getPath().contains("*"))) {
try {
List<ResourceRepresentation> matchingResources = authzClient.protection().resource().findByMatchingUri(targetUri);
if (matchingResources.isEmpty()) {
// if this config is invalidated (e.g.: due to cache expiration) we remove and return null
if (pathConfig != null && pathConfig.isInvalidated()) {
paths.remove(targetUri);
return null;
}
} else {
Map<String, Map<String, Object>> cipConfig = null;
PolicyEnforcerConfig.EnforcementMode enforcementMode = PolicyEnforcerConfig.EnforcementMode.ENFORCING;
ResourceRepresentation targetResource = matchingResources.get(0);
List<PolicyEnforcerConfig.MethodConfig> methodConfig = null;
boolean isStatic = false;
if (pathConfig != null) {
cipConfig = pathConfig.getClaimInformationPointConfig();
enforcementMode = pathConfig.getEnforcementMode();
methodConfig = pathConfig.getMethods();
isStatic = pathConfig.isStatic();
} else {
for (PathConfig existingPath : paths.values()) {
if (targetResource.getId().equals(existingPath.getId())
&& existingPath.isStatic()
&& !PolicyEnforcerConfig.EnforcementMode.DISABLED.equals(existingPath.getEnforcementMode())) {
return null;
}
}
}
pathConfig = PathConfig.createPathConfigs(targetResource).iterator().next();
if (cipConfig != null) {
pathConfig.setClaimInformationPointConfig(cipConfig);
}
if (methodConfig != null) {
pathConfig.setMethods(methodConfig);
}
pathConfig.setStatic(isStatic);
pathConfig.setEnforcementMode(enforcementMode);
}
} catch (Exception cause) {
LOGGER.errorf(cause, "Could not lazy load resource with path [" + targetUri + "] from server");
return null;
}
}
}
pathCache.put(targetUri, pathConfig);
return pathConfig;
}
@Override
protected String getPath(PathConfig entry) {
return entry.getPath();
}
@Override
protected Collection<PathConfig> getPaths() {
return paths.values();
}
public PathCache getPathCache() {
return pathCache;
}
@Override
protected PathConfig resolvePathConfig(PathConfig originalConfig, String path) {
if (originalConfig.hasPattern()) {
ProtectedResource resource = authzClient.protection().resource();
// search by an exact match
List<ResourceRepresentation> search = resource.findByUri(path);
// if exact match not found, try to obtain from current path the parent path.
// if path is /resource/1/test and pattern from pathConfig is /resource/{id}/*, parent path is /resource/1
// this logic allows to match sub resources of a resource instance (/resource/1) to the parent resource,
// so any permission granted to parent also applies to sub resources
if (search.isEmpty()) {
search = resource.findByUri(buildUriFromTemplate(originalConfig.getPath(), path, true));
}
if (!search.isEmpty()) {
ResourceRepresentation targetResource = search.get(0);
PathConfig config = PathConfig.createPathConfigs(targetResource).iterator().next();
config.setScopes(originalConfig.getScopes());
config.setMethods(originalConfig.getMethods());
config.setParentConfig(originalConfig);
config.setEnforcementMode(originalConfig.getEnforcementMode());
config.setClaimInformationPointConfig(originalConfig.getClaimInformationPointConfig());
return config;
}
}
return null;
}
public void removeFromCache(String pathConfig) {
pathCache.remove(pathConfig);
}
};
}

View file

@ -26,11 +26,9 @@ import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
import org.keycloak.adapters.AdapterUtils;
import org.keycloak.adapters.rotation.AdapterTokenVerifier;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.constants.ServiceUrlConstants;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
import org.keycloak.util.JsonSerialization;
@ -96,7 +94,7 @@ public class DirectAccessGrantsLoginModule extends AbstractKeycloakLoginModule {
formparams.add(new BasicNameValuePair(OAuth2Constants.SCOPE, scope));
}
ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams);
AdapterUtils.setClientCredentials(deployment, post, formparams);
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form);
@ -154,7 +152,7 @@ public class DirectAccessGrantsLoginModule extends AbstractKeycloakLoginModule {
HttpPost post = new HttpPost(logoutUri);
List<NameValuePair> formparams = new ArrayList<>();
ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams);
AdapterUtils.setClientCredentials(deployment, post, formparams);
formparams.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken));
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");

View file

@ -0,0 +1,131 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.adapters.pep;
import java.io.InputStream;
import java.util.List;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.OIDCHttpFacade;
import org.keycloak.adapters.authorization.TokenPrincipal;
import org.keycloak.adapters.authorization.spi.HttpRequest;
import org.keycloak.adapters.spi.HttpFacade.Cookie;
import org.keycloak.representations.AccessToken;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class HttpAuthzRequest implements HttpRequest {
private final TokenPrincipal tokenPrincipal;
private final OIDCHttpFacade oidcFacade;
public HttpAuthzRequest(OIDCHttpFacade oidcFacade) {
this.oidcFacade = oidcFacade;
tokenPrincipal = new TokenPrincipal() {
@Override
public String getRawToken() {
KeycloakSecurityContext securityContext = oidcFacade.getSecurityContext();
if (securityContext == null) {
return null;
}
return oidcFacade.getSecurityContext().getTokenString();
}
@Override
public AccessToken getToken() {
KeycloakSecurityContext securityContext = oidcFacade.getSecurityContext();
if (securityContext == null) {
return null;
}
return securityContext.getToken();
}
};
}
@Override
public String getRelativePath() {
return oidcFacade.getRequest().getRelativePath();
}
@Override
public String getMethod() {
return oidcFacade.getRequest().getMethod();
}
@Override
public String getURI() {
return oidcFacade.getRequest().getURI();
}
@Override
public List<String> getHeaders(String name) {
return oidcFacade.getRequest().getHeaders(name);
}
@Override
public String getFirstParam(String name) {
String queryParamValue = oidcFacade.getRequest().getQueryParamValue(name);
if (queryParamValue != null) {
return queryParamValue;
}
return oidcFacade.getRequest().getFirstParam(name);
}
@Override
public String getCookieValue(String name) {
Cookie cookie = oidcFacade.getRequest().getCookie(name);
if (cookie == null) {
return null;
}
return cookie.getValue();
}
@Override
public String getRemoteAddr() {
return oidcFacade.getRequest().getRemoteAddr();
}
@Override
public boolean isSecure() {
return oidcFacade.getRequest().isSecure();
}
@Override
public String getHeader(String name) {
return oidcFacade.getRequest().getHeader(name);
}
@Override
public InputStream getInputStream(boolean buffered) {
return oidcFacade.getRequest().getInputStream(buffered);
}
@Override
public TokenPrincipal getPrincipal() {
return tokenPrincipal;
}
}

View file

@ -0,0 +1,49 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.adapters.pep;
import org.keycloak.adapters.OIDCHttpFacade;
import org.keycloak.adapters.authorization.spi.HttpResponse;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class HttpAuthzResponse implements HttpResponse {
private OIDCHttpFacade oidcFacade;
public HttpAuthzResponse(OIDCHttpFacade oidcFacade) {
this.oidcFacade = oidcFacade;
}
@Override
public void sendError(int statusCode) {
oidcFacade.getResponse().setStatus(statusCode);
}
@Override
public void sendError(int code, String reason) {
oidcFacade.getResponse().sendError(code, reason);
}
@Override
public void setHeader(String name, String value) {
oidcFacade.getResponse().setHeader(name, value);
}
}

View file

@ -20,19 +20,17 @@ package org.keycloak.adapters;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.Configurable;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.CoreConnectionPNames;
import org.hamcrest.CoreMatchers;
import org.junit.Test;
import org.keycloak.adapters.authentication.ClientIdAndSecretCredentialsProvider;
import org.keycloak.adapters.authentication.JWTClientCredentialsProvider;
import org.keycloak.adapters.authentication.JWTClientSecretCredentialsProvider;
import org.keycloak.adapters.rotation.HardcodedPublicKeyLocator;
import org.keycloak.adapters.rotation.JWKPublicKeyLocator;
import org.keycloak.common.enums.RelativeUrlsUsed;
import org.keycloak.common.enums.SslRequired;
import org.keycloak.common.util.PemUtils;
import org.keycloak.enums.TokenStore;
import org.keycloak.protocol.oidc.client.authentication.ClientIdAndSecretCredentialsProvider;
import org.keycloak.protocol.oidc.client.authentication.JWTClientCredentialsProvider;
import org.keycloak.protocol.oidc.client.authentication.JWTClientSecretCredentialsProvider;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
@ -40,7 +38,6 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.lang.reflect.Field;
import java.util.Optional;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>

View file

@ -50,6 +50,10 @@
<groupId>org.keycloak</groupId>
<artifactId>keycloak-adapter-core</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-policy-enforcer</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_4.0_spec</artifactId>

View file

@ -14,7 +14,7 @@
<artifactId>keycloak-authz-client</artifactId>
<packaging>jar</packaging>
<name>KeyCloak Authz: Client API</name>
<name>Keycloak Authz: Client API</name>
<description>KeyCloak AuthZ: Client API</description>
<properties>

View file

@ -91,18 +91,7 @@ public class AuthzClient {
* @return a new instance
*/
public static AuthzClient create(Configuration configuration) {
return new AuthzClient(configuration, configuration.getClientAuthenticator());
}
/**
* <p>Creates a new instance.
*
* @param configuration the client configuration
* @param authenticator the client authenticator
* @return a new instance
*/
public static AuthzClient create(Configuration configuration, ClientAuthenticator authenticator) {
return new AuthzClient(configuration, authenticator);
return new AuthzClient(configuration);
}
private final ServerConfiguration serverConfiguration;
@ -242,7 +231,7 @@ public class AuthzClient {
return this.configuration;
}
private AuthzClient(Configuration configuration, ClientAuthenticator authenticator) {
private AuthzClient(Configuration configuration) {
if (configuration == null) {
throw new IllegalArgumentException("Client configuration can not be null.");
}
@ -256,7 +245,7 @@ public class AuthzClient {
configurationUrl = KeycloakUriBuilder.fromUri(configurationUrl).clone().path(AUTHZ_DISCOVERY_URL).build(configuration.getRealm()).toString();
this.configuration = configuration;
this.http = new Http(configuration, authenticator != null ? authenticator : configuration.getClientAuthenticator());
this.http = new Http(configuration, configuration.getClientCredentialsProvider());
try {
this.serverConfiguration = this.http.<ServerConfiguration>get(configurationUrl)
@ -265,8 +254,6 @@ public class AuthzClient {
} catch (Exception e) {
throw new RuntimeException("Could not obtain configuration from server [" + configurationUrl + "].", e);
}
this.http.setServerConfiguration(this.serverConfiguration);
}
private TokenCallable createPatSupplier(String userName, String password) {

View file

@ -1,28 +0,0 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.keycloak.authorization.client;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public interface ClientAuthenticator {
void configureClientCredentials(Map<String, List<String>> requestParams, Map<String, String> requestHeaders);
}

View file

@ -17,14 +17,14 @@
*/
package org.keycloak.authorization.client;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.HttpClients;
import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider;
import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProviderUtils;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.util.BasicAuthHelper;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@ -35,7 +35,7 @@ public class Configuration extends AdapterConfig {
private HttpClient httpClient;
@JsonIgnore
private ClientAuthenticator clientAuthenticator = createDefaultClientAuthenticator();
private ClientCredentialsProvider clientCredentialsProvider;
public Configuration() {
@ -66,27 +66,18 @@ public class Configuration extends AdapterConfig {
return httpClient;
}
ClientAuthenticator getClientAuthenticator() {
return this.clientAuthenticator;
public void setHttpClient(HttpClient httpClient) {
this.httpClient = httpClient;
}
/**
* Creates a default client authenticator which uses HTTP BASIC and client id and secret to authenticate the client.
*
* @return the default client authenticator
*/
private ClientAuthenticator createDefaultClientAuthenticator() {
return new ClientAuthenticator() {
@Override
public void configureClientCredentials(Map<String, List<String>> requestParams, Map<String, String> requestHeaders) {
String secret = (String) getCredentials().get("secret");
public void setClientCredentialsProvider(ClientCredentialsProvider clientCredentialsProvider) {
this.clientCredentialsProvider = clientCredentialsProvider;
}
if (secret == null) {
throw new RuntimeException("Client secret not provided.");
}
requestHeaders.put("Authorization", BasicAuthHelper.RFC6749.createHeader(getResource(), secret));
}
};
public ClientCredentialsProvider getClientCredentialsProvider() {
if (clientCredentialsProvider == null) {
clientCredentialsProvider = ClientCredentialsProviderUtils.bootstrapClientAuthenticator(this);
}
return clientCredentialsProvider;
}
}

View file

@ -18,9 +18,9 @@
package org.keycloak.authorization.client.util;
import org.apache.http.client.methods.RequestBuilder;
import org.keycloak.authorization.client.ClientAuthenticator;
import org.keycloak.authorization.client.Configuration;
import org.keycloak.authorization.client.representation.ServerConfiguration;
import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@ -28,10 +28,9 @@ import org.keycloak.authorization.client.representation.ServerConfiguration;
public class Http {
private final Configuration configuration;
private final ClientAuthenticator authenticator;
private ServerConfiguration serverConfiguration;
private final ClientCredentialsProvider authenticator;
public Http(Configuration configuration, ClientAuthenticator authenticator) {
public Http(Configuration configuration, ClientCredentialsProvider authenticator) {
this.configuration = configuration;
this.authenticator = authenticator;
}
@ -55,8 +54,4 @@ public class Http {
private <R> HttpMethod<R> method(RequestBuilder builder) {
return new HttpMethod(this.configuration, authenticator, builder);
}
public void setServerConfiguration(ServerConfiguration serverConfiguration) {
this.serverConfiguration = serverConfiguration;
}
}

View file

@ -35,28 +35,29 @@ import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.keycloak.authorization.client.ClientAuthenticator;
import org.keycloak.authorization.client.Configuration;
import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class HttpMethod<R> {
private final HttpClient httpClient;
private final ClientAuthenticator authenticator;
protected final RequestBuilder builder;
protected final Configuration configuration;
protected final Map<String, String> headers;
protected final Map<String, List<String>> params;
private static final Logger logger = Logger.getLogger(HttpMethod.class.getName());
private final HttpClient httpClient;
final RequestBuilder builder;
final Configuration configuration;
final Map<String, String> headers;
final Map<String, List<String>> params;
private final ClientCredentialsProvider authenticator;
private HttpMethodResponse<R> response;
public HttpMethod(Configuration configuration, ClientAuthenticator authenticator, RequestBuilder builder) {
public HttpMethod(Configuration configuration, ClientCredentialsProvider authenticator, RequestBuilder builder) {
this(configuration, authenticator, builder, new HashMap<String, List<String>>(), new HashMap<String, String>());
}
public HttpMethod(Configuration configuration, ClientAuthenticator authenticator, RequestBuilder builder, Map<String, List<String>> params, Map<String, String> headers) {
public HttpMethod(Configuration configuration, ClientCredentialsProvider authenticator, RequestBuilder builder, Map<String, List<String>> params, Map<String, String> headers) {
this.configuration = configuration;
this.httpClient = configuration.getHttpClient();
this.authenticator = authenticator;

View file

@ -18,11 +18,16 @@
package org.keycloak.authorization.client.util;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.http.Header;
import org.keycloak.OAuth2Constants;
import org.keycloak.authorization.client.ClientAuthenticator;
import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider;
import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProviderUtils;
import org.keycloak.representations.idm.authorization.AuthorizationRequest;
import org.keycloak.representations.idm.authorization.AuthorizationRequest.Metadata;
import org.keycloak.representations.idm.authorization.Permission;
@ -34,16 +39,16 @@ import org.keycloak.representations.idm.authorization.PermissionTicketToken;
public class HttpMethodAuthenticator<R> {
private final HttpMethod<R> method;
private final ClientAuthenticator authenticator;
private ClientCredentialsProvider clientCredentialProvider;
public HttpMethodAuthenticator(HttpMethod<R> method, ClientAuthenticator authenticator) {
public HttpMethodAuthenticator(HttpMethod<R> method, ClientCredentialsProvider clientCredentialsProvider) {
this.method = method;
this.authenticator = authenticator;
this.clientCredentialProvider = clientCredentialsProvider;
}
public HttpMethod<R> client() {
this.method.params.put(OAuth2Constants.GRANT_TYPE, Arrays.asList(OAuth2Constants.CLIENT_CREDENTIALS));
authenticator.configureClientCredentials(this.method.params, this.method.headers);
configureClientCredentials(this.method.params, this.method.headers);
return this.method;
}
@ -133,4 +138,12 @@ public class HttpMethodAuthenticator<R> {
return method;
}
private void configureClientCredentials(Map<String, List<String>> requestParams, Map<String, String> requestHeaders) {
Map<String, String> formparams = new HashMap<>();
ClientCredentialsProviderUtils.setClientCredentials(method.configuration, clientCredentialProvider, requestHeaders, formparams);
for (Entry<String, String> param : formparams.entrySet()) {
requestParams.put(param.getKey(), Arrays.asList(param.getValue()));
}
}
}

65
authz/policy-enforcer/pom.xml Executable file
View file

@ -0,0 +1,65 @@
<?xml version="1.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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-parent</artifactId>
<version>999.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-policy-enforcer</artifactId>
<name>Keycloak Authz: Policy Enforcer</name>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-client</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
</dependencies>
</project>

View file

@ -51,7 +51,7 @@ public class PathCache {
* @param maxAge the time in milliseconds that an entry can stay in the cache. If {@code -1}, entries never expire
* @param paths the pre-configured paths
*/
public PathCache(final int maxEntries, long maxAge,
PathCache(final int maxEntries, long maxAge,
Map<String, PathConfig> paths) {
cache = new LinkedHashMap<String, CacheEntry>(16, DEFAULT_LOAD_FACTOR, true) {
@Override

View file

@ -0,0 +1,293 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.adapters.authorization;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.jboss.logging.Logger;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.resource.ProtectedResource;
import org.keycloak.common.util.PathMatcher;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathCacheConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class PathConfigMatcher extends PathMatcher<PathConfig> {
private static Logger LOGGER = Logger.getLogger(PolicyEnforcer.class);
private final Map<String, PathConfig> paths;
private final PathCache pathCache;
private final AuthzClient authzClient;
private final PolicyEnforcerConfig enforcerConfig;
PathConfigMatcher(PolicyEnforcerConfig enforcerConfig, AuthzClient authzClient) {
this.enforcerConfig = enforcerConfig;
PathCacheConfig cacheConfig = enforcerConfig.getPathCacheConfig();
if (cacheConfig == null) {
cacheConfig = new PathCacheConfig();
}
this.authzClient = authzClient;
this.paths = configurePaths();
this.pathCache = new PathCache(cacheConfig.getMaxEntries(), cacheConfig.getLifespan(), paths);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Initialization complete. Path configuration:");
for (PathConfig pathConfig : this.paths.values()) {
LOGGER.debug(pathConfig);
}
}
}
@Override
public PathConfig matches(String targetUri) {
PathConfig pathConfig = pathCache.get(targetUri);
if (pathCache.containsKey(targetUri) || pathConfig != null) {
return pathConfig;
}
pathConfig = super.matches(targetUri);
if (enforcerConfig.getLazyLoadPaths() || enforcerConfig.getPathCacheConfig() != null) {
if ((pathConfig == null || pathConfig.isInvalidated() || pathConfig.getPath().contains("*"))) {
try {
List<ResourceRepresentation> matchingResources = authzClient.protection().resource().findByMatchingUri(targetUri);
if (matchingResources.isEmpty()) {
// if this config is invalidated (e.g.: due to cache expiration) we remove and return null
if (pathConfig != null && pathConfig.isInvalidated()) {
paths.remove(targetUri);
return null;
}
} else {
Map<String, Map<String, Object>> cipConfig = null;
PolicyEnforcerConfig.EnforcementMode enforcementMode = PolicyEnforcerConfig.EnforcementMode.ENFORCING;
ResourceRepresentation targetResource = matchingResources.get(0);
List<org.keycloak.representations.adapters.config.PolicyEnforcerConfig.MethodConfig> methodConfig = null;
boolean isStatic = false;
if (pathConfig != null) {
cipConfig = pathConfig.getClaimInformationPointConfig();
enforcementMode = pathConfig.getEnforcementMode();
methodConfig = pathConfig.getMethods();
isStatic = pathConfig.isStatic();
} else {
for (PathConfig existingPath : paths.values()) {
if (targetResource.getId().equals(existingPath.getId())
&& existingPath.isStatic()
&& !org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode.DISABLED.equals(existingPath.getEnforcementMode())) {
return null;
}
}
}
pathConfig = PathConfig.createPathConfigs(targetResource).iterator().next();
if (cipConfig != null) {
pathConfig.setClaimInformationPointConfig(cipConfig);
}
if (methodConfig != null) {
pathConfig.setMethods(methodConfig);
}
pathConfig.setStatic(isStatic);
pathConfig.setEnforcementMode(enforcementMode);
}
} catch (Exception cause) {
LOGGER.errorf(cause, "Could not lazy load resource with path [" + targetUri + "] from server");
return null;
}
}
}
pathCache.put(targetUri, pathConfig);
return pathConfig;
}
@Override
protected String getPath(PathConfig entry) {
return entry.getPath();
}
@Override
protected Collection<PathConfig> getPaths() {
return paths.values();
}
public PathCache getPathCache() {
return pathCache;
}
@Override
protected PathConfig resolvePathConfig(PathConfig originalConfig, String path) {
if (originalConfig.hasPattern()) {
ProtectedResource resource = authzClient.protection().resource();
// search by an exact match
List<ResourceRepresentation> search = resource.findByUri(path);
// if exact match not found, try to obtain from current path the parent path.
// if path is /resource/1/test and pattern from pathConfig is /resource/{id}/*, parent path is /resource/1
// this logic allows to match sub resources of a resource instance (/resource/1) to the parent resource,
// so any permission granted to parent also applies to sub resources
if (search.isEmpty()) {
search = resource.findByUri(buildUriFromTemplate(originalConfig.getPath(), path, true));
}
if (!search.isEmpty()) {
ResourceRepresentation targetResource = search.get(0);
PathConfig config = PathConfig.createPathConfigs(targetResource).iterator().next();
config.setScopes(originalConfig.getScopes());
config.setMethods(originalConfig.getMethods());
config.setParentConfig(originalConfig);
config.setEnforcementMode(originalConfig.getEnforcementMode());
config.setClaimInformationPointConfig(originalConfig.getClaimInformationPointConfig());
return config;
}
}
return null;
}
public void removeFromCache(String pathConfig) {
pathCache.remove(pathConfig);
}
public Map<String, PathConfig> getPathConfig() {
return paths;
}
private Map<String, PathConfig> configurePaths() {
ProtectedResource protectedResource = this.authzClient.protection().resource();
boolean loadPathsFromServer = !enforcerConfig.getLazyLoadPaths();
for (PathConfig pathConfig : enforcerConfig.getPaths()) {
if (!org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
loadPathsFromServer = false;
break;
}
}
if (loadPathsFromServer) {
LOGGER.info("No path provided in configuration.");
Map<String, PathConfig> paths = configureAllPathsForResourceServer(protectedResource);
paths.putAll(configureDefinedPaths(protectedResource, enforcerConfig));
return paths;
} else {
LOGGER.info("Paths provided in configuration.");
return configureDefinedPaths(protectedResource, enforcerConfig);
}
}
private Map<String, PathConfig> configureDefinedPaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
Map<String, PathConfig> paths = Collections.synchronizedMap(new LinkedHashMap<String, PathConfig>());
for (PathConfig pathConfig : enforcerConfig.getPaths()) {
ResourceRepresentation resource;
String resourceName = pathConfig.getName();
String path = pathConfig.getPath();
if (resourceName != null) {
LOGGER.debugf("Trying to find resource with name [%s] for path [%s].", resourceName, path);
resource = protectedResource.findByName(resourceName);
} else {
LOGGER.debugf("Trying to find resource with uri [%s] for path [%s].", path, path);
List<ResourceRepresentation> resources = protectedResource.findByUri(path);
if (resources.isEmpty()) {
resources = protectedResource.findByMatchingUri(path);
}
if (resources.size() == 1) {
resource = resources.get(0);
} else if (resources.size() > 1) {
throw new RuntimeException("Multiple resources found with the same uri");
} else {
resource = null;
}
}
if (resource != null) {
pathConfig.setId(resource.getId());
// if the resource is statically bound to a resource it means the config can not be invalidated
if (resourceName != null) {
pathConfig.setStatic(true);
}
}
if (org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
pathConfig.setStatic(true);
}
PathConfig existingPath = null;
for (PathConfig current : paths.values()) {
if (current.getPath().equals(pathConfig.getPath())) {
existingPath = current;
break;
}
}
if (existingPath == null) {
paths.put(pathConfig.getPath(), pathConfig);
} else {
existingPath.getMethods().addAll(pathConfig.getMethods());
existingPath.getScopes().addAll(pathConfig.getScopes());
}
}
return paths;
}
private Map<String, PathConfig> configureAllPathsForResourceServer(ProtectedResource protectedResource) {
LOGGER.info("Querying the server for all resources associated with this application.");
Map<String, PathConfig> paths = Collections.synchronizedMap(new HashMap<String, PathConfig>());
if (!enforcerConfig.getLazyLoadPaths()) {
for (String id : protectedResource.findAll()) {
ResourceRepresentation resourceDescription = protectedResource.findById(id);
if (resourceDescription.getUris() != null && !resourceDescription.getUris().isEmpty()) {
for(PathConfig pathConfig : PathConfig.createPathConfigs(resourceDescription)) {
paths.put(pathConfig.getPath(), pathConfig);
}
}
}
}
return paths;
}
}

View file

@ -0,0 +1,647 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.keycloak.adapters.authorization;
import static org.keycloak.adapters.authorization.util.JsonUtils.asAccessToken;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.ServiceLoader;
import java.util.Set;
import org.apache.http.client.HttpClient;
import org.jboss.logging.Logger;
import org.keycloak.AuthorizationContext;
import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProviderFactory;
import org.keycloak.adapters.authorization.spi.HttpRequest;
import org.keycloak.adapters.authorization.spi.HttpResponse;
import org.keycloak.authorization.client.AuthorizationDeniedException;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.ClientAuthorizationContext;
import org.keycloak.authorization.client.Configuration;
import org.keycloak.authorization.client.resource.PermissionResource;
import org.keycloak.authorization.client.resource.ProtectionResource;
import org.keycloak.common.util.Base64;
import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessToken.Authorization;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.MethodConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.ScopeEnforcementMode;
import org.keycloak.representations.idm.authorization.AuthorizationRequest;
import org.keycloak.representations.idm.authorization.AuthorizationResponse;
import org.keycloak.representations.idm.authorization.Permission;
import org.keycloak.representations.idm.authorization.PermissionRequest;
import org.keycloak.util.JsonSerialization;
/**
* <p>A Policy Enforcement Point (PEP) that requests and enforces authorization decisions from Keycloak.
*
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class PolicyEnforcer {
private static Logger LOGGER = Logger.getLogger(PolicyEnforcer.class);
private static final String HTTP_METHOD_DELETE = "DELETE";
public static Builder builder() {
return new Builder();
}
private final AuthzClient authzClient;
private final Map<String, PathConfig> paths;
private final PathConfigMatcher pathMatcher;
private final HttpClient httpClient;
private final PolicyEnforcerConfig enforcerConfig;
private final Map<String, ClaimInformationPointProviderFactory> claimInformationPointProviderFactories = new HashMap<>();
protected PolicyEnforcer(Builder builder) {
enforcerConfig = builder.getEnforcerConfig();
authzClient = AuthzClient.create(builder.authzClientConfig);
httpClient = authzClient.getConfiguration().getHttpClient();
pathMatcher = new PathConfigMatcher(builder.getEnforcerConfig(), authzClient);
paths = pathMatcher.getPathConfig();
loadClaimInformationPointProviders(ServiceLoader.load(ClaimInformationPointProviderFactory.class, ClaimInformationPointProviderFactory.class.getClassLoader()));
loadClaimInformationPointProviders(ServiceLoader.load(ClaimInformationPointProviderFactory.class, Thread.currentThread().getContextClassLoader()));
}
public AuthorizationContext enforce(HttpRequest request, HttpResponse response) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debugv("Policy enforcement is enabled. Enforcing policy decisions for path [{0}].", request.getURI());
}
AuthorizationContext context = authorize(request, response);
if (LOGGER.isDebugEnabled()) {
LOGGER.debugv("Policy enforcement result for path [{0}] is : {1}", request.getURI(), context.isGranted() ? "GRANTED" : "DENIED");
LOGGER.debugv("Returning authorization context with permissions:");
for (Permission permission : context.getPermissions()) {
LOGGER.debug(permission);
}
}
return context;
}
public HttpClient getHttpClient() {
return httpClient;
}
public Map<String, PathConfig> getPaths() {
return Collections.unmodifiableMap(paths);
}
public Map<String, ClaimInformationPointProviderFactory> getClaimInformationPointProviderFactories() {
return claimInformationPointProviderFactories;
}
public PathConfigMatcher getPathMatcher() {
return pathMatcher;
}
private AuthorizationContext authorize(HttpRequest request, HttpResponse response) {
EnforcementMode enforcementMode = enforcerConfig.getEnforcementMode();
TokenPrincipal principal = request.getPrincipal();
boolean anonymous = principal == null || principal.getRawToken() == null;
if (EnforcementMode.DISABLED.equals(enforcementMode)) {
if (anonymous) {
response.sendError(401, "Invalid bearer");
}
return createEmptyAuthorizationContext(true);
}
PathConfig pathConfig = getPathConfig(request);
if (anonymous) {
if (!isDefaultAccessDeniedUri(request)) {
if (pathConfig != null) {
if (EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
return createEmptyAuthorizationContext(true);
} else {
challenge(pathConfig, getRequiredScopes(pathConfig, request), request, response);
}
} else {
handleAccessDenied(response);
}
}
return createEmptyAuthorizationContext(false);
}
AccessToken accessToken = principal.getToken();
if (accessToken != null) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig);
}
if (pathConfig == null) {
if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) {
return createAuthorizationContext(accessToken, null);
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debugf("Could not find a configuration for path [%s]", getPath(request));
}
if (isDefaultAccessDeniedUri(request)) {
return createAuthorizationContext(accessToken, null);
}
handleAccessDenied(response);
return createEmptyAuthorizationContext(false);
}
if (EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
return createAuthorizationContext(accessToken, pathConfig);
}
MethodConfig methodConfig = getRequiredScopes(pathConfig, request);
Map<String, List<String>> claims = resolveClaims(pathConfig, request);
if (isAuthorized(pathConfig, methodConfig, accessToken, request, claims)) {
try {
return createAuthorizationContext(accessToken, pathConfig);
} catch (Exception e) {
throw new RuntimeException("Error processing path [" + pathConfig.getPath() + "].", e);
}
}
AccessToken original = accessToken;
accessToken = requestAuthorizationToken(pathConfig, methodConfig, request, claims);
if (accessToken != null) {
AccessToken.Authorization authorization = original.getAuthorization();
if (authorization == null) {
authorization = new AccessToken.Authorization();
authorization.setPermissions(new ArrayList<Permission>());
}
AccessToken.Authorization newAuthorization = accessToken.getAuthorization();
if (newAuthorization != null) {
Collection<Permission> grantedPermissions = authorization.getPermissions();
Collection<Permission> newPermissions = newAuthorization.getPermissions();
for (Permission newPermission : newPermissions) {
if (!grantedPermissions.contains(newPermission)) {
grantedPermissions.add(newPermission);
}
}
}
original.setAuthorization(authorization);
if (isAuthorized(pathConfig, methodConfig, accessToken, request, claims)) {
try {
return createAuthorizationContext(accessToken, pathConfig);
} catch (Exception e) {
throw new RuntimeException("Error processing path [" + pathConfig.getPath() + "].", e);
}
}
}
if (methodConfig != null && ScopeEnforcementMode.DISABLED.equals(methodConfig.getScopesEnforcementMode())) {
return createEmptyAuthorizationContext(true);
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
}
if (!challenge(pathConfig, methodConfig, request, response)) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debugf("Challenge not sent, sending default forbidden response. Path [%s]", pathConfig);
}
handleAccessDenied(response);
}
}
return createEmptyAuthorizationContext(false);
}
protected boolean isAuthorized(PathConfig actualPathConfig, MethodConfig methodConfig, AccessToken accessToken, HttpRequest request, Map<String, List<String>> claims) {
if (isDefaultAccessDeniedUri(request)) {
return true;
}
Authorization authorization = accessToken.getAuthorization();
if (authorization == null) {
return false;
}
boolean hasPermission = false;
Collection<Permission> grantedPermissions = authorization.getPermissions();
for (Permission permission : grantedPermissions) {
if (permission.getResourceId() != null) {
if (isResourcePermission(actualPathConfig, permission)) {
hasPermission = true;
if (actualPathConfig.isInstance() && !matchResourcePermission(actualPathConfig, permission)) {
continue;
}
if (hasResourceScopePermission(methodConfig, permission)) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, grantedPermissions);
}
if (HTTP_METHOD_DELETE.equalsIgnoreCase(request.getMethod()) && actualPathConfig.isInstance()) {
pathMatcher.removeFromCache(getPath(request));
}
return hasValidClaims(permission, claims);
}
}
} else {
if (hasResourceScopePermission(methodConfig, permission)) {
return true;
}
}
}
if (!hasPermission && EnforcementMode.PERMISSIVE.equals(actualPathConfig.getEnforcementMode())) {
return true;
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debugf("Authorization FAILED for path [%s]. Not enough permissions [%s].", actualPathConfig, grantedPermissions);
}
return false;
}
protected Map<String, List<String>> resolveClaims(PathConfig pathConfig, HttpRequest request) {
Map<String, List<String>> claims = new HashMap<>();
resolveClaims(claims, enforcerConfig.getClaimInformationPointConfig(), request);
resolveClaims(claims, pathConfig.getClaimInformationPointConfig(), request);
return claims;
}
protected boolean challenge(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, HttpRequest request, HttpResponse response) {
if (isBearerAuthorization(request)) {
String ticket = getPermissionTicket(pathConfig, methodConfig, authzClient, request);
if (ticket != null) {
response.sendError(401);
response.setHeader("WWW-Authenticate", new StringBuilder("UMA realm=\"").append(authzClient.getConfiguration().getRealm()).append("\"").append(",as_uri=\"")
.append(authzClient.getServerConfiguration().getIssuer()).append("\"").append(",ticket=\"").append(ticket).append("\"").toString());
} else {
response.sendError(403);
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Sending challenge");
}
return true;
}
handleAccessDenied(response);
return true;
}
protected void handleAccessDenied(HttpResponse response) {
String accessDeniedPath = enforcerConfig.getOnDenyRedirectTo();
if (accessDeniedPath != null) {
response.sendError(302);
response.setHeader("Location", accessDeniedPath);
} else {
response.sendError(403);
}
}
private boolean hasValidClaims(Permission permission, Map<String, List<String>> claims) {
Map<String, Set<String>> grantedClaims = permission.getClaims();
if (grantedClaims != null) {
if (claims.isEmpty()) {
return false;
}
for (Entry<String, Set<String>> entry : grantedClaims.entrySet()) {
List<String> requestClaims = claims.get(entry.getKey());
if (requestClaims == null || requestClaims.isEmpty() || !entry.getValue().containsAll(requestClaims)) {
return false;
}
}
}
return true;
}
private boolean isDefaultAccessDeniedUri(HttpRequest request) {
String accessDeniedPath = enforcerConfig.getOnDenyRedirectTo();
return accessDeniedPath != null && request.getURI().contains(accessDeniedPath);
}
private boolean hasResourceScopePermission(MethodConfig methodConfig, Permission permission) {
List<String> requiredScopes = methodConfig.getScopes();
Set<String> allowedScopes = permission.getScopes();
if (allowedScopes.isEmpty()) {
return true;
}
PolicyEnforcerConfig.ScopeEnforcementMode enforcementMode = methodConfig.getScopesEnforcementMode();
if (PolicyEnforcerConfig.ScopeEnforcementMode.ALL.equals(enforcementMode)) {
return allowedScopes.containsAll(requiredScopes);
}
if (PolicyEnforcerConfig.ScopeEnforcementMode.ANY.equals(enforcementMode)) {
for (String requiredScope : requiredScopes) {
if (allowedScopes.contains(requiredScope)) {
return true;
}
}
}
return requiredScopes.isEmpty();
}
private AuthorizationContext createEmptyAuthorizationContext(final boolean granted) {
return new ClientAuthorizationContext(authzClient) {
@Override
public boolean hasPermission(String resourceName, String scopeName) {
return granted;
}
@Override
public boolean hasResourcePermission(String resourceName) {
return granted;
}
@Override
public boolean hasScopePermission(String scopeName) {
return granted;
}
@Override
public List<Permission> getPermissions() {
return Collections.EMPTY_LIST;
}
@Override
public boolean isGranted() {
return granted;
}
};
}
private String getPath(HttpRequest request) {
return request.getRelativePath();
}
private MethodConfig getRequiredScopes(PathConfig pathConfig, HttpRequest request) {
String method = request.getMethod();
for (MethodConfig methodConfig : pathConfig.getMethods()) {
if (methodConfig.getMethod().equals(method)) {
return methodConfig;
}
}
MethodConfig methodConfig = new MethodConfig();
methodConfig.setMethod(request.getMethod());
List scopes = new ArrayList<>();
if (Boolean.TRUE.equals(enforcerConfig.getHttpMethodAsScope())) {
scopes.add(request.getMethod());
} else {
scopes.addAll(pathConfig.getScopes());
}
methodConfig.setScopes(scopes);
methodConfig.setScopesEnforcementMode(PolicyEnforcerConfig.ScopeEnforcementMode.ANY);
return methodConfig;
}
private AuthorizationContext createAuthorizationContext(AccessToken accessToken, PathConfig pathConfig) {
return new ClientAuthorizationContext(accessToken, pathConfig, authzClient);
}
private boolean isResourcePermission(PathConfig actualPathConfig, Permission permission) {
// first we try a match using resource id
boolean resourceMatch = matchResourcePermission(actualPathConfig, permission);
// as a fallback, check if the current path is an instance and if so, check if parent's id matches the permission
if (!resourceMatch && actualPathConfig.isInstance()) {
resourceMatch = matchResourcePermission(actualPathConfig.getParentConfig(), permission);
}
return resourceMatch;
}
private boolean matchResourcePermission(PathConfig actualPathConfig, Permission permission) {
return permission.getResourceId().equals(actualPathConfig.getId());
}
private PathConfig getPathConfig(HttpRequest request) {
return isDefaultAccessDeniedUri(request) ? null : pathMatcher.matches(getPath(request));
}
private AccessToken requestAuthorizationToken(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, HttpRequest request, Map<String, List<String>> claims) {
if (enforcerConfig.getUserManagedAccess() != null) {
return null;
}
try {
TokenPrincipal principal = request.getPrincipal();
String accessTokenString = principal.getRawToken();
AccessToken accessToken = principal.getToken();
AuthorizationRequest authzRequest = new AuthorizationRequest();
if (isBearerAuthorization(request) || accessToken.getAuthorization() != null) {
authzRequest.addPermission(pathConfig.getId(), methodConfig.getScopes());
}
if (!claims.isEmpty()) {
authzRequest.setClaimTokenFormat("urn:ietf:params:oauth:token-type:jwt");
authzRequest.setClaimToken(Base64.encodeBytes(JsonSerialization.writeValueAsBytes(claims)));
}
if (accessToken.getAuthorization() != null) {
authzRequest.setRpt(accessTokenString);
}
LOGGER.debug("Obtaining authorization for authenticated user.");
AuthorizationResponse authzResponse;
if (isBearerAuthorization(request)) {
authzRequest.setSubjectToken(accessTokenString);
authzResponse = authzClient.authorization().authorize(authzRequest);
} else {
authzResponse = authzClient.authorization(accessTokenString).authorize(authzRequest);
}
if (authzResponse != null) {
return asAccessToken(authzResponse.getToken());
}
} catch (AuthorizationDeniedException ignore) {
LOGGER.debug("Authorization denied", ignore);
} catch (Exception e) {
LOGGER.debug("Authorization failed", e);
}
return null;
}
private String getPermissionTicket(PathConfig pathConfig, MethodConfig methodConfig, AuthzClient authzClient, HttpRequest httpFacade) {
if (enforcerConfig.getUserManagedAccess() != null) {
ProtectionResource protection = authzClient.protection();
PermissionResource permission = protection.permission();
PermissionRequest permissionRequest = new PermissionRequest();
permissionRequest.setResourceId(pathConfig.getId());
permissionRequest.setScopes(new HashSet<>(methodConfig.getScopes()));
Map<String, List<String>> claims = resolveClaims(pathConfig, httpFacade);
if (!claims.isEmpty()) {
permissionRequest.setClaims(claims);
}
return permission.create(permissionRequest).getTicket();
}
return null;
}
private boolean isBearerAuthorization(HttpRequest request) {
List<String> authHeaders = request.getHeaders("Authorization");
if (authHeaders != null) {
for (String authHeader : authHeaders) {
String[] split = authHeader.trim().split("\\s+");
if (split == null || split.length != 2) continue;
if (!split[0].equalsIgnoreCase("Bearer")) continue;
return true;
}
}
return authzClient.getConfiguration().isBearerOnly();
}
private void loadClaimInformationPointProviders(ServiceLoader<ClaimInformationPointProviderFactory> loader) {
for (ClaimInformationPointProviderFactory factory : loader) {
factory.init(this);
claimInformationPointProviderFactories.put(factory.getName(), factory);
}
}
private void resolveClaims(Map<String, List<String>> claims, Map<String, Map<String, Object>> claimInformationPointConfig, HttpRequest request) {
if (claimInformationPointConfig != null) {
for (Entry<String, Map<String, Object>> claimDef : claimInformationPointConfig.entrySet()) {
ClaimInformationPointProviderFactory factory = claimInformationPointProviderFactories.get(claimDef.getKey());
if (factory != null) {
claims.putAll(factory.create(claimDef.getValue()).resolve(request));
}
}
}
}
public static class Builder {
Configuration authzClientConfig = new Configuration();
private Builder() {
}
public Builder authServerUrl(String authServerUrl) {
authzClientConfig.setAuthServerUrl(authServerUrl);
return this;
}
public Builder realm(String realm) {
authzClientConfig.setRealm(realm);
return this;
}
public Builder clientId(String clientId) {
authzClientConfig.setResource(clientId);
return this;
}
public Builder bearerOnly(boolean bearerOnly) {
authzClientConfig.setBearerOnly(bearerOnly);
return this;
}
public Builder credentials(Map<String, Object> credentials) {
authzClientConfig.setCredentials(credentials);
return this;
}
public Builder enforcerConfig(PolicyEnforcerConfig enforcerConfig) {
authzClientConfig.setPolicyEnforcerConfig(enforcerConfig);
return this;
}
public Builder enforcerConfig(InputStream is) {
try {
enforcerConfig(JsonSerialization.readValue(is, PolicyEnforcerConfig.class));
} catch (Exception cause) {
throw new RuntimeException("Failed to read configuration", cause);
}
return this;
}
public Builder httpClient(HttpClient httpClient) {
authzClientConfig.setHttpClient(httpClient);
return this;
}
public Builder credentialProvider(ClientCredentialsProvider credentialsProvider) {
authzClientConfig.setClientCredentialsProvider(credentialsProvider);
return this;
}
public PolicyEnforcer build() {
return new PolicyEnforcer(this);
}
PolicyEnforcerConfig getEnforcerConfig() {
return authzClientConfig.getPolicyEnforcerConfig();
}
}
}

View file

@ -0,0 +1,56 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.adapters.authorization;
import java.security.Principal;
import org.keycloak.adapters.authorization.util.JsonUtils;
import org.keycloak.representations.AccessToken;
/**
* A {@link Principal} backed by a token representing the entity requesting permissions.
*
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public interface TokenPrincipal extends Principal {
/**
* The token in its raw format.
*
* @return the token in its raw format.
*/
String getRawToken();
/**
* The {@link AccessToken} representation of {@link TokenPrincipal#getRawToken()}.
*
* @return the access token representation
*/
default AccessToken getToken() {
return JsonUtils.asAccessToken(getRawToken());
}
/**
* The name of the entity represented by the token.
*
* @return the name of the principal
*/
default String getName() {
return getToken().getPreferredUsername();
}
}

View file

@ -23,9 +23,9 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.keycloak.adapters.authorization.ClaimInformationPointProvider;
import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProvider;
import org.keycloak.adapters.authorization.spi.HttpRequest;
import org.keycloak.adapters.authorization.util.PlaceHolders;
import org.keycloak.adapters.spi.HttpFacade;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@ -39,7 +39,7 @@ public class ClaimsInformationPointProvider implements ClaimInformationPointProv
}
@Override
public Map<String, List<String>> resolve(HttpFacade httpFacade) {
public Map<String, List<String>> resolve(HttpRequest request) {
Map<String, List<String>> claims = new HashMap<>();
for (Entry<String, Object> configEntry : config.entrySet()) {
@ -48,11 +48,11 @@ public class ClaimsInformationPointProvider implements ClaimInformationPointProv
List<String> values = new ArrayList<>();
if (claimValue instanceof String) {
values = getValues(claimValue.toString(), httpFacade);
values = getValues(claimValue.toString(), request);
} else if (claimValue instanceof Collection) {
for (Object value : Collection.class.cast(claimValue)) {
List<String> resolvedValues = getValues(value.toString(), httpFacade);
List<String> resolvedValues = getValues(value.toString(), request);
if (!resolvedValues.isEmpty()) {
values.addAll(resolvedValues);
@ -68,7 +68,7 @@ public class ClaimsInformationPointProvider implements ClaimInformationPointProv
return claims;
}
private List<String> getValues(String value, HttpFacade httpFacade) {
private List<String> getValues(String value, HttpRequest httpFacade) {
return PlaceHolders.resolve(value, httpFacade);
}
}

View file

@ -18,8 +18,7 @@ package org.keycloak.adapters.authorization.cip;
import java.util.Map;
import org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory;
import org.keycloak.adapters.authorization.PolicyEnforcer;
import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProviderFactory;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@ -31,11 +30,6 @@ public class ClaimsInformationPointProviderFactory implements ClaimInformationPo
return "claims";
}
@Override
public void init(PolicyEnforcer policyEnforcer) {
}
@Override
public ClaimsInformationPointProvider create(Map<String, Object> config) {
return new ClaimsInformationPointProvider(config);

View file

@ -36,11 +36,10 @@ import org.apache.http.StatusLine;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.util.EntityUtils;
import org.keycloak.adapters.authorization.ClaimInformationPointProvider;
import org.keycloak.adapters.authorization.PolicyEnforcer;
import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProvider;
import org.keycloak.adapters.authorization.spi.HttpRequest;
import org.keycloak.adapters.authorization.util.JsonUtils;
import org.keycloak.adapters.authorization.util.PlaceHolders;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.authorization.client.util.HttpResponseException;
import org.keycloak.common.util.StreamUtil;
import org.keycloak.util.JsonSerialization;
@ -53,15 +52,15 @@ public class HttpClaimInformationPointProvider implements ClaimInformationPointP
private final Map<String, Object> config;
private final HttpClient httpClient;
public HttpClaimInformationPointProvider(Map<String, Object> config, PolicyEnforcer policyEnforcer) {
public HttpClaimInformationPointProvider(Map<String, Object> config, HttpClient httpClient) {
this.config = config;
this.httpClient = policyEnforcer.getDeployment().getClient();
this.httpClient = httpClient;
}
@Override
public Map<String, List<String>> resolve(HttpFacade httpFacade) {
public Map<String, List<String>> resolve(HttpRequest request) {
try {
InputStream responseStream = executeRequest(httpFacade);
InputStream responseStream = executeRequest(request);
try (InputStream inputStream = new BufferedInputStream(responseStream)) {
JsonNode jsonNode = JsonSerialization.mapper.readTree(inputStream);
@ -102,7 +101,7 @@ public class HttpClaimInformationPointProvider implements ClaimInformationPointP
}
}
private InputStream executeRequest(HttpFacade httpFacade) {
private InputStream executeRequest(HttpRequest request) {
String method = config.get("method").toString();
if (method == null) {
@ -122,10 +121,10 @@ public class HttpClaimInformationPointProvider implements ClaimInformationPointP
byte[] bytes = new byte[0];
try {
setParameters(builder, httpFacade);
setParameters(builder, request);
if (config.containsKey("headers")) {
setHeaders(builder, httpFacade);
setHeaders(builder, request);
}
HttpResponse response = httpClient.execute(builder.build());
@ -152,7 +151,7 @@ public class HttpClaimInformationPointProvider implements ClaimInformationPointP
}
}
private void setHeaders(RequestBuilder builder, HttpFacade httpFacade) {
private void setHeaders(RequestBuilder builder, HttpRequest request) {
Object headersDef = config.get("headers");
if (headersDef != null) {
@ -166,10 +165,10 @@ public class HttpClaimInformationPointProvider implements ClaimInformationPointP
Collection values = Collection.class.cast(value);
for (Object item : values) {
headerValues.addAll(PlaceHolders.resolve(item.toString(), httpFacade));
headerValues.addAll(PlaceHolders.resolve(item.toString(), request));
}
} else {
headerValues.addAll(PlaceHolders.resolve(value.toString(), httpFacade));
headerValues.addAll(PlaceHolders.resolve(value.toString(), request));
}
for (String headerValue : headerValues) {
@ -179,7 +178,7 @@ public class HttpClaimInformationPointProvider implements ClaimInformationPointP
}
}
private void setParameters(RequestBuilder builder, HttpFacade httpFacade) {
private void setParameters(RequestBuilder builder, HttpRequest request) {
Object config = this.config.get("parameters");
if (config != null) {
@ -193,10 +192,10 @@ public class HttpClaimInformationPointProvider implements ClaimInformationPointP
Collection values = Collection.class.cast(value);
for (Object item : values) {
paramValues.addAll(PlaceHolders.resolve(item.toString(), httpFacade));
paramValues.addAll(PlaceHolders.resolve(item.toString(), request));
}
} else {
paramValues.addAll(PlaceHolders.resolve(value.toString(), httpFacade));
paramValues.addAll(PlaceHolders.resolve(value.toString(), request));
}
for (String paramValue : paramValues) {

View file

@ -18,8 +18,8 @@ package org.keycloak.adapters.authorization.cip;
import java.util.Map;
import org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory;
import org.keycloak.adapters.authorization.PolicyEnforcer;
import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProviderFactory;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@ -40,6 +40,6 @@ public class HttpClaimInformationPointProviderFactory implements ClaimInformatio
@Override
public HttpClaimInformationPointProvider create(Map<String, Object> config) {
return new HttpClaimInformationPointProvider(config, policyEnforcer);
return new HttpClaimInformationPointProvider(config, policyEnforcer.getHttpClient());
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright 2018 Red Hat, Inc. and/or its affiliates
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -14,17 +14,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.adapters.authorization;
package org.keycloak.adapters.authorization.cip.spi;
import java.util.List;
import java.util.Map;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.adapters.authorization.spi.HttpRequest;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public interface ClaimInformationPointProvider {
Map<String, List<String>> resolve(HttpFacade httpFacade);
Map<String, List<String>> resolve(HttpRequest request);
}

View file

@ -1,5 +1,5 @@
/*
* Copyright 2018 Red Hat, Inc. and/or its affiliates
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -14,11 +14,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.adapters.authorization;
package org.keycloak.adapters.authorization.cip.spi;
import java.util.Map;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.adapters.authorization.PolicyEnforcer;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@ -27,7 +27,9 @@ public interface ClaimInformationPointProviderFactory<C extends ClaimInformation
String getName();
void init(PolicyEnforcer policyEnforcer);
default void init(PolicyEnforcer policyEnforcer) {
}
C create(Map<String, Object> config);
}

View file

@ -0,0 +1,114 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.adapters.authorization.spi;
import java.io.InputStream;
import java.util.List;
import org.keycloak.adapters.authorization.TokenPrincipal;
/**
* Represents an incoming HTTP request and the contract to manipulate it.
*
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public interface HttpRequest {
/**
* Get the request path. This is the path relative to the context path.
* E.g.: for a HTTP GET request to http://my.appserver.com/my-application/path/sub-path this method is going to return /path/sub-path.
* @return the relative path
*/
String getRelativePath();
/**
* Returns the name of the HTTP method with which this request was made, for example, GET, POST, or PUT.
*
* @return a {@code String} specifying the name of the method with which this request was made
*/
String getMethod();
/**
* Get the URI representation for the current request.
*
* @return a {@code String} representation for the current request
*/
String getURI();
/**
* Get a list of all of the values set for the specified header within the HTTP request.
*
* @param name the header name
* @return a list of the values set for this header, if the header is not set on the request then null should be returned
*/
List<String> getHeaders(String name);
/**
* Get the first value for a parameter with the given {@code name}
*
* @param name the parameter name
* @return the value of the parameter
*/
String getFirstParam(String name);
/**
* Get the first value for a cookie with the given {@code name}.
*
* @param name the parameter name
* @return the value of the cookie
*/
String getCookieValue(String name);
/**
* Returns the client address.
*
* @return the client address.
*/
String getRemoteAddr();
/**
* Indicates if the request is coming from a secure channel through HTTPS.
*
* @return {@code true} if the HTTP scheme is set to 'https'. Otherwise, {@code false}
*/
boolean isSecure();
/**
* Get the first value for a HEADER with the given {@code name}.
*
* @param name the HEADER name
* @return the value of the HEADER
*/
String getHeader(String name);
/**
* Returns the request input stream
*
* @param buffered if the input stream should be buffered and support for multiple reads
* @return the request input stream
*/
InputStream getInputStream(boolean buffered);
/**
* Returns a {@link TokenPrincipal} associated with the request.
*
* @return the principal
*/
TokenPrincipal getPrincipal();
}

View file

@ -0,0 +1,48 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.adapters.authorization.spi;
/**
* Represents an outgoing HTTP response and the contract to manipulate it.
*
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public interface HttpResponse {
/**
* Send an error with the given {@code statusCode}.
*
* @param statusCode the status to set in the response
*/
void sendError(int statusCode);
/**
* Send an error with the given {@code statusCode} and {@code reason} message.
*
* @param statusCode the status to set in the response
*/
void sendError(int statusCode, String reason);
/**
* Set a header with the given {@code name} and {@code value}.
*
* @param name the header name
* @param value the header value
*/
void setHeader(String name, String value);
}

View file

@ -21,9 +21,13 @@ import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.databind.JsonNode;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.representations.AccessToken;
import org.keycloak.util.JsonSerialization;
/**
* Utility methods to manipulate JSON data
*
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class JsonUtils {
@ -36,7 +40,6 @@ public class JsonUtils {
List<String> values = new ArrayList<>();
if (jsonNode.isArray()) {
for (JsonNode node : jsonNode) {
String value;
@ -65,4 +68,11 @@ public class JsonUtils {
return values;
}
public static AccessToken asAccessToken(String rawToken) {
try {
return new JWSInput(rawToken).readJsonContent(AccessToken.class);
} catch (Exception cause) {
throw new RuntimeException("Failed to decode token", cause);
}
}
}

View file

@ -22,9 +22,8 @@ import java.util.Arrays;
import java.util.List;
import com.fasterxml.jackson.databind.JsonNode;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.OIDCHttpFacade;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.adapters.authorization.TokenPrincipal;
import org.keycloak.adapters.authorization.spi.HttpRequest;
import org.keycloak.util.JsonSerialization;
/**
@ -35,29 +34,18 @@ public class KeycloakSecurityContextPlaceHolderResolver implements PlaceHolderRe
public static final String NAME = "keycloak";
@Override
public List<String> resolve(String placeHolder, HttpFacade httpFacade) {
public List<String> resolve(String placeHolder, HttpRequest request) {
String source = placeHolder.substring(placeHolder.indexOf('.') + 1);
OIDCHttpFacade oidcHttpFacade = OIDCHttpFacade.class.cast(httpFacade);
KeycloakSecurityContext securityContext = oidcHttpFacade.getSecurityContext();
if (securityContext == null) {
return null;
}
TokenPrincipal principal = request.getPrincipal();
if (source.endsWith("access_token")) {
return Arrays.asList(securityContext.getTokenString());
}
if (source.endsWith("id_token")) {
return Arrays.asList(securityContext.getIdTokenString());
return Arrays.asList(principal.getRawToken());
}
JsonNode jsonNode;
if (source.startsWith("access_token[")) {
jsonNode = JsonSerialization.mapper.valueToTree(securityContext.getToken());
} else if (source.startsWith("id_token[")) {
jsonNode = JsonSerialization.mapper.valueToTree(securityContext.getIdToken());
jsonNode = JsonSerialization.mapper.valueToTree(principal.getToken());
} else {
throw new RuntimeException("Invalid placeholder [" + placeHolder + "]");
}

View file

@ -18,13 +18,13 @@ package org.keycloak.adapters.authorization.util;
import java.util.List;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.adapters.authorization.spi.HttpRequest;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public interface PlaceHolderResolver {
List<String> resolve(String placeHolder, HttpFacade httpFacade);
List<String> resolve(String placeHolder, HttpRequest httpFacade);
}

View file

@ -17,6 +17,7 @@
package org.keycloak.adapters.authorization.util;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -24,7 +25,7 @@ import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.adapters.authorization.spi.HttpRequest;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@ -41,7 +42,7 @@ public class PlaceHolders {
private static Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{(.+?)\\}");
private static Pattern PLACEHOLDER_PARAM_PATTERN = Pattern.compile("\\[(.+?)\\]");
public static List<String> resolve(String value, HttpFacade httpFacade) {
public static List<String> resolve(String value, HttpRequest httpFacade) {
Map<String, List<String>> placeHolders = parsePlaceHolders(value, httpFacade);
if (!placeHolders.isEmpty()) {
@ -75,27 +76,31 @@ public class PlaceHolders {
return null;
}
private static Map<String, List<String>> parsePlaceHolders(String value, HttpFacade httpFacade) {
Map<String, List<String>> placeHolders = new HashMap<>();
private static Map<String, List<String>> parsePlaceHolders(String value, HttpRequest httpFacade) {
Map<String, List<String>> placeHolders = Collections.emptyMap();
Matcher matcher = PLACEHOLDER_PATTERN.matcher(value);
boolean found = matcher.find();
while (matcher.find()) {
String placeHolder = matcher.group(1);
int resolverNameIdx = placeHolder.indexOf('.');
if (found) {
placeHolders = new HashMap<>();
do {
String placeHolder = matcher.group(1);
int resolverNameIdx = placeHolder.indexOf('.');
if (resolverNameIdx == -1) {
throw new RuntimeException("Invalid placeholder [" + value + "]. Could not find resolver name.");
}
PlaceHolderResolver resolver = resolvers.get(placeHolder.substring(0, resolverNameIdx));
if (resolver != null) {
List<String> resolved = resolver.resolve(placeHolder, httpFacade);
if (resolved != null) {
placeHolders.put(formatPlaceHolder(placeHolder), resolved);
if (resolverNameIdx == -1) {
throw new RuntimeException("Invalid placeholder [" + value + "]. Could not find resolver name.");
}
}
PlaceHolderResolver resolver = resolvers.get(placeHolder.substring(0, resolverNameIdx));
if (resolver != null) {
List<String> resolved = resolver.resolve(placeHolder, httpFacade);
if (resolved != null) {
placeHolders.put(formatPlaceHolder(placeHolder), resolved);
}
}
} while (matcher.find());
}
return placeHolders;

View file

@ -29,9 +29,7 @@ import java.util.Collections;
import java.util.List;
import com.fasterxml.jackson.databind.JsonNode;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.adapters.spi.HttpFacade.Cookie;
import org.keycloak.adapters.spi.HttpFacade.Request;
import org.keycloak.adapters.authorization.spi.HttpRequest;
import org.keycloak.util.JsonSerialization;
/**
@ -42,17 +40,12 @@ public class RequestPlaceHolderResolver implements PlaceHolderResolver {
static String NAME = "request";
@Override
public List<String> resolve(String placeHolder, HttpFacade httpFacade) {
public List<String> resolve(String placeHolder, HttpRequest request) {
String source = placeHolder.substring(placeHolder.indexOf('.') + 1);
Request request = httpFacade.getRequest();
if (source.startsWith("parameter")) {
String parameterName = getParameter(source, "Could not obtain parameter name from placeholder [" + source + "]");
String parameterValue = request.getQueryParamValue(parameterName);
if (parameterValue == null) {
parameterValue = request.getFirstParam(parameterName);
}
String parameterValue = request.getFirstParam(parameterName);
if (parameterValue != null) {
return Arrays.asList(parameterValue);
@ -66,10 +59,10 @@ public class RequestPlaceHolderResolver implements PlaceHolderResolver {
}
} else if (source.startsWith("cookie")) {
String cookieName = getParameter(source, "Could not obtain cookie name from placeholder [" + source + "]");
Cookie cookieValue = request.getCookie(cookieName);
String cookieValue = request.getCookieValue(cookieName);
if (cookieValue != null) {
return Arrays.asList(cookieValue.getValue());
return Arrays.asList(cookieValue);
}
} else if (source.startsWith("remoteAddr")) {
String value = request.getRemoteAddr();

View file

@ -32,7 +32,7 @@
<artifactId>keycloak-authz-policy-common</artifactId>
<packaging>jar</packaging>
<name>KeyCloak AuthZ: Common Policy Providers</name>
<name>Keycloak AuthZ: Common Policy Providers</name>
<description>KeyCloak AuthZ: Common Policy Providers</description>
<dependencies>

View file

@ -14,8 +14,7 @@
<artifactId>keycloak-authz-provider-parent</artifactId>
<packaging>pom</packaging>
<name>KeyCloak AuthZ: Provider Parent</name>
<description>KeyCloak AuthZ: Provider Parent</description>
<name>Keycloak AuthZ: Policy Provider Parent</name>
<modules>
<module>common</module>

View file

@ -14,11 +14,12 @@
<artifactId>keycloak-authz-parent</artifactId>
<packaging>pom</packaging>
<name>KeyCloak Authz: Parent</name>
<name>Keycloak Authz: Parent</name>
<description>KeyCloak AuthZ: Parent</description>
<modules>
<module>policy</module>
<module>client</module>
<module>policy-enforcer</module>
</modules>
</project>

View file

@ -1,5 +1,5 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -15,12 +15,12 @@
* limitations under the License.
*/
package org.keycloak.adapters.authentication;
import org.keycloak.adapters.KeycloakDeployment;
package org.keycloak.protocol.oidc.client.authentication;
import java.util.Map;
import org.keycloak.representations.adapters.config.AdapterConfig;
/**
* The simple SPI for authenticating clients/applications . It's used by adapter during all OIDC backchannel requests to Keycloak server
* (codeToToken exchange, refresh token or backchannel logout) . You can also use it in your application during direct access grants or service account request
@ -30,15 +30,12 @@ import java.util.Map;
* so your server is able to authenticate client
*
* You must specify a file
* META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider in the WAR that this class is contained in (or in the JAR that is attached to the WEB-INF/lib or as jboss module
* META-INF/services/org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider in the WAR that this class is contained in (or in the JAR that is attached to the WEB-INF/lib or as jboss module
* if you want to share the implementation among more WARs).
*
* NOTE: The SPI is not finished and method signatures are still subject to change in future versions (for example to support
* authentication with client certificate)
*
* @see ClientIdAndSecretCredentialsProvider
* @see JWTClientCredentialsProvider
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface ClientCredentialsProvider {
@ -62,17 +59,17 @@ public interface ClientCredentialsProvider {
/**
* Called by adapter during deployment of your application. You can for example read configuration and init your authenticator here
*
* @param deployment the adapter configuration
* @param adapterConfig the adapter configuration
* @param config the configuration of your provider read from keycloak.json . For the kerberos-keytab example above, it will return map with the single key "keytab" with value "/tmp/foo"
*/
void init(KeycloakDeployment deployment, Object config);
void init(AdapterConfig adapterConfig, Object config);
/**
* Called every time adapter needs to perform backchannel request
*
* @param deployment Fully resolved deployment
* @param adapterConfig Fully resolved deployment
* @param requestHeaders You should put any HTTP request headers you want to use for authentication of client. These headers will be attached to the HTTP request sent to Keycloak server
* @param formParams You should put any request parameters you want to use for authentication of client. These parameters will be attached to the HTTP request sent to Keycloak server
*/
void setClientCredentials(KeycloakDeployment deployment, Map<String, String> requestHeaders, Map<String, String> formParams);
void setClientCredentials(AdapterConfig adapterConfig, Map<String, String> requestHeaders, Map<String, String> formParams);
}

View file

@ -1,5 +1,5 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -15,21 +15,17 @@
* limitations under the License.
*/
package org.keycloak.adapters.authentication;
import org.apache.http.NameValuePair;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair;
import org.jboss.logging.Logger;
import org.keycloak.adapters.KeycloakDeployment;
package org.keycloak.protocol.oidc.client.authentication;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
import org.jboss.logging.Logger;
import org.keycloak.representations.adapters.config.AdapterConfig;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@ -37,9 +33,9 @@ public class ClientCredentialsProviderUtils {
private static Logger logger = Logger.getLogger(ClientCredentialsProviderUtils.class);
public static ClientCredentialsProvider bootstrapClientAuthenticator(KeycloakDeployment deployment) {
String clientId = deployment.getResourceName();
Map<String, Object> clientCredentials = deployment.getResourceCredentials();
public static ClientCredentialsProvider bootstrapClientAuthenticator(AdapterConfig deployment) {
String clientId = deployment.getResource();
Map<String, Object> clientCredentials = deployment.getCredentials();
String authenticatorId;
if (clientCredentials == null || clientCredentials.isEmpty()) {
@ -73,7 +69,7 @@ public class ClientCredentialsProviderUtils {
return authenticator;
}
private static void loadAuthenticators(Map<String, ClientCredentialsProvider> authenticators, ClassLoader classLoader) {
public static void loadAuthenticators(Map<String, ClientCredentialsProvider> authenticators, ClassLoader classLoader) {
Iterator<ClientCredentialsProvider> iterator = ServiceLoader.load(ClientCredentialsProvider.class, classLoader).iterator();
while (iterator.hasNext()) {
try {
@ -91,26 +87,7 @@ public class ClientCredentialsProviderUtils {
/**
* Use this method when calling backchannel request directly from your application. See service-account example from demo for more details
*/
public static void setClientCredentials(KeycloakDeployment deployment, Map<String, String> requestHeaders, Map<String, String> formparams) {
ClientCredentialsProvider authenticator = deployment.getClientAuthenticator();
public static void setClientCredentials(AdapterConfig deployment, ClientCredentialsProvider authenticator, Map<String, String> requestHeaders, Map<String, String> formparams) {
authenticator.setClientCredentials(deployment, requestHeaders, formparams);
}
/**
* Don't use directly from your JEE apps to avoid HttpClient linkage errors! Instead use the method {@link #setClientCredentials(KeycloakDeployment, Map, Map)}
*/
public static void setClientCredentials(KeycloakDeployment deployment, HttpPost post, List<NameValuePair> formparams) {
Map<String, String> reqHeaders = new HashMap<>();
Map<String, String> reqParams = new HashMap<>();
setClientCredentials(deployment, reqHeaders, reqParams);
for (Map.Entry<String, String> header : reqHeaders.entrySet()) {
post.setHeader(header.getKey(), header.getValue());
}
for (Map.Entry<String, String> param : reqParams.entrySet()) {
formparams.add(new BasicNameValuePair(param.getKey(), param.getValue()));
}
}
}

View file

@ -15,16 +15,16 @@
* limitations under the License.
*/
package org.keycloak.adapters.authentication;
package org.keycloak.protocol.oidc.client.authentication;
import java.util.Map;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.util.BasicAuthHelper;
import java.util.Map;
/**
* Traditional OAuth2 authentication of clients based on client_id and client_secret
*
@ -44,13 +44,13 @@ public class ClientIdAndSecretCredentialsProvider implements ClientCredentialsPr
}
@Override
public void init(KeycloakDeployment deployment, Object config) {
public void init(AdapterConfig deployment, Object config) {
clientSecret = (config == null ? null : config.toString());
}
@Override
public void setClientCredentials(KeycloakDeployment deployment, Map<String, String> requestHeaders, Map<String, String> formParams) {
String clientId = deployment.getResourceName();
public void setClientCredentials(AdapterConfig deployment, Map<String, String> requestHeaders, Map<String, String> formParams) {
String clientId = deployment.getResource();
if (!deployment.isPublicClient()) {
if (clientSecret != null) {

View file

@ -15,18 +15,15 @@
* limitations under the License.
*/
package org.keycloak.adapters.authentication;
package org.keycloak.protocol.oidc.client.authentication;
import java.security.KeyPair;
import java.security.PublicKey;
import java.util.Map;
import java.util.UUID;
import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.AdapterUtils;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.common.util.KeyUtils;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.common.util.KeystoreUtil;
import org.keycloak.common.util.Time;
import org.keycloak.crypto.Algorithm;
@ -36,6 +33,9 @@ import org.keycloak.crypto.KeyType;
import org.keycloak.crypto.KeyUse;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.representations.adapters.config.AdapterConfig;
/**
* Client authentication based on JWT signed by client private key .
@ -102,16 +102,16 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
}
@Override
public void init(KeycloakDeployment deployment, Object config) {
public void init(AdapterConfig deployment, Object config) {
if (!(config instanceof Map)) {
throw new RuntimeException("Configuration of jwt credentials is missing or incorrect for client '" + deployment.getResourceName() + "'. Check your adapter configuration");
throw new RuntimeException("Configuration of jwt credentials is missing or incorrect for client '" + deployment.getResource() + "'. Check your adapter configuration");
}
Map<String, Object> cfg = (Map<String, Object>) config;
String clientKeystoreFile = (String) cfg.get("client-keystore-file");
if (clientKeystoreFile == null) {
throw new RuntimeException("Missing parameter client-keystore-file in configuration of jwt for client " + deployment.getResourceName());
throw new RuntimeException("Missing parameter client-keystore-file in configuration of jwt for client " + deployment.getResource());
}
String clientKeystoreType = (String) cfg.get("client-keystore-type");
@ -119,7 +119,7 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
String clientKeystorePassword = (String) cfg.get("client-keystore-password");
if (clientKeystorePassword == null) {
throw new RuntimeException("Missing parameter client-keystore-password in configuration of jwt for client " + deployment.getResourceName());
throw new RuntimeException("Missing parameter client-keystore-password in configuration of jwt for client " + deployment.getResource());
}
String clientKeyPassword = (String) cfg.get("client-key-password");
@ -129,7 +129,7 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
String clientKeyAlias = (String) cfg.get("client-key-alias");
if (clientKeyAlias == null) {
clientKeyAlias = deployment.getResourceName();
clientKeyAlias = deployment.getResource();
}
String algorithm = (String) cfg.getOrDefault("algorithm", Algorithm.RS256);
@ -157,8 +157,8 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
}
@Override
public void setClientCredentials(KeycloakDeployment deployment, Map<String, String> requestHeaders, Map<String, String> formParams) {
String signedToken = createSignedRequestToken(deployment.getResourceName(), deployment.getRealmInfoUrl());
public void setClientCredentials(AdapterConfig deployment, Map<String, String> requestHeaders, Map<String, String> formParams) {
String signedToken = createSignedRequestToken(deployment.getResource(), deployment.getRealmInfoUrl());
formParams.put(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT);
formParams.put(OAuth2Constants.CLIENT_ASSERTION, signedToken);
@ -173,7 +173,7 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
protected JsonWebToken createRequestToken(String clientId, String realmInfoUrl) {
JsonWebToken reqToken = new JsonWebToken();
reqToken.id(AdapterUtils.generateId());
reqToken.id(UUID.randomUUID().toString());
reqToken.issuer(clientId);
reqToken.subject(clientId);
reqToken.audience(realmInfoUrl);

View file

@ -14,23 +14,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.adapters.authentication;
package org.keycloak.protocol.oidc.client.authentication;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.UUID;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.AdapterUtils;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.common.util.Time;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.JavaAlgorithm;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.representations.adapters.config.AdapterConfig;
/**
* Client authentication based on JWT signed by client secret instead of private key .
@ -53,15 +53,15 @@ public class JWTClientSecretCredentialsProvider implements ClientCredentialsProv
}
@Override
public void init(KeycloakDeployment deployment, Object config) {
public void init(AdapterConfig deployment, Object config) {
if (!(config instanceof Map)) {
throw new RuntimeException("Configuration of jwt credentials by client secret is missing or incorrect for client '" + deployment.getResourceName() + "'. Check your adapter configuration");
throw new RuntimeException("Configuration of jwt credentials by client secret is missing or incorrect for client '" + deployment.getResource() + "'. Check your adapter configuration");
}
Map<String, Object> cfg = (Map<String, Object>) config;
String clientSecretString = (String) cfg.get("secret");
if (clientSecretString == null) {
throw new RuntimeException("Missing parameter secret-jwt in configuration of jwt for client " + deployment.getResourceName());
throw new RuntimeException("Missing parameter secret-jwt in configuration of jwt for client " + deployment.getResource());
}
String clientSecretJwtAlg = (String) cfg.get("algorithm");
@ -72,7 +72,7 @@ public class JWTClientSecretCredentialsProvider implements ClientCredentialsProv
setClientSecret(clientSecretString, clientSecretJwtAlg);
} else {
// invalid "algorithm" field
throw new RuntimeException("Invalid parameter secret-jwt in configuration of jwt for client " + deployment.getResourceName());
throw new RuntimeException("Invalid parameter secret-jwt in configuration of jwt for client " + deployment.getResource());
}
}
@ -84,8 +84,8 @@ public class JWTClientSecretCredentialsProvider implements ClientCredentialsProv
}
@Override
public void setClientCredentials(KeycloakDeployment deployment, Map<String, String> requestHeaders, Map<String, String> formParams) {
String signedToken = createSignedRequestToken(deployment.getResourceName(), deployment.getRealmInfoUrl());
public void setClientCredentials(AdapterConfig deployment, Map<String, String> requestHeaders, Map<String, String> formParams) {
String signedToken = createSignedRequestToken(deployment.getResource(), deployment.getRealmInfoUrl());
formParams.put(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT);
formParams.put(OAuth2Constants.CLIENT_ASSERTION, signedToken);
}
@ -126,7 +126,7 @@ public class JWTClientSecretCredentialsProvider implements ClientCredentialsProv
// JWT claims is the same as one by private_key_jwt
JsonWebToken reqToken = new JsonWebToken();
reqToken.id(AdapterUtils.generateId());
reqToken.id(UUID.randomUUID().toString());
reqToken.issuer(clientId);
reqToken.subject(clientId);
reqToken.audience(realmInfoUrl);

View file

@ -17,6 +17,7 @@
package org.keycloak.representations.adapters.config;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
@ -320,4 +321,9 @@ public class AdapterConfig extends BaseAdapterConfig implements AdapterHttpClien
public void setConnectionTTL(long connectionTTL) {
this.connectionTTL = connectionTTL;
}
@JsonIgnore
public String getRealmInfoUrl() {
return authServerUrl + "/realms/" + realm;
}
}

View file

@ -15,6 +15,6 @@
# limitations under the License.
#
org.keycloak.adapters.authentication.ClientIdAndSecretCredentialsProvider
org.keycloak.adapters.authentication.JWTClientCredentialsProvider
org.keycloak.adapters.authentication.JWTClientSecretCredentialsProvider
org.keycloak.protocol.oidc.client.authentication.ClientIdAndSecretCredentialsProvider
org.keycloak.protocol.oidc.client.authentication.JWTClientCredentialsProvider
org.keycloak.protocol.oidc.client.authentication.JWTClientSecretCredentialsProvider

View file

@ -154,6 +154,16 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-policy-enforcer</artifactId>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.wildfly</groupId>

View file

@ -36,6 +36,7 @@
<module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-crypto-default" services="import"/>
<module name="org.keycloak.keycloak-authz-client"/>
<module name="org.keycloak.keycloak-policy-enforcer"/>
</dependencies>
</module>

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<module xmlns="urn:jboss:module:1.3" name="org.keycloak.keycloak-policy-enforcer">
<resources>
<artifact name="${org.keycloak:keycloak-policy-enforcer}"/>
</resources>
<dependencies>
<module name="org.bouncycastle" />
<module name="javax.api"/>
<module name="javax.activation.api"/>
<module name="sun.jdk" optional="true" />
<module name="javax.ws.rs.api"/>
<module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-common"/>
<module name="org.apache.httpcomponents"/>
<module name="com.fasterxml.jackson.core.jackson-core"/>
<module name="com.fasterxml.jackson.core.jackson-annotations"/>
<module name="com.fasterxml.jackson.core.jackson-databind"/>
<module name="com.fasterxml.jackson.jaxrs.jackson-jaxrs-json-provider"/>
<module name="org.jboss.logging"/>
</dependencies>
</module>

View file

@ -1443,6 +1443,11 @@
<artifactId>keycloak-authz-policy-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-policy-enforcer</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Quarkus -->
<dependency>

View file

@ -23,7 +23,7 @@ import org.keycloak.provider.Provider;
* This interface is for users that want to add custom client authenticators to an authentication flow.
* You must implement this interface as well as a ClientAuthenticatorFactory.
*
* This interface is for verifying client credentials from request. On the adapter side, you must also implement org.keycloak.adapters.authentication.ClientCredentialsProvider , which is supposed
* This interface is for verifying client credentials from request. On the adapter side, you must also implement org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider , which is supposed
* to add the client credentials to the request, which will ClientAuthenticator verify on server side
*
* @see org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator

View file

@ -29,6 +29,10 @@
<groupId>org.keycloak</groupId>
<artifactId>keycloak-adapter-core</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-policy-enforcer</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>

View file

@ -25,18 +25,13 @@ import static org.junit.Assert.fail;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.AuthorizationResource;
import org.keycloak.admin.client.resource.ClientResource;
@ -44,7 +39,6 @@ import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
import org.keycloak.authentication.authenticators.client.JWTClientSecretAuthenticator;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.ClientAuthenticator;
import org.keycloak.authorization.client.Configuration;
import org.keycloak.authorization.client.resource.ProtectionResource;
import org.keycloak.authorization.client.util.HttpResponseException;
@ -356,16 +350,7 @@ public class AuthzClientCredentialsTest extends AbstractAuthzTest {
private AuthzClient getAuthzClient(String adapterConfig) {
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getConfigurationStream(adapterConfig));
return AuthzClient.create(new Configuration(deployment.getAuthServerBaseUrl(), deployment.getRealm(), deployment.getResourceName(), deployment.getResourceCredentials(), deployment.getClient()), new ClientAuthenticator() {
@Override
public void configureClientCredentials(Map<String, List<String>> requestParams, Map<String, String> requestHeaders) {
Map<String, String> formparams = new HashMap<>();
ClientCredentialsProviderUtils.setClientCredentials(deployment, requestHeaders, formparams);
for (Entry<String, String> param : formparams.entrySet()) {
requestParams.put(param.getKey(), Arrays.asList(param.getValue()));
}
}
});
return AuthzClient.create(new Configuration(deployment.getAuthServerBaseUrl(), deployment.getRealm(), deployment.getResourceName(), deployment.getResourceCredentials(), deployment.getClient()));
}
private InputStream getConfigurationStream(String adapterConfig) {

View file

@ -46,14 +46,14 @@ import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.pep.HttpAuthzRequest;
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.cip.spi.ClaimInformationPointProvider;
import org.keycloak.adapters.authorization.cip.spi.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;
@ -105,7 +105,7 @@ public class ClaimInformationPointProviderTest extends AbstractKeycloakTest {
exchange.setStatusCode(200);
} else if (exchange.getRelativePath().equals("/get-claim-information-provider")) {
if (!"Bearer idTokenString".equals(exchange.getRequestHeaders().getFirst("Authorization"))
if (!"Bearer tokenString".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")
@ -178,8 +178,8 @@ public class ClaimInformationPointProviderTest extends AbstractKeycloakTest {
@Test
public void testBasicClaimsInformationPoint() {
HttpFacade httpFacade = createHttpFacade();
Map<String, List<String>> claims = getClaimInformationProviderForPath("/claims-provider", "claims").resolve(httpFacade);
OIDCHttpFacade httpFacade = createHttpFacade();
Map<String, List<String>> claims = getClaimInformationProviderForPath("/claims-provider", "claims").resolve(new HttpAuthzRequest(httpFacade));
assertEquals("parameter-a", claims.get("claim-from-request-parameter").get(0));
assertEquals("header-b", claims.get("claim-from-header").get(0));
@ -204,9 +204,9 @@ public class ClaimInformationPointProviderTest extends AbstractKeycloakTest {
ObjectMapper mapper = JsonSerialization.mapper;
JsonParser parser = mapper.getFactory().createParser("{\"a\": {\"b\": {\"c\": \"c-value\"}}, \"d\": [\"d-value1\", \"d-value2\"], \"e\": {\"number\": 123}}");
TreeNode treeNode = mapper.readTree(parser);
HttpFacade httpFacade = createHttpFacade(headers, new ByteArrayInputStream(treeNode.toString().getBytes()));
OIDCHttpFacade httpFacade = createHttpFacade(headers, new ByteArrayInputStream(treeNode.toString().getBytes()));
Map<String, List<String>> claims = getClaimInformationProviderForPath("/claims-provider", "claims").resolve(httpFacade);
Map<String, List<String>> claims = getClaimInformationProviderForPath("/claims-provider", "claims").resolve(new HttpAuthzRequest(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));
@ -244,9 +244,9 @@ public class ClaimInformationPointProviderTest extends AbstractKeycloakTest {
+ "\n"
+ "}}");
TreeNode treeNode = mapper.readTree(parser);
HttpFacade httpFacade = createHttpFacade(headers, new ByteArrayInputStream(treeNode.toString().getBytes()));
OIDCHttpFacade httpFacade = createHttpFacade(headers, new ByteArrayInputStream(treeNode.toString().getBytes()));
Map<String, List<String>> claims = getClaimInformationProviderForPath("/claims-from-body-json-object", "claims").resolve(httpFacade);
Map<String, List<String>> claims = getClaimInformationProviderForPath("/claims-from-body-json-object", "claims").resolve(new HttpAuthzRequest(httpFacade));
assertEquals(1, claims.size());
assertEquals(2, claims.get("individualRoles").size());
@ -256,7 +256,7 @@ public class ClaimInformationPointProviderTest extends AbstractKeycloakTest {
headers.put("Content-Type", Arrays.asList("application/json; charset=utf-8"));
httpFacade = createHttpFacade(headers, new ByteArrayInputStream(treeNode.toString().getBytes()));
claims = getClaimInformationProviderForPath("/claims-from-body-json-object", "claims").resolve(httpFacade);
claims = getClaimInformationProviderForPath("/claims-from-body-json-object", "claims").resolve(new HttpAuthzRequest(httpFacade));
assertEquals(1, claims.size());
assertEquals(2, claims.get("individualRoles").size());
@ -266,18 +266,18 @@ public class ClaimInformationPointProviderTest extends AbstractKeycloakTest {
@Test
public void testBodyClaimsInformationPoint() {
HttpFacade httpFacade = createHttpFacade(new HashMap<>(), new ByteArrayInputStream("raw-body-text".getBytes()));
OIDCHttpFacade httpFacade = createHttpFacade(new HashMap<>(), new ByteArrayInputStream("raw-body-text".getBytes()));
Map<String, List<String>> claims = getClaimInformationProviderForPath("/claims-provider", "claims").resolve(httpFacade);
Map<String, List<String>> claims = getClaimInformationProviderForPath("/claims-provider", "claims").resolve(new HttpAuthzRequest(httpFacade));
assertEquals("raw-body-text", claims.get("claim-from-body").get(0));
}
@Test
public void testHttpClaimInformationPointProviderWithoutClaims() {
HttpFacade httpFacade = createHttpFacade();
OIDCHttpFacade httpFacade = createHttpFacade();
Map<String, List<String>> claims = getClaimInformationProviderForPath("/http-get-claim-provider", "http").resolve(httpFacade);
Map<String, List<String>> claims = getClaimInformationProviderForPath("/http-get-claim-provider", "http").resolve(new HttpAuthzRequest(httpFacade));
assertEquals("a-value1", claims.get("a").get(0));
assertEquals("b-value1", claims.get("b").get(0));
@ -292,9 +292,9 @@ public class ClaimInformationPointProviderTest extends AbstractKeycloakTest {
@Test
public void testHttpClaimInformationPointProviderWithClaims() {
HttpFacade httpFacade = createHttpFacade();
OIDCHttpFacade httpFacade = createHttpFacade();
Map<String, List<String>> claims = getClaimInformationProviderForPath("/http-post-claim-provider", "http").resolve(httpFacade);
Map<String, List<String>> claims = getClaimInformationProviderForPath("/http-post-claim-provider", "http").resolve(new HttpAuthzRequest(httpFacade));
assertEquals("a-value1", claims.get("claim-a").get(0));
assertEquals("d-value1", claims.get("claim-d").get(0));
@ -308,7 +308,7 @@ public class ClaimInformationPointProviderTest extends AbstractKeycloakTest {
assertNull(claims.get("d"));
}
private HttpFacade createHttpFacade(Map<String, List<String>> headers, InputStream requestBody) {
private OIDCHttpFacade createHttpFacade(Map<String, List<String>> headers, InputStream requestBody) {
return new OIDCHttpFacade() {
private Request request;
@ -349,7 +349,7 @@ public class ClaimInformationPointProviderTest extends AbstractKeycloakTest {
};
}
private HttpFacade createHttpFacade() {
private OIDCHttpFacade createHttpFacade() {
return createHttpFacade(new HashMap<>(), null);
}

View file

@ -21,10 +21,9 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.keycloak.adapters.authorization.ClaimInformationPointProvider;
import org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory;
import org.keycloak.adapters.authorization.PolicyEnforcer;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProvider;
import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProviderFactory;
import org.keycloak.adapters.authorization.spi.HttpRequest;
public class MyCustomCIPFactory implements ClaimInformationPointProviderFactory<MyCustomCIP> {
@ -33,11 +32,6 @@ public class MyCustomCIPFactory implements ClaimInformationPointProviderFactory<
return "my-custom-cip";
}
@Override
public void init(PolicyEnforcer policyEnforcer) {
}
@Override
public MyCustomCIP create(Map<String, Object> config) {
return new MyCustomCIP(config);
@ -53,7 +47,7 @@ class MyCustomCIP implements ClaimInformationPointProvider {
}
@Override
public Map<String, List<String>> resolve(HttpFacade httpFacade) {
public Map<String, List<String>> resolve(HttpRequest request) {
Map<String, List<String>> claims = new HashMap<>();
claims.put("resolved-claim", Arrays.asList(config.get("claim-value").toString()));

View file

@ -41,6 +41,8 @@ import org.junit.Test;
import org.keycloak.AuthorizationContext;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.pep.HttpAuthzRequest;
import org.keycloak.adapters.pep.HttpAuthzResponse;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.OIDCHttpFacade;
@ -132,7 +134,8 @@ public class PolicyEnforcerClaimsTest extends AbstractKeycloakTest {
headers.put("Authorization", Arrays.asList("Bearer " + token));
AuthorizationContext context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters));
OIDCHttpFacade facade = createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters);
AuthorizationContext context = policyEnforcer.enforce(new HttpAuthzRequest(facade), new HttpAuthzResponse(facade));
assertFalse(context.isGranted());
AuthorizationRequest request = new AuthorizationRequest();
@ -144,22 +147,26 @@ public class PolicyEnforcerClaimsTest extends AbstractKeycloakTest {
assertNotNull(token);
context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters));
facade = createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters);
context = policyEnforcer.enforce(new HttpAuthzRequest(facade), new HttpAuthzResponse(facade));
assertTrue(context.isGranted());
parameters.put("withdrawal.amount", Arrays.asList("200"));
context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters));
facade = createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters);
context = policyEnforcer.enforce(new HttpAuthzRequest(facade), new HttpAuthzResponse(facade));
assertFalse(context.isGranted());
parameters.put("withdrawal.amount", Arrays.asList("50"));
context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters));
facade = createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters);
context = policyEnforcer.enforce(new HttpAuthzRequest(facade), new HttpAuthzResponse(facade));
assertTrue(context.isGranted());
parameters.put("withdrawal.amount", Arrays.asList("10"));
context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters));
facade = createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters);
context = policyEnforcer.enforce(new HttpAuthzRequest(facade), new HttpAuthzResponse(facade));
request = new AuthorizationRequest();
@ -168,7 +175,8 @@ public class PolicyEnforcerClaimsTest extends AbstractKeycloakTest {
response = authzClient.authorization("marta", "password").authorize(request);
token = response.getToken();
context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters));
facade = createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters);
context = policyEnforcer.enforce(new HttpAuthzRequest(facade), new HttpAuthzResponse(facade));
assertTrue(context.isGranted());
request = new AuthorizationRequest();
@ -178,7 +186,8 @@ public class PolicyEnforcerClaimsTest extends AbstractKeycloakTest {
response = authzClient.authorization("marta", "password").authorize(request);
token = response.getToken();
context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", "GET", token, headers, parameters));
facade = createHttpFacade("/api/bank/account/1/withdrawal", "GET", token, headers, parameters);
context = policyEnforcer.enforce(new HttpAuthzRequest(facade), new HttpAuthzResponse(facade));
assertTrue(context.isGranted());
assertEquals(1, context.getPermissions().size());
@ -199,12 +208,14 @@ public class PolicyEnforcerClaimsTest extends AbstractKeycloakTest {
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));
OIDCHttpFacade facade = createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters);
AuthorizationContext context = policyEnforcer.enforce(new HttpAuthzRequest(facade), new HttpAuthzResponse(facade));
assertFalse(context.isGranted());
parameters.put("withdrawal.amount", Arrays.asList("50"));
context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
facade = createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters);
context = policyEnforcer.enforce(new HttpAuthzRequest(facade), new HttpAuthzResponse(facade));
assertTrue(context.isGranted());
assertEquals(1, context.getPermissions().size());
Permission permission = context.getPermissions().get(0);
@ -212,17 +223,20 @@ public class PolicyEnforcerClaimsTest extends AbstractKeycloakTest {
parameters.put("withdrawal.amount", Arrays.asList("200"));
context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
facade = createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters);
context = policyEnforcer.enforce(new HttpAuthzRequest(facade), new HttpAuthzResponse(facade));
assertFalse(context.isGranted());
parameters.put("withdrawal.amount", Arrays.asList("50"));
context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
facade = createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters);
context = policyEnforcer.enforce(new HttpAuthzRequest(facade), new HttpAuthzResponse(facade));
assertTrue(context.isGranted());
parameters.put("withdrawal.amount", Arrays.asList("10"));
context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
facade = createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters);
context = policyEnforcer.enforce(new HttpAuthzRequest(facade), new HttpAuthzResponse(facade));
assertTrue(context.isGranted());
@ -245,27 +259,32 @@ public class PolicyEnforcerClaimsTest extends AbstractKeycloakTest {
headers.put("Authorization", Arrays.asList("Bearer " + token));
AuthorizationContext context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
OIDCHttpFacade facade = createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters);
AuthorizationContext context = policyEnforcer.enforce(new HttpAuthzRequest(facade), new HttpAuthzResponse(facade));
assertFalse(context.isGranted());
parameters.put("withdrawal.amount", Arrays.asList("50"));
context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
facade = createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters);
context = policyEnforcer.enforce(new HttpAuthzRequest(facade), new HttpAuthzResponse(facade));
assertTrue(context.isGranted());
parameters.put("withdrawal.amount", Arrays.asList("200"));
context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
facade = createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters);
context = policyEnforcer.enforce(new HttpAuthzRequest(facade), new HttpAuthzResponse(facade));
assertFalse(context.isGranted());
parameters.put("withdrawal.amount", Arrays.asList("50"));
context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
facade = createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters);
context = policyEnforcer.enforce(new HttpAuthzRequest(facade), new HttpAuthzResponse(facade));
assertTrue(context.isGranted());
parameters.put("withdrawal.amount", Arrays.asList("10"));
context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
facade = createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters);
context = policyEnforcer.enforce(new HttpAuthzRequest(facade), new HttpAuthzResponse(facade));
assertTrue(context.isGranted());
}
@ -289,27 +308,32 @@ public class PolicyEnforcerClaimsTest extends AbstractKeycloakTest {
headers.put("Authorization", Arrays.asList("Bearer " + token));
AuthorizationContext context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
OIDCHttpFacade facade = createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters);
AuthorizationContext context = policyEnforcer.enforce(new HttpAuthzRequest(facade), new HttpAuthzResponse(facade));
assertFalse(context.isGranted());
parameters.put("withdrawal.amount", Arrays.asList("50"));
context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
facade = createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters);
context = policyEnforcer.enforce(new HttpAuthzRequest(facade), new HttpAuthzResponse(facade));
assertTrue(context.isGranted());
parameters.put("withdrawal.amount", Arrays.asList("200"));
context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
facade = createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters);
context = policyEnforcer.enforce(new HttpAuthzRequest(facade), new HttpAuthzResponse(facade));
assertFalse(context.isGranted());
parameters.put("withdrawal.amount", Arrays.asList("50"));
context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
facade = createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters);
context = policyEnforcer.enforce(new HttpAuthzRequest(facade), new HttpAuthzResponse(facade));
assertTrue(context.isGranted());
parameters.put("withdrawal.amount", Arrays.asList("10"));
context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters));
facade = createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters);
context = policyEnforcer.enforce(new HttpAuthzRequest(facade), new HttpAuthzResponse(facade));
assertTrue(context.isGranted());
}

View file

@ -49,6 +49,8 @@ import org.keycloak.KeycloakSecurityContext;
import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.AuthenticatedActionsHandler;
import org.keycloak.adapters.CorsHeaders;
import org.keycloak.adapters.pep.HttpAuthzRequest;
import org.keycloak.adapters.pep.HttpAuthzResponse;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.OIDCHttpFacade;
@ -144,7 +146,7 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getAdapterConfiguration("enforcer-bearer-only.json"));
PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer();
OIDCHttpFacade httpFacade = createHttpFacade("/api/resourcea");
AuthorizationContext context = policyEnforcer.enforce(httpFacade);
AuthorizationContext context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertFalse(context.isGranted());
assertEquals(403, TestResponse.class.cast(httpFacade.getResponse()).getStatus());
@ -159,12 +161,12 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
httpFacade = createHttpFacade("/api/resourcea", token);
context = policyEnforcer.enforce(httpFacade);
context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertTrue(context.isGranted());
httpFacade = createHttpFacade("/api/resourceb");
context = policyEnforcer.enforce(httpFacade);
context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertFalse(context.isGranted());
assertEquals(403, TestResponse.class.cast(httpFacade.getResponse()).getStatus());
}
@ -174,7 +176,7 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getAdapterConfiguration("enforcer-paths.json"));
PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer();
OIDCHttpFacade httpFacade = createHttpFacade("/api/resourcea");
AuthorizationContext context = policyEnforcer.enforce(httpFacade);
AuthorizationContext context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertFalse(context.isGranted());
assertEquals(403, TestResponse.class.cast(httpFacade.getResponse()).getStatus());
@ -189,12 +191,12 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
httpFacade = createHttpFacade("/api/resourcea", token);
context = policyEnforcer.enforce(httpFacade);
context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertTrue(context.isGranted());
httpFacade = createHttpFacade("/");
context = policyEnforcer.enforce(httpFacade);
context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertTrue(context.isGranted());
}
@ -221,7 +223,7 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
}
});
AuthorizationContext context = policyEnforcer.enforce(httpFacade);
AuthorizationContext context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
Permission permission = context.getPermissions().get(0);
Map<String, Set<String>> claims = permission.getClaims();
@ -245,7 +247,7 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
OIDCHttpFacade httpFacade = createHttpFacade("/api/resourcea", token);
AuthorizationContext context = policyEnforcer.enforce(httpFacade);
AuthorizationContext context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
Permission permission = context.getPermissions().get(0);
Map<String, Set<String>> claims = permission.getClaims();
@ -258,7 +260,7 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getAdapterConfiguration("enforcer-on-deny-redirect.json"));
PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer();
OIDCHttpFacade httpFacade = createHttpFacade("/api/resourcea");
AuthorizationContext context = policyEnforcer.enforce(httpFacade);
AuthorizationContext context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertFalse(context.isGranted());
TestResponse response = TestResponse.class.cast(httpFacade.getResponse());
@ -273,7 +275,7 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getAdapterConfiguration("enforcer-bearer-only.json"));
PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer();
OIDCHttpFacade httpFacade = createHttpFacade("/api/unmmaped");
AuthorizationContext context = policyEnforcer.enforce(httpFacade);
AuthorizationContext context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertFalse(context.isGranted());
TestResponse response = TestResponse.class.cast(httpFacade.getResponse());
@ -307,11 +309,11 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer();
OIDCHttpFacade httpFacade = createHttpFacade("/api/resource/public");
AuthorizationContext context = policyEnforcer.enforce(httpFacade);
AuthorizationContext context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertTrue(context.isGranted());
httpFacade = createHttpFacade("/api/resourceb");
context = policyEnforcer.enforce(httpFacade);
context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertFalse(context.isGranted());
TestResponse response = TestResponse.class.cast(httpFacade.getResponse());
assertEquals(403, response.getStatus());
@ -322,17 +324,17 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
String token = oauth.doAccessTokenRequest(oauth.getCurrentQuery().get(OAuth2Constants.CODE), null).getAccessToken();
httpFacade = createHttpFacade("/api/resourcea", token);
context = policyEnforcer.enforce(httpFacade);
context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertTrue(context.isGranted());
httpFacade = createHttpFacade("/api/resourceb", token);
context = policyEnforcer.enforce(httpFacade);
context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertFalse(context.isGranted());
response = TestResponse.class.cast(httpFacade.getResponse());
assertEquals(403, response.getStatus());
httpFacade = createHttpFacade("/api/resource/public", token);
context = policyEnforcer.enforce(httpFacade);
context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertTrue(context.isGranted());
}
@ -342,7 +344,7 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer();
OIDCHttpFacade httpFacade = createHttpFacade("/api/resource/public");
AuthorizationContext context = policyEnforcer.enforce(httpFacade);
AuthorizationContext context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertTrue(context.isGranted());
ClientResource clientResource = getClientResource(RESOURCE_SERVER_CLIENT_ID);
@ -353,14 +355,14 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
// first request caches the path and the entry is invalidated due to the lifespan
httpFacade = createHttpFacade("/api/resource/all-public");
context = policyEnforcer.enforce(httpFacade);
context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertTrue(context.isGranted());
WaitUtils.pause(1000);
// second request can not fail because entry should not be invalidated
httpFacade = createHttpFacade("/api/resource/all-public");
context = policyEnforcer.enforce(httpFacade);
context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertTrue(context.isGranted());
}
@ -389,11 +391,11 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
String token = response.getAccessToken();
OIDCHttpFacade httpFacade = createHttpFacade("/api/any-resource/test", token);
AuthorizationContext context = policyEnforcer.enforce(httpFacade);
AuthorizationContext context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertTrue(context.isGranted());
httpFacade = createHttpFacade("/api/any-resource/test", token);
context = policyEnforcer.enforce(httpFacade);
context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertTrue(context.isGranted());
ResourceRepresentation resource = clientResource.authorization().resources()
@ -402,7 +404,7 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
clientResource.authorization().resources().resource(resource.getId()).remove();
httpFacade = createHttpFacade("/api/any-resource/test", token);
context = policyEnforcer.enforce(httpFacade);
context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertFalse(context.isGranted());
}
@ -412,7 +414,7 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer();
OIDCHttpFacade httpFacade = createHttpFacade("/api/resource/public");
policyEnforcer.enforce(httpFacade);
policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
TestResponse response = TestResponse.class.cast(httpFacade.getResponse());
assertEquals(401, response.getStatus());
}
@ -461,7 +463,7 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
OIDCHttpFacade httpFacade = createHttpFacade("/api/resource-with-scope", token);
AuthorizationContext context = policyEnforcer.enforce(httpFacade);
AuthorizationContext context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertFalse("Should fail because resource does not have any scope named GET", context.isGranted());
assertEquals(403, TestResponse.class.cast(httpFacade.getResponse()).getStatus());
@ -473,18 +475,18 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
deployment = KeycloakDeploymentBuilder.build(getAdapterConfiguration("enforcer-match-http-verbs-scopes.json"));
policyEnforcer = deployment.getPolicyEnforcer();
context = policyEnforcer.enforce(httpFacade);
context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertTrue(context.isGranted());
httpFacade = createHttpFacade("/api/resource-with-scope", token, "POST");
context = policyEnforcer.enforce(httpFacade);
context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertTrue(context.isGranted());
// create a PATCH scope without associated it with the resource so that a PATCH request is denied accordingly even though
// the scope exists on the server
clientResource.authorization().scopes().create(new ScopeRepresentation("PATCH"));
httpFacade = createHttpFacade("/api/resource-with-scope", token, "PATCH");
context = policyEnforcer.enforce(httpFacade);
context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertFalse(context.isGranted());
ScopePermissionRepresentation postPermission = new ScopePermissionRepresentation();
@ -496,7 +498,7 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
permissions.scope().create(postPermission).close();
httpFacade = createHttpFacade("/api/resource-with-scope", token);
context = policyEnforcer.enforce(httpFacade);
context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertFalse(context.isGranted());
postPermission = permissions.scope().findByName(postPermission.getName());
@ -511,11 +513,11 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
token = authorize.getToken();
httpFacade = createHttpFacade("/api/resource-with-scope", token);
context = policyEnforcer.enforce(httpFacade);
context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertTrue(context.isGranted());
httpFacade = createHttpFacade("/api/resource-with-scope", token, "POST");
context = policyEnforcer.enforce(httpFacade);
context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertTrue(context.isGranted());
postPermission = permissions.scope().findByName(postPermission.getName());
@ -526,11 +528,11 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
token = authorize.getToken();
httpFacade = createHttpFacade("/api/resource-with-scope", token);
context = policyEnforcer.enforce(httpFacade);
context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertFalse(context.isGranted());
httpFacade = createHttpFacade("/api/resource-with-scope", token, "POST");
context = policyEnforcer.enforce(httpFacade);
context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertTrue(context.isGranted());
postPermission = permissions.scope().findByName(postPermission.getName());
@ -541,11 +543,11 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
token = authorize.getToken();
httpFacade = createHttpFacade("/api/resource-with-scope", token);
context = policyEnforcer.enforce(httpFacade);
context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertTrue(context.isGranted());
httpFacade = createHttpFacade("/api/resource-with-scope", token, "POST");
context = policyEnforcer.enforce(httpFacade);
context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertTrue(context.isGranted());
postPermission = permissions.scope().findByName(postPermission.getName());
@ -560,11 +562,11 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
token = authorize.getToken();
httpFacade = createHttpFacade("/api/resource-with-scope", token);
context = policyEnforcer.enforce(httpFacade);
context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertTrue(context.isGranted());
httpFacade = createHttpFacade("/api/resource-with-scope", token, "POST");
context = policyEnforcer.enforce(httpFacade);
context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertFalse(context.isGranted());
}
@ -585,7 +587,7 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getAdapterConfiguration("enforcer-bearer-only.json"));
PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer();
OIDCHttpFacade httpFacade = createHttpFacade("/api/check-subject-token");
AuthorizationContext context = policyEnforcer.enforce(httpFacade);
AuthorizationContext context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertFalse(context.isGranted());
assertEquals(403, TestResponse.class.cast(httpFacade.getResponse()).getStatus());
@ -600,7 +602,7 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
httpFacade = createHttpFacade("/api/check-subject-token", token);
context = policyEnforcer.enforce(httpFacade);
context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertTrue(context.isGranted());
}
@ -632,12 +634,12 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
httpFacade = createHttpFacade("/api/check-subject-token", token);
AuthorizationContext context = policyEnforcer.enforce(httpFacade);
AuthorizationContext context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertTrue(context.isGranted());
oauth.doLogout(response.getRefreshToken(), null);
context = policyEnforcer.enforce(httpFacade);
context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertFalse(context.isGranted());
}
@ -686,13 +688,15 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
String token = response.getAccessToken();
for (int i = 0; i < 101; i++) {
policyEnforcer.enforce(createHttpFacade("/api/" + i, token));
OIDCHttpFacade httpFacade = createHttpFacade("/api/" + i, token);
policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
}
assertEquals(101, policyEnforcer.getPathMatcher().getPathCache().size());
for (int i = 101; i < 200; i++) {
policyEnforcer.enforce(createHttpFacade("/api/" + i, token));
OIDCHttpFacade httpFacade = createHttpFacade("/api/" + i, token);
policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
}
assertEquals(200, policyEnforcer.getPathMatcher().getPathCache().size());
@ -706,7 +710,8 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
deployment = KeycloakDeploymentBuilder.build(getAdapterConfiguration("enforcer-lazyload-with-paths.json"));
policyEnforcer = deployment.getPolicyEnforcer();
AuthorizationContext context = policyEnforcer.enforce(createHttpFacade("/api/0", token));
OIDCHttpFacade httpFacade = createHttpFacade("/api/0", token);
AuthorizationContext context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertTrue(context.isGranted());
}
@ -738,7 +743,8 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
OAuthClient.AccessTokenResponse tokeResponse = oauth.doAccessTokenRequest(code, null);
String token = tokeResponse.getAccessToken();
AuthorizationContext context = policyEnforcer.enforce(createHttpFacade("/api-method/foo", token));
OIDCHttpFacade httpFacade = createHttpFacade("/api-method/foo", token);
AuthorizationContext context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
// GET is disabled in the config
assertTrue(context.isGranted());
@ -751,7 +757,8 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest {
assertTrue(PolicyEnforcerConfig.ScopeEnforcementMode.DISABLED.equals(methods.get(0).getScopesEnforcementMode()));
// other verbs should be protected
context = policyEnforcer.enforce(createHttpFacade("/api-method/foo", token, "POST"));
httpFacade = createHttpFacade("/api-method/foo", token, "POST");
context = policyEnforcer.enforce(new HttpAuthzRequest(httpFacade), new HttpAuthzResponse(httpFacade));
assertFalse(context.isGranted());
} finally {

View file

@ -32,7 +32,6 @@ import org.junit.BeforeClass;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.adapters.authentication.JWTClientSecretCredentialsProvider;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
@ -50,6 +49,7 @@ import org.keycloak.models.Constants;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.client.authentication.JWTClientSecretCredentialsProvider;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken;

View file

@ -38,7 +38,6 @@ import org.apache.http.message.BasicNameValuePair;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.authentication.JWTClientCredentialsProvider;
import org.keycloak.client.registration.Auth;
import org.keycloak.common.Profile;
import org.keycloak.common.util.KeycloakUriBuilder;
@ -52,6 +51,7 @@ import org.keycloak.keys.PublicKeyStorageUtils;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.protocol.oidc.client.authentication.JWTClientCredentialsProvider;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;

View file

@ -42,7 +42,6 @@ import org.jetbrains.annotations.NotNull;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.authentication.JWTClientSecretCredentialsProvider;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.authentication.authenticators.client.JWTClientSecretAuthenticator;
import org.keycloak.common.Profile;
@ -56,6 +55,7 @@ import org.keycloak.events.Details;
import org.keycloak.models.ClientSecretConstants;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.protocol.oidc.client.authentication.JWTClientSecretCredentialsProvider;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.representations.idm.ClientPoliciesRepresentation;
import org.keycloak.representations.idm.ClientProfilesRepresentation;

View file

@ -40,7 +40,6 @@ import org.junit.rules.TemporaryFolder;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.adapters.AdapterUtils;
import org.keycloak.adapters.authentication.JWTClientCredentialsProvider;
import org.keycloak.admin.client.resource.ClientAttributeCertificateResource;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.authentication.AuthenticationFlowError;
@ -69,6 +68,7 @@ import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.protocol.oidc.client.authentication.JWTClientCredentialsProvider;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.representations.KeyStoreConfig;

View file

@ -90,12 +90,12 @@
"headers": {
"Content-Type": "application/x-www-form-urlencoded",
"header-b": ["header-b-value1", "header-b-value2"],
"Authorization": "Bearer {keycloak.id_token}"
"Authorization": "Bearer {keycloak.access_token}"
},
"parameters": {
"param-a": ["param-a-value1", "param-a-value2"],
"param-subject": "{keycloak.id_token['/sub']}",
"param-user-name": "{keycloak.id_token['/preferred_username']}"
"param-subject": "{keycloak.access_token['/sub']}",
"param-user-name": "{keycloak.access_token['/preferred_username']}"
}
}
}