mirror of
https://github.com/keycloak/keycloak.git
synced 2026-05-28 04:13:22 -04:00
feat(oid4vci): add issued VC tracking and admin REST endpoint
- Add database schema for tracking issued verifiable credentials - Implement admin REST API endpoints for managing issued credentials - Add support for credential revocation tracking - Include tests for new functionality Fixes #46204 Signed-off-by: Jimmy Chakkalakal <jimmy.chakkalakal@ibm.com>
This commit is contained in:
parent
a0a4156d04
commit
eba0ef3c3b
17 changed files with 815 additions and 1 deletions
|
|
@ -0,0 +1,84 @@
|
|||
package org.keycloak.representations.idm.oid4vc;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class IssuedVerifiableCredentialRepresentation {
|
||||
|
||||
private String id;
|
||||
private String userId;
|
||||
private String credentialType;
|
||||
private Long issuedAt;
|
||||
private Long expiresAt;
|
||||
// This represents UUID of the client, which acts as OID4VCI wallet
|
||||
private String clientId;
|
||||
private String revision;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(String userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getCredentialType() {
|
||||
return credentialType;
|
||||
}
|
||||
|
||||
public void setCredentialType(String credentialType) {
|
||||
this.credentialType = credentialType;
|
||||
}
|
||||
|
||||
public Long getIssuedAt() {
|
||||
return issuedAt;
|
||||
}
|
||||
|
||||
public void setIssuedAt(Long issuedAt) {
|
||||
this.issuedAt = issuedAt;
|
||||
}
|
||||
|
||||
public Long getExpiresAt() {
|
||||
return expiresAt;
|
||||
}
|
||||
|
||||
public void setExpiresAt(Long expiresAt) {
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
|
||||
public String getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
public void setClientId(String clientId) {
|
||||
this.clientId = clientId;
|
||||
}
|
||||
|
||||
public String getRevision() {
|
||||
return revision;
|
||||
}
|
||||
|
||||
public void setRevision(String revision) {
|
||||
this.revision = revision;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
IssuedVerifiableCredentialRepresentation that = (IssuedVerifiableCredentialRepresentation) o;
|
||||
return Objects.equals(id, that.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import jakarta.ws.rs.PathParam;
|
|||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
|
||||
import org.keycloak.representations.idm.oid4vc.IssuedVerifiableCredentialRepresentation;
|
||||
import org.keycloak.representations.idm.oid4vc.UserVerifiableCredentialRepresentation;
|
||||
|
||||
/**
|
||||
|
|
@ -40,5 +41,8 @@ public interface UserVerifiableCredentialResource {
|
|||
@Produces(MediaType.APPLICATION_JSON)
|
||||
UserVerifiableCredentialRepresentation updateCredential(@PathParam("credentialScopeName") String credentialScopeName);
|
||||
|
||||
// TODO: Issued credentials
|
||||
@GET
|
||||
@Path("issued-credentials")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
List<IssuedVerifiableCredentialRepresentation> getIssuedCredentials();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import org.keycloak.models.CredentialValidationOutput;
|
|||
import org.keycloak.models.FederatedIdentityModel;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.IssuedVerifiableCredentialModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakTransaction;
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
|
|
@ -875,6 +876,16 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC
|
|||
return getDelegate().updateVerifiableCredential(userId, credentialScopeName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addIssuedVerifiableCredential(IssuedVerifiableCredentialModel issuedVc) {
|
||||
getDelegate().addIssuedVerifiableCredential(issuedVc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<IssuedVerifiableCredentialModel> getIssuedVerifiableCredentialsStreamByUser(String userId) {
|
||||
return getDelegate().getIssuedVerifiableCredentialsStreamByUser(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNotBeforeForUser(RealmModel realm, UserModel user, int notBefore) {
|
||||
if (!isRegisteredForInvalidation(realm, user.getId())) {
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ import org.keycloak.models.ClientScopeModel;
|
|||
import org.keycloak.models.FederatedIdentityModel;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.IssuedVerifiableCredentialModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ModelDuplicateException;
|
||||
import org.keycloak.models.ModelException;
|
||||
|
|
@ -68,6 +69,7 @@ import org.keycloak.models.UserProvider;
|
|||
import org.keycloak.models.UserVerifiableCredentialModel;
|
||||
import org.keycloak.models.jpa.entities.CredentialEntity;
|
||||
import org.keycloak.models.jpa.entities.FederatedIdentityEntity;
|
||||
import org.keycloak.models.jpa.entities.IssuedVerifiableCredentialEntity;
|
||||
import org.keycloak.models.jpa.entities.UserAttributeEntity;
|
||||
import org.keycloak.models.jpa.entities.UserConsentClientScopeEntity;
|
||||
import org.keycloak.models.jpa.entities.UserConsentEntity;
|
||||
|
|
@ -170,6 +172,7 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore, JpaUs
|
|||
em.createNamedQuery("deleteUserConsentClientScopesByUser").setParameter("user", user).executeUpdate();
|
||||
em.createNamedQuery("deleteUserConsentsByUser").setParameter("user", user).executeUpdate();
|
||||
em.createNamedQuery("deleteVerifiableCredentialsByUser").setParameter("user", user).executeUpdate();
|
||||
em.createNamedQuery("deleteIssuedVcsByUser").setParameter("userId", user.getId()).executeUpdate();
|
||||
|
||||
em.remove(user);
|
||||
em.flush();
|
||||
|
|
@ -518,6 +521,8 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore, JpaUs
|
|||
.setParameter("realmId", realm.getId()).executeUpdate();
|
||||
em.createNamedQuery("deleteVerifiableCredentialsByRealm")
|
||||
.setParameter("realmId", realm.getId()).executeUpdate();
|
||||
em.createNamedQuery("deleteIssuedVcsByRealm")
|
||||
.setParameter("realmId", realm.getId()).executeUpdate();
|
||||
em.createNamedQuery("deleteUserRoleMappingsByRealm")
|
||||
.setParameter("realmId", realm.getId()).executeUpdate();
|
||||
em.createNamedQuery("deleteUserRequiredActionsByRealm")
|
||||
|
|
@ -623,6 +628,9 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore, JpaUs
|
|||
em.createNamedQuery("deleteVerifiableCredentialsByClientScope")
|
||||
.setParameter("scopeName", clientScope.getName())
|
||||
.executeUpdate();
|
||||
em.createNamedQuery("deleteIssuedVcsByClientScope")
|
||||
.setParameter("scopeName", clientScope.getName())
|
||||
.executeUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -1080,6 +1088,45 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore, JpaUs
|
|||
return new org.keycloak.credential.UserCredentialManager(session, session.getContext().getRealm(), user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addIssuedVerifiableCredential(IssuedVerifiableCredentialModel model) {
|
||||
|
||||
String revision;
|
||||
if (model.getRevision() != null) {
|
||||
revision = model.getRevision();
|
||||
} else {
|
||||
UserVerifiableCredentialEntity userVerifiableCredentialEntity = getVerifiableCredentialsEntitiesByUser(model.getUserId())
|
||||
.filter(vc -> model.getCredentialType().equals(vc.getCredentialScopeName()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new ModelException(
|
||||
"Verifiable credential not found: " + model.getCredentialType()));
|
||||
revision = userVerifiableCredentialEntity.getRevision();
|
||||
}
|
||||
|
||||
IssuedVerifiableCredentialEntity issuedVerifiableCredentialEntity = new IssuedVerifiableCredentialEntity();
|
||||
|
||||
issuedVerifiableCredentialEntity.setId(KeycloakModelUtils.generateId());
|
||||
issuedVerifiableCredentialEntity.setUser(em.getReference(UserEntity.class, model.getUserId()));
|
||||
issuedVerifiableCredentialEntity.setCredentialType(model.getCredentialType());
|
||||
issuedVerifiableCredentialEntity.setClientId(model.getClientId());
|
||||
issuedVerifiableCredentialEntity.setRevision(revision);
|
||||
|
||||
long issuedAt = model.getIssuedAt() != null ? model.getIssuedAt() : Time.currentTimeMillis();
|
||||
issuedVerifiableCredentialEntity.setIssuedAt(issuedAt);
|
||||
|
||||
issuedVerifiableCredentialEntity.setExpiresAt(model.getExpiresAt());
|
||||
|
||||
em.persist(issuedVerifiableCredentialEntity);
|
||||
em.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<IssuedVerifiableCredentialModel> getIssuedVerifiableCredentialsStreamByUser(String userId) {
|
||||
TypedQuery<IssuedVerifiableCredentialEntity> query = em.createNamedQuery("issuedVcsByUser", IssuedVerifiableCredentialEntity.class);
|
||||
query.setParameter("userId", userId);
|
||||
return closing(query.getResultStream()).map(this::toIssuedVcModel);
|
||||
}
|
||||
|
||||
// Could override this to provide a custom behavior.
|
||||
protected void ensureEmailConstraint(List<UserEntity> users, RealmModel realm) {
|
||||
UserEntity user = users.get(0);
|
||||
|
|
@ -1260,4 +1307,17 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore, JpaUs
|
|||
return query.select(from.getJoins().isEmpty() ? builder.count(from) : builder.countDistinct(from))
|
||||
.where(predicates);
|
||||
}
|
||||
|
||||
private IssuedVerifiableCredentialModel toIssuedVcModel(IssuedVerifiableCredentialEntity entity) {
|
||||
IssuedVerifiableCredentialModel model = new IssuedVerifiableCredentialModel();
|
||||
model.setId(entity.getId());
|
||||
model.setUserId(entity.getUser().getId());
|
||||
model.setCredentialType(entity.getCredentialType());
|
||||
model.setRevision(entity.getRevision());
|
||||
model.setIssuedAt(entity.getIssuedAt());
|
||||
model.setExpiresAt(entity.getExpiresAt());
|
||||
model.setClientId(entity.getClientId());
|
||||
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
package org.keycloak.models.jpa.entities;
|
||||
|
||||
import jakarta.persistence.Access;
|
||||
import jakarta.persistence.AccessType;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.NamedQueries;
|
||||
import jakarta.persistence.NamedQuery;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name="ISSUED_VER_CREDENTIAL")
|
||||
@NamedQueries({
|
||||
@NamedQuery(name="issuedVcsByUser", query="select vc from IssuedVerifiableCredentialEntity vc where vc.user.id = :userId order by vc.issuedAt desc"),
|
||||
@NamedQuery(name="deleteIssuedVcsByRealm", query="delete from IssuedVerifiableCredentialEntity vc where vc.user IN (select u from UserEntity u where u.realmId = :realmId)"),
|
||||
@NamedQuery(name="deleteIssuedVcsByUser", query="delete from IssuedVerifiableCredentialEntity vc where vc.user.id = :userId"),
|
||||
@NamedQuery(name="deleteIssuedVcsByClientScope", query="delete from IssuedVerifiableCredentialEntity vc where vc.credentialType = :scopeName"),
|
||||
})
|
||||
public class IssuedVerifiableCredentialEntity {
|
||||
|
||||
@Id
|
||||
@Column(name="ID", length = 36)
|
||||
@Access(AccessType.PROPERTY)
|
||||
protected String id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name="USER_ID")
|
||||
protected UserEntity user;
|
||||
|
||||
@Column(name="CREDENTIAL_TYPE")
|
||||
protected String credentialType;
|
||||
|
||||
@Column(name="ISSUED_AT")
|
||||
protected Long issuedAt;
|
||||
|
||||
@Column(name="EXPIRES_AT")
|
||||
protected Long expiresAt;
|
||||
|
||||
// This represents UUID of the client, which acts as OID4VCI wallet
|
||||
@Column(name="CLIENT_ID")
|
||||
protected String clientId;
|
||||
|
||||
@Column(name="REVISION")
|
||||
protected String revision;
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public UserEntity getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(UserEntity user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
public String getCredentialType() {
|
||||
return credentialType;
|
||||
}
|
||||
|
||||
public void setCredentialType(String credentialType) {
|
||||
this.credentialType = credentialType;
|
||||
}
|
||||
|
||||
public Long getIssuedAt() {
|
||||
return issuedAt;
|
||||
}
|
||||
|
||||
public void setIssuedAt(Long issuedAt) {
|
||||
this.issuedAt = issuedAt;
|
||||
}
|
||||
|
||||
public Long getExpiresAt() {
|
||||
return expiresAt;
|
||||
}
|
||||
|
||||
public void setExpiresAt(Long expiresAt) {
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
|
||||
public String getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
public void setClientId(String clientId) {
|
||||
this.clientId = clientId;
|
||||
}
|
||||
|
||||
public String getRevision() {
|
||||
return revision;
|
||||
}
|
||||
|
||||
public void setRevision(String revision) {
|
||||
this.revision = revision;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
IssuedVerifiableCredentialEntity that = (IssuedVerifiableCredentialEntity) o;
|
||||
return id.equals(that.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return id.hashCode();
|
||||
}
|
||||
}
|
||||
|
|
@ -203,4 +203,39 @@
|
|||
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="keycloak" id="26.7.0-46204-issued-ver-credential-table">
|
||||
<createTable tableName="ISSUED_VER_CREDENTIAL">
|
||||
<column name="ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="USER_ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="CREDENTIAL_TYPE" type="VARCHAR(255)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="ISSUED_AT" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="EXPIRES_AT" type="BIGINT"/>
|
||||
<column name="CLIENT_ID" type="VARCHAR(36)"/>
|
||||
<column name="REVISION" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
<addPrimaryKey columnNames="ID" constraintName="PK_ISSUED_VER_CREDENTIAL" tableName="ISSUED_VER_CREDENTIAL"/>
|
||||
|
||||
<addForeignKeyConstraint baseColumnNames="USER_ID"
|
||||
baseTableName="ISSUED_VER_CREDENTIAL"
|
||||
constraintName="FK_ISSUED_VER_CREDENTIAL_USER"
|
||||
referencedColumnNames="ID"
|
||||
referencedTableName="USER_ENTITY"/>
|
||||
|
||||
<!-- Index for listing user's credentials -->
|
||||
<createIndex tableName="ISSUED_VER_CREDENTIAL" indexName="IDX_ISSUED_VER_CREDENTIAL_USER">
|
||||
<column name="USER_ID"/>
|
||||
<column name="ISSUED_AT"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
<class>org.keycloak.models.jpa.entities.UserConsentEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UserConsentClientScopeEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UserVerifiableCredentialEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.IssuedVerifiableCredentialEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.AuthenticationFlowEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.AuthenticationExecutionEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.AuthenticatorConfigEntity</class>
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import org.keycloak.models.CredentialValidationOutput;
|
|||
import org.keycloak.models.FederatedIdentityModel;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.IssuedVerifiableCredentialModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
|
|
@ -967,6 +968,24 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addIssuedVerifiableCredential(IssuedVerifiableCredentialModel issuedVc) {
|
||||
if (StorageId.isLocalStorage(issuedVc.getUserId())) {
|
||||
localStorage().addIssuedVerifiableCredential(issuedVc);
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Issued verifiable credential operations not yet supported on federated users");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<IssuedVerifiableCredentialModel> getIssuedVerifiableCredentialsStreamByUser(String userId) {
|
||||
if (StorageId.isLocalStorage(userId)) {
|
||||
return localStorage().getIssuedVerifiableCredentialsStreamByUser(userId);
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Issued verifiable credential operations not yet supported on federated users");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNotBeforeForUser(RealmModel realm, UserModel user, int notBefore) {
|
||||
if (StorageId.isLocalStorage(user.getId())) {
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ import org.keycloak.models.IdentityProviderMapperModel;
|
|||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.IdentityProviderQuery;
|
||||
import org.keycloak.models.IdentityProviderType;
|
||||
import org.keycloak.models.IssuedVerifiableCredentialModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.ModelIllegalStateException;
|
||||
|
|
@ -127,6 +128,7 @@ import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentatio
|
|||
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
|
||||
import org.keycloak.representations.idm.oid4vc.IssuedVerifiableCredentialRepresentation;
|
||||
import org.keycloak.representations.idm.oid4vc.UserVerifiableCredentialRepresentation;
|
||||
import org.keycloak.storage.StorageId;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
|
@ -1473,4 +1475,17 @@ public class ModelToRepresentation {
|
|||
representation.setVerified(model.isVerified());
|
||||
return representation;
|
||||
}
|
||||
|
||||
public static IssuedVerifiableCredentialRepresentation toRepresentation(IssuedVerifiableCredentialModel model) {
|
||||
IssuedVerifiableCredentialRepresentation rep = new IssuedVerifiableCredentialRepresentation();
|
||||
rep.setId(model.getId());
|
||||
rep.setUserId(model.getUserId());
|
||||
rep.setCredentialType(model.getCredentialType());
|
||||
rep.setRevision(model.getRevision());
|
||||
rep.setIssuedAt(model.getIssuedAt());
|
||||
rep.setExpiresAt(model.getExpiresAt());
|
||||
rep.setClientId(model.getClientId());
|
||||
return rep;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ import org.keycloak.models.FederatedIdentityModel;
|
|||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.IssuedVerifiableCredentialModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ModelDuplicateException;
|
||||
import org.keycloak.models.ModelException;
|
||||
|
|
@ -130,6 +131,7 @@ import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentatio
|
|||
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
|
||||
import org.keycloak.representations.idm.oid4vc.IssuedVerifiableCredentialRepresentation;
|
||||
import org.keycloak.representations.idm.oid4vc.UserVerifiableCredentialRepresentation;
|
||||
import org.keycloak.storage.DatastoreProvider;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
|
@ -1772,4 +1774,16 @@ public class RepresentationToModel {
|
|||
public static OrganizationDomainModel toModel(OrganizationDomainRepresentation domainRepresentation) {
|
||||
return new OrganizationDomainModel(domainRepresentation.getName(), domainRepresentation.isVerified());
|
||||
}
|
||||
|
||||
public static IssuedVerifiableCredentialModel toModel(IssuedVerifiableCredentialRepresentation representation) {
|
||||
IssuedVerifiableCredentialModel model = new IssuedVerifiableCredentialModel();
|
||||
model.setId(representation.getId());
|
||||
model.setUserId(representation.getUserId());
|
||||
model.setCredentialType(representation.getCredentialType());
|
||||
model.setIssuedAt(representation.getIssuedAt());
|
||||
model.setExpiresAt(representation.getExpiresAt());
|
||||
model.setClientId(representation.getClientId());
|
||||
model.setRevision(representation.getRevision());
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
package org.keycloak.models;
|
||||
|
||||
public class IssuedVerifiableCredentialModel {
|
||||
|
||||
private String id;
|
||||
private String userId;
|
||||
private String credentialType;
|
||||
private Long issuedAt;
|
||||
private Long expiresAt;
|
||||
// This represents UUID of the client, which acts as OID4VCI wallet
|
||||
private String clientId;
|
||||
private String revision;
|
||||
|
||||
public IssuedVerifiableCredentialModel() {
|
||||
}
|
||||
|
||||
public IssuedVerifiableCredentialModel(String userId, String credentialType, String clientId) {
|
||||
this.userId = userId;
|
||||
this.credentialType = credentialType;
|
||||
this.clientId = clientId;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(String userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getCredentialType() {
|
||||
return credentialType;
|
||||
}
|
||||
|
||||
public void setCredentialType(String credentialType) {
|
||||
this.credentialType = credentialType;
|
||||
}
|
||||
|
||||
public Long getIssuedAt() {
|
||||
return issuedAt;
|
||||
}
|
||||
|
||||
public void setIssuedAt(Long issuedAt) {
|
||||
this.issuedAt = issuedAt;
|
||||
}
|
||||
|
||||
public Long getExpiresAt() {
|
||||
return expiresAt;
|
||||
}
|
||||
|
||||
public void setExpiresAt(Long expiresAt) {
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
|
||||
public String getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
public void setClientId(String clientId) {
|
||||
this.clientId = clientId;
|
||||
}
|
||||
|
||||
public String getRevision() {
|
||||
return revision;
|
||||
}
|
||||
|
||||
public void setRevision(String revision) {
|
||||
this.revision = revision;
|
||||
}
|
||||
}
|
||||
|
|
@ -340,4 +340,20 @@ public interface UserProvider extends Provider,
|
|||
*/
|
||||
UserCredentialManager getUserCredentialManager(UserModel user);
|
||||
|
||||
/**
|
||||
* Record that a verifiable credential was issued to a user.
|
||||
*
|
||||
* @param issuedVc model with userId, clientId, credentialType set
|
||||
*
|
||||
*/
|
||||
void addIssuedVerifiableCredential(IssuedVerifiableCredentialModel issuedVc);
|
||||
|
||||
/**
|
||||
* Get all issued verifiable credentials for a specific user.
|
||||
*
|
||||
* @param userId user ID
|
||||
* @return stream of issued verifiable credentials, sorted by issuedAt descending
|
||||
*/
|
||||
Stream<IssuedVerifiableCredentialModel> getIssuedVerifiableCredentialsStreamByUser(String userId);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ import org.keycloak.jose.jwk.JWKParser;
|
|||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
import org.keycloak.models.IssuedVerifiableCredentialModel;
|
||||
import org.keycloak.models.KeyManager;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
|
|
@ -1022,6 +1023,9 @@ public class OID4VCIssuerEndpoint {
|
|||
eventBuilder.detail(Details.SCOPE, supportedCredential.getScope())
|
||||
.detail(Details.VERIFIABLE_CREDENTIAL_FORMAT, supportedCredential.getFormat())
|
||||
.detail(Details.VERIFIABLE_CREDENTIALS_ISSUED, String.valueOf(responseVO.getCredentials().size()));
|
||||
|
||||
recordIssuedVerifiableCredentials(userModel, clientModel, supportedCredential.getScope(), responseVO.getCredentials().size());
|
||||
|
||||
eventBuilder.success();
|
||||
|
||||
// Clean up offer state after successful credential issuance
|
||||
|
|
@ -1034,6 +1038,18 @@ public class OID4VCIssuerEndpoint {
|
|||
return response;
|
||||
}
|
||||
|
||||
private void recordIssuedVerifiableCredentials(UserModel userModel, ClientModel clientModel, String credentialScope, int count) {
|
||||
try {
|
||||
if (count > 0) {
|
||||
IssuedVerifiableCredentialModel model = new IssuedVerifiableCredentialModel(userModel.getId(), credentialScope, clientModel.getId());
|
||||
session.users().addIssuedVerifiableCredential(model);
|
||||
LOGGER.debugf("Recorded VC issuance: user=%s, client=%s, type=%s, credentials=%d", userModel.getUsername(), clientModel.getClientId(), credentialScope, count);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.warnf(e, "Failed to record VC issuance for user=%s, client=%s, type=%s", userModel.getUsername(), clientModel.getClientId(), credentialScope);
|
||||
}
|
||||
}
|
||||
|
||||
private List<OID4VCAuthorizationDetail> getAuthorizationDetailsResponse(AccessToken accessToken) {
|
||||
List<AuthorizationDetailsJSONRepresentation> tokenAuthDetails = accessToken.getAuthorizationDetails();
|
||||
AuthorizationDetailsProcessor<OID4VCAuthorizationDetail> oid4vcProcessor = session.getProvider(AuthorizationDetailsProcessor.class, OPENID_CREDENTIAL);
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import org.keycloak.models.utils.KeycloakModelUtils;
|
|||
import org.keycloak.models.utils.ModelToRepresentation;
|
||||
import org.keycloak.models.utils.RepresentationToModel;
|
||||
import org.keycloak.representations.idm.ErrorRepresentation;
|
||||
import org.keycloak.representations.idm.oid4vc.IssuedVerifiableCredentialRepresentation;
|
||||
import org.keycloak.representations.idm.oid4vc.UserVerifiableCredentialRepresentation;
|
||||
import org.keycloak.services.ErrorResponse;
|
||||
import org.keycloak.services.resources.KeycloakOpenAPI;
|
||||
|
|
@ -188,6 +189,24 @@ public class UserVerifiableCredentialResource {
|
|||
adminEvent.operation(OperationType.DELETE).resourcePath(session.getContext().getUri()).success();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("issued-credentials")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Tag(name = KeycloakOpenAPI.Admin.Tags.USERS)
|
||||
@Operation(summary = "Get issued verifiable credentials for the user")
|
||||
@APIResponses(value = {
|
||||
@APIResponse(responseCode = "200", description = "OK"),
|
||||
@APIResponse(responseCode = "403", description = "Forbidden")
|
||||
})
|
||||
public List<IssuedVerifiableCredentialRepresentation> getIssuedCredentials() {
|
||||
auth.users().requireView(user);
|
||||
checkOid4VCIEnabled();
|
||||
|
||||
return session.users().getIssuedVerifiableCredentialsStreamByUser(user.getId())
|
||||
.map(ModelToRepresentation::toRepresentation)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private void checkOid4VCIEnabled() {
|
||||
if (!Profile.isFeatureEnabled(Profile.Feature.OID4VC_VCI)) {
|
||||
throw ErrorResponse.error("Feature " + Profile.Feature.OID4VC_VCI.getKey() + " not enabled", Response.Status.BAD_REQUEST);
|
||||
|
|
|
|||
|
|
@ -428,6 +428,7 @@ public class PermissionsTest extends AbstractPermissionsTest {
|
|||
invoke(realm -> realm.users().get(user.getId()).revokeConsent("testclient"), Resource.USER, true);
|
||||
|
||||
invoke(realm -> realm.users().get(user.getId()).verifiableCredentials().getCredentials(), Resource.USER, false);
|
||||
invoke(realm -> realm.users().get(user.getId()).verifiableCredentials().getIssuedCredentials(), Resource.USER, false);
|
||||
UserVerifiableCredentialRepresentation verifCred = new UserVerifiableCredentialRepresentation();
|
||||
verifCred.setCredentialScopeName("nosuch");
|
||||
invoke(realm -> realm.users().get(user.getId()).verifiableCredentials().createCredential(verifCred), Resource.USER, true);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,218 @@
|
|||
package org.keycloak.tests.admin.user;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.IssuedVerifiableCredentialModel;
|
||||
import org.keycloak.models.UserVerifiableCredentialModel;
|
||||
import org.keycloak.representations.idm.oid4vc.IssuedVerifiableCredentialRepresentation;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.realm.RealmBuilder;
|
||||
import org.keycloak.testframework.realm.RealmConfig;
|
||||
import org.keycloak.tests.oid4vc.OID4VCIssuerTestBase;
|
||||
import org.keycloak.tests.suites.DatabaseTest;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.keycloak.tests.oid4vc.OID4VCIssuerTestBase.jwtTypeNaturalPersonScopeName;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.empty;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.oneOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
|
||||
|
||||
@KeycloakIntegrationTest(config = OID4VCIssuerTestBase.VCTestServerConfig.class)
|
||||
public class IssuedVerifiableCredentialTest extends AbstractUserTest {
|
||||
|
||||
private static final String CREDENTIAL_TYPE_1 = jwtTypeNaturalPersonScopeName;
|
||||
private static final String CREDENTIAL_TYPE_2 = "education-cert";
|
||||
|
||||
@InjectRealm(config = IssuedVcTestRealmConfig.class)
|
||||
protected ManagedRealm testRealm;
|
||||
|
||||
@Test
|
||||
@DatabaseTest
|
||||
public void testGetIssuedCredentials_EmptyList() {
|
||||
String userId = createUser();
|
||||
|
||||
UserResource userResource = managedRealm.admin().users().get(userId);
|
||||
List<IssuedVerifiableCredentialRepresentation> issuedCreds = userResource.verifiableCredentials().getIssuedCredentials();
|
||||
|
||||
assertNotNull(issuedCreds, "Should return empty list, not null");
|
||||
assertThat(issuedCreds, empty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DatabaseTest
|
||||
public void testGetIssuedCredentials_WithData() {
|
||||
String userId = createUser();
|
||||
|
||||
createIssuedVcViaModelLayer(userId, CREDENTIAL_TYPE_1, "wallet-123", "rev-001");
|
||||
createIssuedVcViaModelLayer(userId, CREDENTIAL_TYPE_2, "wallet-456", "rev-002");
|
||||
|
||||
// Retrieve via REST API
|
||||
UserResource userResource = managedRealm.admin().users().get(userId);
|
||||
List<IssuedVerifiableCredentialRepresentation> issuedCreds = userResource.verifiableCredentials().getIssuedCredentials();
|
||||
|
||||
assertThat(issuedCreds, hasSize(2));
|
||||
|
||||
// Verify field population
|
||||
IssuedVerifiableCredentialRepresentation firstCred = issuedCreds.get(0);
|
||||
assertNotNull(firstCred.getId());
|
||||
assertEquals(userId, firstCred.getUserId());
|
||||
assertThat(firstCred.getCredentialType(), is(oneOf(CREDENTIAL_TYPE_1, CREDENTIAL_TYPE_2)));
|
||||
assertNotNull(firstCred.getClientId());
|
||||
assertNotNull(firstCred.getRevision());
|
||||
assertNotNull(firstCred.getIssuedAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DatabaseTest
|
||||
public void testGetIssuedCredentials_SortedByIssuedAtDesc() {
|
||||
String userId = createUser();
|
||||
long baseTime = Time.currentTimeMillis();
|
||||
|
||||
createIssuedVcViaModelLayer(userId, "cert-1", "wallet-1", "rev-1", baseTime);
|
||||
createIssuedVcViaModelLayer(userId, "cert-2", "wallet-2", "rev-2", baseTime + 1000);
|
||||
createIssuedVcViaModelLayer(userId, "cert-3", "wallet-3", "rev-3", baseTime + 2000);
|
||||
|
||||
UserResource userResource = managedRealm.admin().users().get(userId);
|
||||
List<IssuedVerifiableCredentialRepresentation> issuedCreds = userResource.verifiableCredentials().getIssuedCredentials();
|
||||
|
||||
assertThat(issuedCreds, hasSize(3));
|
||||
|
||||
assertEquals("cert-3", issuedCreds.get(0).getCredentialType());
|
||||
assertEquals("cert-2", issuedCreds.get(1).getCredentialType());
|
||||
assertEquals("cert-1", issuedCreds.get(2).getCredentialType());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DatabaseTest
|
||||
public void testGetIssuedCredentials_UserIsolation() {
|
||||
String user1Id = createUser("user1", "user1@test.com");
|
||||
String user2Id = createUser("user2", "user2@test.com");
|
||||
|
||||
createIssuedVcViaModelLayer(user1Id, "user1-cert", "wallet1", "rev1");
|
||||
createIssuedVcViaModelLayer(user2Id, "user2-cert", "wallet2", "rev2");
|
||||
|
||||
// User 1 should only see their VC
|
||||
List<IssuedVerifiableCredentialRepresentation> user1Creds =
|
||||
managedRealm.admin().users().get(user1Id).verifiableCredentials().getIssuedCredentials();
|
||||
|
||||
assertThat(user1Creds, hasSize(1));
|
||||
assertEquals("user1-cert", user1Creds.get(0).getCredentialType());
|
||||
|
||||
// User 2 should only see their VC
|
||||
List<IssuedVerifiableCredentialRepresentation> user2Creds =
|
||||
managedRealm.admin().users().get(user2Id).verifiableCredentials().getIssuedCredentials();
|
||||
|
||||
assertThat(user2Creds, hasSize(1));
|
||||
assertEquals("user2-cert", user2Creds.get(0).getCredentialType());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DatabaseTest
|
||||
public void testGetIssuedCredentials_WithExpiresAt() {
|
||||
String userId = createUser();
|
||||
long issuedAt = Time.currentTimeMillis();
|
||||
long expiresAt = issuedAt + 86400000L; // 24 hours later
|
||||
|
||||
runOnServer.run(session -> {
|
||||
UserVerifiableCredentialModel vcModel = new UserVerifiableCredentialModel(CREDENTIAL_TYPE_1);
|
||||
vcModel.setRevision("rev-001");
|
||||
session.users().addVerifiableCredential(userId, vcModel);
|
||||
});
|
||||
|
||||
runOnServer.run(session -> {
|
||||
IssuedVerifiableCredentialModel model = new IssuedVerifiableCredentialModel(userId, CREDENTIAL_TYPE_1, "wallet-123");
|
||||
model.setRevision("rev-001");
|
||||
model.setIssuedAt(issuedAt);
|
||||
model.setExpiresAt(expiresAt);
|
||||
session.users().addIssuedVerifiableCredential(model);
|
||||
});
|
||||
|
||||
UserResource userResource = managedRealm.admin().users().get(userId);
|
||||
List<IssuedVerifiableCredentialRepresentation> issuedCreds = userResource.verifiableCredentials().getIssuedCredentials();
|
||||
|
||||
assertThat(issuedCreds, hasSize(1));
|
||||
IssuedVerifiableCredentialRepresentation cred = issuedCreds.get(0);
|
||||
assertEquals(expiresAt, cred.getExpiresAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetIssuedCredentials_FeatureDisabled() {
|
||||
managedRealm.updateWithCleanup((realm) -> realm.verifiableCredentialsEnabled(false));
|
||||
adminEvents.clear();
|
||||
|
||||
String userId = createUser();
|
||||
UserResource userResource = managedRealm.admin().users().get(userId);
|
||||
|
||||
try {
|
||||
userResource.verifiableCredentials().getIssuedCredentials();
|
||||
Assertions.fail("Expected BadRequestException when feature is disabled");
|
||||
} catch (BadRequestException e) {
|
||||
assertThat(e.getResponse().getStatus(), is(400));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DatabaseTest
|
||||
public void testGetIssuedCredentials_NonExistentUser() {
|
||||
try {
|
||||
managedRealm.admin().users().get("non-existent-user-id").verifiableCredentials().getIssuedCredentials();
|
||||
Assertions.fail("Expected NotFoundException");
|
||||
} catch (NotFoundException e) {
|
||||
assertThat(e.getResponse().getStatus(), is(404));
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
protected void createIssuedVcViaModelLayer(String userId, String credentialType,
|
||||
String clientId, String revision) {
|
||||
createIssuedVcViaModelLayer(userId, credentialType, clientId, revision, null);
|
||||
}
|
||||
|
||||
protected void createIssuedVcViaModelLayer(String userId, String credentialType,
|
||||
String clientId, String revision, Long issuedAt) {
|
||||
runOnServer.run(session -> {
|
||||
UserVerifiableCredentialModel vcModel = new UserVerifiableCredentialModel(credentialType);
|
||||
vcModel.setRevision(revision);
|
||||
session.users().addVerifiableCredential(userId, vcModel);
|
||||
});
|
||||
|
||||
runOnServer.run(session -> {
|
||||
IssuedVerifiableCredentialModel model = new IssuedVerifiableCredentialModel(userId, credentialType, clientId);
|
||||
model.setRevision(revision);
|
||||
if (issuedAt != null) {
|
||||
model.setIssuedAt(issuedAt);
|
||||
}
|
||||
session.users().addIssuedVerifiableCredential(model);
|
||||
});
|
||||
}
|
||||
|
||||
public static class IssuedVcTestRealmConfig implements RealmConfig {
|
||||
public static final String TEST_REALM_NAME = "test";
|
||||
|
||||
@Override
|
||||
public RealmBuilder configure(RealmBuilder realm) {
|
||||
return realm
|
||||
.name(TEST_REALM_NAME)
|
||||
.eventsEnabled(true)
|
||||
.eventsListeners("jboss-logging")
|
||||
.verifiableCredentialsEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
package org.keycloak.tests.oid4vc;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.admin.client.resource.UserVerifiableCredentialResource;
|
||||
import org.keycloak.models.oid4vci.CredentialScopeModel;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialIssuer;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialResponse;
|
||||
import org.keycloak.protocol.oid4vc.model.OID4VCAuthorizationDetail;
|
||||
import org.keycloak.protocol.oid4vc.model.Proofs;
|
||||
import org.keycloak.representations.idm.oid4vc.IssuedVerifiableCredentialRepresentation;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.keycloak.OID4VCConstants.OPENID_CREDENTIAL;
|
||||
import static org.keycloak.tests.oid4vc.OID4VCProofTestUtils.jwtProofs;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertAll;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@KeycloakIntegrationTest(config = OID4VCIssuerTestBase.VCTestServerWithRestCredentialOfferEnabled.class)
|
||||
public class OID4VCIssuedCredentialsAdminAPITest extends OID4VCIssuerEndpointTest {
|
||||
|
||||
@Test
|
||||
public void testIssuedCredentialsArePersistedAndRetrievableViaAdminAPI() {
|
||||
String scopeName = jwtTypeCredentialScope.getName();
|
||||
String credConfigId = jwtTypeCredentialScope.getAttributes().get(CredentialScopeModel.VC_CONFIGURATION_ID);
|
||||
|
||||
String userId = testRealm.admin().users().search("john").get(0).getId();
|
||||
|
||||
UserVerifiableCredentialResource userVerifiableCredentialResource = testRealm.admin().users().get(userId).verifiableCredentials();
|
||||
|
||||
List<IssuedVerifiableCredentialRepresentation> initialIssuedCreds = userVerifiableCredentialResource.getIssuedCredentials();
|
||||
assertTrue(initialIssuedCreds.isEmpty(), "No issued credentials should exist initially");
|
||||
|
||||
CredentialIssuer credentialIssuer = getCredentialIssuerMetadata();
|
||||
OID4VCAuthorizationDetail authDetail = new OID4VCAuthorizationDetail();
|
||||
authDetail.setType(OPENID_CREDENTIAL);
|
||||
authDetail.setCredentialConfigurationId(credConfigId);
|
||||
authDetail.setLocations(List.of(credentialIssuer.getCredentialIssuer()));
|
||||
|
||||
String authCode = getAuthorizationCode(oauth, client, "john", scopeName);
|
||||
AccessTokenResponse tokenResponse = getBearerToken(oauth, authCode, authDetail);
|
||||
|
||||
String token = tokenResponse.getAccessToken();
|
||||
List<OID4VCAuthorizationDetail> authDetailsResponse = tokenResponse.getOID4VCAuthorizationDetails();
|
||||
String credentialIdentifier = authDetailsResponse.get(0).getCredentialIdentifiers().get(0);
|
||||
|
||||
String cNonce = oauth.oid4vc().nonceRequest().send().getNonce();
|
||||
Proofs proofs = jwtProofs(credentialIssuer.getCredentialIssuer(), cNonce);
|
||||
|
||||
CredentialResponse credentialResponseVO = oauth.oid4vc()
|
||||
.credentialRequest()
|
||||
.bearerToken(token)
|
||||
.credentialIdentifier(credentialIdentifier)
|
||||
.proofs(proofs)
|
||||
.send()
|
||||
.getCredentialResponse();
|
||||
|
||||
assertNotNull(credentialResponseVO, "Credential should have been issued");
|
||||
assertNotNull(credentialResponseVO.getCredentials(), "Credentials array should not be null");
|
||||
assertFalse(credentialResponseVO.getCredentials().isEmpty(), "At least one credential should be issued");
|
||||
|
||||
List<IssuedVerifiableCredentialRepresentation> issuedCreds = testRealm.admin().users().get(userId).verifiableCredentials().getIssuedCredentials();
|
||||
|
||||
assertEquals(1, issuedCreds.size(), "Exactly one issued credential should be stored");
|
||||
|
||||
IssuedVerifiableCredentialRepresentation issuedCred = issuedCreds.get(0);
|
||||
|
||||
assertAll(
|
||||
() -> assertNotNull(issuedCred.getId(), "Issued credential should have an ID"),
|
||||
() -> assertEquals(userId, issuedCred.getUserId(), "User ID should match"),
|
||||
() -> assertEquals(scopeName, issuedCred.getCredentialType(), "Credential type should match the scope name"),
|
||||
() -> assertNotNull(issuedCred.getRevision(), "Revision should be set"),
|
||||
() -> assertNotNull(issuedCred.getIssuedAt(), "IssuedAt timestamp should be set"),
|
||||
() -> assertNotNull(issuedCred.getClientId(), "ClientId should be set")
|
||||
);
|
||||
|
||||
String cNonce2 = oauth.oid4vc().doNonceRequest().getNonce();
|
||||
Proofs proofs2 = jwtProofs(credentialIssuer.getCredentialIssuer(), cNonce2);
|
||||
|
||||
CredentialResponse credentialResponseVO2 = oauth.oid4vc()
|
||||
.credentialRequest()
|
||||
.bearerToken(token)
|
||||
.credentialIdentifier(credentialIdentifier)
|
||||
.proofs(proofs2)
|
||||
.send()
|
||||
.getCredentialResponse();
|
||||
|
||||
assertNotNull(credentialResponseVO2, "Second credential should have been issued");
|
||||
|
||||
List<IssuedVerifiableCredentialRepresentation> multipleIssuedCreds = testRealm.admin().users().get(userId).verifiableCredentials().getIssuedCredentials();
|
||||
|
||||
assertEquals(2, multipleIssuedCreds.size(), "Two issued credentials should be stored");
|
||||
|
||||
// Verify sorting (newest first - DESC by issuedAt)
|
||||
assertTrue(multipleIssuedCreds.get(0).getIssuedAt() >= multipleIssuedCreds.get(1).getIssuedAt(),
|
||||
"Issued credentials should be sorted by issuedAt DESC (newest first)");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue