mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 23:12:06 -03:30
Mandatory claims are not enforced for OID4VCI
closes #44796 Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
parent
6a437521a9
commit
ff1274c07a
@ -25,6 +25,7 @@ import java.security.PublicKey;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -1369,8 +1370,13 @@ public class OID4VCIssuerEndpoint {
|
||||
Map<String, Object> subjectClaims = new HashMap<>();
|
||||
protocolMappers.forEach(mapper -> mapper.setClaim(subjectClaims, authResult.session()));
|
||||
|
||||
Map<String, Object> subjectClaimsWithMetadataPrefix = new HashMap<>();
|
||||
protocolMappers
|
||||
.forEach(mapper -> mapper.setClaimWithMetadataPrefix(subjectClaims, subjectClaimsWithMetadataPrefix));
|
||||
|
||||
// Validate that requested claims from authorization_details are present
|
||||
validateRequestedClaimsArePresent(subjectClaims, authResult.session(), credentialConfig.getScope());
|
||||
String credentialConfigId = credentialConfig.getId();
|
||||
validateRequestedClaimsArePresent(subjectClaimsWithMetadataPrefix, credentialConfig, authResult.session(), credentialConfigId);
|
||||
|
||||
// Include all available claims
|
||||
subjectClaims.forEach((key, value) -> vc.getCredentialSubject().setClaims(key, value));
|
||||
@ -1446,68 +1452,77 @@ public class OID4VCIssuerEndpoint {
|
||||
/**
|
||||
* Validates that all requested claims from authorization_details are present in the available claims.
|
||||
*
|
||||
* @param allClaims all available claims
|
||||
* @param allClaims all available claims. These are the claims including metadata prefix with the resolved path
|
||||
* @param credentialConfig Credential configuration
|
||||
* @param userSession the user session
|
||||
* @param scope the credential scope
|
||||
* @throws BadRequestException if mandatory requested claims are missing
|
||||
*/
|
||||
private void validateRequestedClaimsArePresent(Map<String, Object> allClaims, UserSessionModel userSession, String scope) {
|
||||
try {
|
||||
// Look for stored claims in user session notes
|
||||
String claimsKey = AUTHORIZATION_DETAILS_CLAIMS_PREFIX + scope;
|
||||
String storedClaimsJson = userSession.getNote(claimsKey);
|
||||
private void validateRequestedClaimsArePresent(Map<String, Object> allClaims, SupportedCredentialConfiguration credentialConfig,
|
||||
UserSessionModel userSession, String scope) {
|
||||
// Protocol mappers from configuration
|
||||
Map<List<Object>, ClaimsDescription> claimsConfig = credentialConfig.getCredentialMetadata().getClaims()
|
||||
.stream()
|
||||
.map(claim -> {
|
||||
List<Object> pathObj = new ArrayList<>(claim.getPath());
|
||||
return new ClaimsDescription(pathObj, claim.isMandatory());
|
||||
})
|
||||
.collect(Collectors.toMap(ClaimsDescription::getPath, claimsDescription -> claimsDescription));
|
||||
|
||||
if (storedClaimsJson != null && !storedClaimsJson.isEmpty()) {
|
||||
try {
|
||||
// Parse the stored claims from JSON
|
||||
List<ClaimsDescription> storedClaims =
|
||||
JsonSerialization.readValue(storedClaimsJson,
|
||||
new TypeReference<List<ClaimsDescription>>() {
|
||||
});
|
||||
List<ClaimsDescription> claimsFromAuthzDetails = getClaimsFromAuthzDetails(scope, userSession);
|
||||
|
||||
if (storedClaims != null && !storedClaims.isEmpty()) {
|
||||
// Validate that all requested claims are present in the available claims
|
||||
// We use filterClaimsByAuthorizationDetails to check if claims can be found
|
||||
// but we don't actually filter - we just validate presence
|
||||
try {
|
||||
ClaimsPathPointer.filterClaimsByAuthorizationDetails(allClaims, storedClaims);
|
||||
LOGGER.debugf("All requested claims are present for scope %s", scope);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// If filtering fails, it means some requested claims are missing
|
||||
LOGGER.errorf("Requested claims validation failed for scope %s: %s", scope, e.getMessage());
|
||||
throw new BadRequestException("Credential issuance failed: " + e.getMessage() +
|
||||
". The requested claims are not available in the user profile.");
|
||||
}
|
||||
} else {
|
||||
LOGGER.debug("Stored claims list is null or empty");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.errorf(e, "Failed to parse stored claims for scope %s", scope);
|
||||
// Merge claims from both protocolMappers and authorizationDetails. If either source specifies "mandatory" as true, claim is considered mandatory
|
||||
for (ClaimsDescription claimDescription : claimsFromAuthzDetails) {
|
||||
List<Object> path = claimDescription.getPath();
|
||||
ClaimsDescription existing = claimsConfig.get(path);
|
||||
if (existing == null) {
|
||||
claimsConfig.put(path, claimDescription);
|
||||
} else {
|
||||
if (claimDescription.isMandatory()) {
|
||||
existing.setMandatory(true);
|
||||
}
|
||||
} else {
|
||||
LOGGER.debugf("No stored claims found for scope %s", scope);
|
||||
}
|
||||
// No claims filtering requested, all claims are valid
|
||||
}
|
||||
|
||||
List<ClaimsDescription> claimsDescriptions = new ArrayList<>(claimsConfig.values());
|
||||
|
||||
// Validate that all requested claims are present in the available claims
|
||||
// We use filterClaimsByAuthorizationDetails to check if claims can be found
|
||||
// but we don't actually filter - we just validate presence
|
||||
try {
|
||||
ClaimsPathPointer.filterClaimsByAuthorizationDetails(allClaims, claimsDescriptions);
|
||||
LOGGER.debugf("All requested claims are present for scope %s", scope);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Mandatory claim missing - this should fail credential issuance
|
||||
String errorMessage = e.getMessage();
|
||||
if (errorMessage.contains("Mandatory claim not found:")) {
|
||||
LOGGER.errorf("Mandatory claim missing during claims filtering for scope %s: %s", scope, errorMessage);
|
||||
throw new BadRequestException(getErrorResponse(INVALID_CREDENTIAL_REQUEST,
|
||||
"Credential issuance failed: " + errorMessage +
|
||||
". The requested mandatory claim is not available in the user profile."));
|
||||
} else {
|
||||
LOGGER.errorf("Claims filtering error for scope %s: %s", scope, errorMessage);
|
||||
throw new BadRequestException(getErrorResponse(INVALID_CREDENTIAL_REQUEST,
|
||||
"Credential issuance failed: " + errorMessage));
|
||||
}
|
||||
} catch (BadRequestException e) {
|
||||
// Re-throw BadRequestException to ensure client receives proper error response
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
// Log error but continue with all claims to avoid breaking existing functionality
|
||||
LOGGER.errorf(e, "Unexpected error during claims validation for scope %s, continuing with all claims", scope);
|
||||
// If filtering fails, it means some requested claims are missing
|
||||
LOGGER.warnf("Requested claims validation failed for scope '%s', user '%s', client '%s': %s"
|
||||
, scope, userSession.getUser().getUsername(), session.getContext().getClient().getClientId(), e.getMessage());
|
||||
throw new BadRequestException("Credential issuance failed: " + e.getMessage() +
|
||||
". The requested claims are not available in the user profile.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private List<ClaimsDescription> getClaimsFromAuthzDetails(String scope, UserSessionModel userSession) {
|
||||
String username = userSession.getUser().getUsername();
|
||||
String clientId = session.getContext().getClient().getClientId();
|
||||
|
||||
// Look for stored claims in user session notes
|
||||
String claimsKey = AUTHORIZATION_DETAILS_CLAIMS_PREFIX + scope;
|
||||
String storedClaimsJson = userSession.getNote(claimsKey);
|
||||
|
||||
if (storedClaimsJson != null && !storedClaimsJson.isEmpty()) {
|
||||
try {
|
||||
// Parse the stored claims from JSON
|
||||
return JsonSerialization.readValue(storedClaimsJson,
|
||||
new TypeReference<>() {
|
||||
});
|
||||
} catch (Exception e) {
|
||||
LOGGER.warnf(e, "Failed to parse stored claims for scope '%s', user '%s', client '%s'", scope, username, clientId);
|
||||
}
|
||||
} else {
|
||||
LOGGER.debugf("No stored claims found for scope '%s', user '%s', client '%s'", scope, username, clientId);
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ package org.keycloak.protocol.oid4vc.issuance.mappers;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@ -158,4 +159,34 @@ public abstract class OID4VCMapper implements ProtocolMapper, OID4VCEnvironmentP
|
||||
public abstract void setClaim(Map<String, Object> claims,
|
||||
UserSessionModel userSessionModel);
|
||||
|
||||
/**
|
||||
* Creates new map "claimsWithPrefix" with the resolved claims including path prefix
|
||||
*
|
||||
* @param claimsOrig Map with the original claims, which were returned by {@link #setClaim(Map, UserSessionModel)} . This method usually just reads from this map
|
||||
* @param claimsWithPrefix Map with the claims including path prefix. This method might write to this map
|
||||
*/
|
||||
public void setClaimWithMetadataPrefix(Map<String, Object> claimsOrig, Map<String, Object> claimsWithPrefix) {
|
||||
List<String> attributePath = getMetadataAttributePath();
|
||||
String propertyName = attributePath.get(attributePath.size() - 1);
|
||||
if (claimsOrig.get(propertyName) != null) {
|
||||
Object claimValue = claimsOrig.get(propertyName);
|
||||
Map<String, Object> current = claimsWithPrefix;
|
||||
|
||||
for (int i = 0; i < attributePath.size() ; i++) {
|
||||
String currentSnippetName = attributePath.get(i);
|
||||
if (i < attributePath.size() - 1) {
|
||||
Map<String, Object> obj = (Map<String, Object>) current.get(currentSnippetName);
|
||||
if (obj == null) {
|
||||
obj = new HashMap<>();
|
||||
current.put(currentSnippetName, obj);
|
||||
}
|
||||
current = obj;
|
||||
} else {
|
||||
// Last element
|
||||
current.put(currentSnippetName, claimValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -220,13 +220,13 @@ public class ClaimsPathPointer {
|
||||
// Optional claims that don't exist are simply not included
|
||||
} catch (IllegalArgumentException e) {
|
||||
if (Boolean.TRUE.equals(claim.getMandatory())) {
|
||||
// Log error for mandatory claims before re-throwing
|
||||
logger.errorf("Failed to process mandatory claim path %s: %s", path, e.getMessage());
|
||||
// Log warning for mandatory claims before re-throwing
|
||||
logger.warnf("Failed to process mandatory claim path %s: %s", path, e.getMessage());
|
||||
// Re-throw for mandatory claims
|
||||
throw e;
|
||||
}
|
||||
// For optional claims, log warning and continue
|
||||
logger.warnf("Failed to process optional claim path %s: %s", path, e.getMessage());
|
||||
// For optional claims, log debug and continue
|
||||
logger.debugf("Failed to process optional claim path %s: %s", path, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -28,7 +28,10 @@ import java.util.function.BiFunction;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.admin.client.resource.ClientScopeResource;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.models.oid4vci.CredentialScopeModel;
|
||||
import org.keycloak.models.oid4vci.Oid4vcProtocolMapperModel;
|
||||
import org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailsResponse;
|
||||
import org.keycloak.protocol.oid4vc.model.AuthorizationDetail;
|
||||
import org.keycloak.protocol.oid4vc.model.ClaimsDescription;
|
||||
@ -38,6 +41,10 @@ import org.keycloak.protocol.oid4vc.model.CredentialResponse;
|
||||
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
|
||||
import org.keycloak.representations.AccessTokenResponse;
|
||||
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
||||
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
|
||||
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
@ -88,6 +95,11 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
|
||||
*/
|
||||
protected abstract String getExpectedClaimPath();
|
||||
|
||||
/**
|
||||
* Get the name of the protocol mapper for firstName
|
||||
*/
|
||||
protected abstract String getFirstNameProtocolMapperName();
|
||||
|
||||
/**
|
||||
* Prepare OID4VC test context by fetching issuer metadata and credential offer
|
||||
*/
|
||||
@ -137,10 +149,145 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
|
||||
testCompleteFlowWithClaimsValidationAuthorizationCode(credRequestSupplier);
|
||||
}
|
||||
|
||||
// Test for the authorization_code flow with "mandatory" claim specified in the "authorization_details" parameter
|
||||
@Test
|
||||
public void testCompleteFlow_mandatoryClaimsInAuthzDetailsParameter() throws Exception {
|
||||
Oid4vcTestContext ctx = prepareOid4vcTestContext();
|
||||
BiFunction<String, String, CredentialRequest> credRequestSupplier = (credentialConfigurationId, credentialIdentifier) -> {
|
||||
CredentialRequest credentialRequest = new CredentialRequest();
|
||||
credentialRequest.setCredentialIdentifier(credentialIdentifier);
|
||||
return credentialRequest;
|
||||
};
|
||||
|
||||
// 1 - Update user to have missing "lastName" (mandatory attribute)
|
||||
UserResource user = ApiUtil.findUserByUsernameId(testRealm(), "john");
|
||||
UserRepresentation userRep = user.toRepresentation();
|
||||
// NOTE: Need to call both "setLastName" and set attributes to be able to set last name as null
|
||||
userRep.setAttributes(Collections.emptyMap());
|
||||
userRep.setLastName(null);
|
||||
user.update(userRep);
|
||||
|
||||
// 2 - Test the flow. Credential request should fail due the missing "lastName"
|
||||
// Perform authorization code flow to get authorization code
|
||||
AccessTokenResponse tokenResponse = authzCodeFlow(ctx);
|
||||
String credentialIdentifier = assertTokenResponse(tokenResponse);
|
||||
String credentialConfigurationId = getCredentialClientScope().getAttributes().get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
|
||||
// Request the actual credential using the identifier
|
||||
HttpPost postCredential = getCredentialRequest(ctx, credRequestSupplier, tokenResponse, credentialConfigurationId, credentialIdentifier);
|
||||
|
||||
try (CloseableHttpResponse credentialResponse = httpClient.execute(postCredential)) {
|
||||
assertErrorCredentialResponse(credentialResponse);
|
||||
}
|
||||
|
||||
// 3 - Update user to add "lastName"
|
||||
userRep.setLastName("Doe");
|
||||
user.update(userRep);
|
||||
|
||||
// 4 - Test the credential-request again. Should be OK now
|
||||
try (CloseableHttpResponse credentialResponse = httpClient.execute(postCredential)) {
|
||||
assertSuccessfulCredentialResponse(credentialResponse);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Test for the authorization_code flow with "mandatory" claim specified in the "authorization_details" parameter as well as
|
||||
// mandatory claims in the protocol mappers configuration
|
||||
@Test
|
||||
public void testCompleteFlow_mandatoryClaimsInAuthzDetailsParameterAndProtocolMappersConfig() throws Exception {
|
||||
Oid4vcTestContext ctx = prepareOid4vcTestContext();
|
||||
BiFunction<String, String, CredentialRequest> credRequestSupplier = (credentialConfigurationId, credentialIdentifier) -> {
|
||||
CredentialRequest credentialRequest = new CredentialRequest();
|
||||
credentialRequest.setCredentialIdentifier(credentialIdentifier);
|
||||
return credentialRequest;
|
||||
};
|
||||
|
||||
// 1 - Update "firstName" protocol mapper to be mandatory
|
||||
ClientScopeResource clientScopeResource = ApiUtil.findClientScopeByName(testRealm(), getCredentialClientScope().getName());
|
||||
assertNotNull(clientScopeResource);
|
||||
ProtocolMapperRepresentation protocolMapper = clientScopeResource.getProtocolMappers().getMappers()
|
||||
.stream()
|
||||
.filter(protMapper -> getFirstNameProtocolMapperName().equals(protMapper.getName()))
|
||||
.findFirst()
|
||||
.orElseThrow((() -> new RuntimeException("Not found protocol mapper with name 'firstName-mapper'.")));
|
||||
protocolMapper.getConfig().put(Oid4vcProtocolMapperModel.MANDATORY, "true");
|
||||
clientScopeResource.getProtocolMappers().update(protocolMapper.getId(), protocolMapper);
|
||||
|
||||
try {
|
||||
// 2 - Update user to have missing "lastName" (mandatory attribute by authorization_details parameter) and "firstName" (mandatory attribute by protocol mapper)
|
||||
UserResource user = ApiUtil.findUserByUsernameId(testRealm(), "john");
|
||||
UserRepresentation userRep = user.toRepresentation();
|
||||
// NOTE: Need to call both "setLastName" and set attributes to be able to set last name as null
|
||||
userRep.setAttributes(Collections.emptyMap());
|
||||
userRep.setFirstName(null);
|
||||
userRep.setLastName(null);
|
||||
user.update(userRep);
|
||||
|
||||
// 2 - Test the flow. Credential request should fail due the missing "lastName"
|
||||
// Perform authorization code flow to get authorization code
|
||||
AccessTokenResponse tokenResponse = authzCodeFlow(ctx);
|
||||
String credentialIdentifier = assertTokenResponse(tokenResponse);
|
||||
String credentialConfigurationId = getCredentialClientScope().getAttributes().get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
|
||||
// Request the actual credential using the identifier
|
||||
HttpPost postCredential = getCredentialRequest(ctx, credRequestSupplier, tokenResponse, credentialConfigurationId, credentialIdentifier);
|
||||
|
||||
try (CloseableHttpResponse credentialResponse = httpClient.execute(postCredential)) {
|
||||
assertErrorCredentialResponse(credentialResponse);
|
||||
}
|
||||
|
||||
// 3 - Update user to add "lastName", but keep "firstName" missing. Credential request should still fail
|
||||
userRep.setLastName("Doe");
|
||||
userRep.setFirstName(null);
|
||||
user.update(userRep);
|
||||
|
||||
try (CloseableHttpResponse credentialResponse = httpClient.execute(postCredential)) {
|
||||
assertErrorCredentialResponse(credentialResponse);
|
||||
}
|
||||
|
||||
// 4 - Update user to add "firstName", but missing "lastName"
|
||||
userRep.setLastName(null);
|
||||
userRep.setFirstName("John");
|
||||
user.update(userRep);
|
||||
|
||||
try (CloseableHttpResponse credentialResponse = httpClient.execute(postCredential)) {
|
||||
assertErrorCredentialResponse(credentialResponse);
|
||||
}
|
||||
|
||||
// 5 - Update user to both "firstName" and "lastName". Credential request should be successful
|
||||
userRep.setLastName("Doe");
|
||||
userRep.setFirstName("John");
|
||||
user.update(userRep);
|
||||
|
||||
try (CloseableHttpResponse credentialResponse = httpClient.execute(postCredential)) {
|
||||
assertSuccessfulCredentialResponse(credentialResponse);
|
||||
}
|
||||
} finally {
|
||||
// 6 - Revert protocol mapper config
|
||||
protocolMapper.getConfig().put(Oid4vcProtocolMapperModel.MANDATORY, "false");
|
||||
clientScopeResource.getProtocolMappers().update(protocolMapper.getId(), protocolMapper);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void testCompleteFlowWithClaimsValidationAuthorizationCode(BiFunction<String, String, CredentialRequest> credentialRequestSupplier) throws Exception {
|
||||
Oid4vcTestContext ctx = prepareOid4vcTestContext();
|
||||
|
||||
// Perform authorization code flow to get authorization code
|
||||
AccessTokenResponse tokenResponse = authzCodeFlow(ctx);
|
||||
String credentialIdentifier = assertTokenResponse(tokenResponse);
|
||||
String credentialConfigurationId = getCredentialClientScope().getAttributes().get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
|
||||
// Request the actual credential using the identifier
|
||||
HttpPost postCredential = getCredentialRequest(ctx, credentialRequestSupplier, tokenResponse, credentialConfigurationId, credentialIdentifier);
|
||||
|
||||
try (CloseableHttpResponse credentialResponse = httpClient.execute(postCredential)) {
|
||||
assertSuccessfulCredentialResponse(credentialResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// Successful authorization_code flow
|
||||
private AccessTokenResponse authzCodeFlow(Oid4vcTestContext ctx) throws Exception {
|
||||
// Perform authorization code flow to get authorization code
|
||||
oauth.client(client.getClientId());
|
||||
oauth.scope(getCredentialClientScope().getName()); // Add the credential scope
|
||||
@ -183,13 +330,15 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
|
||||
UrlEncodedFormEntity tokenFormEntity = new UrlEncodedFormEntity(tokenParameters, StandardCharsets.UTF_8);
|
||||
postToken.setEntity(tokenFormEntity);
|
||||
|
||||
AccessTokenResponse tokenResponse;
|
||||
try (CloseableHttpResponse tokenHttpResponse = httpClient.execute(postToken)) {
|
||||
assertEquals(HttpStatus.SC_OK, tokenHttpResponse.getStatusLine().getStatusCode());
|
||||
String tokenResponseBody = IOUtils.toString(tokenHttpResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
tokenResponse = JsonSerialization.readValue(tokenResponseBody, AccessTokenResponse.class);
|
||||
return JsonSerialization.readValue(tokenResponseBody, AccessTokenResponse.class);
|
||||
}
|
||||
}
|
||||
|
||||
// Test successful token response. Returns "Credential identifier" of the VC credential
|
||||
private String assertTokenResponse(AccessTokenResponse tokenResponse) throws Exception {
|
||||
// Extract authorization_details from token response
|
||||
List<OID4VCAuthorizationDetailsResponse> authDetailsResponse = parseAuthorizationDetails(JsonSerialization.writeValueAsString(tokenResponse));
|
||||
assertNotNull("authorization_details should be present in the response", authDetailsResponse);
|
||||
@ -206,8 +355,11 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
|
||||
assertNotNull("Credential identifiers should not be null", credentialIdentifiers);
|
||||
assertEquals("Credential identifiers expected to have 1 item. It had " + credentialIdentifiers.size() + " with value " + credentialIdentifiers,
|
||||
1, credentialIdentifiers.size());
|
||||
String credentialIdentifier = credentialIdentifiers.get(0);
|
||||
return credentialIdentifiers.get(0);
|
||||
}
|
||||
|
||||
private HttpPost getCredentialRequest(Oid4vcTestContext ctx, BiFunction<String, String, CredentialRequest> credentialRequestSupplier, AccessTokenResponse tokenResponse,
|
||||
String credentialConfigurationId, String credentialIdentifier) throws Exception {
|
||||
// Request the actual credential using the identifier
|
||||
HttpPost postCredential = new HttpPost(ctx.credentialIssuer.getCredentialEndpoint());
|
||||
postCredential.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + tokenResponse.getToken());
|
||||
@ -218,27 +370,36 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
|
||||
String requestBody = JsonSerialization.writeValueAsString(credentialRequest);
|
||||
postCredential.setEntity(new StringEntity(requestBody, StandardCharsets.UTF_8));
|
||||
|
||||
try (CloseableHttpResponse credentialResponse = httpClient.execute(postCredential)) {
|
||||
assertEquals(HttpStatus.SC_OK, credentialResponse.getStatusLine().getStatusCode());
|
||||
String responseBody = IOUtils.toString(credentialResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
return postCredential;
|
||||
}
|
||||
|
||||
// Parse the credential response
|
||||
CredentialResponse parsedResponse = JsonSerialization.readValue(responseBody, CredentialResponse.class);
|
||||
assertNotNull("Credential response should not be null", parsedResponse);
|
||||
assertNotNull("Credentials should be present", parsedResponse.getCredentials());
|
||||
assertEquals("Should have exactly one credential", 1, parsedResponse.getCredentials().size());
|
||||
private void assertSuccessfulCredentialResponse(CloseableHttpResponse credentialResponse) throws Exception {
|
||||
assertEquals(HttpStatus.SC_OK, credentialResponse.getStatusLine().getStatusCode());
|
||||
String responseBody = IOUtils.toString(credentialResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
|
||||
// Verify that the issued credential contains the requested claims AND may contain additional claims
|
||||
CredentialResponse.Credential credentialWrapper = parsedResponse.getCredentials().get(0);
|
||||
assertNotNull("Credential wrapper should not be null", credentialWrapper);
|
||||
// Parse the credential response
|
||||
CredentialResponse parsedResponse = JsonSerialization.readValue(responseBody, CredentialResponse.class);
|
||||
assertNotNull("Credential response should not be null", parsedResponse);
|
||||
assertNotNull("Credentials should be present", parsedResponse.getCredentials());
|
||||
assertEquals("Should have exactly one credential", 1, parsedResponse.getCredentials().size());
|
||||
|
||||
// The credential is stored as Object, so we need to cast it
|
||||
Object credentialObj = credentialWrapper.getCredential();
|
||||
assertNotNull("Credential object should not be null", credentialObj);
|
||||
// Verify that the issued credential contains the requested claims AND may contain additional claims
|
||||
CredentialResponse.Credential credentialWrapper = parsedResponse.getCredentials().get(0);
|
||||
assertNotNull("Credential wrapper should not be null", credentialWrapper);
|
||||
|
||||
// Verify the credential structure based on formatfix-authorization_details-processing
|
||||
verifyCredentialStructure(credentialObj);
|
||||
}
|
||||
// The credential is stored as Object, so we need to cast it
|
||||
Object credentialObj = credentialWrapper.getCredential();
|
||||
assertNotNull("Credential object should not be null", credentialObj);
|
||||
|
||||
// Verify the credential structure based on formatfix-authorization_details-processing
|
||||
verifyCredentialStructure(credentialObj);
|
||||
}
|
||||
|
||||
private void assertErrorCredentialResponse(CloseableHttpResponse credentialResponse) throws Exception {
|
||||
assertEquals(HttpStatus.SC_BAD_REQUEST, credentialResponse.getStatusLine().getStatusCode());
|
||||
String responseBody = IOUtils.toString(credentialResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
OAuth2ErrorRepresentation error = JsonSerialization.readValue(responseBody, OAuth2ErrorRepresentation.class);
|
||||
assertEquals("Credential issuance failed: No elements selected after processing claims path pointer. The requested claims are not available in the user profile.", error.getError());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -43,7 +43,12 @@ public class OID4VCJwtAuthorizationCodeFlowTest extends OID4VCAuthorizationCodeF
|
||||
|
||||
@Override
|
||||
protected String getExpectedClaimPath() {
|
||||
return "given_name";
|
||||
return "family_name";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getFirstNameProtocolMapperName() {
|
||||
return "givenName";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -48,6 +48,11 @@ public class OID4VCSdJwtAuthorizationCodeFlowTest extends OID4VCAuthorizationCod
|
||||
return "lastName";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getFirstNameProtocolMapperName() {
|
||||
return "firstName-mapper";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void verifyCredentialStructure(Object credentialObj) {
|
||||
assertNotNull("Credential object should not be null", credentialObj);
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
"protocolMapper": "oid4vc-user-attribute-mapper",
|
||||
"config": {
|
||||
"claim.name": "family_name",
|
||||
"userProperty": "lastName",
|
||||
"userAttribute": "lastName",
|
||||
"vc.mandatory": "false",
|
||||
"vc.display": "[{\"name\": \"اسم العائلة\", \"locale\": \"ar-SA\"}, {\"name\": \"Nachname\", \"locale\": \"de-DE\"}, {\"name\": \"Family Name\", \"locale\": \"en-US\"}, {\"name\": \"Apellido\", \"locale\": \"es-ES\"}, {\"name\": \"نام خانوادگی\", \"locale\": \"fa-IR\"}, {\"name\": \"Sukunimi\", \"locale\": \"fi-FI\"}, {\"name\": \"Nom de famille\", \"locale\": \"fr-FR\"}, {\"name\": \"परिवार का नाम\", \"locale\": \"hi-IN\"}, {\"name\": \"Cognome\", \"locale\": \"it-IT\"}, {\"name\": \"姓\", \"locale\": \"ja-JP\"}, {\"name\": \"өөрийн нэр\", \"locale\": \"mn-MN\"}, {\"name\": \"Achternaam\", \"locale\": \"nl-NL\"}, {\"name\": \"Sobrenome\", \"locale\": \"pt-PT\"}, {\"name\": \"Efternamn\", \"locale\": \"sv-SE\"}, {\"name\": \"خاندانی نام\", \"locale\": \"ur-PK\"}]"
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user