From b2778a679237ab32e663265e77d2947809eae40f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Kn=C3=BCppel?= Date: Wed, 17 Dec 2025 18:39:00 +0100 Subject: [PATCH] [OID4VCI] Add mapper for mapping unmanaged attributes (#44828) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes #44780 Signed-off-by: Pascal Knüppel --- .../oid4vc/issuance/OID4VCIssuerEndpoint.java | 8 ++--- .../issuance/mappers/OID4VCContextMapper.java | 6 ++-- .../mappers/OID4VCGeneratedIdMapper.java | 6 ++-- .../OID4VCIssuedAtTimeClaimMapper.java | 6 ++-- .../oid4vc/issuance/mappers/OID4VCMapper.java | 8 ++--- .../mappers/OID4VCStaticClaimMapper.java | 6 ++-- .../mappers/OID4VCSubjectIdMapper.java | 6 ++-- .../mappers/OID4VCTargetRoleMapper.java | 8 ++--- .../issuance/mappers/OID4VCTypeMapper.java | 6 ++-- .../mappers/OID4VCUserAttributeMapper.java | 31 +++++++++---------- .../mappers/OID4VCTargetRoleMapperTest.java | 2 +- 11 files changed, 44 insertions(+), 49 deletions(-) diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCIssuerEndpoint.java b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCIssuerEndpoint.java index a968d90dca8..c1e5bf3ceaa 100644 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCIssuerEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCIssuerEndpoint.java @@ -849,7 +849,7 @@ public class OID4VCIssuerEndpoint { return credentialRequest; } catch (JsonProcessingException e) { String errorMessage = "Failed to parse JSON request: " + e.getMessage(); - LOGGER.errorf(e, "JSON parsing failed. Request payload length: %d", + LOGGER.errorf(e, "JSON parsing failed. Request payload length: %d", requestPayload != null ? requestPayload.length() : 0); throw new BadRequestException(getErrorResponse(INVALID_CREDENTIAL_REQUEST, errorMessage)); } @@ -1367,8 +1367,7 @@ public class OID4VCIssuerEndpoint { .setType(List.of(credentialConfig.getScope())); Map subjectClaims = new HashMap<>(); - protocolMappers - .forEach(mapper -> mapper.setClaimsForSubject(subjectClaims, authResult.session())); + protocolMappers.forEach(mapper -> mapper.setClaim(subjectClaims, authResult.session())); // Validate that requested claims from authorization_details are present validateRequestedClaimsArePresent(subjectClaims, authResult.session(), credentialConfig.getScope()); @@ -1376,8 +1375,7 @@ public class OID4VCIssuerEndpoint { // Include all available claims subjectClaims.forEach((key, value) -> vc.getCredentialSubject().setClaims(key, value)); - protocolMappers - .forEach(mapper -> mapper.setClaimsForCredential(vc, authResult.session())); + protocolMappers.forEach(mapper -> mapper.setClaim(vc, authResult.session())); LOGGER.debugf("The credential to sign is: %s", vc); diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCContextMapper.java b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCContextMapper.java index d322422056e..74bd073c857 100644 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCContextMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCContextMapper.java @@ -74,8 +74,8 @@ public class OID4VCContextMapper extends OID4VCMapper { return List.of(TYPE_KEY); } - public void setClaimsForCredential(VerifiableCredential verifiableCredential, - UserSessionModel userSessionModel) { + public void setClaim(VerifiableCredential verifiableCredential, + UserSessionModel userSessionModel) { // remove duplicates Set contexts = new HashSet<>(); if (verifiableCredential.getContext() != null) { @@ -86,7 +86,7 @@ public class OID4VCContextMapper extends OID4VCMapper { } @Override - public void setClaimsForSubject(Map claims, UserSessionModel userSessionModel) { + public void setClaim(Map claims, UserSessionModel userSessionModel) { // nothing to do for the mapper. } diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCGeneratedIdMapper.java b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCGeneratedIdMapper.java index f5b8adbe6a3..2abe99f11cc 100644 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCGeneratedIdMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCGeneratedIdMapper.java @@ -77,13 +77,13 @@ public class OID4VCGeneratedIdMapper extends OID4VCMapper { return ListUtils.union(getAttributePrefix(), List.of(property)); } - public void setClaimsForCredential(VerifiableCredential verifiableCredential, - UserSessionModel userSessionModel) { + public void setClaim(VerifiableCredential verifiableCredential, + UserSessionModel userSessionModel) { // nothing to do for the mapper. } @Override - public void setClaimsForSubject(Map claims, UserSessionModel userSessionModel) { + public void setClaim(Map claims, UserSessionModel userSessionModel) { // Assign a generated ID List attributePath = getMetadataAttributePath(); String propertyName = attributePath.get(attributePath.size() - 1); diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCIssuedAtTimeClaimMapper.java b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCIssuedAtTimeClaimMapper.java index 05a34a3475b..d679c6711f8 100644 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCIssuedAtTimeClaimMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCIssuedAtTimeClaimMapper.java @@ -106,8 +106,8 @@ public class OID4VCIssuedAtTimeClaimMapper extends OID4VCMapper { .orElse(false); } - public void setClaimsForCredential(VerifiableCredential verifiableCredential, - UserSessionModel userSessionModel) { + public void setClaim(VerifiableCredential verifiableCredential, + UserSessionModel userSessionModel) { // Set the value List attributePath = getMetadataAttributePath(); String propertyName = attributePath.get(attributePath.size() - 1); @@ -142,7 +142,7 @@ public class OID4VCIssuedAtTimeClaimMapper extends OID4VCMapper { } @Override - public void setClaimsForSubject(Map claims, UserSessionModel userSessionModel) { + public void setClaim(Map claims, UserSessionModel userSessionModel) { // NoOp } diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCMapper.java b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCMapper.java index 5d97cd32617..3ed6f770d4d 100644 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCMapper.java @@ -149,13 +149,13 @@ public abstract class OID4VCMapper implements ProtocolMapper, OID4VCEnvironmentP /** * Set the claims to credential, like f.e. the context */ - public abstract void setClaimsForCredential(VerifiableCredential verifiableCredential, - UserSessionModel userSessionModel); + public abstract void setClaim(VerifiableCredential verifiableCredential, + UserSessionModel userSessionModel); /** * Set the claims to the credential subject. */ - public abstract void setClaimsForSubject(Map claims, - UserSessionModel userSessionModel); + public abstract void setClaim(Map claims, + UserSessionModel userSessionModel); } diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCStaticClaimMapper.java b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCStaticClaimMapper.java index 5083f96bc06..744617a75a0 100644 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCStaticClaimMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCStaticClaimMapper.java @@ -61,13 +61,13 @@ public class OID4VCStaticClaimMapper extends OID4VCMapper { return CONFIG_PROPERTIES; } - public void setClaimsForCredential(VerifiableCredential verifiableCredential, - UserSessionModel userSessionModel) { + public void setClaim(VerifiableCredential verifiableCredential, + UserSessionModel userSessionModel) { // nothing to do for the mapper. } @Override - public void setClaimsForSubject(Map claims, UserSessionModel userSessionModel) { + public void setClaim(Map claims, UserSessionModel userSessionModel) { List attributePath = getMetadataAttributePath(); String propertyName = attributePath.get(attributePath.size() - 1); String staticValue = mapperModel.getConfig().get(STATIC_CLAIM_KEY); diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCSubjectIdMapper.java b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCSubjectIdMapper.java index aad54f815f4..a9814a7c422 100644 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCSubjectIdMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCSubjectIdMapper.java @@ -75,13 +75,13 @@ public class OID4VCSubjectIdMapper extends OID4VCMapper { return mapperModel; } - public void setClaimsForCredential(VerifiableCredential verifiableCredential, - UserSessionModel userSessionModel) { + public void setClaim(VerifiableCredential verifiableCredential, + UserSessionModel userSessionModel) { // nothing to do for the mapper. } @Override - public void setClaimsForSubject(Map claims, UserSessionModel userSessionModel) { + public void setClaim(Map claims, UserSessionModel userSessionModel) { List attributePath = getMetadataAttributePath(); String propertyName = attributePath.get(attributePath.size() - 1); claims.put(propertyName, diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCTargetRoleMapper.java b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCTargetRoleMapper.java index 8607c2418a5..f92fbad5095 100644 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCTargetRoleMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCTargetRoleMapper.java @@ -143,14 +143,14 @@ public class OID4VCTargetRoleMapper extends OID4VCMapper { } @Override - public void setClaimsForCredential(VerifiableCredential verifiableCredential, - UserSessionModel userSessionModel) { + public void setClaim(VerifiableCredential verifiableCredential, + UserSessionModel userSessionModel) { // nothing to do for the mapper. } @Override - public void setClaimsForSubject(Map claims, - UserSessionModel userSessionModel) { + public void setClaim(Map claims, + UserSessionModel userSessionModel) { List attributePath = getMetadataAttributePath(); String propertyName = attributePath.get(attributePath.size() - 1); String client = mapperModel.getConfig().get(CLIENT_CONFIG_KEY); diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCTypeMapper.java b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCTypeMapper.java index ab01c5f4192..83682bac177 100644 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCTypeMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCTypeMapper.java @@ -73,8 +73,8 @@ public class OID4VCTypeMapper extends OID4VCMapper { return List.of("type"); } - public void setClaimsForCredential(VerifiableCredential verifiableCredential, - UserSessionModel userSessionModel) { + public void setClaim(VerifiableCredential verifiableCredential, + UserSessionModel userSessionModel) { // remove duplicates Set types = new HashSet<>(); if (verifiableCredential.getType() != null) { @@ -85,7 +85,7 @@ public class OID4VCTypeMapper extends OID4VCMapper { } @Override - public void setClaimsForSubject(Map claims, UserSessionModel userSessionModel) { + public void setClaim(Map claims, UserSessionModel userSessionModel) { // nothing to do for the mapper. } diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCUserAttributeMapper.java b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCUserAttributeMapper.java index e27b2ed4e77..d90953d9304 100644 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCUserAttributeMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCUserAttributeMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 Red Hat, Inc. and/or its affiliates + * Copyright 2025 Red Hat, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,16 +27,17 @@ import java.util.Optional; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; -import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.ProtocolMapper; +import org.keycloak.protocol.ProtocolMapperUtils; import org.keycloak.protocol.oid4vc.OID4VCLoginProtocolFactory; import org.keycloak.protocol.oid4vc.model.VerifiableCredential; +import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; import org.keycloak.provider.ProviderConfigProperty; /** - * Allows to add user attributes to the credential subject + * Allows adding user properties to the credential subject * * @author Stefan Wiedemann */ @@ -50,20 +51,16 @@ public class OID4VCUserAttributeMapper extends OID4VCMapper { static { ProviderConfigProperty subjectPropertyNameConfig = new ProviderConfigProperty(); subjectPropertyNameConfig.setName(CLAIM_NAME); - subjectPropertyNameConfig.setLabel("Claim Name"); - subjectPropertyNameConfig.setHelpText("The name of the claim added to the credential subject that is extracted " + - "from the user attributes."); + subjectPropertyNameConfig.setLabel(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME_LABEL); + subjectPropertyNameConfig.setHelpText(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME_TOOLTIP); subjectPropertyNameConfig.setType(ProviderConfigProperty.STRING_TYPE); CONFIG_PROPERTIES.add(subjectPropertyNameConfig); ProviderConfigProperty userAttributeConfig = new ProviderConfigProperty(); userAttributeConfig.setName(USER_ATTRIBUTE_KEY); - userAttributeConfig.setLabel("User attribute"); - userAttributeConfig.setHelpText("The user attribute to be added to the credential subject."); - userAttributeConfig.setType(ProviderConfigProperty.LIST_TYPE); - userAttributeConfig.setOptions( - List.of(UserModel.USERNAME, UserModel.LOCALE, UserModel.FIRST_NAME, UserModel.LAST_NAME, - UserModel.DISABLED_REASON, UserModel.EMAIL, UserModel.EMAIL_VERIFIED)); + userAttributeConfig.setLabel(ProtocolMapperUtils.USER_MODEL_ATTRIBUTE_LABEL); + userAttributeConfig.setHelpText(ProtocolMapperUtils.USER_MODEL_ATTRIBUTE_HELP_TEXT); + userAttributeConfig.setType(ProviderConfigProperty.USER_PROFILE_ATTRIBUTE_LIST_TYPE); CONFIG_PROPERTIES.add(userAttributeConfig); ProviderConfigProperty aggregateAttributesConfig = new ProviderConfigProperty(); @@ -79,13 +76,13 @@ public class OID4VCUserAttributeMapper extends OID4VCMapper { return CONFIG_PROPERTIES; } - public void setClaimsForCredential(VerifiableCredential verifiableCredential, - UserSessionModel userSessionModel) { + public void setClaim(VerifiableCredential verifiableCredential, + UserSessionModel userSessionModel) { // nothing to do for the mapper. } @Override - public void setClaimsForSubject(Map claims, UserSessionModel userSessionModel) { + public void setClaim(Map claims, UserSessionModel userSessionModel) { List attributePath = getMetadataAttributePath(); String propertyName = attributePath.get(attributePath.size() - 1); String userAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_KEY); @@ -102,7 +99,7 @@ public class OID4VCUserAttributeMapper extends OID4VCMapper { public static ProtocolMapperModel create(String mapperName, String userAttribute, String propertyName, boolean aggregateAttributes) { - var mapperModel = new ProtocolMapperModel(); + ProtocolMapperModel mapperModel = new ProtocolMapperModel(); mapperModel.setName(mapperName); Map configMap = new HashMap<>(); configMap.put(CLAIM_NAME, propertyName); @@ -121,7 +118,7 @@ public class OID4VCUserAttributeMapper extends OID4VCMapper { @Override public String getHelpText() { - return "Maps user attributes to credential subject properties."; + return "Maps user attributes or properties to credential claims."; } @Override diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/mappers/OID4VCTargetRoleMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/mappers/OID4VCTargetRoleMapperTest.java index 682b27ed70e..d64b11136f4 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/mappers/OID4VCTargetRoleMapperTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/mappers/OID4VCTargetRoleMapperTest.java @@ -64,7 +64,7 @@ public class OID4VCTargetRoleMapperTest extends OID4VCTest { AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session); authenticator.setTokenString(token); UserSessionModel userSessionModel = authenticator.authenticate().session(); - roleMapper.setClaimsForSubject(claimsMap, userSessionModel); + roleMapper.setClaim(claimsMap, userSessionModel); assertTrue("The roles should be included as a claim.", claimsMap.containsKey("roles")); if (claimsMap.get("roles") instanceof HashSet roles) { List rolesList = roles.stream().map(ro -> new ObjectMapper().convertValue(ro, Role.class)).toList();