[OID4VCI] Add mapper for mapping unmanaged attributes (#44828)

closes #44780


Signed-off-by: Pascal Knüppel <pascal.knueppel@governikus.de>
This commit is contained in:
Pascal Knüppel 2025-12-17 18:39:00 +01:00 committed by GitHub
parent 548a89c823
commit b2778a6792
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 44 additions and 49 deletions

View File

@ -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<String, Object> 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);

View File

@ -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<String> contexts = new HashSet<>();
if (verifiableCredential.getContext() != null) {
@ -86,7 +86,7 @@ public class OID4VCContextMapper extends OID4VCMapper {
}
@Override
public void setClaimsForSubject(Map<String, Object> claims, UserSessionModel userSessionModel) {
public void setClaim(Map<String, Object> claims, UserSessionModel userSessionModel) {
// nothing to do for the mapper.
}

View File

@ -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<String, Object> claims, UserSessionModel userSessionModel) {
public void setClaim(Map<String, Object> claims, UserSessionModel userSessionModel) {
// Assign a generated ID
List<String> attributePath = getMetadataAttributePath();
String propertyName = attributePath.get(attributePath.size() - 1);

View File

@ -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<String> attributePath = getMetadataAttributePath();
String propertyName = attributePath.get(attributePath.size() - 1);
@ -142,7 +142,7 @@ public class OID4VCIssuedAtTimeClaimMapper extends OID4VCMapper {
}
@Override
public void setClaimsForSubject(Map<String, Object> claims, UserSessionModel userSessionModel) {
public void setClaim(Map<String, Object> claims, UserSessionModel userSessionModel) {
// NoOp
}

View File

@ -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<String, Object> claims,
UserSessionModel userSessionModel);
public abstract void setClaim(Map<String, Object> claims,
UserSessionModel userSessionModel);
}

View File

@ -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<String, Object> claims, UserSessionModel userSessionModel) {
public void setClaim(Map<String, Object> claims, UserSessionModel userSessionModel) {
List<String> attributePath = getMetadataAttributePath();
String propertyName = attributePath.get(attributePath.size() - 1);
String staticValue = mapperModel.getConfig().get(STATIC_CLAIM_KEY);

View File

@ -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<String, Object> claims, UserSessionModel userSessionModel) {
public void setClaim(Map<String, Object> claims, UserSessionModel userSessionModel) {
List<String> attributePath = getMetadataAttributePath();
String propertyName = attributePath.get(attributePath.size() - 1);
claims.put(propertyName,

View File

@ -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<String, Object> claims,
UserSessionModel userSessionModel) {
public void setClaim(Map<String, Object> claims,
UserSessionModel userSessionModel) {
List<String> attributePath = getMetadataAttributePath();
String propertyName = attributePath.get(attributePath.size() - 1);
String client = mapperModel.getConfig().get(CLIENT_CONFIG_KEY);

View File

@ -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<String> types = new HashSet<>();
if (verifiableCredential.getType() != null) {
@ -85,7 +85,7 @@ public class OID4VCTypeMapper extends OID4VCMapper {
}
@Override
public void setClaimsForSubject(Map<String, Object> claims, UserSessionModel userSessionModel) {
public void setClaim(Map<String, Object> claims, UserSessionModel userSessionModel) {
// nothing to do for the mapper.
}

View File

@ -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 <a href="https://github.com/wistefan">Stefan Wiedemann</a>
*/
@ -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<String, Object> claims, UserSessionModel userSessionModel) {
public void setClaim(Map<String, Object> claims, UserSessionModel userSessionModel) {
List<String> 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<String, String> 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

View File

@ -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<Role> rolesList = roles.stream().map(ro -> new ObjectMapper().convertValue(ro, Role.class)).toList();