add revoke endpoint to issued credentials APIs

Closes #46207

Signed-off-by: Giuseppe Graziano <g.graziano94@gmail.com>
This commit is contained in:
Giuseppe Graziano
2026-05-25 15:14:57 +02:00
committed by Marek Posolda
parent 629e86afd2
commit 77b1d13578
7 changed files with 80 additions and 0 deletions
@@ -45,4 +45,8 @@ public interface UserVerifiableCredentialResource {
@Path("issued-credentials")
@Produces(MediaType.APPLICATION_JSON)
List<IssuedVerifiableCredentialRepresentation> getIssuedCredentials();
@DELETE
@Path("issued-credentials/{id}")
void revokeIssuedCredential(@PathParam("id") String credentialId);
}
@@ -886,6 +886,11 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC
return getDelegate().getIssuedVerifiableCredentialsStreamByUser(userId);
}
@Override
public boolean removeIssuedVerifiableCredential(String credentialId) {
return getDelegate().removeIssuedVerifiableCredential(credentialId);
}
@Override
public void setNotBeforeForUser(RealmModel realm, UserModel user, int notBefore) {
if (!isRegisteredForInvalidation(realm, user.getId())) {
@@ -1127,6 +1127,17 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore, JpaUs
return closing(query.getResultStream()).map(this::toIssuedVcModel);
}
@Override
public boolean removeIssuedVerifiableCredential(String credentialId) {
IssuedVerifiableCredentialEntity entity = em.find(IssuedVerifiableCredentialEntity.class, credentialId, LockModeType.PESSIMISTIC_WRITE);
if (entity == null) {
return false;
}
em.remove(entity);
em.flush();
return true;
}
// Could override this to provide a custom behavior.
protected void ensureEmailConstraint(List<UserEntity> users, RealmModel realm) {
UserEntity user = users.get(0);
@@ -986,6 +986,15 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
}
}
@Override
public boolean removeIssuedVerifiableCredential(String credentialId) {
if (StorageId.isLocalStorage(credentialId)) {
return localStorage().removeIssuedVerifiableCredential(credentialId);
} 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())) {
@@ -356,4 +356,12 @@ public interface UserProvider extends Provider,
*/
Stream<IssuedVerifiableCredentialModel> getIssuedVerifiableCredentialsStreamByUser(String userId);
/**
* Remove an issued verifiable credential by its ID.
*
* @param credentialId the ID of the issued credential to remove
* @return {@code true} if the credential was removed, {@code false} if it was not found
*/
boolean removeIssuedVerifiableCredential(String credentialId);
}
@@ -207,6 +207,29 @@ public class UserVerifiableCredentialResource {
.toList();
}
@DELETE
@Path("issued-credentials/{id}")
@Tag(name = KeycloakOpenAPI.Admin.Tags.USERS)
@Operation(summary = "Revoke an issued verifiable credential")
@APIResponses(value = {
@APIResponse(responseCode = "204", description = "No Content"),
@APIResponse(responseCode = "403", description = "Forbidden"),
@APIResponse(responseCode = "404", description = "Not Found")
})
public void revokeIssuedCredential(@PathParam("id") String credentialId) {
auth.users().requireManage(user);
checkOid4VCIEnabled();
boolean removed = session.users().removeIssuedVerifiableCredential(credentialId);
if (!removed) {
logger.warn(String.format("Issued verifiable credential with ID '%s' not found for user '%s' in realm '%s'.",
credentialId, user.getUsername(), realm.getName()));
throw new NotFoundException("Issued verifiable credential not found");
}
adminEvent.operation(OperationType.DELETE).resourcePath(session.getContext().getUri()).success();
}
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);
@@ -178,6 +178,26 @@ public class IssuedVerifiableCredentialTest extends AbstractUserTest {
}
}
@Test
@DatabaseTest
public void testRevokeIssuedCredential() {
String userId = createUser();
createIssuedVcViaModelLayer(userId, CREDENTIAL_TYPE_1, "wallet-123", "rev-001");
UserResource userResource = managedRealm.admin().users().get(userId);
List<IssuedVerifiableCredentialRepresentation> issuedCreds = userResource.verifiableCredentials().getIssuedCredentials();
assertThat(issuedCreds, hasSize(1));
String credentialId = issuedCreds.get(0).getId();
// Revoke the credential
userResource.verifiableCredentials().revokeIssuedCredential(credentialId);
// Verify
List<IssuedVerifiableCredentialRepresentation> afterRevoke = userResource.verifiableCredentials().getIssuedCredentials();
assertThat(afterRevoke, empty());
}
// Helper methods
protected void createIssuedVcViaModelLayer(String userId, String credentialType,