diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java index 20f6c90b73..bc90b65a74 100644 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java @@ -67,8 +67,12 @@ public abstract class AbstractPolicyEnforcer { KeycloakSecurityContext securityContext = httpFacade.getSecurityContext(); if (securityContext == null) { - if (pathConfig != null) { - challenge(pathConfig, getRequiredScopes(pathConfig, request), httpFacade); + if (!isDefaultAccessDeniedUri(request)) { + if (pathConfig != null) { + challenge(pathConfig, getRequiredScopes(pathConfig, request), httpFacade); + } else { + handleAccessDenied(httpFacade); + } } return createEmptyAuthorizationContext(false); } diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java index ba46088858..632aa66309 100644 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java @@ -122,12 +122,6 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer { @Override protected void handleAccessDenied(OIDCHttpFacade facade) { - KeycloakSecurityContext securityContext = facade.getSecurityContext(); - - if (securityContext == null) { - return; - } - String accessDeniedPath = getEnforcerConfig().getOnDenyRedirectTo(); HttpFacade.Response response = facade.getResponse(); @@ -211,17 +205,16 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer { private boolean isBearerAuthorization(OIDCHttpFacade httpFacade) { List authHeaders = httpFacade.getRequest().getHeaders("Authorization"); - if (authHeaders == null || authHeaders.size() == 0) { - return false; + + 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; + } } - for (String authHeader : authHeaders) { - String[] split = authHeader.trim().split("\\s+"); - if (split == null || split.length != 2) continue; - if (!split[0].equalsIgnoreCase("Bearer")) continue; - return true; - } - - return false; + return getPolicyEnforcer().getDeployment().isBearerOnly(); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/PolicyEnforcerClaimsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/PolicyEnforcerClaimsTest.java new file mode 100644 index 0000000000..cff948b938 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/PolicyEnforcerClaimsTest.java @@ -0,0 +1,590 @@ +/* + * Copyright 2018 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.admin.client.authorization; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.security.cert.X509Certificate; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.keycloak.AuthorizationContext; +import org.keycloak.KeycloakSecurityContext; +import org.keycloak.OAuth2Constants; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.KeycloakDeploymentBuilder; +import org.keycloak.adapters.OIDCHttpFacade; +import org.keycloak.adapters.authorization.PolicyEnforcer; +import org.keycloak.adapters.spi.AuthenticationError; +import org.keycloak.adapters.spi.HttpFacade.Cookie; +import org.keycloak.adapters.spi.HttpFacade.Request; +import org.keycloak.adapters.spi.HttpFacade.Response; +import org.keycloak.adapters.spi.LogoutError; +import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.admin.client.resource.ClientsResource; +import org.keycloak.authorization.client.AuthzClient; +import org.keycloak.authorization.client.Configuration; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.jose.jws.JWSInputException; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.authorization.AuthorizationRequest; +import org.keycloak.representations.idm.authorization.AuthorizationResponse; +import org.keycloak.representations.idm.authorization.JSPolicyRepresentation; +import org.keycloak.representations.idm.authorization.Permission; +import org.keycloak.representations.idm.authorization.ResourceRepresentation; +import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation; +import org.keycloak.representations.idm.authorization.ScopeRepresentation; +import org.keycloak.testsuite.AbstractKeycloakTest; +import org.keycloak.testsuite.ProfileAssume; +import org.keycloak.testsuite.util.ClientBuilder; +import org.keycloak.testsuite.util.OAuthClient; +import org.keycloak.testsuite.util.RealmBuilder; +import org.keycloak.testsuite.util.RoleBuilder; +import org.keycloak.testsuite.util.RolesBuilder; +import org.keycloak.testsuite.util.UserBuilder; +import org.keycloak.util.JsonSerialization; + +/** + * @author Pedro Igor + */ +public class PolicyEnforcerClaimsTest extends AbstractKeycloakTest { + + protected static final String REALM_NAME = "authz-test"; + + @BeforeClass + public static void onBeforeClass() { + ProfileAssume.assumePreview(); + } + + @Override + public void addTestRealms(List testRealms) { + testRealms.add(RealmBuilder.create().name(REALM_NAME) + .roles(RolesBuilder.create() + .realmRole(RoleBuilder.create().name("uma_authorization").build()) + .realmRole(RoleBuilder.create().name("uma_protection").build()) + ) + .user(UserBuilder.create().username("marta").password("password") + .addRoles("uma_authorization", "uma_protection") + .role("resource-server-test", "uma_protection")) + .user(UserBuilder.create().username("kolo").password("password")) + .client(ClientBuilder.create().clientId("resource-server-uma-test") + .secret("secret") + .authorizationServicesEnabled(true) + .redirectUris("http://localhost/resource-server-uma-test") + .defaultRoles("uma_protection") + .directAccessGrants()) + .client(ClientBuilder.create().clientId("resource-server-test") + .secret("secret") + .authorizationServicesEnabled(true) + .redirectUris("http://localhost/resource-server-test") + .defaultRoles("uma_protection") + .directAccessGrants()) + .client(ClientBuilder.create().clientId("public-client-test") + .publicClient() + .redirectUris("http://localhost:8180/auth/realms/master/app/auth/*") + .directAccessGrants()) + .build()); + } + + @Test + public void testEnforceUMAAccessWithClaimsUsingBearerToken() { + initAuthorizationSettings(getClientResource("resource-server-uma-test")); + + KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getAdapterConfiguration("enforcer-uma-claims-test.json")); + PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer(); + HashMap> headers = new HashMap<>(); + HashMap> parameters = new HashMap<>(); + + parameters.put("withdrawal.amount", Arrays.asList("50")); + + AuthzClient authzClient = getAuthzClient("enforcer-uma-claims-test.json"); + String token = authzClient.obtainAccessToken("marta", "password").getToken(); + + headers.put("Authorization", Arrays.asList("Bearer " + token)); + + AuthorizationContext context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters)); + assertFalse(context.isGranted()); + + AuthorizationRequest request = new AuthorizationRequest(); + + request.setTicket(extractTicket(headers)); + + AuthorizationResponse response = authzClient.authorization("marta", "password").authorize(request); + token = response.getToken(); + + assertNotNull(token); + + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters)); + assertTrue(context.isGranted()); + + parameters.put("withdrawal.amount", Arrays.asList("200")); + + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters)); + assertFalse(context.isGranted()); + + parameters.put("withdrawal.amount", Arrays.asList("50")); + + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters)); + assertTrue(context.isGranted()); + + parameters.put("withdrawal.amount", Arrays.asList("10")); + + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters)); + + request = new AuthorizationRequest(); + + request.setTicket(extractTicket(headers)); + + response = authzClient.authorization("marta", "password").authorize(request); + token = response.getToken(); + + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters)); + assertTrue(context.isGranted()); + + request = new AuthorizationRequest(); + + request.setTicket(extractTicket(headers)); + + response = authzClient.authorization("marta", "password").authorize(request); + token = response.getToken(); + + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", "GET", token, headers, parameters)); + assertTrue(context.isGranted()); + + assertEquals(1, context.getPermissions().size()); + Permission permission = context.getPermissions().get(0); + + assertEquals(parameters.get("withdrawal.amount").get(0), permission.getClaims().get("withdrawal.amount").iterator().next()); + } + + @Test + public void testEnforceEntitlementAccessWithClaimsWithoutBearerToken() { + initAuthorizationSettings(getClientResource("resource-server-test")); + + KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getAdapterConfiguration("enforcer-entitlement-claims-test.json")); + PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer(); + HashMap> headers = new HashMap<>(); + HashMap> parameters = new HashMap<>(); + + AuthzClient authzClient = getAuthzClient("enforcer-entitlement-claims-test.json"); + String token = authzClient.obtainAccessToken("marta", "password").getToken(); + + AuthorizationContext context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); + assertFalse(context.isGranted()); + + parameters.put("withdrawal.amount", Arrays.asList("50")); + + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); + assertTrue(context.isGranted()); + assertEquals(1, context.getPermissions().size()); + Permission permission = context.getPermissions().get(0); + assertEquals(parameters.get("withdrawal.amount").get(0), permission.getClaims().get("withdrawal.amount").iterator().next()); + + parameters.put("withdrawal.amount", Arrays.asList("200")); + + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); + assertFalse(context.isGranted()); + + parameters.put("withdrawal.amount", Arrays.asList("50")); + + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); + assertTrue(context.isGranted()); + + parameters.put("withdrawal.amount", Arrays.asList("10")); + + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); + + assertTrue(context.isGranted()); + + assertEquals(1, context.getPermissions().size()); + permission = context.getPermissions().get(0); + assertEquals(parameters.get("withdrawal.amount").get(0), permission.getClaims().get("withdrawal.amount").iterator().next()); + } + + @Test + public void testEnforceEntitlementAccessWithClaimsWithBearerToken() { + initAuthorizationSettings(getClientResource("resource-server-test")); + + KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getAdapterConfiguration("enforcer-entitlement-claims-test.json")); + PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer(); + HashMap> headers = new HashMap<>(); + HashMap> parameters = new HashMap<>(); + + AuthzClient authzClient = getAuthzClient("enforcer-entitlement-claims-test.json"); + String token = authzClient.obtainAccessToken("marta", "password").getToken(); + + headers.put("Authorization", Arrays.asList("Bearer " + token)); + + AuthorizationContext context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); + assertFalse(context.isGranted()); + + parameters.put("withdrawal.amount", Arrays.asList("50")); + + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); + assertTrue(context.isGranted()); + + parameters.put("withdrawal.amount", Arrays.asList("200")); + + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); + assertFalse(context.isGranted()); + + parameters.put("withdrawal.amount", Arrays.asList("50")); + + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); + assertTrue(context.isGranted()); + + parameters.put("withdrawal.amount", Arrays.asList("10")); + + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); + + assertTrue(context.isGranted()); + } + + @Test + public void testEnforceEntitlementAccessWithClaimsWithBearerTokenFromPublicClient() { + initAuthorizationSettings(getClientResource("resource-server-test")); + + KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getAdapterConfiguration("enforcer-entitlement-claims-test.json")); + PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer(); + HashMap> headers = new HashMap<>(); + HashMap> parameters = new HashMap<>(); + + oauth.realm(REALM_NAME); + oauth.clientId("public-client-test"); + oauth.doLogin("marta", "password"); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, null); + String token = response.getAccessToken(); + + headers.put("Authorization", Arrays.asList("Bearer " + token)); + + AuthorizationContext context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); + assertFalse(context.isGranted()); + + parameters.put("withdrawal.amount", Arrays.asList("50")); + + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); + assertTrue(context.isGranted()); + + parameters.put("withdrawal.amount", Arrays.asList("200")); + + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); + assertFalse(context.isGranted()); + + parameters.put("withdrawal.amount", Arrays.asList("50")); + + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); + assertTrue(context.isGranted()); + + parameters.put("withdrawal.amount", Arrays.asList("10")); + + context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); + + assertTrue(context.isGranted()); + } + + private String extractTicket(HashMap> headers) { + List wwwAuthenticateHeader = headers.get("WWW-Authenticate"); + + assertNotNull(wwwAuthenticateHeader); + assertFalse(wwwAuthenticateHeader.isEmpty()); + + String wwwAuthenticate = wwwAuthenticateHeader.get(0); + return wwwAuthenticate.substring(wwwAuthenticate.indexOf("ticket=") + "ticket=\"".length(), wwwAuthenticate.lastIndexOf('"')); + } + + private void initAuthorizationSettings(ClientResource clientResource) { + if (clientResource.authorization().resources().findByName("Bank Account").isEmpty()) { + JSPolicyRepresentation policy = new JSPolicyRepresentation(); + + policy.setName("Withdrawal Limit Policy"); + + StringBuilder code = new StringBuilder(); + + code.append("var context = $evaluation.getContext();"); + code.append("var attributes = context.getAttributes();"); + code.append("var withdrawalAmount = attributes.getValue('withdrawal.amount');"); + code.append("if (withdrawalAmount && withdrawalAmount.asDouble(0) <= 100) {"); + code.append(" $evaluation.grant();"); + code.append("}"); + + policy.setCode(code.toString()); + + clientResource.authorization().policies().js().create(policy); + + createResource(clientResource, "Bank Account", "/api/bank/account/{id}/withdrawal", "withdrawal"); + + ScopePermissionRepresentation permission = new ScopePermissionRepresentation(); + + permission.setName("Withdrawal Permission"); + permission.addScope("withdrawal"); + permission.addPolicy(policy.getName()); + + clientResource.authorization().permissions().scope().create(permission); + } + } + + private InputStream getAdapterConfiguration(String fileName) { + return getClass().getResourceAsStream("/authorization-test/" + fileName); + } + + private ResourceRepresentation createResource(ClientResource clientResource, String name, String uri, String... scopes) { + ResourceRepresentation representation = new ResourceRepresentation(); + + representation.setName(name); + representation.setUri(uri); + representation.setScopes(Arrays.asList(scopes).stream().map(ScopeRepresentation::new).collect(Collectors.toSet())); + + javax.ws.rs.core.Response response = clientResource.authorization().resources().create(representation); + + representation.setId(response.readEntity(ResourceRepresentation.class).getId()); + + return representation; + } + + private ClientResource getClientResource(String name) { + ClientsResource clients = realmsResouce().realm(REALM_NAME).clients(); + ClientRepresentation representation = clients.findByClientId(name).get(0); + return clients.get(representation.getId()); + } + + private OIDCHttpFacade createHttpFacade(String path, String method, String token, Map> headers, Map> parameters, InputStream requestBody) { + return new OIDCHttpFacade() { + Request request; + Response response; + + @Override + public KeycloakSecurityContext getSecurityContext() { + AccessToken accessToken; + try { + accessToken = new JWSInput(token).readJsonContent(AccessToken.class); + } catch (JWSInputException cause) { + throw new RuntimeException(cause); + } + return new KeycloakSecurityContext(token, accessToken, null, null); + } + + @Override + public Request getRequest() { + if (request == null) { + request = createHttpRequest(path, method, headers, parameters, requestBody); + } + return request; + } + + @Override + public Response getResponse() { + if (response == null) { + response = createHttpResponse(headers); + } + return response; + } + + @Override + public X509Certificate[] getCertificateChain() { + return new X509Certificate[0]; + } + }; + } + + private OIDCHttpFacade createHttpFacade(String path, String token, Map> headers, Map> parameters) { + return createHttpFacade(path, null, token, headers, parameters, null); + } + + private OIDCHttpFacade createHttpFacade(String path, String method, String token, Map> headers, Map> parameters) { + return createHttpFacade(path, method, token, headers, parameters, null); + } + + private Response createHttpResponse(Map> headers) { + return new Response() { + + private int status; + + @Override + public void setStatus(int status) { + this.status = status; + } + + @Override + public void addHeader(String name, String value) { + setHeader(name, value); + } + + @Override + public void setHeader(String name, String value) { + headers.put(name, Arrays.asList(value)); + } + + @Override + public void resetCookie(String name, String path) { + + } + + @Override + public void setCookie(String name, String value, String path, String domain, int maxAge, boolean secure, boolean httpOnly) { + + } + + @Override + public OutputStream getOutputStream() { + return null; + } + + @Override + public void sendError(int code) { + + } + + @Override + public void sendError(int code, String message) { + + } + + @Override + public void end() { + + } + }; + } + + private Request createHttpRequest(String path, String method, Map> headers, Map> parameters, InputStream requestBody) { + return new Request() { + + private InputStream inputStream; + + @Override + public String getMethod() { + return method == null ? "GET" : method; + } + + @Override + public String getURI() { + return path; + } + + @Override + public String getRelativePath() { + return path; + } + + @Override + public boolean isSecure() { + return true; + } + + @Override + public String getFirstParam(String param) { + List values = parameters.getOrDefault(param, Collections.emptyList()); + + if (!values.isEmpty()) { + return values.get(0); + } + + return null; + } + + @Override + public String getQueryParamValue(String param) { + return getFirstParam(param); + } + + @Override + public Cookie getCookie(String cookieName) { + return null; + } + + @Override + public String getHeader(String name) { + List headers = getHeaders(name); + + if (!headers.isEmpty()) { + return headers.get(0); + } + + return null; + } + + @Override + public List getHeaders(String name) { + return headers.getOrDefault(name, Collections.emptyList()); + } + + @Override + public InputStream getInputStream() { + return getInputStream(false); + } + + @Override + public InputStream getInputStream(boolean buffer) { + if (requestBody == null) { + return new ByteArrayInputStream(new byte[] {}); + } + + if (inputStream != null) { + return inputStream; + } + + if (buffer) { + return inputStream = new BufferedInputStream(requestBody); + } + + return requestBody; + } + + @Override + public String getRemoteAddr() { + return "user-remote-addr"; + } + + @Override + public void setError(AuthenticationError error) { + + } + + @Override + public void setError(LogoutError error) { + + } + }; + } + + protected AuthzClient getAuthzClient(String fileName) { + try { + return AuthzClient.create(JsonSerialization.readValue(getAdapterConfiguration(fileName), Configuration.class)); + } catch (IOException cause) { + throw new RuntimeException("Failed to create authz client", cause); + } + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/PolicyEnforcerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/PolicyEnforcerTest.java index 4713239fcb..2df71b6e6d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/PolicyEnforcerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/PolicyEnforcerTest.java @@ -18,7 +18,6 @@ package org.keycloak.testsuite.admin.client.authorization; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import java.io.BufferedInputStream; @@ -35,6 +34,7 @@ import java.util.stream.Collectors; import javax.security.cert.X509Certificate; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.keycloak.AuthorizationContext; @@ -58,12 +58,9 @@ import org.keycloak.jose.jws.JWSInputException; import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RealmRepresentation; -import org.keycloak.representations.idm.authorization.AuthorizationRequest; -import org.keycloak.representations.idm.authorization.AuthorizationResponse; import org.keycloak.representations.idm.authorization.JSPolicyRepresentation; -import org.keycloak.representations.idm.authorization.Permission; +import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation; import org.keycloak.representations.idm.authorization.ResourceRepresentation; -import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation; import org.keycloak.representations.idm.authorization.ScopeRepresentation; import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.ProfileAssume; @@ -117,168 +114,20 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest { .build()); } - @Test - public void testEnforceUMAAccessWithClaimsUsingBearerToken() { - initAuthorizationSettings(getClientResource("resource-server-uma-test")); - - KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getAdapterConfiguration("enforcer-uma-claims-test.json")); - PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer(); - HashMap> headers = new HashMap<>(); - HashMap> parameters = new HashMap<>(); - - parameters.put("withdrawal.amount", Arrays.asList("50")); - - AuthzClient authzClient = getAuthzClient("enforcer-uma-claims-test.json"); - String token = authzClient.obtainAccessToken("marta", "password").getToken(); - - headers.put("Authorization", Arrays.asList("Bearer " + token)); - - AuthorizationContext context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters)); - assertFalse(context.isGranted()); - - AuthorizationRequest request = new AuthorizationRequest(); - - request.setTicket(extractTicket(headers)); - - AuthorizationResponse response = authzClient.authorization("marta", "password").authorize(request); - token = response.getToken(); - - assertNotNull(token); - - context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters)); - assertTrue(context.isGranted()); - - parameters.put("withdrawal.amount", Arrays.asList("200")); - - context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters)); - assertFalse(context.isGranted()); - - parameters.put("withdrawal.amount", Arrays.asList("50")); - - context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters)); - assertTrue(context.isGranted()); - - parameters.put("withdrawal.amount", Arrays.asList("10")); - - context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters)); - - request = new AuthorizationRequest(); - - request.setTicket(extractTicket(headers)); - - response = authzClient.authorization("marta", "password").authorize(request); - token = response.getToken(); - - context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", "POST", token, headers, parameters)); - assertTrue(context.isGranted()); - - request = new AuthorizationRequest(); - - request.setTicket(extractTicket(headers)); - - response = authzClient.authorization("marta", "password").authorize(request); - token = response.getToken(); - - context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", "GET", token, headers, parameters)); - assertTrue(context.isGranted()); - - assertEquals(1, context.getPermissions().size()); - Permission permission = context.getPermissions().get(0); - - assertEquals(parameters.get("withdrawal.amount").get(0), permission.getClaims().get("withdrawal.amount").iterator().next()); + @Before + public void onBefore() { + initAuthorizationSettings(getClientResource("resource-server-test")); } @Test - public void testEnforceEntitlementAccessWithClaimsWithoutBearerToken() { - initAuthorizationSettings(getClientResource("resource-server-test")); - - KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getAdapterConfiguration("enforcer-entitlement-claims-test.json")); + public void testBearerOnlyClientResponse() { + KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getAdapterConfiguration("enforcer-bearer-only.json")); PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer(); - HashMap> headers = new HashMap<>(); - HashMap> parameters = new HashMap<>(); + OIDCHttpFacade httpFacade = createHttpFacade("/api/resourcea"); + AuthorizationContext context = policyEnforcer.enforce(httpFacade); - AuthzClient authzClient = getAuthzClient("enforcer-entitlement-claims-test.json"); - String token = authzClient.obtainAccessToken("marta", "password").getToken(); - - AuthorizationContext context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); assertFalse(context.isGranted()); - - parameters.put("withdrawal.amount", Arrays.asList("50")); - - context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); - assertTrue(context.isGranted()); - assertEquals(1, context.getPermissions().size()); - Permission permission = context.getPermissions().get(0); - assertEquals(parameters.get("withdrawal.amount").get(0), permission.getClaims().get("withdrawal.amount").iterator().next()); - - parameters.put("withdrawal.amount", Arrays.asList("200")); - - context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); - assertFalse(context.isGranted()); - - parameters.put("withdrawal.amount", Arrays.asList("50")); - - context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); - assertTrue(context.isGranted()); - - parameters.put("withdrawal.amount", Arrays.asList("10")); - - context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); - - assertTrue(context.isGranted()); - - assertEquals(1, context.getPermissions().size()); - permission = context.getPermissions().get(0); - assertEquals(parameters.get("withdrawal.amount").get(0), permission.getClaims().get("withdrawal.amount").iterator().next()); - } - - @Test - public void testEnforceEntitlementAccessWithClaimsWithBearerToken() { - initAuthorizationSettings(getClientResource("resource-server-test")); - - KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getAdapterConfiguration("enforcer-entitlement-claims-test.json")); - PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer(); - HashMap> headers = new HashMap<>(); - HashMap> parameters = new HashMap<>(); - - AuthzClient authzClient = getAuthzClient("enforcer-entitlement-claims-test.json"); - String token = authzClient.obtainAccessToken("marta", "password").getToken(); - - headers.put("Authorization", Arrays.asList("Bearer " + token)); - - AuthorizationContext context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); - assertFalse(context.isGranted()); - - parameters.put("withdrawal.amount", Arrays.asList("50")); - - context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); - assertTrue(context.isGranted()); - - parameters.put("withdrawal.amount", Arrays.asList("200")); - - context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); - assertFalse(context.isGranted()); - - parameters.put("withdrawal.amount", Arrays.asList("50")); - - context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); - assertTrue(context.isGranted()); - - parameters.put("withdrawal.amount", Arrays.asList("10")); - - context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); - - assertTrue(context.isGranted()); - } - - @Test - public void testEnforceEntitlementAccessWithClaimsWithBearerTokenFromPublicClient() { - initAuthorizationSettings(getClientResource("resource-server-test")); - - KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getAdapterConfiguration("enforcer-entitlement-claims-test.json")); - PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer(); - HashMap> headers = new HashMap<>(); - HashMap> parameters = new HashMap<>(); + assertEquals(403, TestResponse.class.cast(httpFacade.getResponse()).getStatus()); oauth.realm(REALM_NAME); oauth.clientId("public-client-test"); @@ -288,71 +137,92 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest { OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, null); String token = response.getAccessToken(); - headers.put("Authorization", Arrays.asList("Bearer " + token)); + httpFacade = createHttpFacade("/api/resourcea", token); - AuthorizationContext context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); + context = policyEnforcer.enforce(httpFacade); + assertTrue(context.isGranted()); + + httpFacade = createHttpFacade("/api/resourceb"); + + context = policyEnforcer.enforce(httpFacade); assertFalse(context.isGranted()); - - parameters.put("withdrawal.amount", Arrays.asList("50")); - - context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); - assertTrue(context.isGranted()); - - parameters.put("withdrawal.amount", Arrays.asList("200")); - - context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); - assertFalse(context.isGranted()); - - parameters.put("withdrawal.amount", Arrays.asList("50")); - - context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); - assertTrue(context.isGranted()); - - parameters.put("withdrawal.amount", Arrays.asList("10")); - - context = policyEnforcer.enforce(createHttpFacade("/api/bank/account/1/withdrawal", token, headers, parameters)); - - assertTrue(context.isGranted()); + assertEquals(403, TestResponse.class.cast(httpFacade.getResponse()).getStatus()); } - private String extractTicket(HashMap> headers) { - List wwwAuthenticateHeader = headers.get("WWW-Authenticate"); + @Test + public void testOnDenyRedirectTo() { + KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getAdapterConfiguration("enforcer-on-deny-redirect.json")); + PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer(); + OIDCHttpFacade httpFacade = createHttpFacade("/api/resourcea"); + AuthorizationContext context = policyEnforcer.enforce(httpFacade); - assertNotNull(wwwAuthenticateHeader); - assertFalse(wwwAuthenticateHeader.isEmpty()); + assertFalse(context.isGranted()); + TestResponse response = TestResponse.class.cast(httpFacade.getResponse()); + assertEquals(302, response.getStatus()); + List location = response.getHeaders().getOrDefault("Location", Collections.emptyList()); + assertFalse(location.isEmpty()); + assertEquals("/accessDenied", location.get(0)); + } - String wwwAuthenticate = wwwAuthenticateHeader.get(0); - return wwwAuthenticate.substring(wwwAuthenticate.indexOf("ticket=") + "ticket=\"".length(), wwwAuthenticate.lastIndexOf('"')); + @Test + public void testNotAuthenticatedDenyUnmapedPath() { + KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getAdapterConfiguration("enforcer-bearer-only.json")); + PolicyEnforcer policyEnforcer = deployment.getPolicyEnforcer(); + OIDCHttpFacade httpFacade = createHttpFacade("/api/unmmaped"); + AuthorizationContext context = policyEnforcer.enforce(httpFacade); + + assertFalse(context.isGranted()); + TestResponse response = TestResponse.class.cast(httpFacade.getResponse()); + assertEquals(403, response.getStatus()); } private void initAuthorizationSettings(ClientResource clientResource) { - if (clientResource.authorization().resources().findByName("Bank Account").isEmpty()) { + if (clientResource.authorization().resources().findByName("Resource A").isEmpty()) { JSPolicyRepresentation policy = new JSPolicyRepresentation(); - policy.setName("Withdrawal Limit Policy"); + policy.setName("Resource A Policy"); StringBuilder code = new StringBuilder(); - code.append("var context = $evaluation.getContext();"); - code.append("var attributes = context.getAttributes();"); - code.append("var withdrawalAmount = attributes.getValue('withdrawal.amount');"); - code.append("if (withdrawalAmount && withdrawalAmount.asDouble(0) <= 100) {"); - code.append(" $evaluation.grant();"); - code.append("}"); + code.append("$evaluation.grant();"); policy.setCode(code.toString()); clientResource.authorization().policies().js().create(policy); - createResource(clientResource, "Bank Account", "/api/bank/account/{id}/withdrawal", "withdrawal"); + createResource(clientResource, "Resource A", "/api/resourcea"); - ScopePermissionRepresentation permission = new ScopePermissionRepresentation(); + ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation(); - permission.setName("Withdrawal Permission"); - permission.addScope("withdrawal"); + permission.setName("Resource A Permission"); + permission.addResource("Resource A"); permission.addPolicy(policy.getName()); - clientResource.authorization().permissions().scope().create(permission); + clientResource.authorization().permissions().resource().create(permission); + } + + if (clientResource.authorization().resources().findByName("Resource B").isEmpty()) { + JSPolicyRepresentation policy = new JSPolicyRepresentation(); + + policy.setName("Resource B Policy"); + + StringBuilder code = new StringBuilder(); + + code.append("$evaluation.deny();"); + + policy.setCode(code.toString()); + + clientResource.authorization().policies().js().create(policy); + + createResource(clientResource, "Resource B", "/api/resourceb"); + + ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation(); + + permission.setName("Resource B Permission"); + permission.addResource("Resource B"); + permission.addPolicy(policy.getName()); + + clientResource.authorization().permissions().resource().create(permission); } } @@ -387,13 +257,16 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest { @Override public KeycloakSecurityContext getSecurityContext() { - AccessToken accessToken; - try { - accessToken = new JWSInput(token).readJsonContent(AccessToken.class); - } catch (JWSInputException cause) { - throw new RuntimeException(cause); + if (token != null) { + AccessToken accessToken; + try { + accessToken = new JWSInput(token).readJsonContent(AccessToken.class); + } catch (JWSInputException cause) { + throw new RuntimeException(cause); + } + return new KeycloakSecurityContext(token, accessToken, null, null); } - return new KeycloakSecurityContext(token, accessToken, null, null); + return null; } @Override @@ -419,64 +292,16 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest { }; } - private OIDCHttpFacade createHttpFacade(String path, String token, Map> headers, Map> parameters) { - return createHttpFacade(path, null, token, headers, parameters, null); + private OIDCHttpFacade createHttpFacade(String path, String token) { + return createHttpFacade(path, null, token, new HashMap<>(), new HashMap<>(), null); } - private OIDCHttpFacade createHttpFacade(String path, String method, String token, Map> headers, Map> parameters) { - return createHttpFacade(path, method, token, headers, parameters, null); + private OIDCHttpFacade createHttpFacade(String path) { + return createHttpFacade(path, null, null, new HashMap<>(), new HashMap<>(), null); } private Response createHttpResponse(Map> headers) { - return new Response() { - - private int status; - - @Override - public void setStatus(int status) { - this.status = status; - } - - @Override - public void addHeader(String name, String value) { - setHeader(name, value); - } - - @Override - public void setHeader(String name, String value) { - headers.put(name, Arrays.asList(value)); - } - - @Override - public void resetCookie(String name, String path) { - - } - - @Override - public void setCookie(String name, String value, String path, String domain, int maxAge, boolean secure, boolean httpOnly) { - - } - - @Override - public OutputStream getOutputStream() { - return null; - } - - @Override - public void sendError(int code) { - - } - - @Override - public void sendError(int code, String message) { - - } - - @Override - public void end() { - - } - }; + return new TestResponse(); } private Request createHttpRequest(String path, String method, Map> headers, Map> parameters, InputStream requestBody) { @@ -587,4 +412,63 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest { throw new RuntimeException("Failed to create authz client", cause); } } + + private class TestResponse implements Response { + + private final Map> headers = new HashMap<>(); + private int status; + + @Override + public void setStatus(int status) { + this.status = status; + } + + public int getStatus() { + return status; + } + + @Override + public void addHeader(String name, String value) { + setHeader(name, value); + } + + @Override + public void setHeader(String name, String value) { + headers.put(name, Arrays.asList(value)); + } + + public Map> getHeaders() { + return headers; + } + + @Override + public void resetCookie(String name, String path) { + + } + + @Override + public void setCookie(String name, String value, String path, String domain, int maxAge, boolean secure, boolean httpOnly) { + + } + + @Override + public OutputStream getOutputStream() { + return null; + } + + @Override + public void sendError(int code) { + status = code; + } + + @Override + public void sendError(int code, String message) { + status = code; + } + + @Override + public void end() { + + } + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/enforcer-bearer-only.json b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/enforcer-bearer-only.json new file mode 100644 index 0000000000..5f7f4e9e97 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/enforcer-bearer-only.json @@ -0,0 +1,11 @@ +{ + "realm": "authz-test", + "auth-server-url": "http://localhost:8180/auth", + "ssl-required": "external", + "resource": "resource-server-test", + "credentials": { + "secret": "secret" + }, + "bearer-only": true, + "policy-enforcer": {} +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/enforcer-on-deny-redirect.json b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/enforcer-on-deny-redirect.json new file mode 100644 index 0000000000..c254401003 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/enforcer-on-deny-redirect.json @@ -0,0 +1,12 @@ +{ + "realm": "authz-test", + "auth-server-url": "http://localhost:8180/auth", + "ssl-required": "external", + "resource": "resource-server-test", + "credentials": { + "secret": "secret" + }, + "policy-enforcer": { + "on-deny-redirect-to": "/accessDenied" + } +}