[KEYCLOAK-7849] - Improvements to RPT upgrade

This commit is contained in:
Pedro Igor 2018-07-13 14:57:27 -03:00
parent 34407957b9
commit 8b6979ac18
11 changed files with 420 additions and 135 deletions

View file

@ -25,8 +25,8 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.authorization.client.ClientAuthenticator; import org.keycloak.authorization.client.ClientAuthenticator;
import org.keycloak.representations.idm.authorization.AuthorizationRequest; import org.keycloak.representations.idm.authorization.AuthorizationRequest;
import org.keycloak.representations.idm.authorization.AuthorizationRequest.Metadata; 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;
import org.keycloak.representations.idm.authorization.PermissionTicketToken.ResourcePermission;
/** /**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a> * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@ -80,13 +80,13 @@ public class HttpMethodAuthenticator<R> {
method.param("claim_token", request.getClaimToken()); method.param("claim_token", request.getClaimToken());
method.param("claim_token_format", request.getClaimTokenFormat()); method.param("claim_token_format", request.getClaimTokenFormat());
method.param("pct", request.getPct()); method.param("pct", request.getPct());
method.param("rpt", request.getRpt()); method.param("rpt", request.getRptToken());
method.param("scope", request.getScope()); method.param("scope", request.getScope());
method.param("audience", request.getAudience()); method.param("audience", request.getAudience());
method.param("subject_token", request.getSubjectToken()); method.param("subject_token", request.getSubjectToken());
if (permissions != null) { if (permissions != null) {
for (ResourcePermission permission : permissions.getResources()) { for (Permission permission : permissions.getPermissions()) {
String resourceId = permission.getResourceId(); String resourceId = permission.getResourceId();
Set<String> scopes = permission.getScopes(); Set<String> scopes = permission.getScopes();
StringBuilder value = new StringBuilder(); StringBuilder value = new StringBuilder();

View file

@ -23,7 +23,7 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.keycloak.representations.idm.authorization.PermissionTicketToken.ResourcePermission; import org.keycloak.representations.AccessToken;
/** /**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a> * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@ -31,7 +31,6 @@ import org.keycloak.representations.idm.authorization.PermissionTicketToken.Reso
public class AuthorizationRequest { public class AuthorizationRequest {
private String ticket; private String ticket;
private String rpt;
private String claimToken; private String claimToken;
private String claimTokenFormat; private String claimTokenFormat;
private String pct; private String pct;
@ -42,6 +41,8 @@ public class AuthorizationRequest {
private String subjectToken; private String subjectToken;
private boolean submitRequest; private boolean submitRequest;
private Map<String, List<String>> claims; private Map<String, List<String>> claims;
private AccessToken rpt;
private String rptToken;
public AuthorizationRequest(String ticket) { public AuthorizationRequest(String ticket) {
this.ticket = ticket; this.ticket = ticket;
@ -59,14 +60,22 @@ public class AuthorizationRequest {
this.ticket = ticket; this.ticket = ticket;
} }
public String getRpt() { public AccessToken getRpt() {
return this.rpt; return this.rpt;
} }
public void setRpt(String rpt) { public void setRpt(AccessToken rpt) {
this.rpt = rpt; this.rpt = rpt;
} }
public void setRpt(String rpt) {
this.rptToken = rpt;
}
public String getRptToken() {
return rptToken;
}
public void setClaimToken(String claimToken) { public void setClaimToken(String claimToken) {
this.claimToken = claimToken; this.claimToken = claimToken;
} }
@ -145,12 +154,12 @@ public class AuthorizationRequest {
public void addPermission(String resourceId, String... scopes) { public void addPermission(String resourceId, String... scopes) {
if (permissions == null) { if (permissions == null) {
permissions = new PermissionTicketToken(new ArrayList<ResourcePermission>()); permissions = new PermissionTicketToken(new ArrayList<Permission>());
} }
ResourcePermission permission = null; Permission permission = null;
for (ResourcePermission resourcePermission : permissions.getResources()) { for (Permission resourcePermission : permissions.getPermissions()) {
if (resourcePermission.getResourceId() != null && resourcePermission.getResourceId().equals(resourceId)) { if (resourcePermission.getResourceId() != null && resourcePermission.getResourceId().equals(resourceId)) {
permission = resourcePermission; permission = resourcePermission;
break; break;
@ -158,8 +167,8 @@ public class AuthorizationRequest {
} }
if (permission == null) { if (permission == null) {
permission = new ResourcePermission(resourceId, new HashSet<String>()); permission = new Permission(resourceId, new HashSet<String>());
permissions.getResources().add(permission); permissions.getPermissions().add(permission);
} }
permission.getScopes().addAll(Arrays.asList(scopes)); permission.getScopes().addAll(Arrays.asList(scopes));

View file

@ -33,7 +33,7 @@ public class Permission {
private String resourceId; private String resourceId;
@JsonProperty("rsname") @JsonProperty("rsname")
private final String resourceName; private String resourceName;
@JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonInclude(JsonInclude.Include.NON_EMPTY)
private Set<String> scopes; private Set<String> scopes;
@ -45,6 +45,10 @@ public class Permission {
this(null, null, null, null); this(null, null, null, null);
} }
public Permission(final String resourceId, final Set<String> scopes) {
this(resourceId, null, scopes, null);
}
public Permission(final String resourceId, String resourceName, final Set<String> scopes, Map<String, Set<String>> claims) { public Permission(final String resourceId, String resourceName, final Set<String> scopes, Map<String, Set<String>> claims) {
this.resourceId = resourceId; this.resourceId = resourceId;
this.resourceName = resourceName; this.resourceName = resourceName;
@ -52,10 +56,18 @@ public class Permission {
this.claims = claims; this.claims = claims;
} }
public void setResourceId(String resourceId) {
this.resourceId = resourceId;
}
public String getResourceId() { public String getResourceId() {
return this.resourceId; return this.resourceId;
} }
public void setResourceName(String resourceName) {
this.resourceName = resourceName;
}
public String getResourceName() { public String getResourceName() {
return this.resourceName; return this.resourceName;
} }
@ -75,11 +87,29 @@ public class Permission {
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; 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; 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 @Override

View file

@ -33,16 +33,16 @@ import org.keycloak.representations.JsonWebToken;
*/ */
public class PermissionTicketToken extends JsonWebToken { public class PermissionTicketToken extends JsonWebToken {
private final List<ResourcePermission> resources; private final List<Permission> permissions;
@JsonDeserialize(using = StringListMapDeserializer.class) @JsonDeserialize(using = StringListMapDeserializer.class)
private Map<String, List<String>> claims; private Map<String, List<String>> claims;
public PermissionTicketToken() { public PermissionTicketToken() {
this(new ArrayList<ResourcePermission>()); this(new ArrayList<Permission>());
} }
public PermissionTicketToken(List<ResourcePermission> resources, String audience, AccessToken accessToken) { public PermissionTicketToken(List<Permission> permissions, String audience, AccessToken accessToken) {
if (accessToken != null) { if (accessToken != null) {
id(TokenIdGenerator.generateId()); id(TokenIdGenerator.generateId());
subject(accessToken.getSubject()); subject(accessToken.getSubject());
@ -54,15 +54,15 @@ public class PermissionTicketToken extends JsonWebToken {
if (audience != null) { if (audience != null) {
audience(audience); audience(audience);
} }
this.resources = resources; this.permissions = permissions;
} }
public PermissionTicketToken(List<ResourcePermission> resources) { public PermissionTicketToken(List<Permission> resources) {
this(resources, null, null); this(resources, null, null);
} }
public List<ResourcePermission> getResources() { public List<Permission> getPermissions() {
return this.resources; return this.permissions;
} }
public Map<String, List<String>> getClaims() { public Map<String, List<String>> getClaims() {
@ -72,29 +72,4 @@ public class PermissionTicketToken extends JsonWebToken {
public void setClaims(Map<String, List<String>> claims) { public void setClaims(Map<String, List<String>> claims) {
this.claims = claims; this.claims = claims;
} }
public static class ResourcePermission {
@JsonProperty("id")
private String resourceId;
@JsonProperty("scopes")
private Set<String> scopes;
public ResourcePermission() {
}
public ResourcePermission(String resourceId, Set<String> scopes) {
this.resourceId = resourceId;
this.scopes = scopes;
}
public String getResourceId() {
return resourceId;
}
public Set<String> getScopes() {
return scopes;
}
}
} }

View file

@ -31,11 +31,11 @@ import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope; import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.permission.ResourcePermission; 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.ResourceStore;
import org.keycloak.authorization.store.ScopeStore; import org.keycloak.authorization.store.ScopeStore;
import org.keycloak.authorization.store.StoreFactory; import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.representations.idm.authorization.AuthorizationRequest; import org.keycloak.representations.idm.authorization.AuthorizationRequest;
import org.keycloak.representations.idm.authorization.Permission;
import org.keycloak.representations.idm.authorization.PermissionTicketToken; import org.keycloak.representations.idm.authorization.PermissionTicketToken;
/** /**
@ -75,12 +75,12 @@ public class PermissionTicketAwareDecisionResultCollector extends DecisionResult
if ("uma".equals(policy.getType())) { if ("uma".equals(policy.getType())) {
ResourcePermission grantedPermission = evaluation.getPermission(); ResourcePermission grantedPermission = evaluation.getPermission();
List<PermissionTicketToken.ResourcePermission> permissions = ticket.getResources(); List<Permission> permissions = ticket.getPermissions();
Iterator<PermissionTicketToken.ResourcePermission> itPermissions = permissions.iterator(); Iterator<Permission> itPermissions = permissions.iterator();
while (itPermissions.hasNext()) { while (itPermissions.hasNext()) {
PermissionTicketToken.ResourcePermission permission = itPermissions.next(); Permission permission = itPermissions.next();
if (permission.getResourceId().equals(grantedPermission.getResource().getId())) { if (permission.getResourceId().equals(grantedPermission.getResource().getId())) {
Set<String> scopes = permission.getScopes(); Set<String> scopes = permission.getScopes();
@ -109,10 +109,10 @@ public class PermissionTicketAwareDecisionResultCollector extends DecisionResult
if (request.isSubmitRequest()) { if (request.isSubmitRequest()) {
StoreFactory storeFactory = authorization.getStoreFactory(); StoreFactory storeFactory = authorization.getStoreFactory();
ResourceStore resourceStore = storeFactory.getResourceStore(); ResourceStore resourceStore = storeFactory.getResourceStore();
List<PermissionTicketToken.ResourcePermission> permissions = ticket.getResources(); List<Permission> permissions = ticket.getPermissions();
if (permissions != null) { if (permissions != null) {
for (PermissionTicketToken.ResourcePermission permission : permissions) { for (Permission permission : permissions) {
Resource resource = resourceStore.findById(permission.getResourceId(), resourceServer.getId()); Resource resource = resourceStore.findById(permission.getResourceId(), resourceServer.getId());
if (resource == null) { if (resource == null) {

View file

@ -28,10 +28,7 @@ import java.util.Map.Entry;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.ws.rs.HttpMethod; import javax.ws.rs.HttpMethod;
import javax.ws.rs.core.MediaType; 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.protocol.oidc.TokenManager.AccessTokenResponseBuilder;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessToken.Authorization; import org.keycloak.representations.AccessToken.Authorization;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.IDToken; import org.keycloak.representations.IDToken;
import org.keycloak.representations.RefreshToken; import org.keycloak.representations.RefreshToken;
import org.keycloak.representations.idm.authorization.AuthorizationRequest; 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 // 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); 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()); KeycloakIdentity identity = KeycloakIdentity.class.cast(evaluationContext.getIdentity());
List<Result> evaluation; List<Result> evaluation;
if (ticket.getResources().isEmpty() && request.getRpt() == null) { if (ticket.getPermissions().isEmpty() && request.getRpt() == null) {
evaluation = evaluateAllPermissions(request, resourceServer, evaluationContext, identity); 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); evaluation = evaluatePermissions(request, ticket, resourceServer, evaluationContext, identity);
} else { } else {
evaluation = evaluateUserManagedPermissions(request, ticket, resourceServer, evaluationContext, identity); evaluation = evaluateUserManagedPermissions(request, ticket, resourceServer, evaluationContext, identity);
@ -173,21 +169,21 @@ public class AuthorizationTokenService {
List<Permission> permissions = Permissions.permits(evaluation, request.getMetadata(), authorization, resourceServer); List<Permission> permissions = Permissions.permits(evaluation, request.getMetadata(), authorization, resourceServer);
if (permissions.isEmpty()) { if (isGranted(ticket, request, permissions)) {
if (request.isSubmitRequest()) { ClientModel targetClient = this.authorization.getRealm().getClientById(resourceServer.getId());
throw new CorsErrorResponseException(cors, OAuthErrorException.ACCESS_DENIED, "request_submitted", Status.FORBIDDEN); AuthorizationResponse response = createAuthorizationResponse(identity, permissions, request, targetClient);
} else {
throw new CorsErrorResponseException(cors, OAuthErrorException.ACCESS_DENIED, "not_authorized", Status.FORBIDDEN); 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()); if (request.isSubmitRequest()) {
AuthorizationResponse response = new AuthorizationResponse(createRequestingPartyToken(identity, permissions, request, targetClient), request.getRpt() != null); throw new CorsErrorResponseException(cors, OAuthErrorException.ACCESS_DENIED, "request_submitted", Status.FORBIDDEN);
} else {
return Cors.add(httpRequest, Response.status(Status.OK).type(MediaType.APPLICATION_JSON_TYPE).entity(response)) throw new CorsErrorResponseException(cors, OAuthErrorException.ACCESS_DENIED, "not_authorized", Status.FORBIDDEN);
.allowedOrigins(getKeycloakSession().getContext().getUri(), targetClient) }
.allowedMethods(HttpMethod.POST)
.exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
} catch (ErrorResponseException | CorsErrorResponseException cause) { } catch (ErrorResponseException | CorsErrorResponseException cause) {
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("Error while evaluating permissions", cause); 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; return request.getClaimToken() != null && getKeycloakSession().getContext().getClient().isPublicClient() && request.getTicket() == null;
} }
@ -221,7 +217,7 @@ public class AuthorizationTokenService {
.evaluate(); .evaluate();
} }
private AccessTokenResponse createRequestingPartyToken(KeycloakIdentity identity, List<Permission> entitlements, AuthorizationRequest request, ClientModel targetClient) { private AuthorizationResponse createAuthorizationResponse(KeycloakIdentity identity, List<Permission> entitlements, AuthorizationRequest request, ClientModel targetClient) {
KeycloakSession keycloakSession = getKeycloakSession(); KeycloakSession keycloakSession = getKeycloakSession();
AccessToken accessToken = identity.getAccessToken(); AccessToken accessToken = identity.getAccessToken();
UserSessionModel userSessionModel = keycloakSession.sessions().getUserSession(getRealm(), accessToken.getSessionState()); UserSessionModel userSessionModel = keycloakSession.sessions().getUserSession(getRealm(), accessToken.getSessionState());
@ -253,7 +249,31 @@ public class AuthorizationTokenService {
rpt.audience(targetClient.getClientId()); 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<Permission> previousPermissions = previousAuthorization.getPermissions();
if (previousPermissions != null) {
for (Permission previousPermission : previousPermissions) {
if (!authorization.getPermissions().contains(previousPermission)) {
return false;
}
}
}
}
return true;
} }
private PermissionTicketToken getPermissionTicket(AuthorizationRequest request) { private PermissionTicketToken getPermissionTicket(AuthorizationRequest request) {
@ -320,7 +340,7 @@ public class AuthorizationTokenService {
Metadata metadata = request.getMetadata(); Metadata metadata = request.getMetadata();
Integer limit = metadata != null ? metadata.getLimit() : null; Integer limit = metadata != null ? metadata.getLimit() : null;
for (PermissionTicketToken.ResourcePermission requestedResource : ticket.getResources()) { for (Permission requestedResource : ticket.getPermissions()) {
if (limit != null && limit <= 0) { if (limit != null && limit <= 0) {
break; break;
} }
@ -339,16 +359,19 @@ public class AuthorizationTokenService {
if (resource != null) { if (resource != null) {
existingResources.add(resource); existingResources.add(resource);
} else { } 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) { if (ownerResource != null) {
requestedResource.setResourceId(ownerResource.getId());
existingResources.add(ownerResource); existingResources.add(ownerResource);
} }
if (!identity.isResourceServer()) { if (!identity.isResourceServer()) {
Resource serverResource = resourceStore.findByName(requestedResource.getResourceId(), resourceServer.getId()); Resource serverResource = resourceStore.findByName(resourceName, resourceServer.getId());
if (serverResource != null) { if (serverResource != null) {
requestedResource.setResourceId(serverResource.getId());
existingResources.add(serverResource); existingResources.add(serverResource);
} }
} }
@ -403,63 +426,49 @@ public class AuthorizationTokenService {
} }
} }
String rpt = request.getRpt(); AccessToken rpt = request.getRpt();
if (rpt != null) { if (rpt != null && rpt.isActive()) {
if (!Tokens.verifySignature(getKeycloakSession(), getRealm(), rpt)) { AccessToken.Authorization authorizationData = rpt.getAuthorization();
throw new CorsErrorResponseException(cors, "invalid_rpt", "RPT signature is invalid", Status.FORBIDDEN);
}
AccessToken requestingPartyToken; if (authorizationData != null) {
List<Permission> permissions = authorizationData.getPermissions();
try { if (permissions != null) {
requestingPartyToken = new JWSInput(rpt).readJsonContent(AccessToken.class); for (Permission grantedPermission : permissions) {
} catch (JWSInputException e) { if (limit != null && limit <= 0) {
throw new CorsErrorResponseException(cors, "invalid_rpt", "Invalid RPT", Status.FORBIDDEN); break;
} }
if (requestingPartyToken.isActive()) { Resource resource = resourceStore.findById(grantedPermission.getResourceId(), ticket.getAudience()[0]);
AccessToken.Authorization authorizationData = requestingPartyToken.getAuthorization();
if (authorizationData != null) { if (resource != null) {
List<Permission> permissions = authorizationData.getPermissions(); ResourcePermission permission = permissionsToEvaluate.get(resource.getId());
if (permissions != null) { if (permission == null) {
for (Permission grantedPermission : permissions) { permission = new ResourcePermission(resource, new ArrayList<>(), resourceServer, grantedPermission.getClaims());
if (limit != null && limit <= 0) { permissionsToEvaluate.put(resource.getId(), permission);
break; if (limit != null) {
} limit--;
}
} else {
if (grantedPermission.getClaims() != null) {
for (Entry<String, Set<String>> entry : grantedPermission.getClaims().entrySet()) {
Set<String> claims = permission.getClaims().get(entry.getKey());
Resource resourcePermission = resourceStore.findById(grantedPermission.getResourceId(), ticket.getAudience()[0]); if (claims != null) {
claims.addAll(entry.getValue());
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<String, Set<String>> entry : grantedPermission.getClaims().entrySet()) {
Set<String> claims = permission.getClaims().get(entry.getKey());
if (claims != null) {
claims.addAll(entry.getValue());
}
} }
} }
} }
}
for (String scopeName : grantedPermission.getScopes()) { for (String scopeName : grantedPermission.getScopes()) {
Scope scope = scopeStore.findByName(scopeName, resourceServer.getId()); Scope scope = scopeStore.findByName(scopeName, resourceServer.getId());
if (scope != null) { if (scope != null) {
if (!permission.getScopes().contains(scope)) { if (!permission.getScopes().contains(scope)) {
permission.getScopes().add(scope); permission.getScopes().add(scope);
}
} }
} }
} }
@ -492,6 +501,17 @@ public class AuthorizationTokenService {
} }
} }
private boolean isGranted(PermissionTicketToken ticket, AuthorizationRequest request, List<Permission> permissions) {
List<Permission> 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() { private KeycloakSession getKeycloakSession() {
return this.authorization.getKeycloakSession(); return this.authorization.getKeycloakSession();
} }

View file

@ -35,6 +35,7 @@ import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.KeyManager; 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.PermissionRequest;
import org.keycloak.representations.idm.authorization.PermissionResponse; import org.keycloak.representations.idm.authorization.PermissionResponse;
import org.keycloak.representations.idm.authorization.PermissionTicketToken; 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(); return Response.status(Response.Status.CREATED).entity(new PermissionResponse(createPermissionTicket(request))).build();
} }
private List<PermissionTicketToken.ResourcePermission> verifyRequestedResource(List<PermissionRequest> request) { private List<Permission> verifyRequestedResource(List<PermissionRequest> request) {
ResourceStore resourceStore = authorization.getStoreFactory().getResourceStore(); ResourceStore resourceStore = authorization.getStoreFactory().getResourceStore();
List<PermissionTicketToken.ResourcePermission> requestedResources = new ArrayList<>(); List<Permission> requestedResources = new ArrayList<>();
for (PermissionRequest permissionRequest : request) { for (PermissionRequest permissionRequest : request) {
String resourceSetId = permissionRequest.getResourceId(); String resourceSetId = permissionRequest.getResourceId();
@ -102,10 +103,10 @@ public class AbstractPermissionService {
} }
if (resources.isEmpty()) { if (resources.isEmpty()) {
requestedResources.add(new PermissionTicketToken.ResourcePermission(null, verifyRequestedScopes(permissionRequest, null))); requestedResources.add(new Permission(null, verifyRequestedScopes(permissionRequest, null)));
} else { } else {
for (Resource resource : resources) { 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<PermissionRequest> request) { private String createPermissionTicket(List<PermissionRequest> request) {
List<PermissionTicketToken.ResourcePermission> permissions = verifyRequestedResource(request); List<Permission> permissions = verifyRequestedResource(request);
KeyManager.ActiveRsaKey keys = this.authorization.getKeycloakSession().keys().getActiveRsaKey(this.authorization.getRealm()); KeyManager.ActiveRsaKey keys = this.authorization.getKeycloakSession().keys().getActiveRsaKey(this.authorization.getRealm());
ClientModel targetClient = authorization.getRealm().getClientById(resourceServer.getId()); ClientModel targetClient = authorization.getRealm().getClientById(resourceServer.getId());

View file

@ -1079,7 +1079,20 @@ public class TokenEndpoint {
authorizationRequest.setClaimToken(claimToken); authorizationRequest.setClaimToken(claimToken);
authorizationRequest.setClaimTokenFormat(claimTokenFormat); authorizationRequest.setClaimTokenFormat(claimTokenFormat);
authorizationRequest.setPct(formParams.getFirst("pct")); 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.setScope(formParams.getFirst("scope"));
authorizationRequest.setAudience(formParams.getFirst("audience")); authorizationRequest.setAudience(formParams.getFirst("audience"));
authorizationRequest.setSubjectToken(formParams.getFirst("subject_token") != null ? formParams.getFirst("subject_token") : accessTokenString); authorizationRequest.setSubjectToken(formParams.getFirst("subject_token") != null ? formParams.getFirst("subject_token") : accessTokenString);

View file

@ -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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
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())
));
}
}

View file

@ -36,6 +36,7 @@ import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.util.HttpResponseException; import org.keycloak.authorization.client.util.HttpResponseException;
import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInput;
import org.keycloak.representations.idm.authorization.AuthorizationRequest; 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.PermissionRequest;
import org.keycloak.representations.idm.authorization.PermissionResponse; import org.keycloak.representations.idm.authorization.PermissionResponse;
import org.keycloak.representations.idm.authorization.PermissionTicketRepresentation; import org.keycloak.representations.idm.authorization.PermissionTicketRepresentation;
@ -303,14 +304,14 @@ public class PermissionManagementTest extends AbstractResourceServerTest {
PermissionTicketToken token = new JWSInput(ticket).readJsonContent(PermissionTicketToken.class); PermissionTicketToken token = new JWSInput(ticket).readJsonContent(PermissionTicketToken.class);
List<PermissionTicketToken.ResourcePermission> tokenPermissions = token.getResources(); List<Permission> tokenPermissions = token.getPermissions();
assertNotNull(tokenPermissions); assertNotNull(tokenPermissions);
assertEquals(expectedPermissions, scopeNames.length > 0 ? scopeNames.length : tokenPermissions.size()); assertEquals(expectedPermissions, scopeNames.length > 0 ? scopeNames.length : tokenPermissions.size());
Iterator<PermissionTicketToken.ResourcePermission> permissionIterator = tokenPermissions.iterator(); Iterator<Permission> permissionIterator = tokenPermissions.iterator();
while (permissionIterator.hasNext()) { while (permissionIterator.hasNext()) {
PermissionTicketToken.ResourcePermission resourcePermission = permissionIterator.next(); Permission resourcePermission = permissionIterator.next();
long count = tickets.stream().filter(representation -> representation.getResource().equals(resourcePermission.getResourceId())).count(); long count = tickets.stream().filter(representation -> representation.getResource().equals(resourcePermission.getResourceId())).count();
if (count == (scopeNames.length > 0 ? scopeNames.length : 1)) { if (count == (scopeNames.length > 0 ? scopeNames.length : 1)) {
permissionIterator.remove(); permissionIterator.remove();

View file

@ -19,6 +19,7 @@ package org.keycloak.testsuite.authz;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.keycloak.testsuite.util.OAuthClient.AUTH_SERVER_ROOT; import static org.keycloak.testsuite.util.OAuthClient.AUTH_SERVER_ROOT;
import java.net.URI; import java.net.URI;
@ -38,17 +39,18 @@ import org.junit.Test;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.AuthorizationResource; import org.keycloak.admin.client.resource.AuthorizationResource;
import org.keycloak.admin.client.resource.ClientResource; 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.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.RefreshToken;
import org.keycloak.representations.idm.authorization.AuthorizationResponse; import org.keycloak.representations.idm.authorization.AuthorizationResponse;
import org.keycloak.representations.idm.authorization.JSPolicyRepresentation; import org.keycloak.representations.idm.authorization.JSPolicyRepresentation;
import org.keycloak.representations.idm.authorization.Permission; import org.keycloak.representations.idm.authorization.Permission;
import org.keycloak.representations.idm.authorization.PermissionRequest; import org.keycloak.representations.idm.authorization.PermissionRequest;
import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation; import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation; import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation;
import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.util.BasicAuthHelper; import org.keycloak.util.BasicAuthHelper;
@ -81,6 +83,14 @@ public class UmaGrantTypeTest extends AbstractResourceServerTest {
response = authorization.permissions().resource().create(permission); response = authorization.permissions().resource().create(permission);
response.close(); response.close();
policy = new JSPolicyRepresentation();
policy.setName("Deny Policy");
policy.setCode("$evaluation.deny();");
response = authorization.policies().js().create(policy);
response.close();
} }
@Test @Test
@ -117,6 +127,158 @@ public class UmaGrantTypeTest extends AbstractResourceServerTest {
assertTrue(permissions.isEmpty()); 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<Permission> 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<Permission> 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<Permission> 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<Permission> 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 @Test
public void testObtainRptWithOwnerManagedResource() throws Exception { public void testObtainRptWithOwnerManagedResource() throws Exception {
ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation(); ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();