diff --git a/adapters/oidc/adapter-core/pom.xml b/adapters/oidc/adapter-core/pom.xml index 2ae2dcab7e..fc339d314a 100755 --- a/adapters/oidc/adapter-core/pom.xml +++ b/adapters/oidc/adapter-core/pom.xml @@ -82,6 +82,11 @@ keycloak-authz-client provided + + org.keycloak + keycloak-policy-enforcer + provided + com.fasterxml.jackson.core jackson-core diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java index 37fe443ec4..c491541c72 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java @@ -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; diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterUtils.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterUtils.java index 8779eea363..cda1635931 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterUtils.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterUtils.java @@ -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 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 formparams) { + Map reqHeaders = new HashMap<>(); + Map reqParams = new HashMap<>(); + ClientCredentialsProviderUtils.setClientCredentials(deployment.getAdapterConfig(), deployment.getClientAuthenticator(), reqHeaders, reqParams); + + for (Map.Entry header : reqHeaders.entrySet()) { + post.setHeader(header.getKey(), header.getValue()); + } + + for (Map.Entry param : reqParams.entrySet()) { + formparams.add(new BasicNameValuePair(param.getKey(), param.getValue())); + } + } } diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java index 057da2c694..c0d19c1651 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java @@ -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) { diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/BasicAuthRequestAuthenticator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/BasicAuthRequestAuthenticator.java index 1b526eaff4..ebdece45a7 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/BasicAuthRequestAuthenticator.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/BasicAuthRequestAuthenticator.java @@ -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); diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java index f1f52af9af..d34c3fa817 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java @@ -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); diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java index 6a0a9a8e03..590ac4b392 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java @@ -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 callable) { client = callable; } + + public AdapterConfig getAdapterConfig() { + return adapterConfig; + } } diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java index 1c1290215b..3626362de5 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java @@ -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(); } } } diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java index 9b3c5d0fa7..a4dc295dd0 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java @@ -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; diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java index 222b5e8603..dcf0d8f58c 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java @@ -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); diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java deleted file mode 100644 index 1f4438f0d0..0000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java +++ /dev/null @@ -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 Pedro Igor - */ -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> 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> claims) { - Request request = httpFacade.getRequest(); - - if (isDefaultAccessDeniedUri(request)) { - return true; - } - - Authorization authorization = accessToken.getAuthorization(); - - if (authorization == null) { - return false; - } - - boolean hasPermission = false; - Collection grantedPermissions = authorization.getPermissions(); - - for (Permission permission : grantedPermissions) { - if (permission.getResourceId() != null) { - if (isResourcePermission(actualPathConfig, permission)) { - hasPermission = true; - - if (actualPathConfig.isInstance() && !matchResourcePermission(actualPathConfig, permission)) { - continue; - } - - if (hasResourceScopePermission(methodConfig, permission)) { - if (LOGGER.isDebugEnabled()) { - LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, grantedPermissions); - } - if (HTTP_METHOD_DELETE.equalsIgnoreCase(request.getMethod()) && actualPathConfig.isInstance()) { - 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> claims) { - Map> grantedClaims = permission.getClaims(); - - if (grantedClaims != null) { - if (claims.isEmpty()) { - return false; - } - - for (Entry> entry : grantedClaims.entrySet()) { - List requestClaims = claims.get(entry.getKey()); - - if (requestClaims == null || requestClaims.isEmpty() || !entry.getValue().containsAll(requestClaims)) { - return false; - } - } - } - - return true; - } - - 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 requiredScopes = methodConfig.getScopes(); - Set allowedScopes = permission.getScopes(); - - if (allowedScopes.isEmpty()) { - return true; - } - - PolicyEnforcerConfig.ScopeEnforcementMode enforcementMode = methodConfig.getScopesEnforcementMode(); - - if (PolicyEnforcerConfig.ScopeEnforcementMode.ALL.equals(enforcementMode)) { - return allowedScopes.containsAll(requiredScopes); - } - - if (PolicyEnforcerConfig.ScopeEnforcementMode.ANY.equals(enforcementMode)) { - for (String requiredScope : requiredScopes) { - if (allowedScopes.contains(requiredScope)) { - return true; - } - } - } - - return requiredScopes.isEmpty(); - } - - private AuthorizationContext createEmptyAuthorizationContext(final boolean granted) { - return new ClientAuthorizationContext(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 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> resolveClaims(PathConfig pathConfig, OIDCHttpFacade httpFacade) { - Map> claims = new HashMap<>(); - - resolveClaims(claims, getEnforcerConfig().getClaimInformationPointConfig(), httpFacade); - resolveClaims(claims, pathConfig.getClaimInformationPointConfig(), httpFacade); - - return claims; - } - - private void resolveClaims(Map> claims, Map> claimInformationPointConfig, HttpFacade httpFacade) { - if (claimInformationPointConfig != null) { - for (Entry> claimDef : claimInformationPointConfig.entrySet()) { - ClaimInformationPointProviderFactory factory = getPolicyEnforcer().getClaimInformationPointProviderFactories().get(claimDef.getKey()); - - if (factory != null) { - claims.putAll(factory.create(claimDef.getValue()).resolve(httpFacade)); - } - } - } - } -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java deleted file mode 100644 index bd7e72bdb4..0000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java +++ /dev/null @@ -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 Pedro Igor - */ -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> 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()); - } - - AccessToken.Authorization newAuthorization = accessToken.getAuthorization(); - - if (newAuthorization != null) { - Collection grantedPermissions = authorization.getPermissions(); - Collection newPermissions = newAuthorization.getPermissions(); - - for (Permission newPermission : newPermissions) { - if (!grantedPermissions.contains(newPermission)) { - grantedPermissions.add(newPermission); - } - } - } - - original.setAuthorization(authorization); - - 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> 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> claims = resolveClaims(pathConfig, httpFacade); - - if (!claims.isEmpty()) { - permissionRequest.setClaims(claims); - } - - return permission.create(permissionRequest).getTicket(); - } - - return null; - } - - private boolean isBearerAuthorization(OIDCHttpFacade httpFacade) { - List 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(); - } -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java deleted file mode 100644 index 2fd9916f5f..0000000000 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java +++ /dev/null @@ -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 Pedro Igor - */ -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 paths; - private final Map 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> requestParams, Map requestHeaders) { - Map formparams = new HashMap<>(); - ClientCredentialsProviderUtils.setClientCredentials(PolicyEnforcer.this.deployment, requestHeaders, formparams); - for (Entry 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 getPaths() { - return paths; - } - - public PathConfigMatcher getPathMatcher() { - return pathMatcher; - } - - public KeycloakDeployment getDeployment() { - return deployment; - } - - public Map getClaimInformationPointProviderFactories() { - return claimInformationPointProviderFactories; - } - - private void loadClaimInformationPointProviders(ServiceLoader loader) { - - for (ClaimInformationPointProviderFactory factory : loader) { - factory.init(this); - - claimInformationPointProviderFactories.put(factory.getName(), factory); - } - } - - private Map 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 paths = configureAllPathsForResourceServer(protectedResource); - - paths.putAll(configureDefinedPaths(protectedResource, enforcerConfig)); - - return paths; - } else { - LOGGER.info("Paths provided in configuration."); - return configureDefinedPaths(protectedResource, enforcerConfig); - } - } - - private Map configureDefinedPaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) { - Map paths = Collections.synchronizedMap(new LinkedHashMap()); - - for (PathConfig pathConfig : enforcerConfig.getPaths()) { - ResourceRepresentation resource; - String resourceName = pathConfig.getName(); - String path = pathConfig.getPath(); - - if (resourceName != null) { - LOGGER.debugf("Trying to find resource with name [%s] for path [%s].", resourceName, path); - resource = protectedResource.findByName(resourceName); - } else { - LOGGER.debugf("Trying to find resource with uri [%s] for path [%s].", path, path); - List resources = protectedResource.findByUri(path); - - if (resources.isEmpty()) { - resources = protectedResource.findByMatchingUri(path); - } - - if (resources.size() == 1) { - resource = resources.get(0); - } else if (resources.size() > 1) { - throw new RuntimeException("Multiple resources found with the same uri"); - } else { - resource = null; - } - } - - if (resource != null) { - pathConfig.setId(resource.getId()); - // if the resource is statically bound to a resource it means the config can not be invalidated - if (resourceName != null) { - pathConfig.setStatic(true); - } - } - - if (PolicyEnforcerConfig.EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) { - pathConfig.setStatic(true); - } - - PathConfig existingPath = null; - - for (PathConfig current : paths.values()) { - if (current.getPath().equals(pathConfig.getPath())) { - existingPath = current; - break; - } - } - - if (existingPath == null) { - paths.put(pathConfig.getPath(), pathConfig); - } else { - existingPath.getMethods().addAll(pathConfig.getMethods()); - existingPath.getScopes().addAll(pathConfig.getScopes()); - } - } - - return paths; - } - - private Map configureAllPathsForResourceServer(ProtectedResource protectedResource) { - LOGGER.info("Querying the server for all resources associated with this application."); - Map paths = Collections.synchronizedMap(new HashMap()); - - if (!enforcerConfig.getLazyLoadPaths()) { - for (String id : protectedResource.findAll()) { - ResourceRepresentation resourceDescription = protectedResource.findById(id); - - if (resourceDescription.getUris() != null && !resourceDescription.getUris().isEmpty()) { - for(PathConfig pathConfig : PathConfig.createPathConfigs(resourceDescription)) { - paths.put(pathConfig.getPath(), pathConfig); - } - } - } - } - - return paths; - } - - public static class PathConfigMatcher extends PathMatcher { - - private final Map paths; - private final PathCache pathCache; - private final AuthzClient authzClient; - private final PolicyEnforcerConfig enforcerConfig; - - public PathConfigMatcher(Map 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 matchingResources = authzClient.protection().resource().findByMatchingUri(targetUri); - - if (matchingResources.isEmpty()) { - // if this config is invalidated (e.g.: due to cache expiration) we remove and return null - if (pathConfig != null && pathConfig.isInvalidated()) { - paths.remove(targetUri); - return null; - } - } else { - Map> cipConfig = null; - PolicyEnforcerConfig.EnforcementMode enforcementMode = PolicyEnforcerConfig.EnforcementMode.ENFORCING; - ResourceRepresentation targetResource = matchingResources.get(0); - List methodConfig = null; - boolean isStatic = false; - - if (pathConfig != null) { - cipConfig = pathConfig.getClaimInformationPointConfig(); - enforcementMode = pathConfig.getEnforcementMode(); - methodConfig = pathConfig.getMethods(); - isStatic = pathConfig.isStatic(); - } else { - for (PathConfig existingPath : paths.values()) { - if (targetResource.getId().equals(existingPath.getId()) - && existingPath.isStatic() - && !PolicyEnforcerConfig.EnforcementMode.DISABLED.equals(existingPath.getEnforcementMode())) { - return null; - } - } - } - - pathConfig = PathConfig.createPathConfigs(targetResource).iterator().next(); - - if (cipConfig != null) { - pathConfig.setClaimInformationPointConfig(cipConfig); - } - - if (methodConfig != null) { - pathConfig.setMethods(methodConfig); - } - - pathConfig.setStatic(isStatic); - pathConfig.setEnforcementMode(enforcementMode); - } - } catch (Exception cause) { - LOGGER.errorf(cause, "Could not lazy load resource with path [" + targetUri + "] from server"); - return null; - } - } - } - - pathCache.put(targetUri, pathConfig); - - return pathConfig; - } - - @Override - protected String getPath(PathConfig entry) { - return entry.getPath(); - } - - @Override - protected Collection getPaths() { - return paths.values(); - } - - public PathCache getPathCache() { - return pathCache; - } - - @Override - protected PathConfig resolvePathConfig(PathConfig originalConfig, String path) { - if (originalConfig.hasPattern()) { - ProtectedResource resource = authzClient.protection().resource(); - - // search by an exact match - List search = resource.findByUri(path); - - // if exact match not found, try to obtain from current path the parent path. - // if path is /resource/1/test and pattern from pathConfig is /resource/{id}/*, parent path is /resource/1 - // this logic allows to match sub resources of a resource instance (/resource/1) to the parent resource, - // so any permission granted to parent also applies to sub resources - if (search.isEmpty()) { - search = resource.findByUri(buildUriFromTemplate(originalConfig.getPath(), path, true)); - } - - if (!search.isEmpty()) { - ResourceRepresentation targetResource = search.get(0); - PathConfig config = PathConfig.createPathConfigs(targetResource).iterator().next(); - - config.setScopes(originalConfig.getScopes()); - config.setMethods(originalConfig.getMethods()); - config.setParentConfig(originalConfig); - config.setEnforcementMode(originalConfig.getEnforcementMode()); - config.setClaimInformationPointConfig(originalConfig.getClaimInformationPointConfig()); - - return config; - } - } - - return null; - } - - public void removeFromCache(String pathConfig) { - pathCache.remove(pathConfig); - } - }; -} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/DirectAccessGrantsLoginModule.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/DirectAccessGrantsLoginModule.java index 0a339b44d6..945a2606d4 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/DirectAccessGrantsLoginModule.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/DirectAccessGrantsLoginModule.java @@ -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 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"); diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/pep/HttpAuthzRequest.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/pep/HttpAuthzRequest.java new file mode 100644 index 0000000000..54c9d119df --- /dev/null +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/pep/HttpAuthzRequest.java @@ -0,0 +1,131 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.adapters.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 Pedro Igor + */ +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 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; + } +} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/pep/HttpAuthzResponse.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/pep/HttpAuthzResponse.java new file mode 100644 index 0000000000..e14d74bc08 --- /dev/null +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/pep/HttpAuthzResponse.java @@ -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 Pedro Igor + */ +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); + } + +} diff --git a/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java b/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java index b7fac2ee45..c607e7bfe4 100644 --- a/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java +++ b/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java @@ -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 Stian Thorgersen diff --git a/adapters/oidc/spring-security/pom.xml b/adapters/oidc/spring-security/pom.xml index 827de8cec0..a72e321e86 100644 --- a/adapters/oidc/spring-security/pom.xml +++ b/adapters/oidc/spring-security/pom.xml @@ -50,6 +50,10 @@ org.keycloak keycloak-adapter-core + + org.keycloak + keycloak-policy-enforcer + org.jboss.spec.javax.servlet jboss-servlet-api_4.0_spec diff --git a/authz/client/pom.xml b/authz/client/pom.xml index f23d247062..d410042e7e 100644 --- a/authz/client/pom.xml +++ b/authz/client/pom.xml @@ -14,7 +14,7 @@ keycloak-authz-client jar - KeyCloak Authz: Client API + Keycloak Authz: Client API KeyCloak AuthZ: Client API diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/AuthzClient.java b/authz/client/src/main/java/org/keycloak/authorization/client/AuthzClient.java index 7844052b1f..cd06d2a4b8 100644 --- a/authz/client/src/main/java/org/keycloak/authorization/client/AuthzClient.java +++ b/authz/client/src/main/java/org/keycloak/authorization/client/AuthzClient.java @@ -91,18 +91,7 @@ public class AuthzClient { * @return a new instance */ public static AuthzClient create(Configuration configuration) { - return new AuthzClient(configuration, configuration.getClientAuthenticator()); - } - - /** - *

Creates a new instance. - * - * @param configuration the client configuration - * @param authenticator the client authenticator - * @return a new instance - */ - public static AuthzClient create(Configuration configuration, ClientAuthenticator authenticator) { - return new AuthzClient(configuration, authenticator); + return new AuthzClient(configuration); } private final ServerConfiguration serverConfiguration; @@ -242,7 +231,7 @@ public class AuthzClient { return this.configuration; } - private AuthzClient(Configuration configuration, ClientAuthenticator authenticator) { + private AuthzClient(Configuration configuration) { if (configuration == null) { throw new IllegalArgumentException("Client configuration can not be null."); } @@ -256,7 +245,7 @@ public class AuthzClient { configurationUrl = KeycloakUriBuilder.fromUri(configurationUrl).clone().path(AUTHZ_DISCOVERY_URL).build(configuration.getRealm()).toString(); this.configuration = configuration; - this.http = new Http(configuration, authenticator != null ? authenticator : configuration.getClientAuthenticator()); + this.http = new Http(configuration, configuration.getClientCredentialsProvider()); try { this.serverConfiguration = this.http.get(configurationUrl) @@ -265,8 +254,6 @@ public class AuthzClient { } catch (Exception e) { throw new RuntimeException("Could not obtain configuration from server [" + configurationUrl + "].", e); } - - this.http.setServerConfiguration(this.serverConfiguration); } private TokenCallable createPatSupplier(String userName, String password) { diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/ClientAuthenticator.java b/authz/client/src/main/java/org/keycloak/authorization/client/ClientAuthenticator.java deleted file mode 100644 index d9077e5c92..0000000000 --- a/authz/client/src/main/java/org/keycloak/authorization/client/ClientAuthenticator.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.keycloak.authorization.client; - -import java.util.List; -import java.util.Map; - -/** - * @author Pedro Igor - */ -public interface ClientAuthenticator { - void configureClientCredentials(Map> requestParams, Map requestHeaders); -} diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/Configuration.java b/authz/client/src/main/java/org/keycloak/authorization/client/Configuration.java index 5bfe1826cd..3186435cf7 100644 --- a/authz/client/src/main/java/org/keycloak/authorization/client/Configuration.java +++ b/authz/client/src/main/java/org/keycloak/authorization/client/Configuration.java @@ -17,14 +17,14 @@ */ package org.keycloak.authorization.client; -import java.util.List; import java.util.Map; import com.fasterxml.jackson.annotation.JsonIgnore; import org.apache.http.client.HttpClient; import org.apache.http.impl.client.HttpClients; +import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider; +import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProviderUtils; import org.keycloak.representations.adapters.config.AdapterConfig; -import org.keycloak.util.BasicAuthHelper; /** * @author Pedro Igor @@ -35,7 +35,7 @@ public class Configuration extends AdapterConfig { private HttpClient httpClient; @JsonIgnore - private ClientAuthenticator clientAuthenticator = createDefaultClientAuthenticator(); + private ClientCredentialsProvider clientCredentialsProvider; public Configuration() { @@ -66,27 +66,18 @@ public class Configuration extends AdapterConfig { return httpClient; } - ClientAuthenticator getClientAuthenticator() { - return this.clientAuthenticator; + public void setHttpClient(HttpClient httpClient) { + this.httpClient = httpClient; } - /** - * Creates a default client authenticator which uses HTTP BASIC and client id and secret to authenticate the client. - * - * @return the default client authenticator - */ - private ClientAuthenticator createDefaultClientAuthenticator() { - return new ClientAuthenticator() { - @Override - public void configureClientCredentials(Map> requestParams, Map requestHeaders) { - String secret = (String) getCredentials().get("secret"); + public void setClientCredentialsProvider(ClientCredentialsProvider clientCredentialsProvider) { + this.clientCredentialsProvider = clientCredentialsProvider; + } - if (secret == null) { - throw new RuntimeException("Client secret not provided."); - } - - requestHeaders.put("Authorization", BasicAuthHelper.RFC6749.createHeader(getResource(), secret)); - } - }; + public ClientCredentialsProvider getClientCredentialsProvider() { + if (clientCredentialsProvider == null) { + clientCredentialsProvider = ClientCredentialsProviderUtils.bootstrapClientAuthenticator(this); + } + return clientCredentialsProvider; } } diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/Http.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/Http.java index eecb7e30af..2f84d5e3ad 100644 --- a/authz/client/src/main/java/org/keycloak/authorization/client/util/Http.java +++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/Http.java @@ -18,9 +18,9 @@ package org.keycloak.authorization.client.util; import org.apache.http.client.methods.RequestBuilder; -import org.keycloak.authorization.client.ClientAuthenticator; import org.keycloak.authorization.client.Configuration; import org.keycloak.authorization.client.representation.ServerConfiguration; +import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider; /** * @author Pedro Igor @@ -28,10 +28,9 @@ import org.keycloak.authorization.client.representation.ServerConfiguration; public class Http { private final Configuration configuration; - private final ClientAuthenticator authenticator; - private ServerConfiguration serverConfiguration; + private final ClientCredentialsProvider authenticator; - public Http(Configuration configuration, ClientAuthenticator authenticator) { + public Http(Configuration configuration, ClientCredentialsProvider authenticator) { this.configuration = configuration; this.authenticator = authenticator; } @@ -55,8 +54,4 @@ public class Http { private HttpMethod method(RequestBuilder builder) { return new HttpMethod(this.configuration, authenticator, builder); } - - public void setServerConfiguration(ServerConfiguration serverConfiguration) { - this.serverConfiguration = serverConfiguration; - } } diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java index b301e38c77..8f0f3c0d2f 100644 --- a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java +++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java @@ -35,28 +35,29 @@ import org.apache.http.client.methods.RequestBuilder; import org.apache.http.entity.ByteArrayEntity; import org.apache.http.message.BasicNameValuePair; import org.apache.http.util.EntityUtils; -import org.keycloak.authorization.client.ClientAuthenticator; import org.keycloak.authorization.client.Configuration; +import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider; /** * @author Pedro Igor */ public class HttpMethod { - private final HttpClient httpClient; - private final ClientAuthenticator authenticator; - protected final RequestBuilder builder; - protected final Configuration configuration; - protected final Map headers; - protected final Map> params; private static final Logger logger = Logger.getLogger(HttpMethod.class.getName()); + + private final HttpClient httpClient; + final RequestBuilder builder; + final Configuration configuration; + final Map headers; + final Map> params; + private final ClientCredentialsProvider authenticator; private HttpMethodResponse response; - public HttpMethod(Configuration configuration, ClientAuthenticator authenticator, RequestBuilder builder) { + public HttpMethod(Configuration configuration, ClientCredentialsProvider authenticator, RequestBuilder builder) { this(configuration, authenticator, builder, new HashMap>(), new HashMap()); } - public HttpMethod(Configuration configuration, ClientAuthenticator authenticator, RequestBuilder builder, Map> params, Map headers) { + public HttpMethod(Configuration configuration, ClientCredentialsProvider authenticator, RequestBuilder builder, Map> params, Map headers) { this.configuration = configuration; this.httpClient = configuration.getHttpClient(); this.authenticator = authenticator; diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodAuthenticator.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodAuthenticator.java index 9f467228ee..9c9978bfa9 100644 --- a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodAuthenticator.java +++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodAuthenticator.java @@ -18,11 +18,16 @@ package org.keycloak.authorization.client.util; import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import org.apache.http.Header; import org.keycloak.OAuth2Constants; -import org.keycloak.authorization.client.ClientAuthenticator; +import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider; +import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProviderUtils; import org.keycloak.representations.idm.authorization.AuthorizationRequest; import org.keycloak.representations.idm.authorization.AuthorizationRequest.Metadata; import org.keycloak.representations.idm.authorization.Permission; @@ -34,16 +39,16 @@ import org.keycloak.representations.idm.authorization.PermissionTicketToken; public class HttpMethodAuthenticator { private final HttpMethod method; - private final ClientAuthenticator authenticator; + private ClientCredentialsProvider clientCredentialProvider; - public HttpMethodAuthenticator(HttpMethod method, ClientAuthenticator authenticator) { + public HttpMethodAuthenticator(HttpMethod method, ClientCredentialsProvider clientCredentialsProvider) { this.method = method; - this.authenticator = authenticator; + this.clientCredentialProvider = clientCredentialsProvider; } public HttpMethod client() { this.method.params.put(OAuth2Constants.GRANT_TYPE, Arrays.asList(OAuth2Constants.CLIENT_CREDENTIALS)); - authenticator.configureClientCredentials(this.method.params, this.method.headers); + configureClientCredentials(this.method.params, this.method.headers); return this.method; } @@ -133,4 +138,12 @@ public class HttpMethodAuthenticator { return method; } + + private void configureClientCredentials(Map> requestParams, Map requestHeaders) { + Map formparams = new HashMap<>(); + ClientCredentialsProviderUtils.setClientCredentials(method.configuration, clientCredentialProvider, requestHeaders, formparams); + for (Entry param : formparams.entrySet()) { + requestParams.put(param.getKey(), Arrays.asList(param.getValue())); + } + } } diff --git a/authz/policy-enforcer/pom.xml b/authz/policy-enforcer/pom.xml new file mode 100755 index 0000000000..f1706319b6 --- /dev/null +++ b/authz/policy-enforcer/pom.xml @@ -0,0 +1,65 @@ + + + + + + org.keycloak + keycloak-authz-parent + 999.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + keycloak-policy-enforcer + Keycloak Authz: Policy Enforcer + jar + + + + org.jboss.logging + jboss-logging + + + org.keycloak + keycloak-authz-client + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-annotations + + + junit + junit + test + + + org.apache.httpcomponents + httpclient + + + + diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathCache.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PathCache.java similarity index 99% rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathCache.java rename to authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PathCache.java index 8cb2dc4e0a..0d90887dab 100644 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathCache.java +++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PathCache.java @@ -51,7 +51,7 @@ public class PathCache { * @param maxAge the time in milliseconds that an entry can stay in the cache. If {@code -1}, entries never expire * @param paths the pre-configured paths */ - public PathCache(final int maxEntries, long maxAge, + PathCache(final int maxEntries, long maxAge, Map paths) { cache = new LinkedHashMap(16, DEFAULT_LOAD_FACTOR, true) { @Override diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PathConfigMatcher.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PathConfigMatcher.java new file mode 100644 index 0000000000..2e1b75a3fc --- /dev/null +++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PathConfigMatcher.java @@ -0,0 +1,293 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.adapters.authorization; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.jboss.logging.Logger; +import org.keycloak.authorization.client.AuthzClient; +import org.keycloak.authorization.client.resource.ProtectedResource; +import org.keycloak.common.util.PathMatcher; +import org.keycloak.representations.adapters.config.PolicyEnforcerConfig; +import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathCacheConfig; +import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig; +import org.keycloak.representations.idm.authorization.ResourceRepresentation; + +/** + * @author Pedro Igor + */ +public class PathConfigMatcher extends PathMatcher { + + private static Logger LOGGER = Logger.getLogger(PolicyEnforcer.class); + + private final Map paths; + private final PathCache pathCache; + private final AuthzClient authzClient; + private final PolicyEnforcerConfig enforcerConfig; + + PathConfigMatcher(PolicyEnforcerConfig enforcerConfig, AuthzClient authzClient) { + this.enforcerConfig = enforcerConfig; + PathCacheConfig cacheConfig = enforcerConfig.getPathCacheConfig(); + + if (cacheConfig == null) { + cacheConfig = new PathCacheConfig(); + } + + this.authzClient = authzClient; + this.paths = configurePaths(); + this.pathCache = new PathCache(cacheConfig.getMaxEntries(), cacheConfig.getLifespan(), paths); + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Initialization complete. Path configuration:"); + for (PathConfig pathConfig : this.paths.values()) { + LOGGER.debug(pathConfig); + } + } + } + + @Override + public PathConfig matches(String targetUri) { + PathConfig pathConfig = pathCache.get(targetUri); + + if (pathCache.containsKey(targetUri) || pathConfig != null) { + return pathConfig; + } + + pathConfig = super.matches(targetUri); + + if (enforcerConfig.getLazyLoadPaths() || enforcerConfig.getPathCacheConfig() != null) { + if ((pathConfig == null || pathConfig.isInvalidated() || pathConfig.getPath().contains("*"))) { + try { + List matchingResources = authzClient.protection().resource().findByMatchingUri(targetUri); + + if (matchingResources.isEmpty()) { + // if this config is invalidated (e.g.: due to cache expiration) we remove and return null + if (pathConfig != null && pathConfig.isInvalidated()) { + paths.remove(targetUri); + return null; + } + } else { + Map> cipConfig = null; + PolicyEnforcerConfig.EnforcementMode enforcementMode = PolicyEnforcerConfig.EnforcementMode.ENFORCING; + ResourceRepresentation targetResource = matchingResources.get(0); + List methodConfig = null; + boolean isStatic = false; + + if (pathConfig != null) { + cipConfig = pathConfig.getClaimInformationPointConfig(); + enforcementMode = pathConfig.getEnforcementMode(); + methodConfig = pathConfig.getMethods(); + isStatic = pathConfig.isStatic(); + } else { + for (PathConfig existingPath : paths.values()) { + if (targetResource.getId().equals(existingPath.getId()) + && existingPath.isStatic() + && !org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode.DISABLED.equals(existingPath.getEnforcementMode())) { + return null; + } + } + } + + pathConfig = PathConfig.createPathConfigs(targetResource).iterator().next(); + + if (cipConfig != null) { + pathConfig.setClaimInformationPointConfig(cipConfig); + } + + if (methodConfig != null) { + pathConfig.setMethods(methodConfig); + } + + pathConfig.setStatic(isStatic); + pathConfig.setEnforcementMode(enforcementMode); + } + } catch (Exception cause) { + LOGGER.errorf(cause, "Could not lazy load resource with path [" + targetUri + "] from server"); + return null; + } + } + } + + pathCache.put(targetUri, pathConfig); + + return pathConfig; + } + + @Override + protected String getPath(PathConfig entry) { + return entry.getPath(); + } + + @Override + protected Collection getPaths() { + return paths.values(); + } + + public PathCache getPathCache() { + return pathCache; + } + + @Override + protected PathConfig resolvePathConfig(PathConfig originalConfig, String path) { + if (originalConfig.hasPattern()) { + ProtectedResource resource = authzClient.protection().resource(); + + // search by an exact match + List search = resource.findByUri(path); + + // if exact match not found, try to obtain from current path the parent path. + // if path is /resource/1/test and pattern from pathConfig is /resource/{id}/*, parent path is /resource/1 + // this logic allows to match sub resources of a resource instance (/resource/1) to the parent resource, + // so any permission granted to parent also applies to sub resources + if (search.isEmpty()) { + search = resource.findByUri(buildUriFromTemplate(originalConfig.getPath(), path, true)); + } + + if (!search.isEmpty()) { + ResourceRepresentation targetResource = search.get(0); + PathConfig config = PathConfig.createPathConfigs(targetResource).iterator().next(); + + config.setScopes(originalConfig.getScopes()); + config.setMethods(originalConfig.getMethods()); + config.setParentConfig(originalConfig); + config.setEnforcementMode(originalConfig.getEnforcementMode()); + config.setClaimInformationPointConfig(originalConfig.getClaimInformationPointConfig()); + + return config; + } + } + + return null; + } + + public void removeFromCache(String pathConfig) { + pathCache.remove(pathConfig); + } + + public Map getPathConfig() { + return paths; + } + + private Map configurePaths() { + ProtectedResource protectedResource = this.authzClient.protection().resource(); + boolean loadPathsFromServer = !enforcerConfig.getLazyLoadPaths(); + + for (PathConfig pathConfig : enforcerConfig.getPaths()) { + if (!org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) { + loadPathsFromServer = false; + break; + } + } + + if (loadPathsFromServer) { + LOGGER.info("No path provided in configuration."); + Map paths = configureAllPathsForResourceServer(protectedResource); + + paths.putAll(configureDefinedPaths(protectedResource, enforcerConfig)); + + return paths; + } else { + LOGGER.info("Paths provided in configuration."); + return configureDefinedPaths(protectedResource, enforcerConfig); + } + } + + private Map configureDefinedPaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) { + Map paths = Collections.synchronizedMap(new LinkedHashMap()); + + for (PathConfig pathConfig : enforcerConfig.getPaths()) { + ResourceRepresentation resource; + String resourceName = pathConfig.getName(); + String path = pathConfig.getPath(); + + if (resourceName != null) { + LOGGER.debugf("Trying to find resource with name [%s] for path [%s].", resourceName, path); + resource = protectedResource.findByName(resourceName); + } else { + LOGGER.debugf("Trying to find resource with uri [%s] for path [%s].", path, path); + List resources = protectedResource.findByUri(path); + + if (resources.isEmpty()) { + resources = protectedResource.findByMatchingUri(path); + } + + if (resources.size() == 1) { + resource = resources.get(0); + } else if (resources.size() > 1) { + throw new RuntimeException("Multiple resources found with the same uri"); + } else { + resource = null; + } + } + + if (resource != null) { + pathConfig.setId(resource.getId()); + // if the resource is statically bound to a resource it means the config can not be invalidated + if (resourceName != null) { + pathConfig.setStatic(true); + } + } + + if (org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) { + pathConfig.setStatic(true); + } + + PathConfig existingPath = null; + + for (PathConfig current : paths.values()) { + if (current.getPath().equals(pathConfig.getPath())) { + existingPath = current; + break; + } + } + + if (existingPath == null) { + paths.put(pathConfig.getPath(), pathConfig); + } else { + existingPath.getMethods().addAll(pathConfig.getMethods()); + existingPath.getScopes().addAll(pathConfig.getScopes()); + } + } + + return paths; + } + + private Map configureAllPathsForResourceServer(ProtectedResource protectedResource) { + LOGGER.info("Querying the server for all resources associated with this application."); + Map paths = Collections.synchronizedMap(new HashMap()); + + if (!enforcerConfig.getLazyLoadPaths()) { + for (String id : protectedResource.findAll()) { + ResourceRepresentation resourceDescription = protectedResource.findById(id); + + if (resourceDescription.getUris() != null && !resourceDescription.getUris().isEmpty()) { + for(PathConfig pathConfig : PathConfig.createPathConfigs(resourceDescription)) { + paths.put(pathConfig.getPath(), pathConfig); + } + } + } + } + + return paths; + } +} diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java new file mode 100644 index 0000000000..3c25e8d217 --- /dev/null +++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java @@ -0,0 +1,647 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package org.keycloak.adapters.authorization; + +import static org.keycloak.adapters.authorization.util.JsonUtils.asAccessToken; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.ServiceLoader; +import java.util.Set; + +import org.apache.http.client.HttpClient; +import org.jboss.logging.Logger; +import org.keycloak.AuthorizationContext; +import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProviderFactory; +import org.keycloak.adapters.authorization.spi.HttpRequest; +import org.keycloak.adapters.authorization.spi.HttpResponse; +import org.keycloak.authorization.client.AuthorizationDeniedException; +import org.keycloak.authorization.client.AuthzClient; +import org.keycloak.authorization.client.ClientAuthorizationContext; +import org.keycloak.authorization.client.Configuration; +import org.keycloak.authorization.client.resource.PermissionResource; +import org.keycloak.authorization.client.resource.ProtectionResource; +import org.keycloak.common.util.Base64; +import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.AccessToken.Authorization; +import org.keycloak.representations.adapters.config.PolicyEnforcerConfig; +import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode; +import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.MethodConfig; +import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig; +import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.ScopeEnforcementMode; +import org.keycloak.representations.idm.authorization.AuthorizationRequest; +import org.keycloak.representations.idm.authorization.AuthorizationResponse; +import org.keycloak.representations.idm.authorization.Permission; +import org.keycloak.representations.idm.authorization.PermissionRequest; +import org.keycloak.util.JsonSerialization; + +/** + *

A Policy Enforcement Point (PEP) that requests and enforces authorization decisions from Keycloak. + * + * @author Pedro Igor + */ +public class PolicyEnforcer { + + private static Logger LOGGER = Logger.getLogger(PolicyEnforcer.class); + private static final String HTTP_METHOD_DELETE = "DELETE"; + + public static Builder builder() { + return new Builder(); + } + + private final AuthzClient authzClient; + private final Map paths; + private final PathConfigMatcher pathMatcher; + private final HttpClient httpClient; + private final PolicyEnforcerConfig enforcerConfig; + + private final Map claimInformationPointProviderFactories = new HashMap<>(); + + protected PolicyEnforcer(Builder builder) { + enforcerConfig = builder.getEnforcerConfig(); + authzClient = AuthzClient.create(builder.authzClientConfig); + httpClient = authzClient.getConfiguration().getHttpClient(); + pathMatcher = new PathConfigMatcher(builder.getEnforcerConfig(), authzClient); + paths = pathMatcher.getPathConfig(); + + loadClaimInformationPointProviders(ServiceLoader.load(ClaimInformationPointProviderFactory.class, ClaimInformationPointProviderFactory.class.getClassLoader())); + loadClaimInformationPointProviders(ServiceLoader.load(ClaimInformationPointProviderFactory.class, Thread.currentThread().getContextClassLoader())); + } + + public AuthorizationContext enforce(HttpRequest request, HttpResponse response) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debugv("Policy enforcement is enabled. Enforcing policy decisions for path [{0}].", request.getURI()); + } + + AuthorizationContext context = authorize(request, response); + + if (LOGGER.isDebugEnabled()) { + LOGGER.debugv("Policy enforcement result for path [{0}] is : {1}", request.getURI(), context.isGranted() ? "GRANTED" : "DENIED"); + LOGGER.debugv("Returning authorization context with permissions:"); + for (Permission permission : context.getPermissions()) { + LOGGER.debug(permission); + } + } + + return context; + } + + public HttpClient getHttpClient() { + return httpClient; + } + + public Map getPaths() { + return Collections.unmodifiableMap(paths); + } + + public Map getClaimInformationPointProviderFactories() { + return claimInformationPointProviderFactories; + } + + public PathConfigMatcher getPathMatcher() { + return pathMatcher; + } + + private AuthorizationContext authorize(HttpRequest request, HttpResponse response) { + EnforcementMode enforcementMode = enforcerConfig.getEnforcementMode(); + TokenPrincipal principal = request.getPrincipal(); + boolean anonymous = principal == null || principal.getRawToken() == null; + + if (EnforcementMode.DISABLED.equals(enforcementMode)) { + if (anonymous) { + response.sendError(401, "Invalid bearer"); + } + return createEmptyAuthorizationContext(true); + } + + PathConfig pathConfig = getPathConfig(request); + + if (anonymous) { + if (!isDefaultAccessDeniedUri(request)) { + if (pathConfig != null) { + if (EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) { + return createEmptyAuthorizationContext(true); + } else { + challenge(pathConfig, getRequiredScopes(pathConfig, request), request, response); + } + } else { + handleAccessDenied(response); + } + } + return createEmptyAuthorizationContext(false); + } + + AccessToken accessToken = principal.getToken(); + + if (accessToken != null) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig); + } + + if (pathConfig == null) { + if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) { + return createAuthorizationContext(accessToken, null); + } + + if (LOGGER.isDebugEnabled()) { + LOGGER.debugf("Could not find a configuration for path [%s]", getPath(request)); + } + + if (isDefaultAccessDeniedUri(request)) { + return createAuthorizationContext(accessToken, null); + } + + handleAccessDenied(response); + + return createEmptyAuthorizationContext(false); + } + + if (EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) { + return createAuthorizationContext(accessToken, pathConfig); + } + + MethodConfig methodConfig = getRequiredScopes(pathConfig, request); + Map> claims = resolveClaims(pathConfig, request); + + if (isAuthorized(pathConfig, methodConfig, accessToken, request, claims)) { + try { + return createAuthorizationContext(accessToken, pathConfig); + } catch (Exception e) { + throw new RuntimeException("Error processing path [" + pathConfig.getPath() + "].", e); + } + } + + AccessToken original = accessToken; + + accessToken = requestAuthorizationToken(pathConfig, methodConfig, request, claims); + + if (accessToken != null) { + AccessToken.Authorization authorization = original.getAuthorization(); + + if (authorization == null) { + authorization = new AccessToken.Authorization(); + authorization.setPermissions(new ArrayList()); + } + + AccessToken.Authorization newAuthorization = accessToken.getAuthorization(); + + if (newAuthorization != null) { + Collection grantedPermissions = authorization.getPermissions(); + Collection newPermissions = newAuthorization.getPermissions(); + + for (Permission newPermission : newPermissions) { + if (!grantedPermissions.contains(newPermission)) { + grantedPermissions.add(newPermission); + } + } + } + + original.setAuthorization(authorization); + + if (isAuthorized(pathConfig, methodConfig, accessToken, request, claims)) { + try { + return createAuthorizationContext(accessToken, pathConfig); + } catch (Exception e) { + throw new RuntimeException("Error processing path [" + pathConfig.getPath() + "].", e); + } + } + } + + if (methodConfig != null && ScopeEnforcementMode.DISABLED.equals(methodConfig.getScopesEnforcementMode())) { + return createEmptyAuthorizationContext(true); + } + + if (LOGGER.isDebugEnabled()) { + LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig); + } + + if (!challenge(pathConfig, methodConfig, request, response)) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debugf("Challenge not sent, sending default forbidden response. Path [%s]", pathConfig); + } + handleAccessDenied(response); + } + } + + return createEmptyAuthorizationContext(false); + } + + protected boolean isAuthorized(PathConfig actualPathConfig, MethodConfig methodConfig, AccessToken accessToken, HttpRequest request, Map> claims) { + if (isDefaultAccessDeniedUri(request)) { + return true; + } + + Authorization authorization = accessToken.getAuthorization(); + + if (authorization == null) { + return false; + } + + boolean hasPermission = false; + Collection grantedPermissions = authorization.getPermissions(); + + for (Permission permission : grantedPermissions) { + if (permission.getResourceId() != null) { + if (isResourcePermission(actualPathConfig, permission)) { + hasPermission = true; + + if (actualPathConfig.isInstance() && !matchResourcePermission(actualPathConfig, permission)) { + continue; + } + + if (hasResourceScopePermission(methodConfig, permission)) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, grantedPermissions); + } + if (HTTP_METHOD_DELETE.equalsIgnoreCase(request.getMethod()) && actualPathConfig.isInstance()) { + pathMatcher.removeFromCache(getPath(request)); + } + + return hasValidClaims(permission, claims); + } + } + } else { + if (hasResourceScopePermission(methodConfig, permission)) { + return true; + } + } + } + + if (!hasPermission && EnforcementMode.PERMISSIVE.equals(actualPathConfig.getEnforcementMode())) { + return true; + } + + if (LOGGER.isDebugEnabled()) { + LOGGER.debugf("Authorization FAILED for path [%s]. Not enough permissions [%s].", actualPathConfig, grantedPermissions); + } + + return false; + } + + protected Map> resolveClaims(PathConfig pathConfig, HttpRequest request) { + Map> claims = new HashMap<>(); + + resolveClaims(claims, enforcerConfig.getClaimInformationPointConfig(), request); + resolveClaims(claims, pathConfig.getClaimInformationPointConfig(), request); + + return claims; + } + + protected boolean challenge(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, HttpRequest request, HttpResponse response) { + if (isBearerAuthorization(request)) { + String ticket = getPermissionTicket(pathConfig, methodConfig, authzClient, request); + + if (ticket != null) { + response.sendError(401); + response.setHeader("WWW-Authenticate", new StringBuilder("UMA realm=\"").append(authzClient.getConfiguration().getRealm()).append("\"").append(",as_uri=\"") + .append(authzClient.getServerConfiguration().getIssuer()).append("\"").append(",ticket=\"").append(ticket).append("\"").toString()); + } else { + response.sendError(403); + } + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Sending challenge"); + } + + return true; + } + + handleAccessDenied(response); + + return true; + } + + protected void handleAccessDenied(HttpResponse response) { + String accessDeniedPath = enforcerConfig.getOnDenyRedirectTo(); + + if (accessDeniedPath != null) { + response.sendError(302); + response.setHeader("Location", accessDeniedPath); + } else { + response.sendError(403); + } + } + + private boolean hasValidClaims(Permission permission, Map> claims) { + Map> grantedClaims = permission.getClaims(); + + if (grantedClaims != null) { + if (claims.isEmpty()) { + return false; + } + + for (Entry> entry : grantedClaims.entrySet()) { + List requestClaims = claims.get(entry.getKey()); + + if (requestClaims == null || requestClaims.isEmpty() || !entry.getValue().containsAll(requestClaims)) { + return false; + } + } + } + + return true; + } + + private boolean isDefaultAccessDeniedUri(HttpRequest request) { + String accessDeniedPath = enforcerConfig.getOnDenyRedirectTo(); + return accessDeniedPath != null && request.getURI().contains(accessDeniedPath); + } + + private boolean hasResourceScopePermission(MethodConfig methodConfig, Permission permission) { + List requiredScopes = methodConfig.getScopes(); + Set allowedScopes = permission.getScopes(); + + if (allowedScopes.isEmpty()) { + return true; + } + + PolicyEnforcerConfig.ScopeEnforcementMode enforcementMode = methodConfig.getScopesEnforcementMode(); + + if (PolicyEnforcerConfig.ScopeEnforcementMode.ALL.equals(enforcementMode)) { + return allowedScopes.containsAll(requiredScopes); + } + + if (PolicyEnforcerConfig.ScopeEnforcementMode.ANY.equals(enforcementMode)) { + for (String requiredScope : requiredScopes) { + if (allowedScopes.contains(requiredScope)) { + return true; + } + } + } + + return requiredScopes.isEmpty(); + } + + private AuthorizationContext createEmptyAuthorizationContext(final boolean granted) { + return new ClientAuthorizationContext(authzClient) { + @Override + public boolean hasPermission(String resourceName, String scopeName) { + return granted; + } + + @Override + public boolean hasResourcePermission(String resourceName) { + return granted; + } + + @Override + public boolean hasScopePermission(String scopeName) { + return granted; + } + + @Override + public List getPermissions() { + return Collections.EMPTY_LIST; + } + + @Override + public boolean isGranted() { + return granted; + } + }; + } + + private String getPath(HttpRequest request) { + return request.getRelativePath(); + } + + private MethodConfig getRequiredScopes(PathConfig pathConfig, HttpRequest request) { + String method = request.getMethod(); + + for (MethodConfig methodConfig : pathConfig.getMethods()) { + if (methodConfig.getMethod().equals(method)) { + return methodConfig; + } + } + + MethodConfig methodConfig = new MethodConfig(); + + methodConfig.setMethod(request.getMethod()); + List scopes = new ArrayList<>(); + + if (Boolean.TRUE.equals(enforcerConfig.getHttpMethodAsScope())) { + scopes.add(request.getMethod()); + } else { + scopes.addAll(pathConfig.getScopes()); + } + + methodConfig.setScopes(scopes); + methodConfig.setScopesEnforcementMode(PolicyEnforcerConfig.ScopeEnforcementMode.ANY); + + return methodConfig; + } + + private AuthorizationContext createAuthorizationContext(AccessToken accessToken, PathConfig pathConfig) { + return new ClientAuthorizationContext(accessToken, pathConfig, authzClient); + } + + private boolean isResourcePermission(PathConfig actualPathConfig, Permission permission) { + // first we try a match using resource id + boolean resourceMatch = matchResourcePermission(actualPathConfig, permission); + + // as a fallback, check if the current path is an instance and if so, check if parent's id matches the permission + if (!resourceMatch && actualPathConfig.isInstance()) { + resourceMatch = matchResourcePermission(actualPathConfig.getParentConfig(), permission); + } + + return resourceMatch; + } + + private boolean matchResourcePermission(PathConfig actualPathConfig, Permission permission) { + return permission.getResourceId().equals(actualPathConfig.getId()); + } + + private PathConfig getPathConfig(HttpRequest request) { + return isDefaultAccessDeniedUri(request) ? null : pathMatcher.matches(getPath(request)); + } + + private AccessToken requestAuthorizationToken(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, HttpRequest request, Map> claims) { + if (enforcerConfig.getUserManagedAccess() != null) { + return null; + } + + try { + TokenPrincipal principal = request.getPrincipal(); + String accessTokenString = principal.getRawToken(); + AccessToken accessToken = principal.getToken(); + AuthorizationRequest authzRequest = new AuthorizationRequest(); + + if (isBearerAuthorization(request) || accessToken.getAuthorization() != null) { + authzRequest.addPermission(pathConfig.getId(), methodConfig.getScopes()); + } + + if (!claims.isEmpty()) { + authzRequest.setClaimTokenFormat("urn:ietf:params:oauth:token-type:jwt"); + authzRequest.setClaimToken(Base64.encodeBytes(JsonSerialization.writeValueAsBytes(claims))); + } + + if (accessToken.getAuthorization() != null) { + authzRequest.setRpt(accessTokenString); + } + + LOGGER.debug("Obtaining authorization for authenticated user."); + AuthorizationResponse authzResponse; + + if (isBearerAuthorization(request)) { + authzRequest.setSubjectToken(accessTokenString); + authzResponse = authzClient.authorization().authorize(authzRequest); + } else { + authzResponse = authzClient.authorization(accessTokenString).authorize(authzRequest); + } + + if (authzResponse != null) { + return asAccessToken(authzResponse.getToken()); + } + } catch (AuthorizationDeniedException ignore) { + LOGGER.debug("Authorization denied", ignore); + } catch (Exception e) { + LOGGER.debug("Authorization failed", e); + } + + return null; + } + + private String getPermissionTicket(PathConfig pathConfig, MethodConfig methodConfig, AuthzClient authzClient, HttpRequest httpFacade) { + if (enforcerConfig.getUserManagedAccess() != null) { + ProtectionResource protection = authzClient.protection(); + PermissionResource permission = protection.permission(); + PermissionRequest permissionRequest = new PermissionRequest(); + + permissionRequest.setResourceId(pathConfig.getId()); + permissionRequest.setScopes(new HashSet<>(methodConfig.getScopes())); + + Map> claims = resolveClaims(pathConfig, httpFacade); + + if (!claims.isEmpty()) { + permissionRequest.setClaims(claims); + } + + return permission.create(permissionRequest).getTicket(); + } + + return null; + } + + private boolean isBearerAuthorization(HttpRequest request) { + List authHeaders = request.getHeaders("Authorization"); + + if (authHeaders != null) { + for (String authHeader : authHeaders) { + String[] split = authHeader.trim().split("\\s+"); + if (split == null || split.length != 2) continue; + if (!split[0].equalsIgnoreCase("Bearer")) continue; + return true; + } + } + + return authzClient.getConfiguration().isBearerOnly(); + } + + private void loadClaimInformationPointProviders(ServiceLoader loader) { + for (ClaimInformationPointProviderFactory factory : loader) { + factory.init(this); + + claimInformationPointProviderFactories.put(factory.getName(), factory); + } + } + + private void resolveClaims(Map> claims, Map> claimInformationPointConfig, HttpRequest request) { + if (claimInformationPointConfig != null) { + for (Entry> claimDef : claimInformationPointConfig.entrySet()) { + ClaimInformationPointProviderFactory factory = claimInformationPointProviderFactories.get(claimDef.getKey()); + + if (factory != null) { + claims.putAll(factory.create(claimDef.getValue()).resolve(request)); + } + } + } + } + + public static class Builder { + + Configuration authzClientConfig = new Configuration(); + + private Builder() { + } + + public Builder authServerUrl(String authServerUrl) { + authzClientConfig.setAuthServerUrl(authServerUrl); + return this; + } + + public Builder realm(String realm) { + authzClientConfig.setRealm(realm); + return this; + } + + public Builder clientId(String clientId) { + authzClientConfig.setResource(clientId); + return this; + } + + public Builder bearerOnly(boolean bearerOnly) { + authzClientConfig.setBearerOnly(bearerOnly); + return this; + } + + public Builder credentials(Map credentials) { + authzClientConfig.setCredentials(credentials); + return this; + } + + public Builder enforcerConfig(PolicyEnforcerConfig enforcerConfig) { + authzClientConfig.setPolicyEnforcerConfig(enforcerConfig); + return this; + } + + public Builder enforcerConfig(InputStream is) { + try { + enforcerConfig(JsonSerialization.readValue(is, PolicyEnforcerConfig.class)); + } catch (Exception cause) { + throw new RuntimeException("Failed to read configuration", cause); + } + return this; + } + + public Builder httpClient(HttpClient httpClient) { + authzClientConfig.setHttpClient(httpClient); + return this; + } + + public Builder credentialProvider(ClientCredentialsProvider credentialsProvider) { + authzClientConfig.setClientCredentialsProvider(credentialsProvider); + return this; + } + + public PolicyEnforcer build() { + return new PolicyEnforcer(this); + } + + PolicyEnforcerConfig getEnforcerConfig() { + return authzClientConfig.getPolicyEnforcerConfig(); + } + } +} diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/TokenPrincipal.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/TokenPrincipal.java new file mode 100644 index 0000000000..d47434382b --- /dev/null +++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/TokenPrincipal.java @@ -0,0 +1,56 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.adapters.authorization; + +import java.security.Principal; + +import org.keycloak.adapters.authorization.util.JsonUtils; +import org.keycloak.representations.AccessToken; + +/** + * A {@link Principal} backed by a token representing the entity requesting permissions. + * + * @author Pedro Igor + */ +public interface TokenPrincipal extends Principal { + + /** + * The token in its raw format. + * + * @return the token in its raw format. + */ + String getRawToken(); + + /** + * The {@link AccessToken} representation of {@link TokenPrincipal#getRawToken()}. + * + * @return the access token representation + */ + default AccessToken getToken() { + return JsonUtils.asAccessToken(getRawToken()); + } + + /** + * The name of the entity represented by the token. + * + * @return the name of the principal + */ + default String getName() { + return getToken().getPreferredUsername(); + } +} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProvider.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProvider.java similarity index 85% rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProvider.java rename to authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProvider.java index 93d31b6785..00eab06223 100644 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProvider.java +++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProvider.java @@ -23,9 +23,9 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; -import org.keycloak.adapters.authorization.ClaimInformationPointProvider; +import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProvider; +import org.keycloak.adapters.authorization.spi.HttpRequest; import org.keycloak.adapters.authorization.util.PlaceHolders; -import org.keycloak.adapters.spi.HttpFacade; /** * @author Pedro Igor @@ -39,7 +39,7 @@ public class ClaimsInformationPointProvider implements ClaimInformationPointProv } @Override - public Map> resolve(HttpFacade httpFacade) { + public Map> resolve(HttpRequest request) { Map> claims = new HashMap<>(); for (Entry configEntry : config.entrySet()) { @@ -48,11 +48,11 @@ public class ClaimsInformationPointProvider implements ClaimInformationPointProv List values = new ArrayList<>(); if (claimValue instanceof String) { - values = getValues(claimValue.toString(), httpFacade); + values = getValues(claimValue.toString(), request); } else if (claimValue instanceof Collection) { for (Object value : Collection.class.cast(claimValue)) { - List resolvedValues = getValues(value.toString(), httpFacade); + List resolvedValues = getValues(value.toString(), request); if (!resolvedValues.isEmpty()) { values.addAll(resolvedValues); @@ -68,7 +68,7 @@ public class ClaimsInformationPointProvider implements ClaimInformationPointProv return claims; } - private List getValues(String value, HttpFacade httpFacade) { + private List getValues(String value, HttpRequest httpFacade) { return PlaceHolders.resolve(value, httpFacade); } } diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProviderFactory.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProviderFactory.java similarity index 84% rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProviderFactory.java rename to authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProviderFactory.java index c86c2013c8..7c1d82f0c6 100644 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProviderFactory.java +++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/ClaimsInformationPointProviderFactory.java @@ -18,8 +18,7 @@ package org.keycloak.adapters.authorization.cip; import java.util.Map; -import org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory; -import org.keycloak.adapters.authorization.PolicyEnforcer; +import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProviderFactory; /** * @author Pedro Igor @@ -31,11 +30,6 @@ public class ClaimsInformationPointProviderFactory implements ClaimInformationPo return "claims"; } - @Override - public void init(PolicyEnforcer policyEnforcer) { - - } - @Override public ClaimsInformationPointProvider create(Map config) { return new ClaimsInformationPointProvider(config); diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProvider.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProvider.java similarity index 89% rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProvider.java rename to authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProvider.java index 2229cafde3..958df54779 100644 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProvider.java +++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProvider.java @@ -36,11 +36,10 @@ import org.apache.http.StatusLine; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.RequestBuilder; import org.apache.http.util.EntityUtils; -import org.keycloak.adapters.authorization.ClaimInformationPointProvider; -import org.keycloak.adapters.authorization.PolicyEnforcer; +import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProvider; +import org.keycloak.adapters.authorization.spi.HttpRequest; import org.keycloak.adapters.authorization.util.JsonUtils; import org.keycloak.adapters.authorization.util.PlaceHolders; -import org.keycloak.adapters.spi.HttpFacade; import org.keycloak.authorization.client.util.HttpResponseException; import org.keycloak.common.util.StreamUtil; import org.keycloak.util.JsonSerialization; @@ -53,15 +52,15 @@ public class HttpClaimInformationPointProvider implements ClaimInformationPointP private final Map config; private final HttpClient httpClient; - public HttpClaimInformationPointProvider(Map config, PolicyEnforcer policyEnforcer) { + public HttpClaimInformationPointProvider(Map config, HttpClient httpClient) { this.config = config; - this.httpClient = policyEnforcer.getDeployment().getClient(); + this.httpClient = httpClient; } @Override - public Map> resolve(HttpFacade httpFacade) { + public Map> resolve(HttpRequest request) { try { - InputStream responseStream = executeRequest(httpFacade); + InputStream responseStream = executeRequest(request); try (InputStream inputStream = new BufferedInputStream(responseStream)) { JsonNode jsonNode = JsonSerialization.mapper.readTree(inputStream); @@ -102,7 +101,7 @@ public class HttpClaimInformationPointProvider implements ClaimInformationPointP } } - private InputStream executeRequest(HttpFacade httpFacade) { + private InputStream executeRequest(HttpRequest request) { String method = config.get("method").toString(); if (method == null) { @@ -122,10 +121,10 @@ public class HttpClaimInformationPointProvider implements ClaimInformationPointP byte[] bytes = new byte[0]; try { - setParameters(builder, httpFacade); + setParameters(builder, request); if (config.containsKey("headers")) { - setHeaders(builder, httpFacade); + setHeaders(builder, request); } HttpResponse response = httpClient.execute(builder.build()); @@ -152,7 +151,7 @@ public class HttpClaimInformationPointProvider implements ClaimInformationPointP } } - private void setHeaders(RequestBuilder builder, HttpFacade httpFacade) { + private void setHeaders(RequestBuilder builder, HttpRequest request) { Object headersDef = config.get("headers"); if (headersDef != null) { @@ -166,10 +165,10 @@ public class HttpClaimInformationPointProvider implements ClaimInformationPointP Collection values = Collection.class.cast(value); for (Object item : values) { - headerValues.addAll(PlaceHolders.resolve(item.toString(), httpFacade)); + headerValues.addAll(PlaceHolders.resolve(item.toString(), request)); } } else { - headerValues.addAll(PlaceHolders.resolve(value.toString(), httpFacade)); + headerValues.addAll(PlaceHolders.resolve(value.toString(), request)); } for (String headerValue : headerValues) { @@ -179,7 +178,7 @@ public class HttpClaimInformationPointProvider implements ClaimInformationPointP } } - private void setParameters(RequestBuilder builder, HttpFacade httpFacade) { + private void setParameters(RequestBuilder builder, HttpRequest request) { Object config = this.config.get("parameters"); if (config != null) { @@ -193,10 +192,10 @@ public class HttpClaimInformationPointProvider implements ClaimInformationPointP Collection values = Collection.class.cast(value); for (Object item : values) { - paramValues.addAll(PlaceHolders.resolve(item.toString(), httpFacade)); + paramValues.addAll(PlaceHolders.resolve(item.toString(), request)); } } else { - paramValues.addAll(PlaceHolders.resolve(value.toString(), httpFacade)); + paramValues.addAll(PlaceHolders.resolve(value.toString(), request)); } for (String paramValue : paramValues) { diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProviderFactory.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProviderFactory.java similarity index 92% rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProviderFactory.java rename to authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProviderFactory.java index 6e3656203e..3c9a4ec264 100644 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProviderFactory.java +++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/HttpClaimInformationPointProviderFactory.java @@ -18,8 +18,8 @@ package org.keycloak.adapters.authorization.cip; import java.util.Map; -import org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory; import org.keycloak.adapters.authorization.PolicyEnforcer; +import org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProviderFactory; /** * @author Pedro Igor @@ -40,6 +40,6 @@ public class HttpClaimInformationPointProviderFactory implements ClaimInformatio @Override public HttpClaimInformationPointProvider create(Map config) { - return new HttpClaimInformationPointProvider(config, policyEnforcer); + return new HttpClaimInformationPointProvider(config, policyEnforcer.getHttpClient()); } } diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/ClaimInformationPointProvider.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/spi/ClaimInformationPointProvider.java similarity index 77% rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/ClaimInformationPointProvider.java rename to authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/spi/ClaimInformationPointProvider.java index fb594ef177..43325a8c83 100644 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/ClaimInformationPointProvider.java +++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/spi/ClaimInformationPointProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Red Hat, Inc. and/or its affiliates + * Copyright 2023 Red Hat, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,17 +14,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.keycloak.adapters.authorization; +package org.keycloak.adapters.authorization.cip.spi; import java.util.List; import java.util.Map; -import org.keycloak.adapters.spi.HttpFacade; +import org.keycloak.adapters.authorization.spi.HttpRequest; /** * @author Pedro Igor */ public interface ClaimInformationPointProvider { - Map> resolve(HttpFacade httpFacade); + Map> resolve(HttpRequest request); } diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/ClaimInformationPointProviderFactory.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/spi/ClaimInformationPointProviderFactory.java similarity index 79% rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/ClaimInformationPointProviderFactory.java rename to authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/spi/ClaimInformationPointProviderFactory.java index 894debc76a..f9e1e305a7 100644 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/ClaimInformationPointProviderFactory.java +++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/cip/spi/ClaimInformationPointProviderFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Red Hat, Inc. and/or its affiliates + * Copyright 2023 Red Hat, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,11 +14,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.keycloak.adapters.authorization; +package org.keycloak.adapters.authorization.cip.spi; import java.util.Map; -import org.keycloak.adapters.spi.HttpFacade; +import org.keycloak.adapters.authorization.PolicyEnforcer; /** * @author Pedro Igor @@ -27,7 +27,9 @@ public interface ClaimInformationPointProviderFactory config); } diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/spi/HttpRequest.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/spi/HttpRequest.java new file mode 100644 index 0000000000..8fd0722488 --- /dev/null +++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/spi/HttpRequest.java @@ -0,0 +1,114 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.adapters.authorization.spi; + +import java.io.InputStream; +import java.util.List; + +import org.keycloak.adapters.authorization.TokenPrincipal; + +/** + * Represents an incoming HTTP request and the contract to manipulate it. + * + * @author Pedro Igor + */ +public interface HttpRequest { + + /** + * Get the request path. This is the path relative to the context path. + * E.g.: for a HTTP GET request to http://my.appserver.com/my-application/path/sub-path this method is going to return /path/sub-path. + + * @return the relative path + */ + String getRelativePath(); + + /** + * Returns the name of the HTTP method with which this request was made, for example, GET, POST, or PUT. + * + * @return a {@code String} specifying the name of the method with which this request was made + */ + String getMethod(); + + /** + * Get the URI representation for the current request. + * + * @return a {@code String} representation for the current request + */ + String getURI(); + + /** + * Get a list of all of the values set for the specified header within the HTTP request. + * + * @param name the header name + * @return a list of the values set for this header, if the header is not set on the request then null should be returned + */ + List getHeaders(String name); + + /** + * Get the first value for a parameter with the given {@code name} + * + * @param name the parameter name + * @return the value of the parameter + */ + String getFirstParam(String name); + + /** + * Get the first value for a cookie with the given {@code name}. + * + * @param name the parameter name + * @return the value of the cookie + */ + String getCookieValue(String name); + + /** + * Returns the client address. + * + * @return the client address. + */ + String getRemoteAddr(); + + /** + * Indicates if the request is coming from a secure channel through HTTPS. + * + * @return {@code true} if the HTTP scheme is set to 'https'. Otherwise, {@code false} + */ + boolean isSecure(); + + /** + * Get the first value for a HEADER with the given {@code name}. + * + * @param name the HEADER name + * @return the value of the HEADER + */ + String getHeader(String name); + + /** + * Returns the request input stream + * + * @param buffered if the input stream should be buffered and support for multiple reads + * @return the request input stream + */ + InputStream getInputStream(boolean buffered); + + /** + * Returns a {@link TokenPrincipal} associated with the request. + * + * @return the principal + */ + TokenPrincipal getPrincipal(); +} diff --git a/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/spi/HttpResponse.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/spi/HttpResponse.java new file mode 100644 index 0000000000..dcd41a5f99 --- /dev/null +++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/spi/HttpResponse.java @@ -0,0 +1,48 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.adapters.authorization.spi; + +/** + * Represents an outgoing HTTP response and the contract to manipulate it. + * + * @author Pedro Igor + */ +public interface HttpResponse { + + /** + * Send an error with the given {@code statusCode}. + * + * @param statusCode the status to set in the response + */ + void sendError(int statusCode); + + /** + * Send an error with the given {@code statusCode} and {@code reason} message. + * + * @param statusCode the status to set in the response + */ + void sendError(int statusCode, String reason); + + /** + * Set a header with the given {@code name} and {@code value}. + * + * @param name the header name + * @param value the header value + */ + void setHeader(String name, String value); +} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/JsonUtils.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/JsonUtils.java similarity index 82% rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/JsonUtils.java rename to authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/JsonUtils.java index c3d03659fb..5c06ca60b2 100644 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/JsonUtils.java +++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/JsonUtils.java @@ -21,9 +21,13 @@ import java.util.ArrayList; import java.util.List; import com.fasterxml.jackson.databind.JsonNode; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.representations.AccessToken; import org.keycloak.util.JsonSerialization; /** + * Utility methods to manipulate JSON data + * * @author Pedro Igor */ public class JsonUtils { @@ -36,7 +40,6 @@ public class JsonUtils { List values = new ArrayList<>(); if (jsonNode.isArray()) { - for (JsonNode node : jsonNode) { String value; @@ -65,4 +68,11 @@ public class JsonUtils { return values; } + public static AccessToken asAccessToken(String rawToken) { + try { + return new JWSInput(rawToken).readJsonContent(AccessToken.class); + } catch (Exception cause) { + throw new RuntimeException("Failed to decode token", cause); + } + } } diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/KeycloakSecurityContextPlaceHolderResolver.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/KeycloakSecurityContextPlaceHolderResolver.java similarity index 64% rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/KeycloakSecurityContextPlaceHolderResolver.java rename to authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/KeycloakSecurityContextPlaceHolderResolver.java index d59cfd7861..b5f824ad9a 100644 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/KeycloakSecurityContextPlaceHolderResolver.java +++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/KeycloakSecurityContextPlaceHolderResolver.java @@ -22,9 +22,8 @@ import java.util.Arrays; import java.util.List; import com.fasterxml.jackson.databind.JsonNode; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.OIDCHttpFacade; -import org.keycloak.adapters.spi.HttpFacade; +import org.keycloak.adapters.authorization.TokenPrincipal; +import org.keycloak.adapters.authorization.spi.HttpRequest; import org.keycloak.util.JsonSerialization; /** @@ -35,29 +34,18 @@ public class KeycloakSecurityContextPlaceHolderResolver implements PlaceHolderRe public static final String NAME = "keycloak"; @Override - public List resolve(String placeHolder, HttpFacade httpFacade) { + public List resolve(String placeHolder, HttpRequest request) { String source = placeHolder.substring(placeHolder.indexOf('.') + 1); - OIDCHttpFacade oidcHttpFacade = OIDCHttpFacade.class.cast(httpFacade); - KeycloakSecurityContext securityContext = oidcHttpFacade.getSecurityContext(); - - if (securityContext == null) { - return null; - } + TokenPrincipal principal = request.getPrincipal(); if (source.endsWith("access_token")) { - return Arrays.asList(securityContext.getTokenString()); - } - - if (source.endsWith("id_token")) { - return Arrays.asList(securityContext.getIdTokenString()); + return Arrays.asList(principal.getRawToken()); } JsonNode jsonNode; if (source.startsWith("access_token[")) { - jsonNode = JsonSerialization.mapper.valueToTree(securityContext.getToken()); - } else if (source.startsWith("id_token[")) { - jsonNode = JsonSerialization.mapper.valueToTree(securityContext.getIdToken()); + jsonNode = JsonSerialization.mapper.valueToTree(principal.getToken()); } else { throw new RuntimeException("Invalid placeholder [" + placeHolder + "]"); } diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolderResolver.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolderResolver.java similarity index 86% rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolderResolver.java rename to authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolderResolver.java index 62d6e1e51a..935fc47db1 100644 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolderResolver.java +++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolderResolver.java @@ -18,13 +18,13 @@ package org.keycloak.adapters.authorization.util; import java.util.List; -import org.keycloak.adapters.spi.HttpFacade; +import org.keycloak.adapters.authorization.spi.HttpRequest; /** * @author Pedro Igor */ public interface PlaceHolderResolver { - List resolve(String placeHolder, HttpFacade httpFacade); + List resolve(String placeHolder, HttpRequest httpFacade); } diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolders.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolders.java similarity index 71% rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolders.java rename to authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolders.java index 7a39dbf094..d7a4f5f479 100644 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolders.java +++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/PlaceHolders.java @@ -17,6 +17,7 @@ package org.keycloak.adapters.authorization.util; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -24,7 +25,7 @@ import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.keycloak.adapters.spi.HttpFacade; +import org.keycloak.adapters.authorization.spi.HttpRequest; /** * @author Pedro Igor @@ -41,7 +42,7 @@ public class PlaceHolders { private static Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{(.+?)\\}"); private static Pattern PLACEHOLDER_PARAM_PATTERN = Pattern.compile("\\[(.+?)\\]"); - public static List resolve(String value, HttpFacade httpFacade) { + public static List resolve(String value, HttpRequest httpFacade) { Map> placeHolders = parsePlaceHolders(value, httpFacade); if (!placeHolders.isEmpty()) { @@ -75,27 +76,31 @@ public class PlaceHolders { return null; } - private static Map> parsePlaceHolders(String value, HttpFacade httpFacade) { - Map> placeHolders = new HashMap<>(); + private static Map> parsePlaceHolders(String value, HttpRequest httpFacade) { + Map> placeHolders = Collections.emptyMap(); Matcher matcher = PLACEHOLDER_PATTERN.matcher(value); + boolean found = matcher.find(); - while (matcher.find()) { - String placeHolder = matcher.group(1); - int resolverNameIdx = placeHolder.indexOf('.'); + if (found) { + placeHolders = new HashMap<>(); + do { + String placeHolder = matcher.group(1); + int resolverNameIdx = placeHolder.indexOf('.'); - if (resolverNameIdx == -1) { - throw new RuntimeException("Invalid placeholder [" + value + "]. Could not find resolver name."); - } - - PlaceHolderResolver resolver = resolvers.get(placeHolder.substring(0, resolverNameIdx)); - - if (resolver != null) { - List resolved = resolver.resolve(placeHolder, httpFacade); - - if (resolved != null) { - placeHolders.put(formatPlaceHolder(placeHolder), resolved); + if (resolverNameIdx == -1) { + throw new RuntimeException("Invalid placeholder [" + value + "]. Could not find resolver name."); } - } + + PlaceHolderResolver resolver = resolvers.get(placeHolder.substring(0, resolverNameIdx)); + + if (resolver != null) { + List resolved = resolver.resolve(placeHolder, httpFacade); + + if (resolved != null) { + placeHolders.put(formatPlaceHolder(placeHolder), resolved); + } + } + } while (matcher.find()); } return placeHolders; diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/RequestPlaceHolderResolver.java b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/RequestPlaceHolderResolver.java similarity index 91% rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/RequestPlaceHolderResolver.java rename to authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/RequestPlaceHolderResolver.java index 607e28b188..5d49625e99 100644 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/util/RequestPlaceHolderResolver.java +++ b/authz/policy-enforcer/src/main/java/org/keycloak/adapters/authorization/util/RequestPlaceHolderResolver.java @@ -29,9 +29,7 @@ import java.util.Collections; import java.util.List; import com.fasterxml.jackson.databind.JsonNode; -import org.keycloak.adapters.spi.HttpFacade; -import org.keycloak.adapters.spi.HttpFacade.Cookie; -import org.keycloak.adapters.spi.HttpFacade.Request; +import org.keycloak.adapters.authorization.spi.HttpRequest; import org.keycloak.util.JsonSerialization; /** @@ -42,17 +40,12 @@ public class RequestPlaceHolderResolver implements PlaceHolderResolver { static String NAME = "request"; @Override - public List resolve(String placeHolder, HttpFacade httpFacade) { + public List resolve(String placeHolder, HttpRequest request) { String source = placeHolder.substring(placeHolder.indexOf('.') + 1); - Request request = httpFacade.getRequest(); if (source.startsWith("parameter")) { String parameterName = getParameter(source, "Could not obtain parameter name from placeholder [" + source + "]"); - String parameterValue = request.getQueryParamValue(parameterName); - - if (parameterValue == null) { - parameterValue = request.getFirstParam(parameterName); - } + String parameterValue = request.getFirstParam(parameterName); if (parameterValue != null) { return Arrays.asList(parameterValue); @@ -66,10 +59,10 @@ public class RequestPlaceHolderResolver implements PlaceHolderResolver { } } else if (source.startsWith("cookie")) { String cookieName = getParameter(source, "Could not obtain cookie name from placeholder [" + source + "]"); - Cookie cookieValue = request.getCookie(cookieName); + String cookieValue = request.getCookieValue(cookieName); if (cookieValue != null) { - return Arrays.asList(cookieValue.getValue()); + return Arrays.asList(cookieValue); } } else if (source.startsWith("remoteAddr")) { String value = request.getRemoteAddr(); diff --git a/adapters/oidc/adapter-core/src/main/resources/META-INF/services/org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory b/authz/policy-enforcer/src/main/resources/META-INF/services/org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProviderFactory similarity index 100% rename from adapters/oidc/adapter-core/src/main/resources/META-INF/services/org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory rename to authz/policy-enforcer/src/main/resources/META-INF/services/org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProviderFactory diff --git a/authz/policy/common/pom.xml b/authz/policy/common/pom.xml index 63bebbe8d4..d5b13c0cff 100644 --- a/authz/policy/common/pom.xml +++ b/authz/policy/common/pom.xml @@ -32,7 +32,7 @@ keycloak-authz-policy-common jar - KeyCloak AuthZ: Common Policy Providers + Keycloak AuthZ: Common Policy Providers KeyCloak AuthZ: Common Policy Providers diff --git a/authz/policy/pom.xml b/authz/policy/pom.xml index 1245fa38e5..48091592fa 100644 --- a/authz/policy/pom.xml +++ b/authz/policy/pom.xml @@ -14,8 +14,7 @@ keycloak-authz-provider-parent pom - KeyCloak AuthZ: Provider Parent - KeyCloak AuthZ: Provider Parent + Keycloak AuthZ: Policy Provider Parent common diff --git a/authz/pom.xml b/authz/pom.xml index 96fe528f03..31224da503 100644 --- a/authz/pom.xml +++ b/authz/pom.xml @@ -14,11 +14,12 @@ keycloak-authz-parent pom - KeyCloak Authz: Parent + Keycloak Authz: Parent KeyCloak AuthZ: Parent policy client + policy-enforcer \ No newline at end of file diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProvider.java b/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/ClientCredentialsProvider.java similarity index 79% rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProvider.java rename to core/src/main/java/org/keycloak/protocol/oidc/client/authentication/ClientCredentialsProvider.java index c84af87ce7..220ad8f146 100644 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProvider.java +++ b/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/ClientCredentialsProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 Red Hat, Inc. and/or its affiliates + * Copyright 2023 Red Hat, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,12 +15,12 @@ * limitations under the License. */ -package org.keycloak.adapters.authentication; - -import org.keycloak.adapters.KeycloakDeployment; +package org.keycloak.protocol.oidc.client.authentication; import java.util.Map; +import org.keycloak.representations.adapters.config.AdapterConfig; + /** * The simple SPI for authenticating clients/applications . It's used by adapter during all OIDC backchannel requests to Keycloak server * (codeToToken exchange, refresh token or backchannel logout) . You can also use it in your application during direct access grants or service account request @@ -30,15 +30,12 @@ import java.util.Map; * so your server is able to authenticate client * * You must specify a file - * META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider in the WAR that this class is contained in (or in the JAR that is attached to the WEB-INF/lib or as jboss module + * META-INF/services/org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider in the WAR that this class is contained in (or in the JAR that is attached to the WEB-INF/lib or as jboss module * if you want to share the implementation among more WARs). * * NOTE: The SPI is not finished and method signatures are still subject to change in future versions (for example to support * authentication with client certificate) * - * @see ClientIdAndSecretCredentialsProvider - * @see JWTClientCredentialsProvider - * * @author Marek Posolda */ public interface ClientCredentialsProvider { @@ -62,17 +59,17 @@ public interface ClientCredentialsProvider { /** * Called by adapter during deployment of your application. You can for example read configuration and init your authenticator here * - * @param deployment the adapter configuration + * @param adapterConfig the adapter configuration * @param config the configuration of your provider read from keycloak.json . For the kerberos-keytab example above, it will return map with the single key "keytab" with value "/tmp/foo" */ - void init(KeycloakDeployment deployment, Object config); + void init(AdapterConfig adapterConfig, Object config); /** * Called every time adapter needs to perform backchannel request * - * @param deployment Fully resolved deployment + * @param adapterConfig Fully resolved deployment * @param requestHeaders You should put any HTTP request headers you want to use for authentication of client. These headers will be attached to the HTTP request sent to Keycloak server * @param formParams You should put any request parameters you want to use for authentication of client. These parameters will be attached to the HTTP request sent to Keycloak server */ - void setClientCredentials(KeycloakDeployment deployment, Map requestHeaders, Map formParams); + void setClientCredentials(AdapterConfig adapterConfig, Map requestHeaders, Map formParams); } diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProviderUtils.java b/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/ClientCredentialsProviderUtils.java similarity index 69% rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProviderUtils.java rename to core/src/main/java/org/keycloak/protocol/oidc/client/authentication/ClientCredentialsProviderUtils.java index 4df727d937..dacd9aa40f 100644 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientCredentialsProviderUtils.java +++ b/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/ClientCredentialsProviderUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 Red Hat, Inc. and/or its affiliates + * Copyright 2023 Red Hat, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,21 +15,17 @@ * limitations under the License. */ -package org.keycloak.adapters.authentication; - -import org.apache.http.NameValuePair; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.message.BasicNameValuePair; -import org.jboss.logging.Logger; -import org.keycloak.adapters.KeycloakDeployment; +package org.keycloak.protocol.oidc.client.authentication; import java.util.HashMap; import java.util.Iterator; -import java.util.List; import java.util.Map; import java.util.ServiceConfigurationError; import java.util.ServiceLoader; +import org.jboss.logging.Logger; +import org.keycloak.representations.adapters.config.AdapterConfig; + /** * @author Marek Posolda */ @@ -37,9 +33,9 @@ public class ClientCredentialsProviderUtils { private static Logger logger = Logger.getLogger(ClientCredentialsProviderUtils.class); - public static ClientCredentialsProvider bootstrapClientAuthenticator(KeycloakDeployment deployment) { - String clientId = deployment.getResourceName(); - Map clientCredentials = deployment.getResourceCredentials(); + public static ClientCredentialsProvider bootstrapClientAuthenticator(AdapterConfig deployment) { + String clientId = deployment.getResource(); + Map clientCredentials = deployment.getCredentials(); String authenticatorId; if (clientCredentials == null || clientCredentials.isEmpty()) { @@ -73,7 +69,7 @@ public class ClientCredentialsProviderUtils { return authenticator; } - private static void loadAuthenticators(Map authenticators, ClassLoader classLoader) { + public static void loadAuthenticators(Map authenticators, ClassLoader classLoader) { Iterator iterator = ServiceLoader.load(ClientCredentialsProvider.class, classLoader).iterator(); while (iterator.hasNext()) { try { @@ -91,26 +87,7 @@ public class ClientCredentialsProviderUtils { /** * Use this method when calling backchannel request directly from your application. See service-account example from demo for more details */ - public static void setClientCredentials(KeycloakDeployment deployment, Map requestHeaders, Map formparams) { - ClientCredentialsProvider authenticator = deployment.getClientAuthenticator(); + public static void setClientCredentials(AdapterConfig deployment, ClientCredentialsProvider authenticator, Map requestHeaders, Map formparams) { authenticator.setClientCredentials(deployment, requestHeaders, formparams); } - - /** - * Don't use directly from your JEE apps to avoid HttpClient linkage errors! Instead use the method {@link #setClientCredentials(KeycloakDeployment, Map, Map)} - */ - public static void setClientCredentials(KeycloakDeployment deployment, HttpPost post, List formparams) { - Map reqHeaders = new HashMap<>(); - Map reqParams = new HashMap<>(); - setClientCredentials(deployment, reqHeaders, reqParams); - - for (Map.Entry header : reqHeaders.entrySet()) { - post.setHeader(header.getKey(), header.getValue()); - } - - for (Map.Entry param : reqParams.entrySet()) { - formparams.add(new BasicNameValuePair(param.getKey(), param.getValue())); - } - } - } diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientIdAndSecretCredentialsProvider.java b/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/ClientIdAndSecretCredentialsProvider.java similarity index 84% rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientIdAndSecretCredentialsProvider.java rename to core/src/main/java/org/keycloak/protocol/oidc/client/authentication/ClientIdAndSecretCredentialsProvider.java index 61f1081555..b90233463d 100644 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/ClientIdAndSecretCredentialsProvider.java +++ b/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/ClientIdAndSecretCredentialsProvider.java @@ -15,16 +15,16 @@ * limitations under the License. */ -package org.keycloak.adapters.authentication; +package org.keycloak.protocol.oidc.client.authentication; + +import java.util.Map; import org.jboss.logging.Logger; import org.keycloak.OAuth2Constants; -import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.representations.adapters.config.AdapterConfig; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.util.BasicAuthHelper; -import java.util.Map; - /** * Traditional OAuth2 authentication of clients based on client_id and client_secret * @@ -44,13 +44,13 @@ public class ClientIdAndSecretCredentialsProvider implements ClientCredentialsPr } @Override - public void init(KeycloakDeployment deployment, Object config) { + public void init(AdapterConfig deployment, Object config) { clientSecret = (config == null ? null : config.toString()); } @Override - public void setClientCredentials(KeycloakDeployment deployment, Map requestHeaders, Map formParams) { - String clientId = deployment.getResourceName(); + public void setClientCredentials(AdapterConfig deployment, Map requestHeaders, Map formParams) { + String clientId = deployment.getResource(); if (!deployment.isPublicClient()) { if (clientSecret != null) { diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java b/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/JWTClientCredentialsProvider.java similarity index 91% rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java rename to core/src/main/java/org/keycloak/protocol/oidc/client/authentication/JWTClientCredentialsProvider.java index f2ed150639..9451b359e7 100644 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java +++ b/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/JWTClientCredentialsProvider.java @@ -15,18 +15,15 @@ * limitations under the License. */ -package org.keycloak.adapters.authentication; +package org.keycloak.protocol.oidc.client.authentication; import java.security.KeyPair; import java.security.PublicKey; import java.util.Map; +import java.util.UUID; import org.keycloak.OAuth2Constants; -import org.keycloak.adapters.AdapterUtils; -import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.common.util.KeyUtils; -import org.keycloak.jose.jws.JWSBuilder; -import org.keycloak.representations.JsonWebToken; import org.keycloak.common.util.KeystoreUtil; import org.keycloak.common.util.Time; import org.keycloak.crypto.Algorithm; @@ -36,6 +33,9 @@ import org.keycloak.crypto.KeyType; import org.keycloak.crypto.KeyUse; import org.keycloak.crypto.KeyWrapper; import org.keycloak.crypto.SignatureSignerContext; +import org.keycloak.jose.jws.JWSBuilder; +import org.keycloak.representations.JsonWebToken; +import org.keycloak.representations.adapters.config.AdapterConfig; /** * Client authentication based on JWT signed by client private key . @@ -102,16 +102,16 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider { } @Override - public void init(KeycloakDeployment deployment, Object config) { + public void init(AdapterConfig deployment, Object config) { if (!(config instanceof Map)) { - throw new RuntimeException("Configuration of jwt credentials is missing or incorrect for client '" + deployment.getResourceName() + "'. Check your adapter configuration"); + throw new RuntimeException("Configuration of jwt credentials is missing or incorrect for client '" + deployment.getResource() + "'. Check your adapter configuration"); } Map cfg = (Map) config; String clientKeystoreFile = (String) cfg.get("client-keystore-file"); if (clientKeystoreFile == null) { - throw new RuntimeException("Missing parameter client-keystore-file in configuration of jwt for client " + deployment.getResourceName()); + throw new RuntimeException("Missing parameter client-keystore-file in configuration of jwt for client " + deployment.getResource()); } String clientKeystoreType = (String) cfg.get("client-keystore-type"); @@ -119,7 +119,7 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider { String clientKeystorePassword = (String) cfg.get("client-keystore-password"); if (clientKeystorePassword == null) { - throw new RuntimeException("Missing parameter client-keystore-password in configuration of jwt for client " + deployment.getResourceName()); + throw new RuntimeException("Missing parameter client-keystore-password in configuration of jwt for client " + deployment.getResource()); } String clientKeyPassword = (String) cfg.get("client-key-password"); @@ -129,7 +129,7 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider { String clientKeyAlias = (String) cfg.get("client-key-alias"); if (clientKeyAlias == null) { - clientKeyAlias = deployment.getResourceName(); + clientKeyAlias = deployment.getResource(); } String algorithm = (String) cfg.getOrDefault("algorithm", Algorithm.RS256); @@ -157,8 +157,8 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider { } @Override - public void setClientCredentials(KeycloakDeployment deployment, Map requestHeaders, Map formParams) { - String signedToken = createSignedRequestToken(deployment.getResourceName(), deployment.getRealmInfoUrl()); + public void setClientCredentials(AdapterConfig deployment, Map requestHeaders, Map formParams) { + String signedToken = createSignedRequestToken(deployment.getResource(), deployment.getRealmInfoUrl()); formParams.put(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT); formParams.put(OAuth2Constants.CLIENT_ASSERTION, signedToken); @@ -173,7 +173,7 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider { protected JsonWebToken createRequestToken(String clientId, String realmInfoUrl) { JsonWebToken reqToken = new JsonWebToken(); - reqToken.id(AdapterUtils.generateId()); + reqToken.id(UUID.randomUUID().toString()); reqToken.issuer(clientId); reqToken.subject(clientId); reqToken.audience(realmInfoUrl); diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientSecretCredentialsProvider.java b/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/JWTClientSecretCredentialsProvider.java similarity index 91% rename from adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientSecretCredentialsProvider.java rename to core/src/main/java/org/keycloak/protocol/oidc/client/authentication/JWTClientSecretCredentialsProvider.java index 454be93cc5..2764cb3761 100644 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientSecretCredentialsProvider.java +++ b/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/JWTClientSecretCredentialsProvider.java @@ -14,23 +14,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.keycloak.adapters.authentication; +package org.keycloak.protocol.oidc.client.authentication; import java.nio.charset.StandardCharsets; import java.util.Map; +import java.util.UUID; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import org.jboss.logging.Logger; import org.keycloak.OAuth2Constants; -import org.keycloak.adapters.AdapterUtils; -import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.common.util.Time; import org.keycloak.crypto.Algorithm; import org.keycloak.crypto.JavaAlgorithm; import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.representations.JsonWebToken; +import org.keycloak.representations.adapters.config.AdapterConfig; /** * Client authentication based on JWT signed by client secret instead of private key . @@ -53,15 +53,15 @@ public class JWTClientSecretCredentialsProvider implements ClientCredentialsProv } @Override - public void init(KeycloakDeployment deployment, Object config) { + public void init(AdapterConfig deployment, Object config) { if (!(config instanceof Map)) { - throw new RuntimeException("Configuration of jwt credentials by client secret is missing or incorrect for client '" + deployment.getResourceName() + "'. Check your adapter configuration"); + throw new RuntimeException("Configuration of jwt credentials by client secret is missing or incorrect for client '" + deployment.getResource() + "'. Check your adapter configuration"); } Map cfg = (Map) config; String clientSecretString = (String) cfg.get("secret"); if (clientSecretString == null) { - throw new RuntimeException("Missing parameter secret-jwt in configuration of jwt for client " + deployment.getResourceName()); + throw new RuntimeException("Missing parameter secret-jwt in configuration of jwt for client " + deployment.getResource()); } String clientSecretJwtAlg = (String) cfg.get("algorithm"); @@ -72,7 +72,7 @@ public class JWTClientSecretCredentialsProvider implements ClientCredentialsProv setClientSecret(clientSecretString, clientSecretJwtAlg); } else { // invalid "algorithm" field - throw new RuntimeException("Invalid parameter secret-jwt in configuration of jwt for client " + deployment.getResourceName()); + throw new RuntimeException("Invalid parameter secret-jwt in configuration of jwt for client " + deployment.getResource()); } } @@ -84,8 +84,8 @@ public class JWTClientSecretCredentialsProvider implements ClientCredentialsProv } @Override - public void setClientCredentials(KeycloakDeployment deployment, Map requestHeaders, Map formParams) { - String signedToken = createSignedRequestToken(deployment.getResourceName(), deployment.getRealmInfoUrl()); + public void setClientCredentials(AdapterConfig deployment, Map requestHeaders, Map formParams) { + String signedToken = createSignedRequestToken(deployment.getResource(), deployment.getRealmInfoUrl()); formParams.put(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT); formParams.put(OAuth2Constants.CLIENT_ASSERTION, signedToken); } @@ -126,7 +126,7 @@ public class JWTClientSecretCredentialsProvider implements ClientCredentialsProv // JWT claims is the same as one by private_key_jwt JsonWebToken reqToken = new JsonWebToken(); - reqToken.id(AdapterUtils.generateId()); + reqToken.id(UUID.randomUUID().toString()); reqToken.issuer(clientId); reqToken.subject(clientId); reqToken.audience(realmInfoUrl); diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java index a358eb392c..0c99011a4a 100755 --- a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java +++ b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java @@ -17,6 +17,7 @@ package org.keycloak.representations.adapters.config; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; @@ -320,4 +321,9 @@ public class AdapterConfig extends BaseAdapterConfig implements AdapterHttpClien public void setConnectionTTL(long connectionTTL) { this.connectionTTL = connectionTTL; } + + @JsonIgnore + public String getRealmInfoUrl() { + return authServerUrl + "/realms/" + realm; + } } diff --git a/adapters/oidc/adapter-core/src/main/resources/META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider b/core/src/main/resources/META-INF/services/org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider similarity index 72% rename from adapters/oidc/adapter-core/src/main/resources/META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider rename to core/src/main/resources/META-INF/services/org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider index b02accf8dc..e08602e2d9 100644 --- a/adapters/oidc/adapter-core/src/main/resources/META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider +++ b/core/src/main/resources/META-INF/services/org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProvider @@ -15,6 +15,6 @@ # limitations under the License. # -org.keycloak.adapters.authentication.ClientIdAndSecretCredentialsProvider -org.keycloak.adapters.authentication.JWTClientCredentialsProvider -org.keycloak.adapters.authentication.JWTClientSecretCredentialsProvider \ No newline at end of file +org.keycloak.protocol.oidc.client.authentication.ClientIdAndSecretCredentialsProvider +org.keycloak.protocol.oidc.client.authentication.JWTClientCredentialsProvider +org.keycloak.protocol.oidc.client.authentication.JWTClientSecretCredentialsProvider \ No newline at end of file diff --git a/distribution/feature-packs/adapter-feature-pack/pom.xml b/distribution/feature-packs/adapter-feature-pack/pom.xml index 28e8780a15..c76f58d7f2 100755 --- a/distribution/feature-packs/adapter-feature-pack/pom.xml +++ b/distribution/feature-packs/adapter-feature-pack/pom.xml @@ -154,6 +154,16 @@ + + org.keycloak + keycloak-policy-enforcer + + + * + * + + + org.wildfly diff --git a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-adapter-core/main/module.xml b/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-adapter-core/main/module.xml index 1d89bc9e49..4e2e48ac34 100755 --- a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-adapter-core/main/module.xml +++ b/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-adapter-core/main/module.xml @@ -36,6 +36,7 @@ + diff --git a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-policy-enforcer/main/module.xml b/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-policy-enforcer/main/module.xml new file mode 100755 index 0000000000..06bf8df622 --- /dev/null +++ b/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/add-ons/keycloak/org/keycloak/keycloak-policy-enforcer/main/module.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index 6a589015a0..2161294b15 100644 --- a/pom.xml +++ b/pom.xml @@ -1443,6 +1443,11 @@ keycloak-authz-policy-common ${project.version} + + org.keycloak + keycloak-policy-enforcer + ${project.version} + diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/ClientAuthenticator.java b/server-spi-private/src/main/java/org/keycloak/authentication/ClientAuthenticator.java index c91353ec10..8457c91a56 100755 --- a/server-spi-private/src/main/java/org/keycloak/authentication/ClientAuthenticator.java +++ b/server-spi-private/src/main/java/org/keycloak/authentication/ClientAuthenticator.java @@ -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 diff --git a/testsuite/integration-arquillian/test-apps/servlets/pom.xml b/testsuite/integration-arquillian/test-apps/servlets/pom.xml index 9d50cb7843..0c028efd87 100644 --- a/testsuite/integration-arquillian/test-apps/servlets/pom.xml +++ b/testsuite/integration-arquillian/test-apps/servlets/pom.xml @@ -29,6 +29,10 @@ org.keycloak keycloak-adapter-core + + org.keycloak + keycloak-policy-enforcer + org.keycloak keycloak-core diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzClientCredentialsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzClientCredentialsTest.java index 574431b897..07e53dfaf1 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzClientCredentialsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzClientCredentialsTest.java @@ -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> requestParams, Map requestHeaders) { - Map formparams = new HashMap<>(); - ClientCredentialsProviderUtils.setClientCredentials(deployment, requestHeaders, formparams); - for (Entry 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) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/admin/ClaimInformationPointProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/admin/ClaimInformationPointProviderTest.java index e85592a54a..029add61c4 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/admin/ClaimInformationPointProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/admin/ClaimInformationPointProviderTest.java @@ -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> claims = getClaimInformationProviderForPath("/claims-provider", "claims").resolve(httpFacade); + OIDCHttpFacade httpFacade = createHttpFacade(); + Map> 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> claims = getClaimInformationProviderForPath("/claims-provider", "claims").resolve(httpFacade); + Map> 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> claims = getClaimInformationProviderForPath("/claims-from-body-json-object", "claims").resolve(httpFacade); + Map> 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> claims = getClaimInformationProviderForPath("/claims-provider", "claims").resolve(httpFacade); + Map> 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> claims = getClaimInformationProviderForPath("/http-get-claim-provider", "http").resolve(httpFacade); + Map> 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> claims = getClaimInformationProviderForPath("/http-post-claim-provider", "http").resolve(httpFacade); + Map> 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> headers, InputStream requestBody) { + private OIDCHttpFacade createHttpFacade(Map> 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); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/admin/MyCustomCIPFactory.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/admin/MyCustomCIPFactory.java index b653bdc29d..6d603da181 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/admin/MyCustomCIPFactory.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/admin/MyCustomCIPFactory.java @@ -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 { @@ -33,11 +32,6 @@ public class MyCustomCIPFactory implements ClaimInformationPointProviderFactory< return "my-custom-cip"; } - @Override - public void init(PolicyEnforcer policyEnforcer) { - - } - @Override public MyCustomCIP create(Map config) { return new MyCustomCIP(config); @@ -53,7 +47,7 @@ class MyCustomCIP implements ClaimInformationPointProvider { } @Override - public Map> resolve(HttpFacade httpFacade) { + public Map> resolve(HttpRequest request) { Map> claims = new HashMap<>(); claims.put("resolved-claim", Arrays.asList(config.get("claim-value").toString())); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/admin/PolicyEnforcerClaimsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/admin/PolicyEnforcerClaimsTest.java index ffbf9c4b54..c5d7c6182f 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/admin/PolicyEnforcerClaimsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/admin/PolicyEnforcerClaimsTest.java @@ -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()); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/admin/PolicyEnforcerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/admin/PolicyEnforcerTest.java index b7a23d5817..c5b5ef7f02 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/admin/PolicyEnforcerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/admin/PolicyEnforcerTest.java @@ -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> 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> 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 { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/FAPI1Test.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/FAPI1Test.java index a367fcc3fc..c0106a225b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/FAPI1Test.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/FAPI1Test.java @@ -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; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCJwksClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCJwksClientRegistrationTest.java index 40b47e3170..790aa51d7c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCJwksClientRegistrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCJwksClientRegistrationTest.java @@ -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; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSecretSignedJWTTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSecretSignedJWTTest.java index 2f9f4f0155..698007abd2 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSecretSignedJWTTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSecretSignedJWTTest.java @@ -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; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java index 91beef8f3b..a832a7cc23 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java @@ -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; diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/services/org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/services/org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProviderFactory similarity index 100% rename from testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/services/org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory rename to testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/services/org.keycloak.adapters.authorization.cip.spi.ClaimInformationPointProviderFactory diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/enforcer-config-claims-provider.json b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/enforcer-config-claims-provider.json index af2bd2f34e..3b74027391 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/enforcer-config-claims-provider.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/enforcer-config-claims-provider.json @@ -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']}" } } }