keycloak-uncached
Changes
core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationRequest.java 10(+10 -0)
core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketToken.java 10(+10 -0)
services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java 7(+5 -2)
Details
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationRequest.java b/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationRequest.java
index 764ae02..8a34e98 100644
--- a/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationRequest.java
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationRequest.java
@@ -21,6 +21,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import org.keycloak.representations.idm.authorization.PermissionTicketToken.ResourcePermission;
@@ -40,6 +41,7 @@ public class AuthorizationRequest {
private String audience;
private String accessToken;
private boolean submitRequest;
+ private Map<String, Object> claims;
public AuthorizationRequest(String ticket) {
this.ticket = ticket;
@@ -129,6 +131,14 @@ public class AuthorizationRequest {
return accessToken;
}
+ public Map<String, Object> getClaims() {
+ return claims;
+ }
+
+ public void setClaims(Map<String, Object> claims) {
+ this.claims = claims;
+ }
+
public void addPermission(String resourceId, List<String> scopes) {
addPermission(resourceId, scopes.toArray(new String[scopes.size()]));
}
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionRequest.java b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionRequest.java
index 5830e16..bd2a94b 100644
--- a/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionRequest.java
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionRequest.java
@@ -18,10 +18,15 @@
package org.keycloak.representations.idm.authorization;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import org.keycloak.json.StringListMapDeserializer;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@@ -32,6 +37,9 @@ public class PermissionRequest {
private Set<String> scopes;
private String resourceServerId;
+ @JsonDeserialize(using = StringListMapDeserializer.class)
+ private Map<String, List<String>> claims;
+
public PermissionRequest(String resourceId, String... scopes) {
this.resourceId = resourceId;
if (scopes != null) {
@@ -69,4 +77,28 @@ public class PermissionRequest {
public String getResourceServerId() {
return resourceServerId;
}
+
+ public Map<String, List<String>> getClaims() {
+ return claims;
+ }
+
+ public void setClaims(Map<String, List<String>> claims) {
+ this.claims = claims;
+ }
+
+ public void setClaim(String name, String... value) {
+ if (claims == null) {
+ claims = new HashMap<>();
+ }
+
+ claims.put(name, Arrays.asList(value));
+ }
+
+ public void addScope(String... name) {
+ if (scopes == null) {
+ scopes = new HashSet<>();
+ }
+
+ scopes.addAll(Arrays.asList(name));
+ }
}
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketToken.java b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketToken.java
index ff4a927..d370cf7 100644
--- a/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketToken.java
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketToken.java
@@ -18,10 +18,13 @@ package org.keycloak.representations.idm.authorization;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import org.keycloak.TokenIdGenerator;
+import org.keycloak.json.StringListMapDeserializer;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.JsonWebToken;
@@ -32,6 +35,9 @@ public class PermissionTicketToken extends JsonWebToken {
private final List<ResourcePermission> resources;
+ @JsonDeserialize(using = StringListMapDeserializer.class)
+ private Map<String, List<String>> claims;
+
public PermissionTicketToken() {
this(new ArrayList<ResourcePermission>());
}
@@ -59,6 +65,10 @@ public class PermissionTicketToken extends JsonWebToken {
return this.resources;
}
+ public Map<String, List<String>> getClaims() {
+ return claims;
+ }
+
public static class ResourcePermission {
@JsonProperty("id")
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/attribute/Attributes.java b/server-spi-private/src/main/java/org/keycloak/authorization/attribute/Attributes.java
index 0719bab..8f33fcf 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/attribute/Attributes.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/attribute/Attributes.java
@@ -142,5 +142,9 @@ public interface Attributes {
public long asLong(int idx) {
return Long.parseLong(asString(idx));
}
+
+ public double asDouble(int idx) {
+ return Double.parseDouble(asString(idx));
+ }
}
}
diff --git a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java
index 989d228..0c23294 100644
--- a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java
+++ b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java
@@ -95,13 +95,13 @@ public class AuthorizationTokenService {
claimToken = authorizationRequest.getAccessToken();
}
- return new KeycloakEvaluationContext(new KeycloakIdentity(authorization.getKeycloakSession(), Tokens.getAccessToken(claimToken, authorization.getKeycloakSession())), authorization.getKeycloakSession());
+ return new KeycloakEvaluationContext(new KeycloakIdentity(authorization.getKeycloakSession(), Tokens.getAccessToken(claimToken, authorization.getKeycloakSession())), authorizationRequest.getClaims(), authorization.getKeycloakSession());
});
SUPPORTED_CLAIM_TOKEN_FORMATS.put("http://openid.net/specs/openid-connect-core-1_0.html#IDToken", (authorizationRequest, authorization) -> {
try {
KeycloakSession keycloakSession = authorization.getKeycloakSession();
IDToken idToken = new TokenManager().verifyIDTokenSignature(keycloakSession, authorization.getRealm(), authorizationRequest.getClaimToken());
- return new KeycloakEvaluationContext(new KeycloakIdentity(keycloakSession, idToken), keycloakSession);
+ return new KeycloakEvaluationContext(new KeycloakIdentity(keycloakSession, idToken), authorizationRequest.getClaims(), keycloakSession);
} catch (OAuthErrorException cause) {
throw new RuntimeException("Failed to verify ID token", cause);
}
@@ -129,6 +129,9 @@ public class AuthorizationTokenService {
try {
PermissionTicketToken ticket = getPermissionTicket(request);
+
+ request.setClaims(ticket.getOtherClaims());
+
ResourceServer resourceServer = getResourceServer(ticket);
KeycloakEvaluationContext evaluationContext = createEvaluationContext(request);
KeycloakIdentity identity = KeycloakIdentity.class.cast(evaluationContext.getIdentity());
diff --git a/services/src/main/java/org/keycloak/authorization/common/DefaultEvaluationContext.java b/services/src/main/java/org/keycloak/authorization/common/DefaultEvaluationContext.java
index dec33cd..5d67819 100644
--- a/services/src/main/java/org/keycloak/authorization/common/DefaultEvaluationContext.java
+++ b/services/src/main/java/org/keycloak/authorization/common/DefaultEvaluationContext.java
@@ -22,15 +22,16 @@ import org.keycloak.authorization.attribute.Attributes;
import org.keycloak.authorization.identity.Identity;
import org.keycloak.authorization.policy.evaluation.EvaluationContext;
import org.keycloak.models.KeycloakSession;
-import org.keycloak.representations.AccessToken;
import java.text.SimpleDateFormat;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Map.Entry;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@@ -39,10 +40,16 @@ public class DefaultEvaluationContext implements EvaluationContext {
protected final KeycloakSession keycloakSession;
protected final Identity identity;
+ private final Map<String, Object> claims;
public DefaultEvaluationContext(Identity identity, KeycloakSession keycloakSession) {
- this.keycloakSession = keycloakSession;
+ this(identity, null, keycloakSession);
+ }
+
+ public DefaultEvaluationContext(Identity identity, Map<String, Object> claims, KeycloakSession keycloakSession) {
this.identity = identity;
+ this.claims = claims;
+ this.keycloakSession = keycloakSession;
}
@Override
@@ -51,7 +58,7 @@ public class DefaultEvaluationContext implements EvaluationContext {
}
public Map<String, Collection<String>> getBaseAttributes() {
- HashMap<String, Collection<String>> attributes = new HashMap<>();
+ Map<String, Collection<String>> attributes = new HashMap<>();
attributes.put("kc.time.date_time", Arrays.asList(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())));
attributes.put("kc.client.network.ip_address", Arrays.asList(this.keycloakSession.getContext().getConnection().getRemoteAddr()));
@@ -65,6 +72,20 @@ public class DefaultEvaluationContext implements EvaluationContext {
attributes.put("kc.realm.name", Arrays.asList(this.keycloakSession.getContext().getRealm().getName()));
+ if (claims != null) {
+ for (Entry<String, Object> entry : claims.entrySet()) {
+ Object value = entry.getValue();
+
+ if (value.getClass().isArray()) {
+ attributes.put(entry.getKey(), Arrays.asList(String[].class.cast(value)));
+ } else if (value instanceof Collection) {
+ attributes.put(entry.getKey(), Collection.class.cast(value));
+ } else {
+ attributes.put(entry.getKey(), Arrays.asList(String.valueOf(value)));
+ }
+ }
+ }
+
return attributes;
}
diff --git a/services/src/main/java/org/keycloak/authorization/common/KeycloakEvaluationContext.java b/services/src/main/java/org/keycloak/authorization/common/KeycloakEvaluationContext.java
index 047ff5a..fd43153 100644
--- a/services/src/main/java/org/keycloak/authorization/common/KeycloakEvaluationContext.java
+++ b/services/src/main/java/org/keycloak/authorization/common/KeycloakEvaluationContext.java
@@ -34,7 +34,11 @@ public class KeycloakEvaluationContext extends DefaultEvaluationContext {
private final KeycloakIdentity identity;
public KeycloakEvaluationContext(KeycloakIdentity identity, KeycloakSession keycloakSession) {
- super(identity, keycloakSession);
+ this(identity, null, keycloakSession);
+ }
+
+ public KeycloakEvaluationContext(KeycloakIdentity identity, Map<String, Object> claims, KeycloakSession keycloakSession) {
+ super(identity, claims, keycloakSession);
this.identity = identity;
}
diff --git a/services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java b/services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java
index 7dd3496..272ad3a 100644
--- a/services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java
+++ b/services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java
@@ -16,32 +16,30 @@
*/
package org.keycloak.authorization.protection.permission;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.ws.rs.core.Response;
+
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.common.KeycloakIdentity;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
-import org.keycloak.models.ClientModel;
-import org.keycloak.representations.idm.authorization.PermissionRequest;
-import org.keycloak.representations.idm.authorization.PermissionResponse;
import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.jose.jws.JWSBuilder;
+import org.keycloak.models.ClientModel;
import org.keycloak.models.KeyManager;
-import org.keycloak.models.utils.ModelToRepresentation;
+import org.keycloak.representations.idm.authorization.PermissionRequest;
+import org.keycloak.representations.idm.authorization.PermissionResponse;
import org.keycloak.representations.idm.authorization.PermissionTicketToken;
-import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentation;
-import org.keycloak.representations.idm.authorization.ResourceRepresentation;
-import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.services.ErrorResponseException;
-import javax.ws.rs.core.Response;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Set;
-import java.util.stream.Collectors;
-
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@@ -65,9 +63,9 @@ public class AbstractPermissionService {
return Response.status(Response.Status.CREATED).entity(new PermissionResponse(createPermissionTicket(request))).build();
}
- private List<ResourceRepresentation> verifyRequestedResource(List<PermissionRequest> request) {
+ private List<PermissionTicketToken.ResourcePermission> verifyRequestedResource(List<PermissionRequest> request) {
ResourceStore resourceStore = authorization.getStoreFactory().getResourceStore();
- List<ResourceRepresentation> requestedResources = new ArrayList<>();
+ List<PermissionTicketToken.ResourcePermission> requestedResources = new ArrayList<>();
for (PermissionRequest permissionRequest : request) {
String resourceSetId = permissionRequest.getResourceId();
@@ -104,19 +102,10 @@ public class AbstractPermissionService {
}
if (resources.isEmpty()) {
- requestedResources.add(new ResourceRepresentation(null, verifyRequestedScopes(permissionRequest, null)));
-
+ requestedResources.add(new PermissionTicketToken.ResourcePermission(null, verifyRequestedScopes(permissionRequest, null)));
} else {
for (Resource resource : resources) {
- Set<ScopeRepresentation> scopes = verifyRequestedScopes(permissionRequest, resource);
-
- ResourceRepresentation representation = new ResourceRepresentation(resource.getName(), scopes);
-
- representation.setId(resource.getId());
- representation.setOwnerManagedAccess(resource.isOwnerManagedAccess());
- representation.setOwner(new ResourceOwnerRepresentation(resource.getOwner()));
-
- requestedResources.add(representation);
+ requestedResources.add(new PermissionTicketToken.ResourcePermission(resource.getId(), verifyRequestedScopes(permissionRequest, resource)));
}
}
}
@@ -124,7 +113,7 @@ public class AbstractPermissionService {
return requestedResources;
}
- private Set<ScopeRepresentation> verifyRequestedScopes(PermissionRequest request, Resource resource) {
+ private Set<String> verifyRequestedScopes(PermissionRequest request, Resource resource) {
Set<String> requestScopes = request.getScopes();
if (requestScopes == null) {
@@ -153,24 +142,28 @@ public class AbstractPermissionService {
throw new ErrorResponseException("invalid_scope", "Scope [" + scopeName + "] is invalid", Response.Status.BAD_REQUEST);
}
- return ModelToRepresentation.toRepresentation(scope);
+ return scope.getName();
}).collect(Collectors.toSet());
}
private String createPermissionTicket(List<PermissionRequest> request) {
- List<PermissionTicketToken.ResourcePermission> permissions = verifyRequestedResource(request).stream().flatMap(resource -> {
- List<PermissionTicketToken.ResourcePermission> perms = new ArrayList<>();
- Set<ScopeRepresentation> scopes = resource.getScopes();
-
- perms.add(new PermissionTicketToken.ResourcePermission(resource.getId(), scopes.stream().map(ScopeRepresentation::getName).collect(Collectors.toSet())));
-
- return perms.stream();
- }).collect(Collectors.toList());
+ List<PermissionTicketToken.ResourcePermission> permissions = verifyRequestedResource(request);
KeyManager.ActiveRsaKey keys = this.authorization.getKeycloakSession().keys().getActiveRsaKey(this.authorization.getRealm());
ClientModel targetClient = authorization.getRealm().getClientById(resourceServer.getId());
+ PermissionTicketToken token = new PermissionTicketToken(permissions, targetClient.getClientId(), this.identity.getAccessToken());
+
+ for (PermissionRequest permissionRequest : request) {
+ Map<String, List<String>> claims = permissionRequest.getClaims();
+
+ if (claims != null) {
+ for (Entry<String, List<String>> claim : claims.entrySet()) {
+ token.setOtherClaims(claim.getKey(), claim.getValue());
+ }
+ }
+ }
- return new JWSBuilder().kid(keys.getKid()).jsonContent(new PermissionTicketToken(permissions, targetClient.getClientId(), this.identity.getAccessToken()))
+ return new JWSBuilder().kid(keys.getKid()).jsonContent(token)
.rsa256(keys.getPrivateKey());
}
}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UmaPermissionTicketPushedClaimsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UmaPermissionTicketPushedClaimsTest.java
new file mode 100644
index 0000000..0d36913
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UmaPermissionTicketPushedClaimsTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2018 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.authz;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+import org.keycloak.admin.client.resource.AuthorizationResource;
+import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.representations.idm.authorization.AuthorizationRequest;
+import org.keycloak.representations.idm.authorization.AuthorizationResponse;
+import org.keycloak.representations.idm.authorization.JSPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.PermissionRequest;
+import org.keycloak.representations.idm.authorization.PermissionResponse;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class UmaPermissionTicketPushedClaimsTest extends AbstractResourceServerTest {
+
+ @Test
+ public void testEvaluatePermissionsWithPushedClaims() throws Exception {
+ ResourceRepresentation resource = addResource("Bank Account", "withdraw");
+ JSPolicyRepresentation policy = new JSPolicyRepresentation();
+
+ policy.setName("Withdraw Limit Policy");
+
+ StringBuilder code = new StringBuilder();
+
+ code.append("var context = $evaluation.getContext();");
+ code.append("var attributes = context.getAttributes();");
+ code.append("var withdrawValue = attributes.getValue('my.bank.account.withdraw.value');");
+ code.append("if (withdrawValue && withdrawValue.asDouble(0) <= 100) {");
+ code.append(" $evaluation.grant();");
+ code.append("}");
+
+ policy.setCode(code.toString());
+
+ AuthorizationResource authorization = getClient(getRealm()).authorization();
+
+ authorization.policies().js().create(policy);
+
+ ScopePermissionRepresentation representation = new ScopePermissionRepresentation();
+
+ representation.setName("Withdraw Permission");
+ representation.addScope("withdraw");
+ representation.addPolicy(policy.getName());
+
+ authorization.permissions().scope().create(representation);
+
+ AuthzClient authzClient = getAuthzClient();
+ PermissionRequest permissionRequest = new PermissionRequest(resource.getId());
+
+ permissionRequest.addScope("withdraw");
+ permissionRequest.setClaim("my.bank.account.withdraw.value", "50.5");
+
+ PermissionResponse response = authzClient.protection("marta", "password").permission().create(permissionRequest);
+ AuthorizationRequest request = new AuthorizationRequest();
+
+ request.setTicket(response.getTicket());
+ request.setClaimToken(authzClient.obtainAccessToken("marta", "password").getToken());
+
+ AuthorizationResponse authorizationResponse = authzClient.authorization().authorize(request);
+
+ assertNotNull(authorizationResponse);
+ assertNotNull(authorizationResponse.getToken());
+
+ permissionRequest.setClaim("my.bank.account.withdraw.value", "100.5");
+
+ response = authzClient.protection("marta", "password").permission().create(permissionRequest);
+ request = new AuthorizationRequest();
+
+ request.setTicket(response.getTicket());
+ request.setClaimToken(authzClient.obtainAccessToken("marta", "password").getToken());
+
+ try {
+ authorizationResponse = authzClient.authorization().authorize(request);
+ fail("Access should be denied");
+ } catch (Exception ignore) {
+
+ }
+ }
+}