diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java index a939b8ec711..10ddde68117 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java @@ -848,7 +848,12 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC for (String clientScopeId : cachedConsent.getClientScopeIds()) { ClientScopeModel clientScope = KeycloakModelUtils.findClientScopeById(realm, client, clientScopeId); if (clientScope != null) { - consentModel.addGrantedClientScope(clientScope); + if (ClientScopeModel.isDynamicScope(clientScope)) { + cachedConsent.getParameters(clientScopeId).stream() + .forEach(p -> consentModel.addGrantedClientScope(clientScope, p)); + } else { + consentModel.addGrantedClientScope(clientScope); + } } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUserConsent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUserConsent.java index c1387cab7ea..7c8943f9a9d 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUserConsent.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUserConsent.java @@ -18,8 +18,10 @@ package org.keycloak.models.cache.infinispan.entities; import java.util.HashSet; +import java.util.List; import java.util.Set; +import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.models.ClientScopeModel; import org.keycloak.models.UserConsentModel; @@ -30,6 +32,7 @@ public class CachedUserConsent { private final String clientDbId; private final Set clientScopeIds = new HashSet<>(); + private final MultivaluedHashMap parameters = new MultivaluedHashMap<>(); private final Long createdDate; private final Long lastUpdatedDate; private boolean notExistent; @@ -38,6 +41,9 @@ public class CachedUserConsent { this.clientDbId = consentModel.getClient().getId(); for (ClientScopeModel clientScope : consentModel.getGrantedClientScopes()) { this.clientScopeIds.add(clientScope.getId()); + if (ClientScopeModel.isDynamicScope(clientScope)) { + this.parameters.addAll(clientScope.getId(), consentModel.getParameters(clientScope)); + } } this.createdDate = consentModel.getCreatedDate(); this.lastUpdatedDate = consentModel.getLastUpdatedDate(); @@ -58,6 +64,10 @@ public class CachedUserConsent { return clientScopeIds; } + public List getParameters(String scopeId) { + return parameters.getList(scopeId); + } + public Long getCreatedDate() { return createdDate; } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java index 4df2ad0c560..4e11d9f950f 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java @@ -334,7 +334,10 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore, JpaUs for (UserConsentClientScopeEntity grantedClientScope : grantedClientScopeEntities) { ClientScopeModel grantedClientScopeModel = KeycloakModelUtils.findClientScopeById(realm, client, grantedClientScope.getScopeId()); if (grantedClientScopeModel != null) { - model.addGrantedClientScope(grantedClientScopeModel); + model.addGrantedClientScope(grantedClientScopeModel, + ClientScopeModel.isDynamicScope(grantedClientScopeModel) + ? grantedClientScope.getParameter().orElse(null) + : null); } } } @@ -348,17 +351,11 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore, JpaUs Collection scopesToRemove = new HashSet<>(grantedClientScopeEntities); for (ClientScopeModel clientScope : consentModel.getGrantedClientScopes()) { - UserConsentClientScopeEntity grantedClientScopeEntity = new UserConsentClientScopeEntity(); - grantedClientScopeEntity.setUserConsent(consentEntity); - grantedClientScopeEntity.setScopeId(clientScope.getId()); - - // Check if it's already there - if (!grantedClientScopeEntities.contains(grantedClientScopeEntity)) { - em.persist(grantedClientScopeEntity); - em.flush(); - grantedClientScopeEntities.add(grantedClientScopeEntity); + if (ClientScopeModel.isDynamicScope(clientScope)) { + consentModel.getParameters(clientScope).forEach(p -> createUserConsentClientScopeEntity( + consentEntity, clientScope, p, grantedClientScopeEntities, scopesToRemove)); } else { - scopesToRemove.remove(grantedClientScopeEntity); + createUserConsentClientScopeEntity(consentEntity, clientScope, null, grantedClientScopeEntities, scopesToRemove); } } // Those client scopes were no longer on consentModel and will be removed @@ -372,6 +369,25 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore, JpaUs em.flush(); } + private void createUserConsentClientScopeEntity( + UserConsentEntity consentEntity, ClientScopeModel clientScope, String parameter, + Collection grantedClientScopeEntities, + Collection scopesToRemove) { + UserConsentClientScopeEntity grantedClientScopeEntity = new UserConsentClientScopeEntity(); + grantedClientScopeEntity.setUserConsent(consentEntity); + grantedClientScopeEntity.setScopeId(clientScope.getId()); + grantedClientScopeEntity.setParameter(parameter); + + // Check if it's already there + if (!grantedClientScopeEntities.contains(grantedClientScopeEntity)) { + em.persist(grantedClientScopeEntity); + em.flush(); + grantedClientScopeEntities.add(grantedClientScopeEntity); + } else { + scopesToRemove.remove(grantedClientScopeEntity); + } + } + @Override public UserVerifiableCredentialModel addVerifiableCredential(String userId, UserVerifiableCredentialModel verifCredentialModel) { if (verifCredentialModel.getCredentialScopeName() == null) { diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentClientScopeEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentClientScopeEntity.java index 2c5c8fb1d23..c23cc501d97 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentClientScopeEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentClientScopeEntity.java @@ -18,6 +18,7 @@ package org.keycloak.models.jpa.entities; import java.io.Serializable; +import java.util.Optional; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -47,6 +48,8 @@ import jakarta.persistence.Table; @IdClass(UserConsentClientScopeEntity.Key.class) public class UserConsentClientScopeEntity { + public static String NOT_AVAILABLE_PARAM = "#N A#"; + @Id @ManyToOne(fetch= FetchType.LAZY) @JoinColumn(name = "USER_CONSENT_ID") @@ -56,6 +59,10 @@ public class UserConsentClientScopeEntity { @Column(name="SCOPE_ID") protected String scopeId; + @Id + @Column(name="PARAMETER") + protected String parameter; + public UserConsentEntity getUserConsent() { return userConsent; } @@ -72,6 +79,18 @@ public class UserConsentClientScopeEntity { this.scopeId = scopeId; } + public Optional getParameter() { + return Optional.ofNullable(NOT_AVAILABLE_PARAM.equals(parameter) + ? null + : parameter); + } + + public void setParameter(String parameter) { + this.parameter = parameter == null + ? NOT_AVAILABLE_PARAM + : parameter; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -79,14 +98,14 @@ public class UserConsentClientScopeEntity { if (!(o instanceof UserConsentClientScopeEntity)) return false; UserConsentClientScopeEntity that = (UserConsentClientScopeEntity)o; - UserConsentClientScopeEntity.Key myKey = new UserConsentClientScopeEntity.Key(this.userConsent, this.scopeId); - UserConsentClientScopeEntity.Key hisKey = new UserConsentClientScopeEntity.Key(that.userConsent, that.scopeId); + UserConsentClientScopeEntity.Key myKey = new UserConsentClientScopeEntity.Key(this.userConsent, this.scopeId, this.parameter); + UserConsentClientScopeEntity.Key hisKey = new UserConsentClientScopeEntity.Key(that.userConsent, that.scopeId, that.parameter); return myKey.equals(hisKey); } @Override public int hashCode() { - UserConsentClientScopeEntity.Key myKey = new UserConsentClientScopeEntity.Key(this.userConsent, this.scopeId); + UserConsentClientScopeEntity.Key myKey = new UserConsentClientScopeEntity.Key(this.userConsent, this.scopeId, this.parameter); return myKey.hashCode(); } @@ -96,12 +115,15 @@ public class UserConsentClientScopeEntity { protected String scopeId; + protected String parameter; + public Key() { } - public Key(UserConsentEntity userConsent, String scopeId) { + public Key(UserConsentEntity userConsent, String scopeId, String parameter) { this.userConsent = userConsent; this.scopeId = scopeId; + this.parameter = parameter; } public UserConsentEntity getUserConsent() { @@ -112,6 +134,10 @@ public class UserConsentClientScopeEntity { return scopeId; } + public String getParameter() { + return parameter; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -121,6 +147,7 @@ public class UserConsentClientScopeEntity { if (userConsent != null ? !userConsent.getId().equals(key.userConsent != null ? key.userConsent.getId() : null) : key.userConsent != null) return false; if (scopeId != null ? !scopeId.equals(key.scopeId) : key.scopeId != null) return false; + if (parameter != null ? !parameter.equals(key.parameter) : key.parameter != null) return false; return true; } @@ -129,6 +156,7 @@ public class UserConsentClientScopeEntity { public int hashCode() { int result = userConsent != null ? userConsent.getId().hashCode() : 0; result = 31 * result + (scopeId != null ? scopeId.hashCode() : 0); + result = 31 * result + (parameter != null ? parameter.hashCode() : 0); return result; } } diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java index d22e878fe3b..66c76e4bad7 100644 --- a/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java +++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java @@ -385,7 +385,10 @@ public class JpaUserFederatedStorageProvider implements grantedClientScopeModel = realm.getClientById(grantedClientScope.getScopeId()); } if (grantedClientScopeModel != null) { - model.addGrantedClientScope(grantedClientScopeModel); + model.addGrantedClientScope(grantedClientScopeModel, + ClientScopeModel.isDynamicScope(grantedClientScopeModel) + ? grantedClientScope.getParameter().orElse(null) + : null); } } } @@ -399,17 +402,11 @@ public class JpaUserFederatedStorageProvider implements Collection scopesToRemove = new HashSet<>(grantedClientScopeEntities); for (ClientScopeModel clientScope : consentModel.getGrantedClientScopes()) { - FederatedUserConsentClientScopeEntity grantedClientScopeEntity = new FederatedUserConsentClientScopeEntity(); - grantedClientScopeEntity.setUserConsent(consentEntity); - grantedClientScopeEntity.setScopeId(clientScope.getId()); - - // Check if it's already there - if (!grantedClientScopeEntities.contains(grantedClientScopeEntity)) { - em.persist(grantedClientScopeEntity); - em.flush(); - grantedClientScopeEntities.add(grantedClientScopeEntity); + if (ClientScopeModel.isDynamicScope(clientScope)) { + consentModel.getParameters(clientScope).forEach(p -> createFederatedUserConsentClientScopeEntity( + consentEntity, clientScope, p, grantedClientScopeEntities, scopesToRemove)); } else { - scopesToRemove.remove(grantedClientScopeEntity); + createFederatedUserConsentClientScopeEntity(consentEntity, clientScope, null, grantedClientScopeEntities, scopesToRemove); } } // Those mappers were no longer on consentModel and will be removed @@ -423,6 +420,24 @@ public class JpaUserFederatedStorageProvider implements em.flush(); } + private void createFederatedUserConsentClientScopeEntity( + FederatedUserConsentEntity consentEntity, ClientScopeModel clientScope, String parameter, + Collection grantedClientScopeEntities, + Collection scopesToRemove) { + FederatedUserConsentClientScopeEntity grantedClientScopeEntity = new FederatedUserConsentClientScopeEntity(); + grantedClientScopeEntity.setUserConsent(consentEntity); + grantedClientScopeEntity.setScopeId(clientScope.getId()); + grantedClientScopeEntity.setParameter(parameter); + + // Check if it's already there + if (!grantedClientScopeEntities.contains(grantedClientScopeEntity)) { + em.persist(grantedClientScopeEntity); + em.flush(); + grantedClientScopeEntities.add(grantedClientScopeEntity); + } else { + scopesToRemove.remove(grantedClientScopeEntity); + } + } @Override public void setNotBeforeForUser(RealmModel realm, String userId, int notBefore) { diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentClientScopeEntity.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentClientScopeEntity.java index ec287a57255..9e4e89bdd3f 100644 --- a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentClientScopeEntity.java +++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentClientScopeEntity.java @@ -18,6 +18,7 @@ package org.keycloak.storage.jpa.entity; import java.io.Serializable; +import java.util.Optional; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -30,6 +31,8 @@ import jakarta.persistence.NamedQueries; import jakarta.persistence.NamedQuery; import jakarta.persistence.Table; +import org.keycloak.models.jpa.entities.UserConsentClientScopeEntity; + /** * @author Marek Posolda */ @@ -56,6 +59,10 @@ public class FederatedUserConsentClientScopeEntity { @Column(name="SCOPE_ID") protected String scopeId; + @Id + @Column(name="PARAMETER") + protected String parameter; + public FederatedUserConsentEntity getUserConsent() { return userConsent; } @@ -72,6 +79,18 @@ public class FederatedUserConsentClientScopeEntity { this.scopeId = scopeId; } + public Optional getParameter() { + return Optional.ofNullable(UserConsentClientScopeEntity.NOT_AVAILABLE_PARAM.equals(parameter) + ? null + : parameter); + } + + public void setParameter(String parameter) { + this.parameter = parameter == null + ? UserConsentClientScopeEntity.NOT_AVAILABLE_PARAM + : parameter; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -79,14 +98,14 @@ public class FederatedUserConsentClientScopeEntity { if (!(o instanceof FederatedUserConsentClientScopeEntity)) return false; FederatedUserConsentClientScopeEntity that = ( FederatedUserConsentClientScopeEntity)o; - FederatedUserConsentClientScopeEntity.Key myKey = new FederatedUserConsentClientScopeEntity.Key(this.userConsent, this.scopeId); - FederatedUserConsentClientScopeEntity.Key hisKey = new FederatedUserConsentClientScopeEntity.Key(that.userConsent, that.scopeId); + FederatedUserConsentClientScopeEntity.Key myKey = new FederatedUserConsentClientScopeEntity.Key(this.userConsent, this.scopeId, this.parameter); + FederatedUserConsentClientScopeEntity.Key hisKey = new FederatedUserConsentClientScopeEntity.Key(that.userConsent, that.scopeId, that.parameter); return myKey.equals(hisKey); } @Override public int hashCode() { - FederatedUserConsentClientScopeEntity.Key myKey = new FederatedUserConsentClientScopeEntity.Key(this.userConsent, this.scopeId); + FederatedUserConsentClientScopeEntity.Key myKey = new FederatedUserConsentClientScopeEntity.Key(this.userConsent, this.scopeId, this.parameter); return myKey.hashCode(); } @@ -96,12 +115,15 @@ public class FederatedUserConsentClientScopeEntity { protected String scopeId; + protected String parameter; + public Key() { } - public Key(FederatedUserConsentEntity userConsent, String scopeId) { + public Key(FederatedUserConsentEntity userConsent, String scopeId, String parameter) { this.userConsent = userConsent; this.scopeId = scopeId; + this.parameter = parameter; } public FederatedUserConsentEntity getUserConsent() { @@ -112,6 +134,10 @@ public class FederatedUserConsentClientScopeEntity { return scopeId; } + public String getParameter() { + return parameter; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -121,6 +147,7 @@ public class FederatedUserConsentClientScopeEntity { if (userConsent != null ? !userConsent.getId().equals(key.userConsent != null ? key.userConsent.getId() : null) : key.userConsent != null) return false; if (scopeId != null ? !scopeId.equals(key.scopeId) : key.scopeId != null) return false; + if (parameter != null ? !parameter.equals(key.parameter) : key.parameter != null) return false; return true; } @@ -129,6 +156,7 @@ public class FederatedUserConsentClientScopeEntity { public int hashCode() { int result = userConsent != null ? userConsent.getId().hashCode() : 0; result = 31 * result + (scopeId != null ? scopeId.hashCode() : 0); + result = 31 * result + (parameter != null ? parameter.hashCode() : 0); return result; } } diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-26.7.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-26.7.0.xml index 6a871ae8c81..02c3f38fcc2 100644 --- a/model/jpa/src/main/resources/META-INF/jpa-changelog-26.7.0.xml +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-26.7.0.xml @@ -200,7 +200,24 @@ + + + + + + + + + + + + + + + + + diff --git a/model/storage-private/src/main/java/org/keycloak/storage/datastore/DefaultExportImportManager.java b/model/storage-private/src/main/java/org/keycloak/storage/datastore/DefaultExportImportManager.java index 0a5a0af0545..681c77ff786 100644 --- a/model/storage-private/src/main/java/org/keycloak/storage/datastore/DefaultExportImportManager.java +++ b/model/storage-private/src/main/java/org/keycloak/storage/datastore/DefaultExportImportManager.java @@ -1009,7 +1009,7 @@ public class DefaultExportImportManager implements ExportImportManager { createRoleMappings(userRep, user, newRealm); if (userRep.getClientConsents() != null) { for (UserConsentRepresentation consentRep : userRep.getClientConsents()) { - UserConsentModel consentModel = RepresentationToModel.toModel(newRealm, consentRep); + UserConsentModel consentModel = RepresentationToModel.toModel(newRealm, consentRep, session); session.users().addConsent(newRealm, user.getId(), consentModel); } } @@ -1675,7 +1675,7 @@ public class DefaultExportImportManager implements ExportImportManager { } if (userRep.getClientConsents() != null) { for (UserConsentRepresentation consentRep : userRep.getClientConsents()) { - UserConsentModel consentModel = RepresentationToModel.toModel(newRealm, consentRep); + UserConsentModel consentModel = RepresentationToModel.toModel(newRealm, consentRep, session); federatedStorage.addConsent(newRealm, userRep.getId(), consentModel); } } diff --git a/server-spi-private/src/main/java/org/keycloak/models/light/LightweightConsentEntity.java b/server-spi-private/src/main/java/org/keycloak/models/light/LightweightConsentEntity.java index e135faf8631..7aef4063a6a 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/light/LightweightConsentEntity.java +++ b/server-spi-private/src/main/java/org/keycloak/models/light/LightweightConsentEntity.java @@ -17,9 +17,12 @@ package org.keycloak.models.light; import java.util.HashSet; +import java.util.List; import java.util.Objects; import java.util.Set; +import org.keycloak.common.util.CollectionUtil; +import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.common.util.Time; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientScopeModel; @@ -37,6 +40,7 @@ class LightweightConsentEntity { private String clientId; private Long createdDate; private Set grantedClientScopesIds; + private final MultivaluedHashMap parameters = new MultivaluedHashMap<>(); private Long lastUpdatedDate; public static LightweightConsentEntity fromModel(UserConsentModel model) { @@ -49,8 +53,8 @@ class LightweightConsentEntity { model.getGrantedClientScopes() .stream() - .map(ClientScopeModel::getId) - .forEach(consentEntity::addGrantedClientScopesId); + .forEach(m -> consentEntity.addGrantedClientScopesId(m.getId(), + ClientScopeModel.isDynamicScope(m) ? model.getParameters(m) : null)); return consentEntity; } @@ -72,9 +76,16 @@ class LightweightConsentEntity { if (grantedClientScopesIds != null && !grantedClientScopesIds.isEmpty()) { grantedClientScopesIds.stream() - .map(scopeId -> KeycloakModelUtils.findClientScopeById(realm, client, scopeId)) - .filter(Objects::nonNull) - .forEach(model::addGrantedClientScope); + .map(scopeId -> KeycloakModelUtils.findClientScopeById(realm, client, scopeId)) + .filter(Objects::nonNull) + .forEach(m -> { + List parameters = entity.parameters.getList(m.getId()); + if (parameters.isEmpty()) { + model.addGrantedClientScope(m); + } else { + parameters.stream().forEach(p -> model.addGrantedClientScope(m, p)); + } + }); } return model; @@ -127,28 +138,7 @@ class LightweightConsentEntity { return grantedClientScopesIds; } - public void removeGrantedClientScopesId(String clientScopeId) { - if (grantedClientScopesIds == null) { - return; - } - if (grantedClientScopesIds.remove(clientScopeId)) { - this.lastUpdatedDate = Time.currentTimeMillis(); - } - } - - public void setGrantedClientScopesIds(Set clientScopeIds) { - clientScopeIds = clientScopeIds == null ? null : new HashSet<>(clientScopeIds); - if (clientScopeIds != null) { - clientScopeIds.removeIf(Objects::isNull); - if (clientScopeIds.isEmpty()) { - clientScopeIds = null; - } - } - grantedClientScopesIds = clientScopeIds; - this.lastUpdatedDate = Time.currentTimeMillis(); - } - - public void addGrantedClientScopesId(String clientScopeId) { + public void addGrantedClientScopesId(String clientScopeId, List parameters) { if (clientScopeId == null) { return; } @@ -156,9 +146,16 @@ class LightweightConsentEntity { grantedClientScopesIds = new HashSet<>(); } grantedClientScopesIds.add(clientScopeId); + if (CollectionUtil.isNotEmpty(parameters)) { + this.parameters.addAll(clientScopeId, parameters); + } this.lastUpdatedDate = Time.currentTimeMillis(); } + public void addGrantedClientScopesId(String clientScopeId) { + addGrantedClientScopesId(clientScopeId, null); + } + public Long getLastUpdatedDate() { return lastUpdatedDate; } diff --git a/server-spi-private/src/main/java/org/keycloak/models/light/LightweightUserAdapter.java b/server-spi-private/src/main/java/org/keycloak/models/light/LightweightUserAdapter.java index f290b3e3102..b879f77a281 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/light/LightweightUserAdapter.java +++ b/server-spi-private/src/main/java/org/keycloak/models/light/LightweightUserAdapter.java @@ -23,7 +23,6 @@ import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.function.Consumer; -import java.util.stream.Collectors; import java.util.stream.Stream; import org.keycloak.common.Profile; @@ -284,11 +283,13 @@ public class LightweightUserAdapter extends AbstractInMemoryUserAdapter { : consent.getClient().getId(); LightweightConsentEntity userConsentEntity = getConsentEntityByClient(clientId); - userConsentEntity.setGrantedClientScopesIds( - consent.getGrantedClientScopes().stream() - .map(ClientScopeModel::getId) - .collect(Collectors.toSet()) - ); + for (ClientScopeModel clientScope : consent.getGrantedClientScopes()) { + if (ClientScopeModel.isDynamicScope(clientScope)) { + userConsentEntity.addGrantedClientScopesId(clientScope.getId(), consent.getParameters(clientScope)); + } else { + userConsentEntity.addGrantedClientScopesId(clientScope.getId()); + } + } update(); } diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index ffb4570df82..0e1e688f67a 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -1026,6 +1026,9 @@ public class ModelToRepresentation { for (ClientScopeModel clientScope : model.getGrantedClientScopes()) { if (clientScope instanceof ClientModel) { grantedClientScopes.add(((ClientModel) clientScope).getClientId()); + } else if (ClientScopeModel.isDynamicScope(clientScope)) { + model.getParameters(clientScope).stream().forEach(p -> + grantedClientScopes.add(clientScope.getDynamicScopeRegexp().replace("*", p))); } else { grantedClientScopes.add(clientScope.getName()); } diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index 8c4b39ece73..fd820d90d3f 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -102,7 +102,10 @@ import org.keycloak.models.credential.dto.OTPSecretData; import org.keycloak.models.credential.dto.PasswordCredentialData; import org.keycloak.organization.OrganizationProvider; import org.keycloak.policy.PasswordPolicyNotMetException; +import org.keycloak.protocol.oidc.rar.AuthorizationRequestParserProvider; import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.rar.AuthorizationDetails; +import org.keycloak.rar.AuthorizationRequestContext; import org.keycloak.representations.idm.AuthenticationExecutionRepresentation; import org.keycloak.representations.idm.AuthenticationFlowRepresentation; import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; @@ -962,7 +965,7 @@ public class RepresentationToModel { return model; } - public static UserConsentModel toModel(RealmModel newRealm, UserConsentRepresentation consentRep) { + public static UserConsentModel toModel(RealmModel newRealm, UserConsentRepresentation consentRep, KeycloakSession session) { ClientModel client = newRealm.getClientByClientId(consentRep.getClientId()); if (client == null) { throw new RuntimeException("Unable to find client consent mappings for client: " + consentRep.getClientId()); @@ -975,10 +978,28 @@ public class RepresentationToModel { if (consentRep.getGrantedClientScopes() != null) { for (String scopeName : consentRep.getGrantedClientScopes()) { ClientScopeModel clientScope = KeycloakModelUtils.getClientScopeByName(newRealm, scopeName); - if (clientScope == null) { + if (clientScope != null) { + consentModel.addGrantedClientScope(clientScope); + } else if (Profile.isFeatureEnabled(Feature.DYNAMIC_SCOPES)) { + // check for dynamic scopes + AuthorizationRequestParserProvider clientScopeParser = session.getProvider( + AuthorizationRequestParserProvider.class, "client-scope"); + if (clientScopeParser == null) { + throw new RuntimeException("No provider found for authorization requests parser client-scope"); + } + + AuthorizationRequestContext ctx = clientScopeParser.parseScopes(client, scopeName); + AuthorizationDetails authDetails = ctx.getAuthorizationDetailEntries().stream() + .filter(a -> a.getAuthorizationDetails().getDynamicScopeParamFromCustomData() != null) + .findAny().orElse(null); + if (authDetails == null) { + throw new RuntimeException("Unable to find client scope referenced in consent mappings of user. Client scope name: " + scopeName); + } + + consentModel.addGrantedClientScope(authDetails.getClientScope(), authDetails.getAuthorizationDetails().getDynamicScopeParamFromCustomData()); + } else { throw new RuntimeException("Unable to find client scope referenced in consent mappings of user. Client scope name: " + scopeName); } - consentModel.addGrantedClientScope(clientScope); } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/rar/AuthorizationRequestParserProvider.java b/server-spi-private/src/main/java/org/keycloak/protocol/oidc/rar/AuthorizationRequestParserProvider.java similarity index 100% rename from services/src/main/java/org/keycloak/protocol/oidc/rar/AuthorizationRequestParserProvider.java rename to server-spi-private/src/main/java/org/keycloak/protocol/oidc/rar/AuthorizationRequestParserProvider.java diff --git a/services/src/main/java/org/keycloak/protocol/oidc/rar/AuthorizationRequestParserProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/protocol/oidc/rar/AuthorizationRequestParserProviderFactory.java similarity index 100% rename from services/src/main/java/org/keycloak/protocol/oidc/rar/AuthorizationRequestParserProviderFactory.java rename to server-spi-private/src/main/java/org/keycloak/protocol/oidc/rar/AuthorizationRequestParserProviderFactory.java diff --git a/services/src/main/java/org/keycloak/protocol/oidc/rar/AuthorizationRequestParserSpi.java b/server-spi-private/src/main/java/org/keycloak/protocol/oidc/rar/AuthorizationRequestParserSpi.java similarity index 100% rename from services/src/main/java/org/keycloak/protocol/oidc/rar/AuthorizationRequestParserSpi.java rename to server-spi-private/src/main/java/org/keycloak/protocol/oidc/rar/AuthorizationRequestParserSpi.java diff --git a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi index a60251f8806..97742a635fc 100755 --- a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -111,5 +111,6 @@ org.keycloak.models.workflow.WorkflowStateSpi org.keycloak.models.workflow.WorkflowStepSpi org.keycloak.models.workflow.WorkflowSpi org.keycloak.protocol.oidc.rar.AuthorizationDetailsProcessorSpi +org.keycloak.protocol.oidc.rar.AuthorizationRequestParserSpi org.keycloak.cache.AlternativeLookupSPI org.keycloak.cache.LocalCacheSPI diff --git a/server-spi/src/main/java/org/keycloak/models/ClientScopeModel.java b/server-spi/src/main/java/org/keycloak/models/ClientScopeModel.java index 60e58c47cda..20e77455e5b 100755 --- a/server-spi/src/main/java/org/keycloak/models/ClientScopeModel.java +++ b/server-spi/src/main/java/org/keycloak/models/ClientScopeModel.java @@ -19,6 +19,7 @@ package org.keycloak.models; import java.util.Map; +import org.keycloak.common.Profile; import org.keycloak.common.util.ObjectUtil; import org.keycloak.provider.ProviderEvent; @@ -33,6 +34,16 @@ public interface ClientScopeModel extends ProtocolMapperContainerModel, ScopeCon */ String VALUE_SEPARATOR = ":"; + /** + * Returns true when dynamic scopes are enabled and the scope is defined as dynamic. + * + * @param scope The scope to check + * @return true when the dynamic scopes feature is enabled and the scope is dynamic, false otherwise + */ + static boolean isDynamicScope(ClientScopeModel scope) { + return Profile.isFeatureEnabled(Profile.Feature.DYNAMIC_SCOPES) && scope.isDynamicScope(); + } + interface ClientScopeRemovedEvent extends ProviderEvent { ClientScopeModel getClientScope(); @@ -89,7 +100,7 @@ public interface ClientScopeModel extends ProtocolMapperContainerModel, ScopeCon String consentScreenText = getAttribute(CONSENT_SCREEN_TEXT); if (ObjectUtil.isBlank(consentScreenText)) { consentScreenText = getName(); - if (isDynamicScope()) { + if (isDynamicScope(this)) { consentScreenText += ": {0}"; } } diff --git a/server-spi/src/main/java/org/keycloak/models/UserConsentModel.java b/server-spi/src/main/java/org/keycloak/models/UserConsentModel.java index e2a55f737c8..59ce347288d 100644 --- a/server-spi/src/main/java/org/keycloak/models/UserConsentModel.java +++ b/server-spi/src/main/java/org/keycloak/models/UserConsentModel.java @@ -17,16 +17,21 @@ package org.keycloak.models; +import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Set; +import org.keycloak.common.util.MultivaluedHashMap; + /** * @author Marek Posolda */ public class UserConsentModel { private final ClientModel client; - private Set clientScopes = new HashSet<>(); + private final Set clientScopes = new HashSet<>(); + private final MultivaluedHashMap parameters = new MultivaluedHashMap<>(); private Long createdDate; private Long lastUpdatedDate; @@ -39,17 +44,43 @@ public class UserConsentModel { } public void addGrantedClientScope(ClientScopeModel clientScope) { + addGrantedClientScope(clientScope, null); + } + + public void addGrantedClientScope(ClientScopeModel clientScope, String parameter) { clientScopes.add(clientScope); + if (ClientScopeModel.isDynamicScope(clientScope)) { + if (parameter == null) { + throw new IllegalArgumentException("Paramater value is compulsory for Dynamic Scope " + clientScope.getName()); + } + parameters.add(clientScope.getId(), parameter); + } } public Set getGrantedClientScopes() { return clientScopes; } + public List getParameters(ClientScopeModel clientScope) { + if (ClientScopeModel.isDynamicScope(clientScope)) { + return parameters.getList(clientScope.getId()); + } + return Collections.emptyList(); + } + public boolean isClientScopeGranted(ClientScopeModel clientScope) { - // TODO: May need to be changed with adding support for client scopes inheritance + return isClientScopeGranted(clientScope, null); + } + + public boolean isClientScopeGranted(ClientScopeModel clientScope, String parameter) { for (ClientScopeModel apprClientScope : clientScopes) { - if (apprClientScope.getId().equals(clientScope.getId())) return true; + if (apprClientScope.getId().equals(clientScope.getId())) { + if (ClientScopeModel.isDynamicScope(clientScope)) { + return parameter != null && parameters.getList(apprClientScope.getId()).contains(parameter); + } else { + return parameter == null && parameters.getList(apprClientScope.getId()).isEmpty(); + } + } } return false; } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java index 3e75c7358b1..93f6af106c3 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -258,7 +258,7 @@ public class TokenManager { ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndScopeParameter(clientSession, oldTokenScope, session); // Check user didn't revoke granted consent - if (!verifyConsentStillAvailable(session, user, client, clientSessionCtx.getClientScopesStream())) { + if (!verifyConsentStillAvailable(session, user, client, oldTokenScope)) { throw new OAuthErrorException(OAuthErrorException.INVALID_SCOPE, "Client no longer has requested consent from user"); } @@ -861,24 +861,36 @@ public class TokenManager { } // Check if user still has granted consents to all requested client scopes - public static boolean verifyConsentStillAvailable(KeycloakSession session, UserModel user, ClientModel client, - Stream requestedClientScopes) { + public static boolean verifyConsentStillAvailable(KeycloakSession session, UserModel user, ClientModel client, String scopeParam) { if (!client.isConsentRequired()) { return true; } UserConsentModel grantedConsent = UserConsentManager.getConsentByClient(session, client.getRealm(), user, client.getId()); - return requestedClientScopes - .filter(ClientScopeModel::isDisplayOnConsentScreen) - .noneMatch(requestedScope -> { - if (grantedConsent == null || !grantedConsent.getGrantedClientScopes().contains(requestedScope)) { - logger.debugf("Client '%s' no longer has requested consent from user '%s' for client scope '%s'", - client.getClientId(), user.getUsername(), requestedScope.getName()); - return true; - } + if (Profile.isFeatureEnabled(Profile.Feature.DYNAMIC_SCOPES)) { + AuthorizationRequestContext ctx = AuthorizationContextUtil.getAuthorizationRequestContextFromScopesWithClient( + session, client, scopeParam); + for (AuthorizationDetails authDetails : ctx.getAuthorizationDetailEntries()) { + ClientScopeModel requestedScope = authDetails.getClientScope(); + String parameter = authDetails.getDynamicScopeParam(); + if (requestedScope.isDisplayOnConsentScreen() && (grantedConsent == null || !grantedConsent.isClientScopeGranted(requestedScope, parameter))) { return false; - }); + } + } + return true; + } else { + return getRequestedClientScopes(session, scopeParam, client, user) + .filter(ClientScopeModel::isDisplayOnConsentScreen) + .noneMatch(requestedScope -> { + if (grantedConsent == null || !grantedConsent.isClientScopeGranted(requestedScope)) { + logger.debugf("Client '%s' no longer has requested consent from user '%s' for client scope '%s'", + client.getClientId(), user.getUsername(), requestedScope.getName()); + return true; + } + return false; + }); + } } public AccessToken transformAccessToken(KeycloakSession session, AccessToken token, diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/AuthorizationCodeGrantType.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/AuthorizationCodeGrantType.java index 84660b5cfee..ce9400c61c8 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/AuthorizationCodeGrantType.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/AuthorizationCodeGrantType.java @@ -18,8 +18,6 @@ package org.keycloak.protocol.oidc.grants; import java.util.List; -import java.util.function.Supplier; -import java.util.stream.Stream; import jakarta.ws.rs.core.Response; @@ -30,7 +28,6 @@ import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventType; import org.keycloak.models.AuthenticatedClientSessionModel; -import org.keycloak.models.ClientScopeModel; import org.keycloak.models.ClientSessionContext; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; @@ -205,8 +202,7 @@ public class AuthorizationCodeGrantType extends OAuth2GrantTypeBase { // Compute client scopes again from scope parameter. Check if user still has them granted // (but in code-to-token request, it could just theoretically happen that they are not available) - Supplier> clientScopesSupplier = () -> TokenManager.getRequestedClientScopes(session, scopeParam, client, user); - if (!TokenManager.verifyConsentStillAvailable(session, user, client, clientScopesSupplier.get())) { + if (!TokenManager.verifyConsentStillAvailable(session, user, client, scopeParam)) { String errorMessage = "Client no longer has requested consent from user"; event.detail(Details.REASON, errorMessage); event.error(Errors.NOT_ALLOWED); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/CibaGrantType.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/CibaGrantType.java index 4f6d1ee9f54..c275f960e90 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/CibaGrantType.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/CibaGrantType.java @@ -36,7 +36,6 @@ import org.keycloak.models.OAuth2DeviceCodeModel; import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; -import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.protocol.oidc.TokenManager; @@ -46,6 +45,7 @@ import org.keycloak.protocol.oidc.grants.ciba.clientpolicy.context.BackchannelTo import org.keycloak.protocol.oidc.grants.ciba.clientpolicy.context.BackchannelTokenResponseContext; import org.keycloak.protocol.oidc.grants.ciba.endpoints.CibaRootEndpoint; import org.keycloak.protocol.oidc.grants.device.DeviceGrantType; +import org.keycloak.rar.AuthorizationDetails; import org.keycloak.services.CorsErrorResponseException; import org.keycloak.services.ErrorResponseException; import org.keycloak.services.Urls; @@ -185,9 +185,7 @@ public class CibaGrantType extends OAuth2GrantTypeBase { // (but in code-to-token request, it could just theoretically happen that they are not available) String scopeParam = request.getScope(); - if (!TokenManager - .verifyConsentStillAvailable(session, - user, client, TokenManager.getRequestedClientScopes(session, scopeParam, client, user))) { + if (!TokenManager.verifyConsentStillAvailable(session, user, client, scopeParam)) { String errorMessage = "Client no longer has requested consent from user"; event.detail(Details.REASON, errorMessage); event.error(Errors.NOT_ALLOWED); @@ -270,10 +268,11 @@ public class CibaGrantType extends OAuth2GrantTypeBase { boolean updateConsentRequired = false; - for (String clientScopeId : authSession.getClientScopes()) { - ClientScopeModel clientScope = KeycloakModelUtils.findClientScopeById(realm, client, clientScopeId); - if (clientScope != null && !grantedConsent.isClientScopeGranted(clientScope) && clientScope.isDisplayOnConsentScreen()) { - grantedConsent.addGrantedClientScope(clientScope); + for (AuthorizationDetails authDetails : AuthenticationManager.getClientScopeModelStream(session, client).toList()) { + ClientScopeModel clientScope = authDetails.getClientScope(); + String parameter = authDetails.getDynamicScopeParam(); + if (clientScope != null && !grantedConsent.isClientScopeGranted(clientScope, parameter) && clientScope.isDisplayOnConsentScreen()) { + grantedConsent.addGrantedClientScope(clientScope, parameter); updateConsentRequired = true; } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/device/DeviceGrantType.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/device/DeviceGrantType.java index 9f3e60617f0..6ddd436cf39 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/device/DeviceGrantType.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/device/DeviceGrantType.java @@ -345,7 +345,7 @@ public class DeviceGrantType extends OAuth2GrantTypeBase { // Compute client scopes again from scope parameter. Check if user still has them granted // (but in device_code-to-token request, it could just theoretically happen that they are not available) String scopeParam = deviceCodeModel.getScope(); - if (!TokenManager.verifyConsentStillAvailable(session, user, client, TokenManager.getRequestedClientScopes(session, scopeParam, client, user))) { + if (!TokenManager.verifyConsentStillAvailable(session, user, client, scopeParam)) { String errorMessage = "Client no longer has requested consent from user"; event.detail(Details.REASON, errorMessage); event.error(Errors.NOT_ALLOWED); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/StandardTokenExchangeProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/StandardTokenExchangeProvider.java index 151127a543b..52be94d8ab3 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/StandardTokenExchangeProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/StandardTokenExchangeProvider.java @@ -212,8 +212,8 @@ public class StandardTokenExchangeProvider extends AbstractTokenExchangeProvider } } - protected void validateConsents(UserModel targetUser, ClientSessionContext clientSessionCtx) { - if (!TokenManager.verifyConsentStillAvailable(session, targetUser, client, clientSessionCtx.getClientScopesStream())) { + protected void validateConsents(UserModel targetUser, String scope) { + if (!TokenManager.verifyConsentStillAvailable(session, targetUser, client, scope)) { event.detail(Details.REASON, "Missing consents for Token Exchange in client " + client.getClientId()); event.error(Errors.CONSENT_DENIED); throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_SCOPE, @@ -287,7 +287,7 @@ public class StandardTokenExchangeProvider extends AbstractTokenExchangeProvider clientSessionCtx.setAttribute(Constants.REQUESTED_AUDIENCE_CLIENTS, targetAudienceClients.toArray(ClientModel[]::new)); } - validateConsents(targetUser, clientSessionCtx); + validateConsents(targetUser, scope); clientSessionCtx.setAttribute(Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE); TokenContextEncoderProvider encoder = session.getProvider(TokenContextEncoderProvider.class); diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index eef2a0d5088..c0743a48387 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -107,6 +107,7 @@ import org.keycloak.protocol.oidc.encode.AccessTokenContext; import org.keycloak.protocol.oidc.encode.TokenContextEncoderProvider; import org.keycloak.rar.AuthorizationDetails; import org.keycloak.representations.AccessToken; +import org.keycloak.representations.AuthorizationDetailsJSONRepresentation; import org.keycloak.services.ErrorPage; import org.keycloak.services.ErrorResponseException; import org.keycloak.services.ServicesLogger; @@ -1247,7 +1248,9 @@ public class AuthenticationManager { } // we need to add dynamic scopes with params to the scopes to consent every time for now - if (grantedConsent == null || !grantedConsent.isClientScopeGranted(clientScope) || isDynamicScopeWithParam(authDetails)) { + AuthorizationDetailsJSONRepresentation rep = authDetails.getAuthorizationDetails(); + String parameter = rep != null ? rep.getDynamicScopeParamFromCustomData() : null; + if (grantedConsent == null || !grantedConsent.isClientScopeGranted(clientScope, parameter)) { clientScopesToDisplay.add(authDetails); } } @@ -1271,7 +1274,7 @@ public class AuthenticationManager { } - private static Stream getClientScopeModelStream(KeycloakSession session, ClientModel client) { + public static Stream getClientScopeModelStream(KeycloakSession session, ClientModel client) { AuthenticationSessionModel authSession = session.getContext().getAuthenticationSession(); //if Dynamic Scopes are enabled, get the scopes from the AuthorizationRequestContext, passing the session and scopes as parameters // then concat a Stream with the ClientModel, as it's discarded in the getAuthorizationRequestContext method diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java index 8075912bbb2..5715197c699 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -85,7 +85,6 @@ import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.AuthenticationFlowResolver; import org.keycloak.models.utils.FormMessage; -import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.SystemClientUtil; import org.keycloak.organization.OrganizationProvider; import org.keycloak.organization.utils.Organizations; @@ -97,6 +96,7 @@ import org.keycloak.protocol.oidc.grants.device.DeviceGrantType; import org.keycloak.protocol.oidc.utils.OIDCResponseMode; import org.keycloak.protocol.oidc.utils.OIDCResponseType; import org.keycloak.protocol.oidc.utils.RedirectUtils; +import org.keycloak.rar.AuthorizationDetails; import org.keycloak.representations.JsonWebToken; import org.keycloak.services.ErrorPage; import org.keycloak.services.ErrorPageException; @@ -1018,6 +1018,16 @@ public class LoginActionsService { return Response.status(302).location(redirect).build(); } + private boolean checkGranted(AuthorizationDetails details, UserConsentModel grantedConsent) { + ClientScopeModel clientScope = details.getClientScope(); + String parameter = details.getDynamicScopeParam(); + if (!grantedConsent.isClientScopeGranted(clientScope, parameter) && clientScope.isDisplayOnConsentScreen()) { + grantedConsent.addGrantedClientScope(clientScope, parameter); + return true; + } + return false; + } + /** * OAuth grant page. You should not invoked this directly! * @@ -1062,26 +1072,19 @@ public class LoginActionsService { return DeviceGrantType.denyOAuth2DeviceAuthorization(authSession, Error.CONSENT_DENIED, session); } - UserConsentModel grantedConsent = UserConsentManager.getConsentByClient(session, realm, user, client.getId()); - if (grantedConsent == null) { + UserConsentModel existingConsent = UserConsentManager.getConsentByClient(session, realm, user, client.getId()); + UserConsentModel grantedConsent; + if (existingConsent == null) { grantedConsent = new UserConsentModel(client); UserConsentManager.addConsent(session, realm, user, grantedConsent); + } else { + grantedConsent = existingConsent; } // Update may not be required if all clientScopes were already granted (May happen for example with prompt=consent) - boolean updateConsentRequired = false; - - for (String clientScopeId : authSession.getClientScopes()) { - ClientScopeModel clientScope = KeycloakModelUtils.findClientScopeById(realm, client, clientScopeId); - if (clientScope != null) { - if (!grantedConsent.isClientScopeGranted(clientScope) && clientScope.isDisplayOnConsentScreen()) { - grantedConsent.addGrantedClientScope(clientScope); - updateConsentRequired = true; - } - } else { - logger.warnf("Client scope or client with ID '%s' not found", clientScopeId); - } - } + Boolean updateConsentRequired = AuthenticationManager.getClientScopeModelStream(session, client) + .map(d -> checkGranted(d, grantedConsent)) + .reduce(Boolean::logicalOr).orElse(Boolean.FALSE); if (updateConsentRequired) { UserConsentManager.updateConsent(session, realm, user, grantedConsent); diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java b/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java index 6516bd20e4a..72a551ed938 100755 --- a/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java @@ -24,7 +24,6 @@ import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Objects; import java.util.Properties; import java.util.Set; import java.util.function.Function; @@ -50,7 +49,6 @@ import org.keycloak.common.ClientConnection; import org.keycloak.common.Profile; import org.keycloak.common.Profile.Feature; import org.keycloak.common.enums.AccountRestApiVersion; -import org.keycloak.common.util.StringPropertyReplacer; import org.keycloak.events.Details; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; @@ -78,6 +76,7 @@ import org.keycloak.services.resources.account.resources.ResourcesService; import org.keycloak.services.util.ResolveRelative; import org.keycloak.storage.ReadOnlyException; import org.keycloak.theme.Theme; +import org.keycloak.theme.beans.AdvancedMessageFormatterMethod; import org.keycloak.userprofile.AttributeMetadata; import org.keycloak.userprofile.Attributes; import org.keycloak.userprofile.EventAuditingAttributeChangeListener; @@ -87,6 +86,7 @@ import org.keycloak.userprofile.UserProfileProvider; import org.keycloak.userprofile.ValidationException; import org.keycloak.userprofile.ValidationException.Error; +import freemarker.template.TemplateModelException; import org.jboss.resteasy.reactive.NoCache; /** @@ -266,7 +266,7 @@ public class AccountRestService { representation.setEffectiveUrl(ResolveRelative.resolveRelativeUri(session, model.getRootUrl(), model.getBaseUrl())); UserConsentModel consentModel = consents.get(model.getClientId()); if(consentModel != null) { - representation.setConsent(modelToBriefRepresentation(consentModel)); + representation.setConsent(modelToRepresentation(consentModel, true)); representation.setLogoUri(model.getAttribute(ClientModel.LOGO_URI)); representation.setPolicyUri(model.getAttribute(ClientModel.POLICY_URI)); representation.setTosUri(model.getAttribute(ClientModel.TOS_URI)); @@ -274,25 +274,27 @@ public class AccountRestService { return representation; } - private ConsentRepresentation modelToRepresentation(final UserConsentModel model) { - final List grantedScopes = model.getGrantedClientScopes().stream() - .map(clientScopeModel -> new ConsentScopeRepresentation( - clientScopeModel.getId(), + private ConsentScopeRepresentation createContentScopeRepresentation(ClientScopeModel clientScopeModel, String parameter, boolean briefRepresentation) { + return briefRepresentation + ? new ConsentScopeRepresentation(clientScopeModel.getId(), + getClientScopeName(clientScopeModel), + getClientScopeDisplayText(clientScopeModel, parameter)) + : new ConsentScopeRepresentation(clientScopeModel.getId(), getClientScopeName(clientScopeModel), clientScopeModel.getDescription(), clientScopeModel.getProtocol(), - getClientScopeDisplayText(clientScopeModel)) - ).toList(); - return new ConsentRepresentation(grantedScopes, model.getCreatedDate(), model.getLastUpdatedDate()); + getClientScopeDisplayText(clientScopeModel, parameter)); } - private ConsentRepresentation modelToBriefRepresentation(final UserConsentModel model) { - final List grantedScopes = model.getGrantedClientScopes().stream() - .map(clientScopeModel -> new ConsentScopeRepresentation( - clientScopeModel.getId(), - getClientScopeName(clientScopeModel), - getClientScopeDisplayText(clientScopeModel)) - ).toList(); + private ConsentRepresentation modelToRepresentation(UserConsentModel model, boolean briefRepresentation) { + List grantedScopes = new ArrayList<>(); + model.getGrantedClientScopes().stream().forEach(m -> { + if (ClientScopeModel.isDynamicScope(m)) { + model.getParameters(m).forEach(p -> grantedScopes.add(createContentScopeRepresentation(m, p, briefRepresentation))); + } else { + grantedScopes.add(createContentScopeRepresentation(m, null, briefRepresentation)); + } + }); return new ConsentRepresentation(grantedScopes, model.getCreatedDate(), model.getLastUpdatedDate()); } @@ -303,12 +305,21 @@ public class AccountRestService { return clientScopeModel.getConsentScreenText(); } - private String getClientScopeDisplayText(final ClientScopeModel clientScopeModel) { - final var consentScreenText = clientScopeModel.getConsentScreenText(); - return StringPropertyReplacer.replaceProperties( - consentScreenText, - Objects.requireNonNull(getProperties())::getProperty - ); + private String getClientScopeDisplayText(final ClientScopeModel clientScopeModel, final String parameter) { + if (clientScopeModel.getConsentScreenText() == null) { + return null; + } + AdvancedMessageFormatterMethod method = new AdvancedMessageFormatterMethod(locale, getProperties()); + List inputs = new ArrayList<>(); + inputs.add(clientScopeModel.getConsentScreenText()); + if (parameter != null) { + inputs.add(parameter); + } + try { + return (String) method.exec(inputs); + } catch (TemplateModelException e) { + return clientScopeModel.getConsentScreenText(); + } } private Properties getProperties() { @@ -343,11 +354,7 @@ public class AccountRestService { return Response.noContent().build(); } - if (briefRepresentation) { - return Response.ok(modelToBriefRepresentation(consent)).build(); - } - - return Response.ok(modelToRepresentation(consent)).build(); + return Response.ok(modelToRepresentation(consent, briefRepresentation)).build(); } /** @@ -442,7 +449,7 @@ public class AccountRestService { String scopeString = grantedConsent.getGrantedClientScopes().stream().map(cs->cs.getName()).collect(Collectors.joining(" ")); event.detail(Details.SCOPE, scopeString).success(); grantedConsent = UserConsentManager.getConsentByClient(session, realm, user, client.getId()); - return Response.ok(modelToBriefRepresentation(grantedConsent)).build(); + return Response.ok(modelToRepresentation(grantedConsent, true)).build(); } catch (IllegalArgumentException e) { throw ErrorResponse.error(e.getMessage(), Response.Status.BAD_REQUEST); } @@ -472,9 +479,13 @@ public class AccountRestService { String msg = String.format("Scope id %s does not exist for client %s.", scopeRepresentation, consent.getClient().getName()); event.error(msg); throw new IllegalArgumentException(msg); - } else { - consent.addGrantedClientScope(scopeModel); } + if (ClientScopeModel.isDynamicScope(scopeModel)) { + String msg = String.format("Cannot create Scope id %s for client %s because is dynamic.", scopeRepresentation, consent.getClient().getName()); + event.error(msg); + throw new IllegalArgumentException(msg); + } + consent.addGrantedClientScope(scopeModel, null); } return consent; } diff --git a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi index 8c4fd6940d7..85161531fcc 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -28,7 +28,6 @@ org.keycloak.encoding.ResourceEncodingSpi org.keycloak.protocol.oidc.encode.TokenContextEncoderSpi org.keycloak.protocol.oidc.grants.ciba.channel.AuthenticationChannelSpi org.keycloak.protocol.oidc.grants.ciba.resolvers.CIBALoginUserResolverSpi -org.keycloak.protocol.oidc.rar.AuthorizationRequestParserSpi org.keycloak.services.resources.admin.ext.AdminRealmResourceSpi org.keycloak.theme.freemarker.FreeMarkerSPI org.keycloak.protocol.oid4vc.issuance.credentialbuilder.CredentialBuilderSpi diff --git a/tests/base/src/test/java/org/keycloak/tests/oauth/DynamicScopesOAuthGrantTest.java b/tests/base/src/test/java/org/keycloak/tests/oauth/DynamicScopesOAuthGrantTest.java index ab9f9d81c44..9fb2fdcbe81 100644 --- a/tests/base/src/test/java/org/keycloak/tests/oauth/DynamicScopesOAuthGrantTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/oauth/DynamicScopesOAuthGrantTest.java @@ -32,17 +32,21 @@ import org.keycloak.testframework.server.KeycloakServerConfigBuilder; import org.keycloak.testframework.ui.annotations.InjectPage; import org.keycloak.testframework.ui.page.OAuthGrantPage; import org.keycloak.testframework.util.ApiUtil; +import org.keycloak.tests.suites.DatabaseTest; import org.keycloak.testsuite.util.AccountHelper; import org.keycloak.testsuite.util.oauth.AccessTokenResponse; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; /** * * @author rmartinc */ +@DatabaseTest @KeycloakIntegrationTest(config = DynamicScopesOAuthGrantTest.DynamicScopesServerConfig.class) public class DynamicScopesOAuthGrantTest { @@ -79,8 +83,8 @@ public class DynamicScopesOAuthGrantTest { thirdParty.admin().addOptionalClientScope(DYNAMIC_SCOPE_ID); } - @BeforeEach - public void beforeEach() { + @AfterEach + public void afterEach() { // logout user and revoke consents if present AccountHelper.logout(realm.admin(), DEFAULT_USERNAME); List> userConsents = AccountHelper.getUserConsents(realm.admin(), DEFAULT_USERNAME); @@ -95,18 +99,19 @@ public class DynamicScopesOAuthGrantTest { // login using the dynamic scope oauth.client(THIRD_PARTY_APP, "password"); - oauth.scope("foo-dynamic-scope:withparam"); + oauth.scope("foo-dynamic-scope:param1"); oauth.openLoginForm(); oauth.fillLoginForm(DEFAULT_USERNAME, DEFAULT_PASSWORD); grantPage.assertCurrent(); List grants = grantPage.getDisplayedGrants(); - Assertions.assertTrue(grants.contains("foo-dynamic-scope: withparam")); + Assertions.assertTrue(grants.contains("foo-dynamic-scope: param1")); grantPage.accept(); EventRepresentation loginEvent = events.poll(); EventAssertion.assertSuccess(loginEvent).type(EventType.LOGIN) .clientId(THIRD_PARTY_APP) .details(Details.REDIRECT_URI, oauth.getRedirectUri()) + .details(Details.USERNAME, DEFAULT_USERNAME) .details(Details.CONSENT, Details.CONSENT_VALUE_CONSENT_GRANTED); String code = oauth.parseLoginResponse().getCode(); @@ -117,6 +122,12 @@ public class DynamicScopesOAuthGrantTest { .sessionId(loginEvent.getSessionId()) .details(Details.CODE_ID, loginEvent.getDetails().get(Details.CODE_ID)); + List> userConsents = AccountHelper.getUserConsents(realm.admin(), DEFAULT_USERNAME); + Assertions.assertTrue(((List) userConsents.get(0).get("grantedClientScopes")).stream().anyMatch(p -> p.equals("foo-dynamic-scope:param1"))); + + res = oauth.doRefreshTokenRequest(res.getRefreshToken()); + MatcherAssert.assertThat(List.of(res.getScope().split(" ")), Matchers.hasItems("foo-dynamic-scope:param1")); + oauth.logoutForm().idTokenHint(res.getIdToken()).open(); EventAssertion.assertSuccess(events.poll()).type(EventType.LOGOUT) @@ -124,20 +135,57 @@ public class DynamicScopesOAuthGrantTest { .clientId(THIRD_PARTY_APP) .withoutDetails(Details.REDIRECT_URI); - // login again to check whether the Dynamic scope and only the dynamic scope is requested again - oauth.scope("foo-dynamic-scope:withparam"); + // login again with the same param and a new one, only param2 should be requested + oauth.scope("foo-dynamic-scope:param1 foo-dynamic-scope:param2"); oauth.openLoginForm(); oauth.fillLoginForm(DEFAULT_USERNAME, DEFAULT_PASSWORD); grantPage.assertCurrent(); grants = grantPage.getDisplayedGrants(); Assertions.assertEquals(1, grants.size()); - Assertions.assertTrue(grants.contains("foo-dynamic-scope: withparam")); + Assertions.assertTrue(grants.contains("foo-dynamic-scope: param2")); grantPage.accept(); + loginEvent = events.poll(); + EventAssertion.expectLoginSuccess(loginEvent) + .clientId(THIRD_PARTY_APP) + .details(Details.REDIRECT_URI, oauth.getRedirectUri()) + .details(Details.CONSENT, Details.CONSENT_VALUE_CONSENT_GRANTED); + + code = oauth.parseLoginResponse().getCode(); + res = oauth.doAccessTokenRequest(code); + + EventAssertion.assertSuccess(events.poll()).type(EventType.CODE_TO_TOKEN) + .clientId(THIRD_PARTY_APP) + .sessionId(loginEvent.getSessionId()) + .details(Details.CODE_ID, loginEvent.getDetails().get(Details.CODE_ID)); + + userConsents = AccountHelper.getUserConsents(realm.admin(), DEFAULT_USERNAME); + Assertions.assertTrue(((List) userConsents.get(0).get("grantedClientScopes")).stream().anyMatch(p -> p.equals("foo-dynamic-scope:param1"))); + Assertions.assertTrue(((List) userConsents.get(0).get("grantedClientScopes")).stream().anyMatch(p -> p.equals("foo-dynamic-scope:param2"))); + + res = oauth.doRefreshTokenRequest(res.getRefreshToken()); + MatcherAssert.assertThat(List.of(res.getScope().split(" ")), Matchers.hasItems("foo-dynamic-scope:param1", "foo-dynamic-scope:param2")); + + res = oauth.scope("foo-dynamic-scope:param2").doRefreshTokenRequest(res.getRefreshToken()); + MatcherAssert.assertThat(List.of(res.getScope().split(" ")), Matchers.not(Matchers.hasItems("foo-dynamic-scope:param1"))); + MatcherAssert.assertThat(List.of(res.getScope().split(" ")), Matchers.hasItems("foo-dynamic-scope:param2")); + + oauth.logoutForm().idTokenHint(res.getIdToken()).open(); + + EventAssertion.assertSuccess(events.poll()).type(EventType.LOGOUT) + .sessionId(loginEvent.getSessionId()) + .clientId(THIRD_PARTY_APP) + .withoutDetails(Details.REDIRECT_URI); + + // login again with the same two params + oauth.scope("foo-dynamic-scope:param1 foo-dynamic-scope:param2"); + oauth.openLoginForm(); + oauth.fillLoginForm(DEFAULT_USERNAME, DEFAULT_PASSWORD); + EventAssertion.expectLoginSuccess(events.poll()) .clientId(THIRD_PARTY_APP) .details(Details.REDIRECT_URI, oauth.getRedirectUri()) - .details(Details.CONSENT, Details.CONSENT_VALUE_CONSENT_GRANTED); + .details(Details.CONSENT, Details.CONSENT_VALUE_PERSISTED_CONSENT); } @Test diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java index 68ec93aef91..b5a57d63ce2 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java @@ -90,6 +90,7 @@ import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.admin.AdminApiUtil; import org.keycloak.testsuite.broker.util.SimpleHttpDefault; import org.keycloak.testsuite.exportimport.ExportImportUtil; +import org.keycloak.testsuite.util.AccountHelper; import org.keycloak.testsuite.util.oauth.AccessTokenResponse; import org.keycloak.testsuite.util.runonserver.RunHelpers; import org.keycloak.theme.DefaultThemeSelectorProvider; @@ -155,7 +156,7 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest { protected void testMigratedMigrationData(boolean supportsAuthzService) { assertNames(migrationRealm.roles().list(), "offline_access", "uma_authorization", "default-roles-migration", "migration-test-realm-role"); List expectedClientIds = new ArrayList<>(Arrays.asList("account", "account-console", "admin-cli", "broker", "migration-test-client", "migration-saml-client", - "realm-management", "security-admin-console", "http://localhost:8280/sales-post-enc/")); + "realm-management", "security-admin-console", "http://localhost:8280/sales-post-enc/", "migration-consent-client")); if (supportsAuthzService) { expectedClientIds.add("authz-servlet"); @@ -165,8 +166,16 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest { assertNames(migrationRealm.clients().findAll(), expectedClientIds.toArray(new String[expectedClientIds.size()])); String id2 = migrationRealm.clients().findByClientId("migration-test-client").get(0).getId(); assertNames(migrationRealm.clients().get(id2).roles().list(), "migration-test-client-role"); - assertNames(migrationRealm.users().search("", 0, 5), "migration-test-user", "offline-test-user"); + assertNames(migrationRealm.users().search("", 0, 5), "migration-test-user", "offline-test-user", "consent-user"); assertNames(migrationRealm.groups().groups(), "migration-test-group"); + + // check consents have migrated OK after dynamic scopes + List> userConsents = AccountHelper.getUserConsents(migrationRealm, "consent-user"); + Assertions.assertNotNull(userConsents); + Assertions.assertEquals(1, userConsents.size()); + Assertions.assertEquals("migration-consent-client", userConsents.get(0).get("clientId")); + assertThat((List) userConsents.get(0).get("grantedClientScopes"), + Matchers.containsInAnyOrder("roles", "email", "profile")); } protected void testMigratedMasterData() { diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/migration-test/migration-realm-1.9.8.Final.json b/testsuite/integration-arquillian/tests/base/src/test/resources/migration-test/migration-realm-1.9.8.Final.json index 9bb7a4097d3..21b126f53b9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/migration-test/migration-realm-1.9.8.Final.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/migration-test/migration-realm-1.9.8.Final.json @@ -1795,7 +1795,29 @@ "account" : [ "manage-account", "view-profile" ] }, "groups" : [ ] - } ], + }, { + "id" : "08420acf-d86f-4eba-8a69-73e7da43c668", + "username" : "consent-user", + "emailVerified" : false, + "createdTimestamp" : 1779110441255, + "enabled" : true, + "totp" : false, + "credentials" : [ ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "offline_access" ], + "clientRoles" : { + "account" : [ "manage-account", "view-profile" ] + }, + "clientConsents" : [ { + "clientId" : "migration-consent-client", + "grantedClientScopes" : [ "roles", "email", "profile" ], + "createdDate" : 1779175201074, + "lastUpdatedDate" : 1779175201101 + } ], + "notBefore" : 0, + "groups" : [ ] + } ], "clientScopeMappings" : { "realm-management" : [ { "client" : "admin-cli", @@ -2481,6 +2503,35 @@ "useTemplateConfig" : false, "useTemplateScope" : false, "useTemplateMappers" : false + }, { + "id" : "e908e55c-7cfe-46de-8c86-9ed6c65c11d9", + "clientId" : "migration-consent-client", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : true, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+", + "backchannel.logout.session.required" : "true", + "backchannel.logout.revoke.offline.tokens" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "defaultClientScopes" : [ "acr", "web-origins", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] } ], "clientTemplates" : [ ], "browserSecurityHeaders" : { diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/migration-test/migration-realm-24.0.4.json b/testsuite/integration-arquillian/tests/base/src/test/resources/migration-test/migration-realm-24.0.4.json index e603347013d..cd9784db009 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/migration-test/migration-realm-24.0.4.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/migration-test/migration-realm-24.0.4.json @@ -2651,6 +2651,28 @@ }, "notBefore" : 0, "groups" : [ ] + }, { + "id" : "08420acf-d86f-4eba-8a69-73e7da43c668", + "username" : "consent-user", + "emailVerified" : false, + "createdTimestamp" : 1779110441255, + "enabled" : true, + "totp" : false, + "credentials" : [ ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "offline_access", "uma_authorization" ], + "clientRoles" : { + "account" : [ "manage-account", "view-profile" ] + }, + "clientConsents" : [ { + "clientId" : "migration-consent-client", + "grantedClientScopes" : [ "roles", "email", "profile" ], + "createdDate" : 1779175201074, + "lastUpdatedDate" : 1779175201101 + } ], + "notBefore" : 0, + "groups" : [ ] } ], "scopeMappings" : [ { "clientScope" : "offline_access", @@ -2970,6 +2992,36 @@ "saml.signing.certificate": "MIIDBjCCAe6gAwIBAgIJANPu/mvxOREdMA0GCSqGSIb3DQEBCwUAMDAxLjAsBgNVBAMTJWh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9zYWxlcy1wb3N0LWVuYy8wIBcNMjQwNjIxMTkzMTE3WhgPMjEyNDA1MjgxOTMxMTdaMDAxLjAsBgNVBAMTJWh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9zYWxlcy1wb3N0LWVuYy8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDE5iKDNNW5XxHAF0ITErZcHDYZI68z7u68n7o4dsiywkfOWf7jVnw7PJVnMeDEtLWtTO6f0tRTqJ4OV5HYdJ9+mhPJtn+2UuvrepyYa2IsC1eFPH98ZEtYapsE6ObvhKBQMcu5G/tQrxkCFY2ssDa99unwBH5STLyX78UvqKiYnkPCvIhkiPIHy8ab7DQowc+EE9XhlE3b63A65rp4G9R87rwgJX5VTM3h81WcDuWLPOg7YRYLZoorWz2p38/qL9gXY5NxIRK16EHGfw2W1dPrX3GyMOJbXVyrBNZ6m5IL9Wn7lBEJ/Dl7ZFMFB5W36QkJ+3aaNLT/Tu/Gz+7f24inAgMBAAGjITAfMB0GA1UdDgQWBBSk7RegFbEBruVbt/VFl2gZhZ2IpDANBgkqhkiG9w0BAQsFAAOCAQEAGyH1sXVU3HDMhCzP2k5fsJBGA+1iKLMsyyiGcaD/22anQ1uVU7iWPZH8mSJGWqkvo/4oFb7RjB2KzO/50wP0q/P/tymGsYoznt+MEJKKxYEqAYmIns7SKRIgv3xEfF8yQy2jOuULC9FTq/Pb3gd9Om40jmeJtYccDSICjEC+A2fcGe56ScuRRLt+3WFyIZUFH7Y9FYZQ3EYQ88UZg//5F1ddAzGtdMSeTanMxLKow7rUIm/+Sx6cd+Vkwo/SYdk4hsD8xZCYx8Ln4i3NKh+SzyvbYykyWVI2fwjplqvM5Md/M+SNvPtU9tkOCUxQqVfz/bwtTiqfjdSaUJlasgGByg==", "saml.encryption.certificate": "MIIDBjCCAe6gAwIBAgIJANPu/mvxOREdMA0GCSqGSIb3DQEBCwUAMDAxLjAsBgNVBAMTJWh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9zYWxlcy1wb3N0LWVuYy8wIBcNMjQwNjIxMTkzMTE3WhgPMjEyNDA1MjgxOTMxMTdaMDAxLjAsBgNVBAMTJWh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9zYWxlcy1wb3N0LWVuYy8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDE5iKDNNW5XxHAF0ITErZcHDYZI68z7u68n7o4dsiywkfOWf7jVnw7PJVnMeDEtLWtTO6f0tRTqJ4OV5HYdJ9+mhPJtn+2UuvrepyYa2IsC1eFPH98ZEtYapsE6ObvhKBQMcu5G/tQrxkCFY2ssDa99unwBH5STLyX78UvqKiYnkPCvIhkiPIHy8ab7DQowc+EE9XhlE3b63A65rp4G9R87rwgJX5VTM3h81WcDuWLPOg7YRYLZoorWz2p38/qL9gXY5NxIRK16EHGfw2W1dPrX3GyMOJbXVyrBNZ6m5IL9Wn7lBEJ/Dl7ZFMFB5W36QkJ+3aaNLT/Tu/Gz+7f24inAgMBAAGjITAfMB0GA1UdDgQWBBSk7RegFbEBruVbt/VFl2gZhZ2IpDANBgkqhkiG9w0BAQsFAAOCAQEAGyH1sXVU3HDMhCzP2k5fsJBGA+1iKLMsyyiGcaD/22anQ1uVU7iWPZH8mSJGWqkvo/4oFb7RjB2KzO/50wP0q/P/tymGsYoznt+MEJKKxYEqAYmIns7SKRIgv3xEfF8yQy2jOuULC9FTq/Pb3gd9Om40jmeJtYccDSICjEC+A2fcGe56ScuRRLt+3WFyIZUFH7Y9FYZQ3EYQ88UZg//5F1ddAzGtdMSeTanMxLKow7rUIm/+Sx6cd+Vkwo/SYdk4hsD8xZCYx8Ln4i3NKh+SzyvbYykyWVI2fwjplqvM5Md/M+SNvPtU9tkOCUxQqVfz/bwtTiqfjdSaUJlasgGByg==" } + }, + { + "id" : "e908e55c-7cfe-46de-8c86-9ed6c65c11d9", + "clientId" : "migration-consent-client", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : true, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+", + "backchannel.logout.session.required" : "true", + "backchannel.logout.revoke.offline.tokens" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "defaultClientScopes" : [ "acr", "web-origins", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] } ], "clientScopes" : [ { "id" : "adef1610-70ec-4282-88ef-bcb26b1f5edf",