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 e0a8a99cef..64f0f6f0be 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 @@ -25,8 +25,8 @@ import org.keycloak.OAuth2Constants; import org.keycloak.authorization.client.ClientAuthenticator; import org.keycloak.representations.idm.authorization.AuthorizationRequest; import org.keycloak.representations.idm.authorization.AuthorizationRequest.Metadata; +import org.keycloak.representations.idm.authorization.Permission; import org.keycloak.representations.idm.authorization.PermissionTicketToken; -import org.keycloak.representations.idm.authorization.PermissionTicketToken.ResourcePermission; /** * @author Pedro Igor @@ -80,13 +80,13 @@ public class HttpMethodAuthenticator { method.param("claim_token", request.getClaimToken()); method.param("claim_token_format", request.getClaimTokenFormat()); method.param("pct", request.getPct()); - method.param("rpt", request.getRpt()); + method.param("rpt", request.getRptToken()); method.param("scope", request.getScope()); method.param("audience", request.getAudience()); method.param("subject_token", request.getSubjectToken()); if (permissions != null) { - for (ResourcePermission permission : permissions.getResources()) { + for (Permission permission : permissions.getPermissions()) { String resourceId = permission.getResourceId(); Set scopes = permission.getScopes(); StringBuilder value = new StringBuilder(); 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 90cba85cc9..a50bf2d952 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 @@ -23,7 +23,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; -import org.keycloak.representations.idm.authorization.PermissionTicketToken.ResourcePermission; +import org.keycloak.representations.AccessToken; /** * @author Pedro Igor @@ -31,7 +31,6 @@ import org.keycloak.representations.idm.authorization.PermissionTicketToken.Reso public class AuthorizationRequest { private String ticket; - private String rpt; private String claimToken; private String claimTokenFormat; private String pct; @@ -42,6 +41,8 @@ public class AuthorizationRequest { private String subjectToken; private boolean submitRequest; private Map> claims; + private AccessToken rpt; + private String rptToken; public AuthorizationRequest(String ticket) { this.ticket = ticket; @@ -59,14 +60,22 @@ public class AuthorizationRequest { this.ticket = ticket; } - public String getRpt() { + public AccessToken getRpt() { return this.rpt; } - public void setRpt(String rpt) { + public void setRpt(AccessToken rpt) { this.rpt = rpt; } + public void setRpt(String rpt) { + this.rptToken = rpt; + } + + public String getRptToken() { + return rptToken; + } + public void setClaimToken(String claimToken) { this.claimToken = claimToken; } @@ -145,12 +154,12 @@ public class AuthorizationRequest { public void addPermission(String resourceId, String... scopes) { if (permissions == null) { - permissions = new PermissionTicketToken(new ArrayList()); + permissions = new PermissionTicketToken(new ArrayList()); } - ResourcePermission permission = null; + Permission permission = null; - for (ResourcePermission resourcePermission : permissions.getResources()) { + for (Permission resourcePermission : permissions.getPermissions()) { if (resourcePermission.getResourceId() != null && resourcePermission.getResourceId().equals(resourceId)) { permission = resourcePermission; break; @@ -158,8 +167,8 @@ public class AuthorizationRequest { } if (permission == null) { - permission = new ResourcePermission(resourceId, new HashSet()); - permissions.getResources().add(permission); + permission = new Permission(resourceId, new HashSet()); + permissions.getPermissions().add(permission); } permission.getScopes().addAll(Arrays.asList(scopes)); diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/Permission.java b/core/src/main/java/org/keycloak/representations/idm/authorization/Permission.java index 53760c39c3..c1afa01954 100644 --- a/core/src/main/java/org/keycloak/representations/idm/authorization/Permission.java +++ b/core/src/main/java/org/keycloak/representations/idm/authorization/Permission.java @@ -33,7 +33,7 @@ public class Permission { private String resourceId; @JsonProperty("rsname") - private final String resourceName; + private String resourceName; @JsonInclude(JsonInclude.Include.NON_EMPTY) private Set scopes; @@ -45,6 +45,10 @@ public class Permission { this(null, null, null, null); } + public Permission(final String resourceId, final Set scopes) { + this(resourceId, null, scopes, null); + } + public Permission(final String resourceId, String resourceName, final Set scopes, Map> claims) { this.resourceId = resourceId; this.resourceName = resourceName; @@ -52,10 +56,18 @@ public class Permission { this.claims = claims; } + public void setResourceId(String resourceId) { + this.resourceId = resourceId; + } + public String getResourceId() { return this.resourceId; } + public void setResourceName(String resourceName) { + this.resourceName = resourceName; + } + public String getResourceName() { return this.resourceName; } @@ -75,11 +87,29 @@ public class Permission { @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (o == null || !getClass().isAssignableFrom(o.getClass())) return false; Permission that = (Permission) o; - return getResourceId().equals(that.resourceId); + if (getResourceId() != null || getResourceName() != null) { + if (!getResourceId().equals(that.resourceId)) { + return false; + } + + if (getScopes().isEmpty() && that.getScopes().isEmpty()) { + return true; + } + } else if (that.resourceId != null) { + return false; + } + + for (String scope : that.getScopes()) { + if (getScopes().contains(scope)) { + return true; + } + } + + return false; } @Override 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 a9f6ba5c70..b20728d021 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 @@ -33,16 +33,16 @@ import org.keycloak.representations.JsonWebToken; */ public class PermissionTicketToken extends JsonWebToken { - private final List resources; + private final List permissions; @JsonDeserialize(using = StringListMapDeserializer.class) private Map> claims; public PermissionTicketToken() { - this(new ArrayList()); + this(new ArrayList()); } - public PermissionTicketToken(List resources, String audience, AccessToken accessToken) { + public PermissionTicketToken(List permissions, String audience, AccessToken accessToken) { if (accessToken != null) { id(TokenIdGenerator.generateId()); subject(accessToken.getSubject()); @@ -54,15 +54,15 @@ public class PermissionTicketToken extends JsonWebToken { if (audience != null) { audience(audience); } - this.resources = resources; + this.permissions = permissions; } - public PermissionTicketToken(List resources) { + public PermissionTicketToken(List resources) { this(resources, null, null); } - public List getResources() { - return this.resources; + public List getPermissions() { + return this.permissions; } public Map> getClaims() { @@ -72,29 +72,4 @@ public class PermissionTicketToken extends JsonWebToken { public void setClaims(Map> claims) { this.claims = claims; } - - public static class ResourcePermission { - - @JsonProperty("id") - private String resourceId; - - @JsonProperty("scopes") - private Set scopes; - - public ResourcePermission() { - } - - public ResourcePermission(String resourceId, Set scopes) { - this.resourceId = resourceId; - this.scopes = scopes; - } - - public String getResourceId() { - return resourceId; - } - - public Set getScopes() { - return scopes; - } - } } diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/PermissionTicketAwareDecisionResultCollector.java b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/PermissionTicketAwareDecisionResultCollector.java index 6ab219b35f..6b671e3e1c 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/PermissionTicketAwareDecisionResultCollector.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/PermissionTicketAwareDecisionResultCollector.java @@ -31,11 +31,11 @@ import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.Scope; import org.keycloak.authorization.permission.ResourcePermission; -import org.keycloak.authorization.policy.evaluation.Result.PolicyResult; import org.keycloak.authorization.store.ResourceStore; import org.keycloak.authorization.store.ScopeStore; import org.keycloak.authorization.store.StoreFactory; import org.keycloak.representations.idm.authorization.AuthorizationRequest; +import org.keycloak.representations.idm.authorization.Permission; import org.keycloak.representations.idm.authorization.PermissionTicketToken; /** @@ -75,12 +75,12 @@ public class PermissionTicketAwareDecisionResultCollector extends DecisionResult if ("uma".equals(policy.getType())) { ResourcePermission grantedPermission = evaluation.getPermission(); - List permissions = ticket.getResources(); + List permissions = ticket.getPermissions(); - Iterator itPermissions = permissions.iterator(); + Iterator itPermissions = permissions.iterator(); while (itPermissions.hasNext()) { - PermissionTicketToken.ResourcePermission permission = itPermissions.next(); + Permission permission = itPermissions.next(); if (permission.getResourceId().equals(grantedPermission.getResource().getId())) { Set scopes = permission.getScopes(); @@ -109,10 +109,10 @@ public class PermissionTicketAwareDecisionResultCollector extends DecisionResult if (request.isSubmitRequest()) { StoreFactory storeFactory = authorization.getStoreFactory(); ResourceStore resourceStore = storeFactory.getResourceStore(); - List permissions = ticket.getResources(); + List permissions = ticket.getPermissions(); if (permissions != null) { - for (PermissionTicketToken.ResourcePermission permission : permissions) { + for (Permission permission : permissions) { Resource resource = resourceStore.findById(permission.getResourceId(), resourceServer.getId()); if (resource == null) { 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 97e6727b7e..05a732f319 100644 --- a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java +++ b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java @@ -28,10 +28,7 @@ import java.util.Map.Entry; import java.util.Objects; import java.util.Set; import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.function.Predicate; import java.util.stream.Collectors; -import java.util.stream.Stream; import javax.ws.rs.HttpMethod; import javax.ws.rs.core.MediaType; @@ -70,7 +67,6 @@ import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.TokenManager.AccessTokenResponseBuilder; import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken.Authorization; -import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.IDToken; import org.keycloak.representations.RefreshToken; import org.keycloak.representations.idm.authorization.AuthorizationRequest; @@ -149,7 +145,7 @@ public class AuthorizationTokenService { } // it is not secure to allow public clients to push arbitrary claims because message can be tampered - if (isPublicClientRequestingEntitlemesWithClaims(request)) { + if (isPublicClientRequestingEntitlementWithClaims(request)) { throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Public clients are not allowed to send claims", Status.FORBIDDEN); } @@ -163,9 +159,9 @@ public class AuthorizationTokenService { KeycloakIdentity identity = KeycloakIdentity.class.cast(evaluationContext.getIdentity()); List evaluation; - if (ticket.getResources().isEmpty() && request.getRpt() == null) { + if (ticket.getPermissions().isEmpty() && request.getRpt() == null) { evaluation = evaluateAllPermissions(request, resourceServer, evaluationContext, identity); - } else if(!request.getPermissions().getResources().isEmpty()) { + } else if(!request.getPermissions().getPermissions().isEmpty()) { evaluation = evaluatePermissions(request, ticket, resourceServer, evaluationContext, identity); } else { evaluation = evaluateUserManagedPermissions(request, ticket, resourceServer, evaluationContext, identity); @@ -173,21 +169,21 @@ public class AuthorizationTokenService { List permissions = Permissions.permits(evaluation, request.getMetadata(), authorization, resourceServer); - if (permissions.isEmpty()) { - if (request.isSubmitRequest()) { - throw new CorsErrorResponseException(cors, OAuthErrorException.ACCESS_DENIED, "request_submitted", Status.FORBIDDEN); - } else { - throw new CorsErrorResponseException(cors, OAuthErrorException.ACCESS_DENIED, "not_authorized", Status.FORBIDDEN); - } + if (isGranted(ticket, request, permissions)) { + ClientModel targetClient = this.authorization.getRealm().getClientById(resourceServer.getId()); + AuthorizationResponse response = createAuthorizationResponse(identity, permissions, request, targetClient); + + return Cors.add(httpRequest, Response.status(Status.OK).type(MediaType.APPLICATION_JSON_TYPE).entity(response)) + .allowedOrigins(getKeycloakSession().getContext().getUri(), targetClient) + .allowedMethods(HttpMethod.POST) + .exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build(); } - ClientModel targetClient = this.authorization.getRealm().getClientById(resourceServer.getId()); - AuthorizationResponse response = new AuthorizationResponse(createRequestingPartyToken(identity, permissions, request, targetClient), request.getRpt() != null); - - return Cors.add(httpRequest, Response.status(Status.OK).type(MediaType.APPLICATION_JSON_TYPE).entity(response)) - .allowedOrigins(getKeycloakSession().getContext().getUri(), targetClient) - .allowedMethods(HttpMethod.POST) - .exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build(); + if (request.isSubmitRequest()) { + throw new CorsErrorResponseException(cors, OAuthErrorException.ACCESS_DENIED, "request_submitted", Status.FORBIDDEN); + } else { + throw new CorsErrorResponseException(cors, OAuthErrorException.ACCESS_DENIED, "not_authorized", Status.FORBIDDEN); + } } catch (ErrorResponseException | CorsErrorResponseException cause) { if (logger.isDebugEnabled()) { logger.debug("Error while evaluating permissions", cause); @@ -199,7 +195,7 @@ public class AuthorizationTokenService { } } - private boolean isPublicClientRequestingEntitlemesWithClaims(AuthorizationRequest request) { + private boolean isPublicClientRequestingEntitlementWithClaims(AuthorizationRequest request) { return request.getClaimToken() != null && getKeycloakSession().getContext().getClient().isPublicClient() && request.getTicket() == null; } @@ -221,7 +217,7 @@ public class AuthorizationTokenService { .evaluate(); } - private AccessTokenResponse createRequestingPartyToken(KeycloakIdentity identity, List entitlements, AuthorizationRequest request, ClientModel targetClient) { + private AuthorizationResponse createAuthorizationResponse(KeycloakIdentity identity, List entitlements, AuthorizationRequest request, ClientModel targetClient) { KeycloakSession keycloakSession = getKeycloakSession(); AccessToken accessToken = identity.getAccessToken(); UserSessionModel userSessionModel = keycloakSession.sessions().getUserSession(getRealm(), accessToken.getSessionState()); @@ -253,7 +249,31 @@ public class AuthorizationTokenService { rpt.audience(targetClient.getClientId()); } - return responseBuilder.build(); + return new AuthorizationResponse(responseBuilder.build(), isUpgraded(request, authorization)); + } + + private boolean isUpgraded(AuthorizationRequest request, Authorization authorization) { + AccessToken previousRpt = request.getRpt(); + + if (previousRpt == null) { + return false; + } + + Authorization previousAuthorization = previousRpt.getAuthorization(); + + if (previousAuthorization != null) { + List previousPermissions = previousAuthorization.getPermissions(); + + if (previousPermissions != null) { + for (Permission previousPermission : previousPermissions) { + if (!authorization.getPermissions().contains(previousPermission)) { + return false; + } + } + } + } + + return true; } private PermissionTicketToken getPermissionTicket(AuthorizationRequest request) { @@ -320,7 +340,7 @@ public class AuthorizationTokenService { Metadata metadata = request.getMetadata(); Integer limit = metadata != null ? metadata.getLimit() : null; - for (PermissionTicketToken.ResourcePermission requestedResource : ticket.getResources()) { + for (Permission requestedResource : ticket.getPermissions()) { if (limit != null && limit <= 0) { break; } @@ -339,16 +359,19 @@ public class AuthorizationTokenService { if (resource != null) { existingResources.add(resource); } else { - Resource ownerResource = resourceStore.findByName(requestedResource.getResourceId(), identity.getId(), resourceServer.getId()); + String resourceName = requestedResource.getResourceId(); + Resource ownerResource = resourceStore.findByName(resourceName, identity.getId(), resourceServer.getId()); if (ownerResource != null) { + requestedResource.setResourceId(ownerResource.getId()); existingResources.add(ownerResource); } if (!identity.isResourceServer()) { - Resource serverResource = resourceStore.findByName(requestedResource.getResourceId(), resourceServer.getId()); + Resource serverResource = resourceStore.findByName(resourceName, resourceServer.getId()); if (serverResource != null) { + requestedResource.setResourceId(serverResource.getId()); existingResources.add(serverResource); } } @@ -403,63 +426,49 @@ public class AuthorizationTokenService { } } - String rpt = request.getRpt(); + AccessToken rpt = request.getRpt(); - if (rpt != null) { - if (!Tokens.verifySignature(getKeycloakSession(), getRealm(), rpt)) { - throw new CorsErrorResponseException(cors, "invalid_rpt", "RPT signature is invalid", Status.FORBIDDEN); - } + if (rpt != null && rpt.isActive()) { + AccessToken.Authorization authorizationData = rpt.getAuthorization(); - AccessToken requestingPartyToken; + if (authorizationData != null) { + List permissions = authorizationData.getPermissions(); - try { - requestingPartyToken = new JWSInput(rpt).readJsonContent(AccessToken.class); - } catch (JWSInputException e) { - throw new CorsErrorResponseException(cors, "invalid_rpt", "Invalid RPT", Status.FORBIDDEN); - } + if (permissions != null) { + for (Permission grantedPermission : permissions) { + if (limit != null && limit <= 0) { + break; + } - if (requestingPartyToken.isActive()) { - AccessToken.Authorization authorizationData = requestingPartyToken.getAuthorization(); + Resource resource = resourceStore.findById(grantedPermission.getResourceId(), ticket.getAudience()[0]); - if (authorizationData != null) { - List permissions = authorizationData.getPermissions(); + if (resource != null) { + ResourcePermission permission = permissionsToEvaluate.get(resource.getId()); - if (permissions != null) { - for (Permission grantedPermission : permissions) { - if (limit != null && limit <= 0) { - break; - } + if (permission == null) { + permission = new ResourcePermission(resource, new ArrayList<>(), resourceServer, grantedPermission.getClaims()); + permissionsToEvaluate.put(resource.getId(), permission); + if (limit != null) { + limit--; + } + } else { + if (grantedPermission.getClaims() != null) { + for (Entry> entry : grantedPermission.getClaims().entrySet()) { + Set claims = permission.getClaims().get(entry.getKey()); - Resource resourcePermission = resourceStore.findById(grantedPermission.getResourceId(), ticket.getAudience()[0]); - - if (resourcePermission != null) { - ResourcePermission permission = permissionsToEvaluate.get(resourcePermission.getId()); - - if (permission == null) { - permission = new ResourcePermission(resourcePermission, new ArrayList<>(), resourceServer, grantedPermission.getClaims()); - permissionsToEvaluate.put(resourcePermission.getId(), permission); - if (limit != null) { - limit--; - } - } else { - if (grantedPermission.getClaims() != null) { - for (Entry> entry : grantedPermission.getClaims().entrySet()) { - Set claims = permission.getClaims().get(entry.getKey()); - - if (claims != null) { - claims.addAll(entry.getValue()); - } + if (claims != null) { + claims.addAll(entry.getValue()); } } } + } - for (String scopeName : grantedPermission.getScopes()) { - Scope scope = scopeStore.findByName(scopeName, resourceServer.getId()); + for (String scopeName : grantedPermission.getScopes()) { + Scope scope = scopeStore.findByName(scopeName, resourceServer.getId()); - if (scope != null) { - if (!permission.getScopes().contains(scope)) { - permission.getScopes().add(scope); - } + if (scope != null) { + if (!permission.getScopes().contains(scope)) { + permission.getScopes().add(scope); } } } @@ -492,6 +501,17 @@ public class AuthorizationTokenService { } } + private boolean isGranted(PermissionTicketToken ticket, AuthorizationRequest request, List permissions) { + List requestedPermissions = ticket.getPermissions(); + + // denies in case a rpt was provided along with the authorization request but any requested permission was not granted + if (request.getRpt() != null && !requestedPermissions.isEmpty() && requestedPermissions.stream().anyMatch(permission -> !permissions.contains(permission))) { + return false; + } + + return !permissions.isEmpty(); + } + private KeycloakSession getKeycloakSession() { return this.authorization.getKeycloakSession(); } 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 e045d31de1..2386f12af4 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 @@ -35,6 +35,7 @@ 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.representations.idm.authorization.Permission; import org.keycloak.representations.idm.authorization.PermissionRequest; import org.keycloak.representations.idm.authorization.PermissionResponse; import org.keycloak.representations.idm.authorization.PermissionTicketToken; @@ -63,9 +64,9 @@ public class AbstractPermissionService { return Response.status(Response.Status.CREATED).entity(new PermissionResponse(createPermissionTicket(request))).build(); } - private List verifyRequestedResource(List request) { + private List verifyRequestedResource(List request) { ResourceStore resourceStore = authorization.getStoreFactory().getResourceStore(); - List requestedResources = new ArrayList<>(); + List requestedResources = new ArrayList<>(); for (PermissionRequest permissionRequest : request) { String resourceSetId = permissionRequest.getResourceId(); @@ -102,10 +103,10 @@ public class AbstractPermissionService { } if (resources.isEmpty()) { - requestedResources.add(new PermissionTicketToken.ResourcePermission(null, verifyRequestedScopes(permissionRequest, null))); + requestedResources.add(new Permission(null, verifyRequestedScopes(permissionRequest, null))); } else { for (Resource resource : resources) { - requestedResources.add(new PermissionTicketToken.ResourcePermission(resource.getId(), verifyRequestedScopes(permissionRequest, resource))); + requestedResources.add(new Permission(resource.getId(), verifyRequestedScopes(permissionRequest, resource))); } } } @@ -147,7 +148,7 @@ public class AbstractPermissionService { } private String createPermissionTicket(List request) { - List permissions = verifyRequestedResource(request); + List permissions = verifyRequestedResource(request); KeyManager.ActiveRsaKey keys = this.authorization.getKeycloakSession().keys().getActiveRsaKey(this.authorization.getRealm()); ClientModel targetClient = authorization.getRealm().getClientById(resourceServer.getId()); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java index ad92195e69..67cb0dc8d5 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java @@ -1079,7 +1079,20 @@ public class TokenEndpoint { authorizationRequest.setClaimToken(claimToken); authorizationRequest.setClaimTokenFormat(claimTokenFormat); authorizationRequest.setPct(formParams.getFirst("pct")); - authorizationRequest.setRpt(formParams.getFirst("rpt")); + String rpt = formParams.getFirst("rpt"); + + if (rpt != null) { + if (!Tokens.verifySignature(session, realm, rpt)) { + throw new CorsErrorResponseException(cors, "invalid_rpt", "RPT signature is invalid", Status.FORBIDDEN); + } + + try { + authorizationRequest.setRpt(new JWSInput(rpt).readJsonContent(AccessToken.class)); + } catch (JWSInputException e) { + throw new CorsErrorResponseException(cors, "invalid_rpt", "Invalid RPT", Status.FORBIDDEN); + } + } + authorizationRequest.setScope(formParams.getFirst("scope")); authorizationRequest.setAudience(formParams.getFirst("audience")); authorizationRequest.setSubjectToken(formParams.getFirst("subject_token") != null ? formParams.getFirst("subject_token") : accessTokenString); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionEqualsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionEqualsTest.java new file mode 100644 index 0000000000..e96fda370f --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionEqualsTest.java @@ -0,0 +1,74 @@ +/* + * 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.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; + +import org.junit.Test; +import org.keycloak.representations.idm.authorization.Permission; + +/** + * @author Pedro Igor + */ +public class PermissionEqualsTest { + + @Test + public void testEquals() { + assertTrue(new Permission("1", null, Collections.emptySet(), Collections.emptyMap()).equals( + new Permission("1", null, Collections.emptySet(), Collections.emptyMap()) + )); + assertFalse(new Permission("1", null, Collections.emptySet(), Collections.emptyMap()).equals( + new Permission("2", null, Collections.emptySet(), Collections.emptyMap()) + )); + assertFalse(new Permission("1", null, new HashSet<>(Arrays.asList("read", "write")), Collections.emptyMap()).equals( + new Permission("1", null, Collections.emptySet(), Collections.emptyMap()) + )); + assertTrue(new Permission("1", null, new HashSet<>(Arrays.asList("read", "write")), Collections.emptyMap()).equals( + new Permission("1", null, new HashSet<>(Arrays.asList("read", "write")), Collections.emptyMap()) + )); + assertTrue(new Permission("1", null, new HashSet<>(Arrays.asList("read", "write")), Collections.emptyMap()).equals( + new Permission("1", null, new HashSet<>(Arrays.asList("write")), Collections.emptyMap()) + )); + assertFalse(new Permission("1", null, new HashSet<>(Arrays.asList("read")), Collections.emptyMap()).equals( + new Permission("1", null, new HashSet<>(Arrays.asList("write")), Collections.emptyMap()) + )); + assertFalse(new Permission(null, null, new HashSet<>(Arrays.asList("write")), Collections.emptyMap()).equals( + new Permission("1", null, new HashSet<>(Arrays.asList("write")), Collections.emptyMap()) + )); + assertFalse(new Permission("1", null, new HashSet<>(Arrays.asList("write")), Collections.emptyMap()).equals( + new Permission(null, null, new HashSet<>(Arrays.asList("write")), Collections.emptyMap()) + )); + assertTrue(new Permission(null, null, new HashSet<>(Arrays.asList("write")), Collections.emptyMap()).equals( + new Permission(null, null, new HashSet<>(Arrays.asList("write")), Collections.emptyMap()) + )); + assertTrue(new Permission(null, null, new HashSet<>(Arrays.asList("read", "write")), Collections.emptyMap()).equals( + new Permission(null, null, new HashSet<>(Arrays.asList("read")), Collections.emptyMap()) + )); + assertFalse(new Permission(null, null, new HashSet<>(Arrays.asList("read", "write")), Collections.emptyMap()).equals( + new Permission(null, null, new HashSet<>(Arrays.asList("update")), Collections.emptyMap()) + )); + assertFalse(new Permission(null, null, Collections.emptySet(), Collections.emptyMap()).equals( + new Permission(null, null, new HashSet<>(Arrays.asList("read")), Collections.emptyMap()) + )); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionManagementTest.java index aa9ce50db7..87172f67d7 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionManagementTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionManagementTest.java @@ -36,6 +36,7 @@ import org.keycloak.authorization.client.AuthzClient; import org.keycloak.authorization.client.util.HttpResponseException; import org.keycloak.jose.jws.JWSInput; import org.keycloak.representations.idm.authorization.AuthorizationRequest; +import org.keycloak.representations.idm.authorization.Permission; import org.keycloak.representations.idm.authorization.PermissionRequest; import org.keycloak.representations.idm.authorization.PermissionResponse; import org.keycloak.representations.idm.authorization.PermissionTicketRepresentation; @@ -303,14 +304,14 @@ public class PermissionManagementTest extends AbstractResourceServerTest { PermissionTicketToken token = new JWSInput(ticket).readJsonContent(PermissionTicketToken.class); - List tokenPermissions = token.getResources(); + List tokenPermissions = token.getPermissions(); assertNotNull(tokenPermissions); assertEquals(expectedPermissions, scopeNames.length > 0 ? scopeNames.length : tokenPermissions.size()); - Iterator permissionIterator = tokenPermissions.iterator(); + Iterator permissionIterator = tokenPermissions.iterator(); while (permissionIterator.hasNext()) { - PermissionTicketToken.ResourcePermission resourcePermission = permissionIterator.next(); + Permission resourcePermission = permissionIterator.next(); long count = tickets.stream().filter(representation -> representation.getResource().equals(resourcePermission.getResourceId())).count(); if (count == (scopeNames.length > 0 ? scopeNames.length : 1)) { permissionIterator.remove(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UmaGrantTypeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UmaGrantTypeTest.java index 5d4c1a12c1..a6be35e2d5 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UmaGrantTypeTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UmaGrantTypeTest.java @@ -19,6 +19,7 @@ package org.keycloak.testsuite.authz; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.keycloak.testsuite.util.OAuthClient.AUTH_SERVER_ROOT; import java.net.URI; @@ -38,17 +39,18 @@ import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.admin.client.resource.AuthorizationResource; import org.keycloak.admin.client.resource.ClientResource; -import org.keycloak.jose.jws.JWSInput; +import org.keycloak.authorization.client.AuthorizationDeniedException; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessTokenResponse; -import org.keycloak.representations.RefreshToken; 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.PermissionRequest; import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation; import org.keycloak.representations.idm.authorization.ResourceRepresentation; +import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation; import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.util.BasicAuthHelper; @@ -81,6 +83,14 @@ public class UmaGrantTypeTest extends AbstractResourceServerTest { response = authorization.permissions().resource().create(permission); response.close(); + + policy = new JSPolicyRepresentation(); + + policy.setName("Deny Policy"); + policy.setCode("$evaluation.deny();"); + + response = authorization.policies().js().create(policy); + response.close(); } @Test @@ -117,6 +127,158 @@ public class UmaGrantTypeTest extends AbstractResourceServerTest { assertTrue(permissions.isEmpty()); } + @Test + public void testObtainRptWithUpgradeOnlyScopes() throws Exception { + AuthorizationResponse response = authorize("marta", "password", null, new String[] {"ScopeA", "ScopeB"}); + String rpt = response.getToken(); + AccessToken.Authorization authorization = toAccessToken(rpt).getAuthorization(); + List permissions = authorization.getPermissions(); + + assertFalse(response.isUpgraded()); + assertNotNull(permissions); + assertPermissions(permissions, "Resource A", "ScopeA", "ScopeB"); + assertTrue(permissions.isEmpty()); + + response = authorize("marta", "password", "Resource A", new String[] {"ScopeC"}, rpt); + + authorization = toAccessToken(response.getToken()).getAuthorization(); + permissions = authorization.getPermissions(); + + assertTrue(response.isUpgraded()); + assertNotNull(permissions); + assertPermissions(permissions, "Resource A", "ScopeA", "ScopeB", "ScopeC"); + assertTrue(permissions.isEmpty()); + } + + @Test + public void testObtainRptWithUpgradeWithUnauthorizedResource() throws Exception { + AuthorizationResponse response = authorize("marta", "password", "Resource A", new String[] {"ScopeA", "ScopeB"}); + String rpt = response.getToken(); + AccessToken.Authorization authorization = toAccessToken(rpt).getAuthorization(); + List permissions = authorization.getPermissions(); + + assertFalse(response.isUpgraded()); + assertNotNull(permissions); + assertPermissions(permissions, "Resource A", "ScopeA", "ScopeB"); + assertTrue(permissions.isEmpty()); + + ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation(); + ResourceRepresentation resourceB = addResource("Resource B", "ScopeA", "ScopeB", "ScopeC"); + + permission.setName(resourceB.getName() + " Permission"); + permission.addResource(resourceB.getName()); + permission.addPolicy("Deny Policy"); + + getClient(getRealm()).authorization().permissions().resource().create(permission).close(); + + try { + authorize("marta", "password", "Resource B", new String[]{"ScopeC"}, rpt); + fail("Should be denied, resource b not granted"); + } catch (AuthorizationDeniedException ignore) { + + } + } + + @Test + public void testObtainRptWithUpgradeWithUnauthorizedResourceFromRpt() throws Exception { + ResourcePermissionRepresentation permissionA = new ResourcePermissionRepresentation(); + ResourceRepresentation resourceA = addResource(KeycloakModelUtils.generateId(), "ScopeA", "ScopeB", "ScopeC"); + + permissionA.setName(resourceA.getName() + " Permission"); + permissionA.addResource(resourceA.getName()); + permissionA.addPolicy("Default Policy"); + + AuthorizationResource authzResource = getClient(getRealm()).authorization(); + + authzResource.permissions().resource().create(permissionA).close(); + AuthorizationResponse response = authorize("marta", "password", resourceA.getId(), new String[] {"ScopeA", "ScopeB"}); + String rpt = response.getToken(); + AccessToken.Authorization authorization = toAccessToken(rpt).getAuthorization(); + List permissions = authorization.getPermissions(); + + assertFalse(response.isUpgraded()); + assertNotNull(permissions); + assertPermissions(permissions, resourceA.getName(), "ScopeA", "ScopeB"); + assertTrue(permissions.isEmpty()); + + ResourceRepresentation resourceB = addResource(KeycloakModelUtils.generateId(), "ScopeA", "ScopeB", "ScopeC"); + ResourcePermissionRepresentation permissionB = new ResourcePermissionRepresentation(); + + permissionB.setName(resourceB.getName() + " Permission"); + permissionB.addResource(resourceB.getName()); + permissionB.addPolicy("Default Policy"); + + authzResource.permissions().resource().create(permissionB).close(); + response = authorize("marta", "password", resourceB.getId(), new String[] {"ScopeC"}, rpt); + rpt = response.getToken(); + authorization = toAccessToken(rpt).getAuthorization(); + permissions = authorization.getPermissions(); + + assertTrue(response.isUpgraded()); + assertNotNull(permissions); + assertPermissions(permissions, resourceA.getName(), "ScopeA", "ScopeB"); + assertPermissions(permissions, resourceB.getName(), "ScopeC"); + assertTrue(permissions.isEmpty()); + + permissionB = authzResource.permissions().resource().findByName(permissionB.getName()); + permissionB.removePolicy("Default Policy"); + permissionB.addPolicy("Deny Policy"); + + authzResource.permissions().resource().findById(permissionB.getId()).update(permissionB); + + response = authorize("marta", "password", resourceA.getId(), new String[] {"ScopeC"}, rpt); + rpt = response.getToken(); + authorization = toAccessToken(rpt).getAuthorization(); + permissions = authorization.getPermissions(); + + assertFalse(response.isUpgraded()); + assertNotNull(permissions); + assertPermissions(permissions, resourceA.getName(), "ScopeA", "ScopeB", "ScopeC"); + assertTrue(permissions.isEmpty()); + } + + @Test + public void testObtainRptOnlyAuthorizedScopes() throws Exception { + ResourceRepresentation resourceA = addResource(KeycloakModelUtils.generateId(), "READ", "WRITE"); + ScopePermissionRepresentation permissionA = new ScopePermissionRepresentation(); + + permissionA.setName(KeycloakModelUtils.generateId()); + permissionA.addScope("READ"); + permissionA.addPolicy("Default Policy"); + + AuthorizationResource authzResource = getClient(getRealm()).authorization(); + + authzResource.permissions().scope().create(permissionA).close(); + + ScopePermissionRepresentation permissionB = new ScopePermissionRepresentation(); + + permissionB.setName(KeycloakModelUtils.generateId()); + permissionB.addScope("WRITE"); + permissionB.addPolicy("Deny Policy"); + + authzResource.permissions().scope().create(permissionB).close(); + + AuthorizationResponse response = authorize("marta", "password", resourceA.getName(), new String[] {"READ"}); + String rpt = response.getToken(); + AccessToken.Authorization authorization = toAccessToken(rpt).getAuthorization(); + List permissions = authorization.getPermissions(); + + assertFalse(response.isUpgraded()); + assertNotNull(permissions); + assertPermissions(permissions, resourceA.getName(), "READ"); + assertTrue(permissions.isEmpty()); + + response = authorize("marta", "password", resourceA.getName(), new String[] {"READ", "WRITE"}); + rpt = response.getToken(); + authorization = toAccessToken(rpt).getAuthorization(); + permissions = authorization.getPermissions(); + + assertFalse(response.isUpgraded()); + assertNotNull(permissions); + assertPermissions(permissions, resourceA.getName(), "READ"); + assertTrue(permissions.isEmpty()); + } + @Test public void testObtainRptWithOwnerManagedResource() throws Exception { ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();