mirror of
https://github.com/keycloak/keycloak.git
synced 2025-12-30 03:19:54 -06:00
[OID4VCI] Add support for credential_response_encryption in credential request (#41001)
Closes #39310 Closes #41031 Signed-off-by: Ogenbertrand <ogenbertrand@gmail.com>
This commit is contained in:
@@ -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");
|
||||
@@ -41,6 +41,11 @@ import org.keycloak.component.ComponentFactory;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.jose.jwe.JWE;
|
||||
import org.keycloak.jose.jwe.JWEException;
|
||||
import org.keycloak.jose.jwe.JWEHeader;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
import org.keycloak.jose.jwk.JWKParser;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
@@ -62,6 +67,8 @@ import org.keycloak.protocol.oid4vc.issuance.signing.CredentialSigner;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialOfferURI;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialRequest;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialResponse;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialResponseEncryption;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialResponseEncryptionMetadata;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialsOffer;
|
||||
import org.keycloak.protocol.oid4vc.model.ErrorResponse;
|
||||
import org.keycloak.protocol.oid4vc.model.ErrorType;
|
||||
@@ -89,7 +96,9 @@ import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.PublicKey;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -174,10 +183,10 @@ public class OID4VCIssuerEndpoint {
|
||||
private Map<String, CredentialBuilder> loadCredentialBuilders(KeycloakSession keycloakSession) {
|
||||
KeycloakSessionFactory keycloakSessionFactory = keycloakSession.getKeycloakSessionFactory();
|
||||
return keycloakSessionFactory.getProviderFactoriesStream(CredentialBuilder.class)
|
||||
.map(factory -> (CredentialBuilderFactory) factory)
|
||||
.map(factory -> factory.create(keycloakSession, null))
|
||||
.collect(Collectors.toMap(CredentialBuilder::getSupportedFormat,
|
||||
credentialBuilder -> credentialBuilder));
|
||||
.map(factory -> (CredentialBuilderFactory) factory)
|
||||
.map(factory -> factory.create(keycloakSession, null))
|
||||
.collect(Collectors.toMap(CredentialBuilder::getSupportedFormat,
|
||||
credentialBuilder -> credentialBuilder));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -346,16 +355,16 @@ public class OID4VCIssuerEndpoint {
|
||||
if (Arrays.stream(accessToken.getScope().split(" "))
|
||||
.noneMatch(tokenScope -> tokenScope.equals(requestedCredential.getScope()))) {
|
||||
LOGGER.debugf("Scope check failure: required scope = %s, " +
|
||||
"scope in access token = %s.",
|
||||
requestedCredential.getName(), accessToken.getScope());
|
||||
"scope in access token = %s.",
|
||||
requestedCredential.getName(), accessToken.getScope());
|
||||
throw new CorsErrorResponseException(cors,
|
||||
ErrorType.UNSUPPORTED_CREDENTIAL_TYPE.toString(),
|
||||
"Scope check failure",
|
||||
Response.Status.BAD_REQUEST);
|
||||
} else {
|
||||
LOGGER.debugf("Scope check success: required scope = %s, #" +
|
||||
"scope in access token = %s.",
|
||||
requestedCredential.getScope(), accessToken.getScope());
|
||||
"scope in access token = %s.",
|
||||
requestedCredential.getScope(), accessToken.getScope());
|
||||
}
|
||||
} else {
|
||||
clientSession.removeNote(PreAuthorizedCodeGrantType.VC_ISSUANCE_FLOW);
|
||||
@@ -367,17 +376,48 @@ public class OID4VCIssuerEndpoint {
|
||||
*/
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_JWT})
|
||||
@Path(CREDENTIAL_PATH)
|
||||
public Response requestCredential(
|
||||
CredentialRequest credentialRequestVO) {
|
||||
public Response requestCredential(CredentialRequest credentialRequestVO) {
|
||||
LOGGER.debugf("Received credentials request %s.", credentialRequestVO);
|
||||
|
||||
cors = Cors.builder().auth().allowedMethods("POST").auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS);
|
||||
|
||||
// do first to fail fast on auth
|
||||
// Authenticate first to fail fast on auth errors
|
||||
AuthenticationManager.AuthResult authResult = getAuthResult();
|
||||
|
||||
// Validate encryption parameters if present
|
||||
CredentialResponseEncryption encryptionParams = credentialRequestVO.getCredentialResponseEncryption();
|
||||
CredentialResponseEncryptionMetadata encryptionMetadata = OID4VCIssuerWellKnownProvider.getCredentialResponseEncryption(session);
|
||||
boolean isEncryptionRequired = Optional.ofNullable(encryptionMetadata)
|
||||
.map(CredentialResponseEncryptionMetadata::getEncryptionRequired)
|
||||
.orElse(false);
|
||||
|
||||
// Check if encryption is required but not provided
|
||||
if (isEncryptionRequired && encryptionParams == null) {
|
||||
String errorMessage = "Encryption is required by the Credential Issuer, but no encryption parameters were provided.";
|
||||
LOGGER.debug(errorMessage);
|
||||
throw new BadRequestException(getErrorResponse(ErrorType.INVALID_ENCRYPTION_PARAMETERS, errorMessage));
|
||||
}
|
||||
|
||||
// Validate encryption parameters if provided
|
||||
if (encryptionParams != null) {
|
||||
try {
|
||||
validateEncryptionParameters(encryptionParams);
|
||||
|
||||
// Check if the encryption algorithms are supported
|
||||
if (!isSupportedEncryption(encryptionMetadata, encryptionParams.getAlg(), encryptionParams.getEnc())) {
|
||||
String errorMessage = String.format("Unsupported encryption parameters: alg=%s, enc=%s",
|
||||
encryptionParams.getAlg(), encryptionParams.getEnc());
|
||||
LOGGER.debug(errorMessage);
|
||||
throw new BadRequestException(getErrorResponse(ErrorType.INVALID_ENCRYPTION_PARAMETERS, errorMessage));
|
||||
}
|
||||
} catch (BadRequestException e) {
|
||||
// Re-throw with proper error type
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// checkClientEnabled call after authentication
|
||||
checkClientEnabled();
|
||||
|
||||
@@ -395,9 +435,9 @@ public class OID4VCIssuerEndpoint {
|
||||
throw new BadRequestException(getErrorResponse(ErrorType.MISSING_CREDENTIAL_IDENTIFIER_AND_CONFIGURATION_ID));
|
||||
}
|
||||
|
||||
// Find the requested credential scope
|
||||
CredentialScopeModel requestedCredential = credentialRequestVO.findCredentialScope(session).orElseThrow(() -> {
|
||||
LOGGER.debugf("Credential for request '%s' not found.",
|
||||
credentialRequestVO.toString());
|
||||
LOGGER.debugf("Credential for request '%s' not found.", credentialRequestVO.toString());
|
||||
return new BadRequestException(getErrorResponse(ErrorType.UNSUPPORTED_CREDENTIAL_TYPE));
|
||||
});
|
||||
|
||||
@@ -408,13 +448,151 @@ public class OID4VCIssuerEndpoint {
|
||||
|
||||
Object theCredential = getCredential(authResult, supportedCredential, credentialRequestVO);
|
||||
|
||||
// Generate credential response
|
||||
CredentialResponse responseVO = new CredentialResponse();
|
||||
responseVO
|
||||
.addCredential(theCredential)
|
||||
.setNotificationId(generateNotificationId());
|
||||
.addCredential(theCredential)
|
||||
.setNotificationId(generateNotificationId());
|
||||
|
||||
if (encryptionParams != null) {
|
||||
String jwe = encryptCredentialResponse(responseVO, encryptionParams);
|
||||
return Response.ok()
|
||||
.type(MediaType.APPLICATION_JWT)
|
||||
.entity(jwe)
|
||||
.build();
|
||||
}
|
||||
|
||||
return Response.ok().entity(responseVO).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts a CredentialResponse as a JWE using the provided encryption parameters.
|
||||
*
|
||||
* @param response The CredentialResponse to encrypt
|
||||
* @param encryptionParams The encryption parameters (alg, enc, jwk)
|
||||
* @return The compact JWE serialization
|
||||
* @throws BadRequestException If encryption parameters are invalid
|
||||
* @throws WebApplicationException If encryption fails due to server issues
|
||||
*/
|
||||
private String encryptCredentialResponse(CredentialResponse response, CredentialResponseEncryption encryptionParams) {
|
||||
// Validate input parameters
|
||||
validateEncryptionParameters(encryptionParams);
|
||||
|
||||
String alg = encryptionParams.getAlg();
|
||||
String enc = encryptionParams.getEnc();
|
||||
JWK jwk = encryptionParams.getJwk();
|
||||
|
||||
// Parse public key
|
||||
PublicKey publicKey;
|
||||
try {
|
||||
publicKey = JWKParser.create(jwk).toPublicKey();
|
||||
if (publicKey == null) {
|
||||
LOGGER.debug("Invalid JWK: Failed to parse public key");
|
||||
throw new BadRequestException(getErrorResponse(ErrorType.INVALID_ENCRYPTION_PARAMETERS,
|
||||
"Invalid JWK: Failed to parse public key."));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.debugf("Failed to parse JWK: %s", e.getMessage());
|
||||
throw new BadRequestException(getErrorResponse(ErrorType.INVALID_ENCRYPTION_PARAMETERS,
|
||||
"Invalid JWK: Failed to parse public key."));
|
||||
}
|
||||
|
||||
// Perform encryption
|
||||
try {
|
||||
byte[] content = JsonSerialization.writeValueAsBytes(response);
|
||||
JWEHeader header = new JWEHeader.JWEHeaderBuilder()
|
||||
.algorithm(alg)
|
||||
.encryptionAlgorithm(enc)
|
||||
.build();
|
||||
JWE jwe = new JWE()
|
||||
.header(header)
|
||||
.content(content);
|
||||
jwe.getKeyStorage().setEncryptionKey(publicKey);
|
||||
return jwe.encodeJwe();
|
||||
} catch (IOException e) {
|
||||
LOGGER.errorf("Serialization failed: %s", e.getMessage());
|
||||
throw new WebApplicationException(
|
||||
Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse()
|
||||
.setErrorDescription("Failed to serialize response"))
|
||||
.type(MediaType.APPLICATION_JSON)
|
||||
.build());
|
||||
} catch (JWEException e) {
|
||||
LOGGER.errorf("Encryption operation failed: %s", e.getMessage());
|
||||
throw new WebApplicationException(
|
||||
Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(new ErrorResponse()
|
||||
.setErrorDescription("Encryption operation failed"))
|
||||
.type(MediaType.APPLICATION_JSON)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the encryption parameters for a credential response.
|
||||
*
|
||||
* @param encryptionParams The encryption parameters to validate
|
||||
* @throws BadRequestException If the encryption parameters are invalid
|
||||
*/
|
||||
private void validateEncryptionParameters(CredentialResponseEncryption encryptionParams) {
|
||||
if (encryptionParams == null) {
|
||||
throw new BadRequestException(getErrorResponse(ErrorType.INVALID_ENCRYPTION_PARAMETERS,
|
||||
"Missing required encryption parameters (alg, enc, and jwk)."));
|
||||
}
|
||||
|
||||
List<String> missingParams = new ArrayList<>();
|
||||
if (encryptionParams.getAlg() == null) missingParams.add("alg");
|
||||
if (encryptionParams.getEnc() == null) missingParams.add("enc");
|
||||
if (encryptionParams.getJwk() == null) missingParams.add("jwk");
|
||||
|
||||
if (!missingParams.isEmpty()) {
|
||||
String errorMessage = String.format("Missing required encryption parameters: %s", String.join(", ", missingParams));
|
||||
LOGGER.debug(errorMessage);
|
||||
throw new BadRequestException(getErrorResponse(ErrorType.INVALID_ENCRYPTION_PARAMETERS, errorMessage));
|
||||
}
|
||||
|
||||
if (!isValidJwkForEncryption(encryptionParams.getJwk(), encryptionParams.getAlg())) {
|
||||
String errorMessage = String.format("Invalid JWK: Not suitable for encryption with algorithm %s", encryptionParams.getAlg());
|
||||
LOGGER.debug(errorMessage);
|
||||
throw new BadRequestException(getErrorResponse(ErrorType.INVALID_ENCRYPTION_PARAMETERS, errorMessage));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if the provided JWK is suitable for encryption.
|
||||
*
|
||||
* @param jwk The JWK to validate
|
||||
* @param expectedAlg The expected algorithm (e.g., "RSA-OAEP")
|
||||
* @return true if the JWK is valid for encryption, false otherwise
|
||||
*/
|
||||
private boolean isValidJwkForEncryption(JWK jwk, String expectedAlg) {
|
||||
if (jwk == null) {
|
||||
return false;
|
||||
}
|
||||
if (expectedAlg != null && !expectedAlg.equals(jwk.getAlgorithm())) {
|
||||
return false;
|
||||
}
|
||||
String publicKeyUse = jwk.getPublicKeyUse();
|
||||
return publicKeyUse == null || "enc".equals(publicKeyUse);
|
||||
}
|
||||
|
||||
private boolean isSupportedEncryption(CredentialResponseEncryptionMetadata metadata, String alg, String enc) {
|
||||
if (metadata == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (metadata.getAlgValuesSupported() == null ||
|
||||
metadata.getEncValuesSupported() == null ||
|
||||
metadata.getAlgValuesSupported().isEmpty() ||
|
||||
metadata.getEncValuesSupported().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return metadata.getAlgValuesSupported().contains(alg) &&
|
||||
metadata.getEncValuesSupported().contains(enc);
|
||||
}
|
||||
|
||||
private AuthenticatedClientSessionModel getAuthenticatedClientSession() {
|
||||
AuthenticationManager.AuthResult authResult = getAuthResult();
|
||||
UserSessionModel userSessionModel = authResult.getSession();
|
||||
@@ -479,7 +657,7 @@ public class OID4VCIssuerEndpoint {
|
||||
|
||||
// Retrieve matching credential signer
|
||||
CredentialSigner<?> credentialSigner = session.getProvider(CredentialSigner.class,
|
||||
credentialConfig.getFormat());
|
||||
credentialConfig.getFormat());
|
||||
|
||||
return Optional.ofNullable(credentialSigner)
|
||||
.map(signer -> signer.signCredential(
|
||||
@@ -540,8 +718,12 @@ public class OID4VCIssuerEndpoint {
|
||||
}
|
||||
|
||||
private Response getErrorResponse(ErrorType errorType) {
|
||||
return getErrorResponse(errorType, null);
|
||||
}
|
||||
|
||||
private Response getErrorResponse(ErrorType errorType, String errorDescription) {
|
||||
var errorResponse = new ErrorResponse();
|
||||
errorResponse.setError(errorType);
|
||||
errorResponse.setError(errorType).setErrorDescription(errorDescription);
|
||||
return Response
|
||||
.status(Response.Status.BAD_REQUEST)
|
||||
.entity(errorResponse)
|
||||
@@ -570,7 +752,7 @@ public class OID4VCIssuerEndpoint {
|
||||
|
||||
// Build format-specific credential
|
||||
CredentialBody credentialBody = this.findCredentialBuilder(credentialConfig)
|
||||
.buildCredentialBody(vc, credentialConfig.getCredentialBuildConfig());
|
||||
.buildCredentialBody(vc, credentialConfig.getCredentialBuildConfig());
|
||||
|
||||
return new VCIssuanceContext()
|
||||
.setAuthResult(authResult)
|
||||
|
||||
@@ -21,6 +21,7 @@ import jakarta.ws.rs.core.UriInfo;
|
||||
import org.keycloak.constants.Oid4VciConstants;
|
||||
import org.keycloak.crypto.KeyUse;
|
||||
import org.keycloak.crypto.KeyWrapper;
|
||||
import org.keycloak.jose.jwe.JWEConstants;
|
||||
import org.keycloak.models.KeyManager;
|
||||
import org.keycloak.models.KeycloakContext;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
@@ -29,20 +30,22 @@ import org.keycloak.models.oid4vci.CredentialScopeModel;
|
||||
import org.keycloak.protocol.oid4vc.OID4VCLoginProtocolFactory;
|
||||
import org.keycloak.protocol.oid4vc.issuance.credentialbuilder.CredentialBuilder;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialIssuer;
|
||||
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialResponseEncryptionMetadata;
|
||||
import org.keycloak.protocol.oid4vc.model.SupportedCredentialConfiguration;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.urls.UrlType;
|
||||
import org.keycloak.wellknown.WellKnownProvider;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.keycloak.crypto.KeyType.RSA;
|
||||
|
||||
/**
|
||||
* {@link WellKnownProvider} implementation to provide the .well-known/openid-credential-issuer endpoint, offering
|
||||
* {@link WellKnownProvider} implementation to provide the .well-known/openid-credential-issuer endpoint, offering
|
||||
* the Credential Issuer Metadata as defined by the OID4VCI protocol
|
||||
* {@see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#section-11.2.2}
|
||||
*
|
||||
@@ -56,6 +59,8 @@ public class OID4VCIssuerWellKnownProvider implements WellKnownProvider {
|
||||
|
||||
protected final KeycloakSession keycloakSession;
|
||||
|
||||
public static final String ATTR_ENCRYPTION_REQUIRED = "oid4vci.encryption.required";
|
||||
|
||||
public OID4VCIssuerWellKnownProvider(KeycloakSession keycloakSession) {
|
||||
this.keycloakSession = keycloakSession;
|
||||
}
|
||||
@@ -85,27 +90,6 @@ public class OID4VCIssuerWellKnownProvider implements WellKnownProvider {
|
||||
return getIssuer(context) + "/protocol/" + OID4VCLoginProtocolFactory.PROTOCOL_ID + "/deferred_credential";
|
||||
}
|
||||
|
||||
private CredentialIssuer.CredentialResponseEncryption getCredentialResponseEncryption(KeycloakSession session) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
String algs = realm.getAttribute("credential_response_encryption.alg_values_supported");
|
||||
String encs = realm.getAttribute("credential_response_encryption.enc_values_supported");
|
||||
String required = realm.getAttribute("credential_response_encryption.encryption_required");
|
||||
if (algs != null && encs != null && required != null) {
|
||||
try {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
return new CredentialIssuer.CredentialResponseEncryption()
|
||||
.setAlgValuesSupported(mapper.readValue(algs, new TypeReference<List<String>>() {
|
||||
}))
|
||||
.setEncValuesSupported(mapper.readValue(encs, new TypeReference<List<String>>() {
|
||||
}))
|
||||
.setEncryptionRequired(Boolean.parseBoolean(required));
|
||||
} catch (Exception e) {
|
||||
LOGGER.warnf(e, "Failed to parse credential_response_encryption fields from realm attributes.");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private CredentialIssuer.BatchCredentialIssuance getBatchCredentialIssuance(KeycloakSession session) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
String batchSize = realm.getAttribute("batch_credential_issuance.batch_size");
|
||||
@@ -125,9 +109,71 @@ public class OID4VCIssuerWellKnownProvider implements WellKnownProvider {
|
||||
return realm.getAttribute("signed_metadata");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the credential response encryption высоко for the issuer.
|
||||
* Now determines supported algorithms from available realm keys.
|
||||
*
|
||||
* @param session The Keycloak session
|
||||
* @return The credential response encryption metadata
|
||||
*/
|
||||
public static CredentialResponseEncryptionMetadata getCredentialResponseEncryption(KeycloakSession session) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
CredentialResponseEncryptionMetadata metadata = new CredentialResponseEncryptionMetadata();
|
||||
|
||||
// Get supported algorithms from available encryption keys
|
||||
metadata.setAlgValuesSupported(getSupportedEncryptionAlgorithms(session));
|
||||
metadata.setEncValuesSupported(getSupportedEncryptionMethods());
|
||||
metadata.setEncryptionRequired(isEncryptionRequired(realm));
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the supported encryption algorithms from realm attributes.
|
||||
*/
|
||||
public static List<String> getSupportedEncryptionAlgorithms(KeycloakSession session) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
KeyManager keyManager = session.keys();
|
||||
|
||||
List<String> supportedEncryptionAlgorithms = keyManager.getKeysStream(realm)
|
||||
.filter(key -> KeyUse.ENC.equals(key.getUse()))
|
||||
.map(KeyWrapper::getAlgorithm)
|
||||
.filter(algorithm -> algorithm != null && !algorithm.isEmpty())
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (supportedEncryptionAlgorithms.isEmpty()) {
|
||||
boolean hasRsaKeys = keyManager.getKeysStream(realm)
|
||||
.filter(key -> KeyUse.ENC.equals(key.getUse()))
|
||||
.anyMatch(key -> RSA.equals(key.getType()));
|
||||
|
||||
if (hasRsaKeys) {
|
||||
supportedEncryptionAlgorithms.add(JWEConstants.RSA_OAEP);
|
||||
supportedEncryptionAlgorithms.add(JWEConstants.RSA_OAEP_256);
|
||||
}
|
||||
}
|
||||
|
||||
return supportedEncryptionAlgorithms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the supported encryption methods from realm attributes.
|
||||
*/
|
||||
private static List<String> getSupportedEncryptionMethods() {
|
||||
return List.of(JWEConstants.A256GCM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether encryption is required from realm attributes.
|
||||
*/
|
||||
private static boolean isEncryptionRequired(RealmModel realm) {
|
||||
String required = realm.getAttribute(ATTR_ENCRYPTION_REQUIRED);
|
||||
return Boolean.parseBoolean(required);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the supported credentials from the current session.
|
||||
* It will take into account the configured {@link CredentialBuilder}'s and there supported format
|
||||
* It will take into account the configured {@link CredentialBuilder}'s and their supported format
|
||||
* and the credentials supported by the clients available in the session.
|
||||
*/
|
||||
public static Map<String, SupportedCredentialConfiguration> getSupportedCredentials(KeycloakSession keycloakSession) {
|
||||
@@ -136,17 +182,16 @@ public class OID4VCIssuerWellKnownProvider implements WellKnownProvider {
|
||||
RealmModel realm = keycloakSession.getContext().getRealm();
|
||||
Map<String, SupportedCredentialConfiguration> supportedCredentialConfigurations =
|
||||
keycloakSession.clientScopes()
|
||||
.getClientScopesByProtocol(realm, Oid4VciConstants.OID4VC_PROTOCOL)
|
||||
.map(CredentialScopeModel::new)
|
||||
.map(clientScope -> {
|
||||
return SupportedCredentialConfiguration.parse(keycloakSession,
|
||||
clientScope,
|
||||
globalSupportedSigningAlgorithms
|
||||
);
|
||||
})
|
||||
.collect(Collectors.toMap(SupportedCredentialConfiguration::getId, sc -> sc, (sc1, sc2) -> sc1));
|
||||
.getClientScopesByProtocol(realm, Oid4VciConstants.OID4VC_PROTOCOL)
|
||||
.map(CredentialScopeModel::new)
|
||||
.map(clientScope -> {
|
||||
return SupportedCredentialConfiguration.parse(keycloakSession,
|
||||
clientScope,
|
||||
globalSupportedSigningAlgorithms
|
||||
);
|
||||
})
|
||||
.collect(Collectors.toMap(SupportedCredentialConfiguration::getId, sc -> sc, (sc1, sc2) -> sc1));
|
||||
|
||||
// Aggregating attributes. Having realm attributes take preference.
|
||||
return supportedCredentialConfigurations;
|
||||
}
|
||||
|
||||
@@ -154,8 +199,8 @@ public class OID4VCIssuerWellKnownProvider implements WellKnownProvider {
|
||||
CredentialScopeModel credentialModel) {
|
||||
List<String> globalSupportedSigningAlgorithms = getSupportedSignatureAlgorithms(keycloakSession);
|
||||
return SupportedCredentialConfiguration.parse(keycloakSession,
|
||||
credentialModel,
|
||||
globalSupportedSigningAlgorithms);
|
||||
credentialModel,
|
||||
globalSupportedSigningAlgorithms);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,7 +210,6 @@ public class OID4VCIssuerWellKnownProvider implements WellKnownProvider {
|
||||
UriInfo frontendUriInfo = context.getUri(UrlType.FRONTEND);
|
||||
return Urls.realmIssuer(frontendUriInfo.getBaseUri(),
|
||||
context.getRealm().getName());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -52,9 +52,6 @@ public class CredentialIssuer {
|
||||
@JsonProperty("notification_endpoint")
|
||||
private String notificationEndpoint;
|
||||
|
||||
@JsonProperty("credential_response_encryption")
|
||||
private CredentialResponseEncryption credentialResponseEncryption;
|
||||
|
||||
@JsonProperty("batch_credential_issuance")
|
||||
private BatchCredentialIssuance batchCredentialIssuance;
|
||||
|
||||
@@ -67,6 +64,9 @@ public class CredentialIssuer {
|
||||
@JsonProperty("display")
|
||||
private List<DisplayObject> display;
|
||||
|
||||
@JsonProperty("credential_response_encryption")
|
||||
private CredentialResponseEncryptionMetadata credentialResponseEncryption;
|
||||
|
||||
public String getCredentialIssuer() {
|
||||
return credentialIssuer;
|
||||
}
|
||||
@@ -121,15 +121,6 @@ public class CredentialIssuer {
|
||||
return this;
|
||||
}
|
||||
|
||||
public CredentialResponseEncryption getCredentialResponseEncryption() {
|
||||
return credentialResponseEncryption;
|
||||
}
|
||||
|
||||
public CredentialIssuer setCredentialResponseEncryption(CredentialResponseEncryption credentialResponseEncryption) {
|
||||
this.credentialResponseEncryption = credentialResponseEncryption;
|
||||
return this;
|
||||
}
|
||||
|
||||
public BatchCredentialIssuance getBatchCredentialIssuance() {
|
||||
return batchCredentialIssuance;
|
||||
}
|
||||
@@ -169,46 +160,13 @@ public class CredentialIssuer {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the credential_response_encryption metadata parameter.
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class CredentialResponseEncryption {
|
||||
@JsonProperty("alg_values_supported")
|
||||
private List<String> algValuesSupported;
|
||||
public CredentialResponseEncryptionMetadata getCredentialResponseEncryption() {
|
||||
return credentialResponseEncryption;
|
||||
}
|
||||
|
||||
@JsonProperty("enc_values_supported")
|
||||
private List<String> encValuesSupported;
|
||||
|
||||
@JsonProperty("encryption_required")
|
||||
private Boolean encryptionRequired;
|
||||
|
||||
public List<String> getAlgValuesSupported() {
|
||||
return algValuesSupported;
|
||||
}
|
||||
|
||||
public CredentialResponseEncryption setAlgValuesSupported(List<String> algValuesSupported) {
|
||||
this.algValuesSupported = algValuesSupported;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<String> getEncValuesSupported() {
|
||||
return encValuesSupported;
|
||||
}
|
||||
|
||||
public CredentialResponseEncryption setEncValuesSupported(List<String> encValuesSupported) {
|
||||
this.encValuesSupported = encValuesSupported;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Boolean getEncryptionRequired() {
|
||||
return encryptionRequired;
|
||||
}
|
||||
|
||||
public CredentialResponseEncryption setEncryptionRequired(Boolean encryptionRequired) {
|
||||
this.encryptionRequired = encryptionRequired;
|
||||
return this;
|
||||
}
|
||||
public CredentialIssuer setCredentialResponseEncryption(CredentialResponseEncryptionMetadata credentialResponseEncryption) {
|
||||
this.credentialResponseEncryption = credentialResponseEncryption;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,7 +31,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
|
||||
/**
|
||||
* Represents a CredentialRequest according to OID4VCI
|
||||
* Represents a CredentialRequest according to OID4VCI
|
||||
* {@see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-request}
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
@@ -57,6 +57,11 @@ public class CredentialRequest {
|
||||
@JsonProperty("credential_definition")
|
||||
private CredentialDefinition credentialDefinition;
|
||||
|
||||
@JsonProperty("credential_response_encryption")
|
||||
private CredentialResponseEncryption credentialResponseEncryption;
|
||||
|
||||
private String format;
|
||||
|
||||
public String getCredentialIdentifier() {
|
||||
return credentialIdentifier;
|
||||
}
|
||||
@@ -93,6 +98,24 @@ public class CredentialRequest {
|
||||
return this;
|
||||
}
|
||||
|
||||
public CredentialResponseEncryption getCredentialResponseEncryption() {
|
||||
return credentialResponseEncryption;
|
||||
}
|
||||
|
||||
public CredentialRequest setCredentialResponseEncryption(CredentialResponseEncryption credentialResponseEncryption) {
|
||||
this.credentialResponseEncryption = credentialResponseEncryption;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getFormat() {
|
||||
return format;
|
||||
}
|
||||
|
||||
public CredentialRequest setFormat(String format) {
|
||||
this.format = format;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Optional<CredentialScopeModel> findCredentialScope(KeycloakSession keycloakSession) {
|
||||
Map<String, String> searchAttributeMap =
|
||||
Optional.ofNullable(credentialConfigurationId)
|
||||
@@ -105,9 +128,9 @@ public class CredentialRequest {
|
||||
RealmModel currentRealm = keycloakSession.getContext().getRealm();
|
||||
final boolean useOrExpression = false;
|
||||
return keycloakSession.clientScopes()
|
||||
.getClientScopesByAttributes(currentRealm, searchAttributeMap, useOrExpression)
|
||||
.map(CredentialScopeModel::new)
|
||||
.findAny();
|
||||
.getClientScopesByAttributes(currentRealm, searchAttributeMap, useOrExpression)
|
||||
.map(CredentialScopeModel::new)
|
||||
.findAny();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.protocol.oid4vc.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
|
||||
/**
|
||||
* Represents the credential_response_encryption object in a Credential Request.
|
||||
* {@see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-request}
|
||||
*
|
||||
* @author <a href="mailto:Bertrand.Ogen@adorsys.com">Bertrand Ogen</a>
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class CredentialResponseEncryption {
|
||||
|
||||
/**
|
||||
* REQUIRED. A string specifying the algorithm to be used for encrypting the Credential Response,
|
||||
* as per the supported key management algorithms in the Credential Issuer Metadata.
|
||||
*/
|
||||
private String alg;
|
||||
|
||||
/**
|
||||
* REQUIRED. A string specifying the content encryption algorithm to be used for encrypting the
|
||||
* Credential Response, as per the supported content encryption algorithms in the Credential Issuer Metadata.
|
||||
*/
|
||||
private String enc;
|
||||
|
||||
/**
|
||||
* REQUIRED if credential_response_encryption is included in the Credential Request.
|
||||
* A JSON Web Key (JWK) that represents the public key to which the Credential Response will be encrypted.
|
||||
*/
|
||||
private JWK jwk;
|
||||
|
||||
public String getAlg() {
|
||||
return alg;
|
||||
}
|
||||
|
||||
public CredentialResponseEncryption setAlg(String alg) {
|
||||
this.alg = alg;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getEnc() {
|
||||
return enc;
|
||||
}
|
||||
|
||||
public CredentialResponseEncryption setEnc(String enc) {
|
||||
this.enc = enc;
|
||||
return this;
|
||||
}
|
||||
|
||||
public JWK getJwk() {
|
||||
return jwk;
|
||||
}
|
||||
|
||||
public CredentialResponseEncryption setJwk(JWK jwk) {
|
||||
this.jwk = jwk;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.protocol.oid4vc.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents the credential_response_encryption metadata for an OID4VCI Credential Issuer.
|
||||
* {@see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-15.html#name-credential-issuer-metadata-p}
|
||||
*
|
||||
* @author <a href="mailto:Bertrand.Ogen@adorsys.com">Bertrand Ogen</a>
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class CredentialResponseEncryptionMetadata {
|
||||
|
||||
@JsonProperty("alg_values_supported")
|
||||
private List<String> algValuesSupported;
|
||||
|
||||
@JsonProperty("enc_values_supported")
|
||||
private List<String> encValuesSupported;
|
||||
|
||||
@JsonProperty("encryption_required")
|
||||
private Boolean encryptionRequired;
|
||||
|
||||
public List<String> getAlgValuesSupported() {
|
||||
return algValuesSupported;
|
||||
}
|
||||
|
||||
public void setAlgValuesSupported(List<String> algValuesSupported) {
|
||||
this.algValuesSupported = algValuesSupported;
|
||||
}
|
||||
|
||||
public List<String> getEncValuesSupported() {
|
||||
return encValuesSupported;
|
||||
}
|
||||
|
||||
public void setEncValuesSupported(List<String> encValuesSupported) {
|
||||
this.encValuesSupported = encValuesSupported;
|
||||
}
|
||||
|
||||
public Boolean getEncryptionRequired() {
|
||||
return encryptionRequired;
|
||||
}
|
||||
|
||||
public void setEncryptionRequired(Boolean encryptionRequired) {
|
||||
this.encryptionRequired = encryptionRequired;
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ public enum ErrorType {
|
||||
UNSUPPORTED_CREDENTIAL_TYPE("unsupported_credential_type"),
|
||||
UNSUPPORTED_CREDENTIAL_FORMAT("unsupported_credential_format"),
|
||||
INVALID_PROOF("invalid_proof"),
|
||||
INVALID_ENCRYPTION_PARAMETER("invalid_encryption_parameters"),
|
||||
INVALID_ENCRYPTION_PARAMETERS("invalid_encryption_parameters"),
|
||||
MISSING_CREDENTIAL_CONFIG("missing_credential_config"),
|
||||
MISSING_CREDENTIAL_IDENTIFIER_AND_CONFIGURATION_ID("missing_credential_identifier_and_configuration_id");
|
||||
|
||||
|
||||
@@ -38,10 +38,15 @@ import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.common.crypto.CryptoIntegration;
|
||||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.common.util.SecretGenerator;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.constants.Oid4VciConstants;
|
||||
import org.keycloak.jose.jwe.JWE;
|
||||
import org.keycloak.jose.jwe.JWEException;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
import org.keycloak.jose.jwk.RSAPublicJWK;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
import org.keycloak.models.oid4vci.CredentialScopeModel;
|
||||
@@ -60,7 +65,6 @@ import org.keycloak.protocol.oid4vc.model.DisplayObject;
|
||||
import org.keycloak.protocol.oid4vc.model.Format;
|
||||
import org.keycloak.protocol.oid4vc.model.SupportedCredentialConfiguration;
|
||||
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.utils.OAuth2Code;
|
||||
import org.keycloak.protocol.oidc.utils.OAuth2CodeParser;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
@@ -86,6 +90,11 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -99,6 +108,8 @@ import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.keycloak.jose.jwe.JWEConstants.RSA_OAEP;
|
||||
import static org.keycloak.jose.jwe.JWEConstants.RSA_OAEP_256;
|
||||
import static org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint.CREDENTIAL_OFFER_URI_CODE_SCOPE;
|
||||
import static org.keycloak.testsuite.forms.PassThroughClientAuthenticator.clientId;
|
||||
|
||||
@@ -327,6 +338,59 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
|
||||
user.logout();
|
||||
}
|
||||
|
||||
public static JWK generateRsaJwk() throws NoSuchAlgorithmException {
|
||||
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
|
||||
keyGen.initialize(2048);
|
||||
KeyPair keyPair = keyGen.generateKeyPair();
|
||||
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
|
||||
|
||||
String modulus = Base64Url.encode(publicKey.getModulus().toByteArray());
|
||||
String exponent = Base64Url.encode(publicKey.getPublicExponent().toByteArray());
|
||||
|
||||
RSAPublicJWK jwk = new RSAPublicJWK();
|
||||
jwk.setKeyType("RSA");
|
||||
jwk.setPublicKeyUse("enc");
|
||||
jwk.setAlgorithm("RSA-OAEP");
|
||||
jwk.setModulus(modulus);
|
||||
jwk.setPublicExponent(exponent);
|
||||
|
||||
return jwk;
|
||||
}
|
||||
|
||||
public static Map<String, Object> generateRsaJwkWithPrivateKey() throws NoSuchAlgorithmException {
|
||||
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
|
||||
keyGen.initialize(2048);
|
||||
KeyPair keyPair = keyGen.generateKeyPair();
|
||||
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
|
||||
PrivateKey privateKey = keyPair.getPrivate();
|
||||
|
||||
String modulus = Base64Url.encode(publicKey.getModulus().toByteArray());
|
||||
String exponent = Base64Url.encode(publicKey.getPublicExponent().toByteArray());
|
||||
|
||||
RSAPublicJWK jwk = new RSAPublicJWK();
|
||||
jwk.setKeyType("RSA");
|
||||
jwk.setPublicKeyUse("enc");
|
||||
jwk.setAlgorithm("RSA-OAEP");
|
||||
jwk.setModulus(modulus);
|
||||
jwk.setPublicExponent(exponent);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("jwk", jwk);
|
||||
result.put("privateKey", privateKey);
|
||||
return result;
|
||||
}
|
||||
|
||||
protected static CredentialResponse decryptJweResponse(String encryptedResponse, PrivateKey privateKey) throws IOException, JWEException {
|
||||
assertNotNull("Encrypted response should not be null", encryptedResponse);
|
||||
assertEquals("Response should be a JWE", 5, encryptedResponse.split("\\.").length);
|
||||
|
||||
JWE jwe = new JWE(encryptedResponse);
|
||||
jwe.getKeyStorage().setDecryptionKey(privateKey);
|
||||
jwe.verifyAndDecodeJwe();
|
||||
byte[] decryptedContent = jwe.getContent();
|
||||
return JsonSerialization.readValue(decryptedContent, CredentialResponse.class);
|
||||
}
|
||||
|
||||
void setClientOid4vciEnabled(String clientId, boolean enabled) {
|
||||
ClientRepresentation clientRepresentation = adminClient.realm(TEST_REALM_NAME).clients().findByClientId(clientId).get(0);
|
||||
ClientResource clientResource = adminClient.realm(TEST_REALM_NAME).clients().get(clientRepresentation.getId());
|
||||
@@ -456,6 +520,11 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
|
||||
|
||||
testRealm.getComponents().add("org.keycloak.keys.KeyProvider", getKeyProvider());
|
||||
|
||||
testRealm.getComponents().add("org.keycloak.keys.KeyProvider",
|
||||
getRsaEncKeyProvider(RSA_OAEP_256, "enc-key-oaep256"));
|
||||
testRealm.getComponents().add("org.keycloak.keys.KeyProvider",
|
||||
getRsaEncKeyProvider(RSA_OAEP, "enc-key-oaep"));
|
||||
|
||||
// Find existing client representation
|
||||
ClientRepresentation existingClient = testRealm.getClients().stream()
|
||||
.filter(client -> client.getClientId().equals(clientId))
|
||||
|
||||
@@ -19,12 +19,18 @@ package org.keycloak.testsuite.oid4vc.issuance.signing;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import jakarta.ws.rs.client.Client;
|
||||
import jakarta.ws.rs.client.WebTarget;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.UriBuilder;
|
||||
import org.hamcrest.MatcherAssert;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.crypto.Algorithm;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.oid4vci.CredentialScopeModel;
|
||||
import org.keycloak.models.oid4vci.Oid4vcProtocolMapperModel;
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
@@ -32,11 +38,13 @@ import org.keycloak.protocol.ProtocolMapper;
|
||||
import org.keycloak.protocol.oid4vc.OID4VCLoginProtocolFactory;
|
||||
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint;
|
||||
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerWellKnownProvider;
|
||||
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerWellKnownProviderFactory;
|
||||
import org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCMapper;
|
||||
import org.keycloak.protocol.oid4vc.model.Claim;
|
||||
import org.keycloak.protocol.oid4vc.model.ClaimDisplay;
|
||||
import org.keycloak.protocol.oid4vc.model.Claims;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialIssuer;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialResponseEncryptionMetadata;
|
||||
import org.keycloak.protocol.oid4vc.model.DisplayObject;
|
||||
import org.keycloak.protocol.oid4vc.model.Format;
|
||||
import org.keycloak.protocol.oid4vc.model.ProofTypesSupported;
|
||||
@@ -45,12 +53,17 @@ import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
||||
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.services.resources.RealmsResource;
|
||||
import org.keycloak.testsuite.arquillian.SuiteContext;
|
||||
import org.keycloak.testsuite.client.KeycloakTestingClient;
|
||||
import org.keycloak.testsuite.util.AdminClientUtil;
|
||||
import org.keycloak.testsuite.util.oauth.OAuthClient;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.utils.StringUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
@@ -58,22 +71,38 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.keycloak.jose.jwe.JWEConstants.A256GCM;
|
||||
import static org.keycloak.jose.jwe.JWEConstants.RSA_OAEP;
|
||||
import static org.keycloak.jose.jwe.JWEConstants.RSA_OAEP_256;
|
||||
import static org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerWellKnownProvider.ATTR_ENCRYPTION_REQUIRED;
|
||||
|
||||
|
||||
public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest {
|
||||
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
Map<String, String> attributes = Optional.ofNullable(testRealm.getAttributes()).orElseGet(HashMap::new);
|
||||
attributes.put("credential_response_encryption.alg_values_supported", "[\"RSA-OAEP\"]");
|
||||
attributes.put("credential_response_encryption.enc_values_supported", "[\"A256GCM\"]");
|
||||
attributes.put("credential_response_encryption.encryption_required", "true");
|
||||
attributes.put("batch_credential_issuance.batch_size", "10");
|
||||
attributes.put("signed_metadata", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.XYZ123abc");
|
||||
attributes.put(ATTR_ENCRYPTION_REQUIRED, "true");
|
||||
testRealm.setAttributes(attributes);
|
||||
|
||||
if (testRealm.getComponents() == null) {
|
||||
testRealm.setComponents(new MultivaluedHashMap<>());
|
||||
}
|
||||
|
||||
testRealm.getComponents().add("org.keycloak.keys.KeyProvider",
|
||||
getRsaEncKeyProvider(RSA_OAEP_256, "enc-key-oaep256"));
|
||||
testRealm.getComponents().add("org.keycloak.keys.KeyProvider",
|
||||
getRsaEncKeyProvider(RSA_OAEP, "enc-key-oaep"));
|
||||
|
||||
super.configureTestRealm(testRealm);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* this test will use the configured scopes {@link #jwtTypeCredentialClientScope} and
|
||||
* This test uses the configured scopes {@link #jwtTypeCredentialClientScope} and
|
||||
* {@link #sdJwtTypeCredentialClientScope} to verify that the metadata endpoint is presenting the expected data
|
||||
*/
|
||||
@Test
|
||||
@@ -82,50 +111,46 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
|
||||
|
||||
Assert.assertEquals(getRealmPath(TEST_REALM_NAME), credentialIssuer.getCredentialIssuer());
|
||||
Assert.assertEquals(getBasePath(TEST_REALM_NAME) + OID4VCIssuerEndpoint.CREDENTIAL_PATH,
|
||||
credentialIssuer.getCredentialEndpoint());
|
||||
credentialIssuer.getCredentialEndpoint());
|
||||
Assert.assertNull("Display was not configured", credentialIssuer.getDisplay());
|
||||
Assert.assertEquals("Authorization Server should have the realm-address.",
|
||||
1,
|
||||
credentialIssuer.getAuthorizationServers().size());
|
||||
1,
|
||||
credentialIssuer.getAuthorizationServers().size());
|
||||
Assert.assertEquals("Authorization Server should point to the realm-address.",
|
||||
getRealmPath(TEST_REALM_NAME),
|
||||
credentialIssuer.getAuthorizationServers().get(0));
|
||||
getRealmPath(TEST_REALM_NAME),
|
||||
credentialIssuer.getAuthorizationServers().get(0));
|
||||
|
||||
// Check credential_response_encryption
|
||||
CredentialIssuer.CredentialResponseEncryption encryption = credentialIssuer.getCredentialResponseEncryption();
|
||||
CredentialResponseEncryptionMetadata encryption = credentialIssuer.getCredentialResponseEncryption();
|
||||
Assert.assertNotNull("credential_response_encryption should be present", encryption);
|
||||
Assert.assertEquals(List.of("RSA-OAEP"), encryption.getAlgValuesSupported());
|
||||
Assert.assertEquals(List.of("A256GCM"), encryption.getEncValuesSupported());
|
||||
Assert.assertEquals(List.of(RSA_OAEP, RSA_OAEP_256), encryption.getAlgValuesSupported());
|
||||
Assert.assertEquals(List.of(A256GCM), encryption.getEncValuesSupported());
|
||||
Assert.assertTrue("encryption_required should be true", encryption.getEncryptionRequired());
|
||||
|
||||
// Check batch_credential_issuance
|
||||
CredentialIssuer.BatchCredentialIssuance batch = credentialIssuer.getBatchCredentialIssuance();
|
||||
Assert.assertNotNull("batch_credential_issuance should be present", batch);
|
||||
Assert.assertEquals(Integer.valueOf(10), batch.getBatchSize());
|
||||
|
||||
// Check signed_metadata
|
||||
Assert.assertEquals(
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.XYZ123abc",
|
||||
credentialIssuer.getSignedMetadata()
|
||||
);
|
||||
|
||||
for (ClientScopeRepresentation clientScope : List.of(jwtTypeCredentialClientScope,
|
||||
sdJwtTypeCredentialClientScope,
|
||||
minimalJwtTypeCredentialClientScope)) {
|
||||
sdJwtTypeCredentialClientScope,
|
||||
minimalJwtTypeCredentialClientScope)) {
|
||||
compareMetadataToClientScope(credentialIssuer, clientScope);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* this test will make sure that the default values are correctly added into the metadata endpoint
|
||||
* This test will make sure that the default values are correctly added into the metadata endpoint
|
||||
*/
|
||||
@Test
|
||||
public void testMinimalJwtCredentialHardcodedTest()
|
||||
{
|
||||
public void testMinimalJwtCredentialHardcodedTest() {
|
||||
ClientScopeRepresentation clientScope = minimalJwtTypeCredentialClientScope;
|
||||
CredentialIssuer credentialIssuer = getCredentialIssuerMetadata();
|
||||
SupportedCredentialConfiguration supportedConfig = credentialIssuer.getCredentialsSupported()
|
||||
.get(clientScope.getName());
|
||||
.get(clientScope.getName());
|
||||
Assert.assertNotNull(supportedConfig);
|
||||
Assert.assertEquals(Format.SD_JWT_VC, supportedConfig.getFormat());
|
||||
Assert.assertEquals(clientScope.getName(), supportedConfig.getScope());
|
||||
@@ -139,18 +164,77 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
|
||||
compareClaims(supportedConfig.getFormat(), supportedConfig.getClaims(), clientScope.getProtocolMappers());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testCredentialIssuerMetadataFields() {
|
||||
KeycloakTestingClient testingClient = this.testingClient;
|
||||
|
||||
testingClient
|
||||
.server(TEST_REALM_NAME)
|
||||
.run(session -> {
|
||||
CredentialIssuer issuer = getCredentialIssuer(session);
|
||||
|
||||
CredentialResponseEncryptionMetadata encryption = issuer.getCredentialResponseEncryption();
|
||||
Assert.assertNotNull(encryption);
|
||||
|
||||
Assert.assertTrue(encryption.getAlgValuesSupported().contains(RSA_OAEP));
|
||||
Assert.assertTrue("Supported encryption methods should include A256GCM", encryption.getEncValuesSupported().contains(A256GCM));
|
||||
Assert.assertTrue(encryption.getEncryptionRequired());
|
||||
Assert.assertEquals(Integer.valueOf(10), issuer.getBatchCredentialIssuance().getBatchSize());
|
||||
Assert.assertEquals("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.XYZ123abc",
|
||||
issuer.getSignedMetadata());
|
||||
});
|
||||
}
|
||||
|
||||
private static CredentialIssuer getCredentialIssuer(KeycloakSession session) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
|
||||
realm.setAttribute(ATTR_ENCRYPTION_REQUIRED, "true");
|
||||
realm.setAttribute("batch_credential_issuance.batch_size", "10");
|
||||
realm.setAttribute("signed_metadata", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.XYZ123abc");
|
||||
|
||||
OID4VCIssuerWellKnownProvider provider = new OID4VCIssuerWellKnownProvider(session);
|
||||
return (CredentialIssuer) provider.getConfig();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIssuerMetadataIncludesEncryptionSupport() throws IOException {
|
||||
try (Client client = AdminClientUtil.createResteasyClient()) {
|
||||
UriBuilder builder = UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT);
|
||||
URI oid4vciDiscoveryUri = RealmsResource.wellKnownProviderUrl(builder)
|
||||
.build(TEST_REALM_NAME, OID4VCIssuerWellKnownProviderFactory.PROVIDER_ID);
|
||||
WebTarget oid4vciDiscoveryTarget = client.target(oid4vciDiscoveryUri);
|
||||
|
||||
try (Response discoveryResponse = oid4vciDiscoveryTarget.request().get()) {
|
||||
CredentialIssuer oid4vciIssuerConfig = JsonSerialization.readValue(
|
||||
discoveryResponse.readEntity(String.class), CredentialIssuer.class);
|
||||
|
||||
Assert.assertNotNull("Encryption support should be advertised in metadata",
|
||||
oid4vciIssuerConfig.getCredentialResponseEncryption());
|
||||
Assert.assertFalse("Supported algorithms should not be empty",
|
||||
oid4vciIssuerConfig.getCredentialResponseEncryption().getAlgValuesSupported().isEmpty());
|
||||
Assert.assertFalse("Supported encryption methods should not be empty",
|
||||
oid4vciIssuerConfig.getCredentialResponseEncryption().getEncValuesSupported().isEmpty());
|
||||
Assert.assertTrue("Supported algorithms should include RSA-OAEP",
|
||||
oid4vciIssuerConfig.getCredentialResponseEncryption().getAlgValuesSupported().contains("RSA-OAEP"));
|
||||
Assert.assertTrue("Supported encryption methods should include A256GCM",
|
||||
oid4vciIssuerConfig.getCredentialResponseEncryption().getEncValuesSupported().contains("A256GCM"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void compareMetadataToClientScope(CredentialIssuer credentialIssuer, ClientScopeRepresentation clientScope) {
|
||||
String credentialConfigurationId = Optional.ofNullable(clientScope.getAttributes()
|
||||
.get(CredentialScopeModel.CONFIGURATION_ID))
|
||||
.orElse(clientScope.getName());
|
||||
.get(CredentialScopeModel.CONFIGURATION_ID))
|
||||
.orElse(clientScope.getName());
|
||||
SupportedCredentialConfiguration supportedConfig = credentialIssuer.getCredentialsSupported()
|
||||
.get(credentialConfigurationId);
|
||||
.get(credentialConfigurationId);
|
||||
Assert.assertNotNull("Configuration of type '" + credentialConfigurationId + "' must be present",
|
||||
supportedConfig);
|
||||
supportedConfig);
|
||||
Assert.assertEquals(credentialConfigurationId, supportedConfig.getId());
|
||||
|
||||
String expectedFormat = Optional.ofNullable(clientScope.getAttributes().get(CredentialScopeModel.FORMAT))
|
||||
.orElse(Format.SD_JWT_VC);
|
||||
.orElse(Format.SD_JWT_VC);
|
||||
Assert.assertEquals(expectedFormat, supportedConfig.getFormat());
|
||||
|
||||
Assert.assertEquals(clientScope.getName(), supportedConfig.getScope());
|
||||
@@ -158,36 +242,36 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
|
||||
// TODO this is still hardcoded
|
||||
Assert.assertEquals(1, supportedConfig.getCryptographicBindingMethodsSupported().size());
|
||||
Assert.assertEquals(CredentialScopeModel.CRYPTOGRAPHIC_BINDING_METHODS_DEFAULT,
|
||||
supportedConfig.getCryptographicBindingMethodsSupported().get(0));
|
||||
supportedConfig.getCryptographicBindingMethodsSupported().get(0));
|
||||
}
|
||||
|
||||
compareDisplay(supportedConfig, clientScope);
|
||||
|
||||
String expectedVct = Optional.ofNullable(clientScope.getAttributes().get(CredentialScopeModel.VCT))
|
||||
.orElse(clientScope.getName());
|
||||
.orElse(clientScope.getName());
|
||||
Assert.assertEquals(expectedVct, supportedConfig.getVct());
|
||||
|
||||
Assert.assertNotNull(supportedConfig.getCredentialDefinition());
|
||||
Assert.assertNotNull(supportedConfig.getCredentialDefinition().getType());
|
||||
List<String> credentialDefinitionTypes = Optional.ofNullable(clientScope.getAttributes()
|
||||
.get(CredentialScopeModel.TYPES))
|
||||
.map(s -> s.split(","))
|
||||
.map(Arrays::asList)
|
||||
.orElseGet(() -> List.of(clientScope.getName()));
|
||||
.get(CredentialScopeModel.TYPES))
|
||||
.map(s -> s.split(","))
|
||||
.map(Arrays::asList)
|
||||
.orElseGet(() -> List.of(clientScope.getName()));
|
||||
Assert.assertEquals(credentialDefinitionTypes.size(),
|
||||
supportedConfig.getCredentialDefinition().getType().size());
|
||||
supportedConfig.getCredentialDefinition().getType().size());
|
||||
|
||||
MatcherAssert.assertThat(supportedConfig.getCredentialDefinition().getContext(),
|
||||
Matchers.containsInAnyOrder(credentialDefinitionTypes.toArray()));
|
||||
Matchers.containsInAnyOrder(credentialDefinitionTypes.toArray()));
|
||||
List<String> credentialDefinitionContexts = Optional.ofNullable(clientScope.getAttributes()
|
||||
.get(CredentialScopeModel.CONTEXTS))
|
||||
.map(s -> s.split(","))
|
||||
.map(Arrays::asList)
|
||||
.orElseGet(() -> List.of(clientScope.getName()));
|
||||
.get(CredentialScopeModel.CONTEXTS))
|
||||
.map(s -> s.split(","))
|
||||
.map(Arrays::asList)
|
||||
.orElseGet(() -> List.of(clientScope.getName()));
|
||||
Assert.assertEquals(credentialDefinitionContexts.size(),
|
||||
supportedConfig.getCredentialDefinition().getContext().size());
|
||||
supportedConfig.getCredentialDefinition().getContext().size());
|
||||
MatcherAssert.assertThat(supportedConfig.getCredentialDefinition().getContext(),
|
||||
Matchers.containsInAnyOrder(credentialDefinitionTypes.toArray()));
|
||||
Matchers.containsInAnyOrder(credentialDefinitionTypes.toArray()));
|
||||
|
||||
List<String> signingAlgsSupported = new ArrayList<>(supportedConfig.getCredentialSigningAlgValuesSupported());
|
||||
String proofTypesSupportedString = supportedConfig.getProofTypesSupported().toJsonString();
|
||||
@@ -195,14 +279,13 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
|
||||
try {
|
||||
withCausePropagation(() -> testingClient.server(TEST_REALM_NAME).run((session -> {
|
||||
ProofTypesSupported expectedProofTypesSupported = ProofTypesSupported.parse(session,
|
||||
List.of(Algorithm.RS256));
|
||||
List.of(Algorithm.RS256));
|
||||
Assert.assertEquals(expectedProofTypesSupported,
|
||||
ProofTypesSupported.fromJsonString(proofTypesSupportedString));
|
||||
ProofTypesSupported.fromJsonString(proofTypesSupportedString));
|
||||
|
||||
List<String> expectedSigningAlgs = OID4VCIssuerWellKnownProvider.getSupportedSignatureAlgorithms(session);
|
||||
MatcherAssert.assertThat(signingAlgsSupported,
|
||||
Matchers.containsInAnyOrder(expectedSigningAlgs.toArray()));
|
||||
|
||||
Matchers.containsInAnyOrder(expectedSigningAlgs.toArray()));
|
||||
})));
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
@@ -227,12 +310,12 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
|
||||
|
||||
Assert.assertEquals(expectedDisplayObjectList.size(), supportedConfig.getDisplay().size());
|
||||
MatcherAssert.assertThat("Must contain all expected display-objects",
|
||||
supportedConfig.getDisplay(),
|
||||
Matchers.containsInAnyOrder(expectedDisplayObjectList.toArray()));
|
||||
supportedConfig.getDisplay(),
|
||||
Matchers.containsInAnyOrder(expectedDisplayObjectList.toArray()));
|
||||
}
|
||||
|
||||
/**
|
||||
* each claim representation from the metadata is based on a protocol-mapper which we compare here
|
||||
* Each claim representation from the metadata is based on a protocol-mapper which we compare here
|
||||
*/
|
||||
private void compareClaims(String credentialFormat,
|
||||
Claims originalClaims,
|
||||
@@ -245,7 +328,7 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
|
||||
withCausePropagation(() -> testingClient.server(TEST_REALM_NAME).run((session -> {
|
||||
Claims actualClaims = fromJsonString(claimsString, Claims.class);
|
||||
List<ProtocolMapperRepresentation> protocolMappers = fromJsonString(protocolMappersString,
|
||||
new SerializableProtocolMapperReference());
|
||||
new SerializableProtocolMapperReference());
|
||||
// check only protocol-mappers of type oid4vc
|
||||
protocolMappers = protocolMappers.stream().filter(protocolMapper -> {
|
||||
return OID4VCLoginProtocolFactory.PROTOCOL_ID.equals(protocolMapper.getProtocol());
|
||||
@@ -253,13 +336,13 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
|
||||
|
||||
for (ProtocolMapperRepresentation protocolMapper : protocolMappers) {
|
||||
OID4VCMapper mapper = (OID4VCMapper) session.getProvider(ProtocolMapper.class,
|
||||
protocolMapper.getProtocolMapper());
|
||||
protocolMapper.getProtocolMapper());
|
||||
ProtocolMapperModel protocolMapperModel = new ProtocolMapperModel();
|
||||
protocolMapperModel.setConfig(protocolMapper.getConfig());
|
||||
mapper.setMapperModel(protocolMapperModel, credentialFormat);
|
||||
Claim claim = actualClaims.stream()
|
||||
.filter(c -> c.getPath().equals(mapper.getMetadataAttributePath()))
|
||||
.findFirst().orElse(null);
|
||||
.filter(c -> c.getPath().equals(mapper.getMetadataAttributePath()))
|
||||
.findFirst().orElse(null);
|
||||
if (mapper.includeInMetadata()) {
|
||||
Assert.assertNotNull("There should be a claim matching the protocol-mappers config!", claim);
|
||||
}
|
||||
@@ -269,13 +352,13 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
|
||||
continue;
|
||||
}
|
||||
Assert.assertEquals(claim.isMandatory(),
|
||||
Optional.ofNullable(protocolMapper.getConfig()
|
||||
.get(Oid4vcProtocolMapperModel.MANDATORY))
|
||||
.map(Boolean::parseBoolean)
|
||||
.orElse(false));
|
||||
Optional.ofNullable(protocolMapper.getConfig()
|
||||
.get(Oid4vcProtocolMapperModel.MANDATORY))
|
||||
.map(Boolean::parseBoolean)
|
||||
.orElse(false));
|
||||
String expectedDisplayString = protocolMapper.getConfig().get(Oid4vcProtocolMapperModel.DISPLAY);
|
||||
List<ClaimDisplay> expectedDisplayList = fromJsonString(expectedDisplayString,
|
||||
new SerializableClaimDisplayReference());
|
||||
new SerializableClaimDisplayReference());
|
||||
List<ClaimDisplay> actualDisplayList = claim.getDisplay();
|
||||
if (expectedDisplayList == null) {
|
||||
Assert.assertNull(actualDisplayList);
|
||||
@@ -283,7 +366,7 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
|
||||
else {
|
||||
Assert.assertEquals(expectedDisplayList.size(), actualDisplayList.size());
|
||||
MatcherAssert.assertThat(actualDisplayList,
|
||||
Matchers.containsInAnyOrder(expectedDisplayList.toArray()));
|
||||
Matchers.containsInAnyOrder(expectedDisplayList.toArray()));
|
||||
}
|
||||
}
|
||||
})));
|
||||
@@ -293,14 +376,14 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
|
||||
}
|
||||
|
||||
/**
|
||||
* a jackson type-reference that can be used in the run-server-block
|
||||
* A jackson type-reference that can be used in the run-server-block
|
||||
*/
|
||||
public static class SerializableProtocolMapperReference extends TypeReference<List<ProtocolMapperRepresentation>>
|
||||
implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* a jackson type-reference that can be used in the run-server-block
|
||||
* A jackson type-reference that can be used in the run-server-block
|
||||
*/
|
||||
public static class SerializableClaimDisplayReference extends TypeReference<List<ClaimDisplay>>
|
||||
implements Serializable {
|
||||
@@ -338,7 +421,6 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
|
||||
testRealm.getComponents().add("org.keycloak.keys.KeyProvider", getRsaKeyProvider(RSA_KEY));
|
||||
testRealm.getComponents().add("org.keycloak.protocol.oid4vc.issuance.credentialbuilder.CredentialBuilder", getCredentialBuilderProvider(Format.JWT_VC));
|
||||
|
||||
|
||||
if (testRealm.getClients() != null) {
|
||||
testRealm.getClients().add(clientRepresentation);
|
||||
} else {
|
||||
|
||||
@@ -35,8 +35,12 @@ import org.junit.Test;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.TokenVerifier;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.models.oid4vci.CredentialScopeModel;
|
||||
import org.keycloak.constants.Oid4VciConstants;
|
||||
import org.keycloak.jose.jwe.JWEException;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
import org.keycloak.jose.jwk.JWKParser;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.oid4vci.CredentialScopeModel;
|
||||
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint;
|
||||
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerWellKnownProvider;
|
||||
import org.keycloak.protocol.oid4vc.model.Claim;
|
||||
@@ -46,7 +50,10 @@ import org.keycloak.protocol.oid4vc.model.CredentialIssuer;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialOfferURI;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialRequest;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialResponse;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialResponseEncryption;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialsOffer;
|
||||
import org.keycloak.protocol.oid4vc.model.ErrorResponse;
|
||||
import org.keycloak.protocol.oid4vc.model.ErrorType;
|
||||
import org.keycloak.protocol.oid4vc.model.Format;
|
||||
import org.keycloak.protocol.oid4vc.model.OfferUriType;
|
||||
import org.keycloak.protocol.oid4vc.model.PreAuthorizedCode;
|
||||
@@ -63,6 +70,8 @@ import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -95,7 +104,6 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
OID4VCIssuerEndpoint oid4VCIssuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
oid4VCIssuerEndpoint.getCredentialOfferURI("inexistent-id", OfferUriType.URI, 0, 0);
|
||||
})));
|
||||
|
||||
}
|
||||
|
||||
@Test(expected = BadRequestException.class)
|
||||
@@ -126,7 +134,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
public void testGetCredentialOfferURI() {
|
||||
final String scopeName = jwtTypeCredentialClientScope.getName();
|
||||
final String credentialConfigurationId = jwtTypeCredentialClientScope.getAttributes()
|
||||
.get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
.get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
String token = getBearerToken(oauth, client, scopeName);
|
||||
|
||||
testingClient.server(TEST_REALM_NAME).run((session) -> {
|
||||
@@ -137,13 +145,13 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
OID4VCIssuerEndpoint oid4VCIssuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
|
||||
Response response = oid4VCIssuerEndpoint.getCredentialOfferURI(credentialConfigurationId,
|
||||
OfferUriType.URI,
|
||||
0,
|
||||
0);
|
||||
OfferUriType.URI,
|
||||
0,
|
||||
0);
|
||||
|
||||
assertEquals("An offer uri should have been returned.", HttpStatus.SC_OK, response.getStatus());
|
||||
CredentialOfferURI credentialOfferURI = JsonSerialization.mapper.convertValue(response.getEntity(),
|
||||
CredentialOfferURI.class);
|
||||
CredentialOfferURI.class);
|
||||
assertNotNull("A nonce should be included.", credentialOfferURI.getNonce());
|
||||
assertNotNull("The issuer uri should be provided.", credentialOfferURI.getIssuer());
|
||||
} catch (Exception e) {
|
||||
@@ -229,7 +237,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
.setCredentialConfigurationIds(List.of("credential-configuration-id"));
|
||||
|
||||
String sessionCode = prepareSessionCode(session, authenticator, JsonSerialization.writeValueAsString(credentialsOffer));
|
||||
// the cache transactions need to be commited explicitly in the test. Without that, the OAuth2Code will only be commited to
|
||||
// The cache transactions need to be committed explicitly in the test. Without that, the OAuth2Code will only be committed to
|
||||
// the cache after .run((session)-> ...)
|
||||
session.getTransactionManager().commit();
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
@@ -279,27 +287,27 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
@Test
|
||||
public void testRequestCredentialNoMatchingCredentialBuilder() throws Throwable {
|
||||
final String credentialConfigurationId = jwtTypeCredentialClientScope.getAttributes()
|
||||
.get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
.get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
final String scopeName = jwtTypeCredentialClientScope.getName();
|
||||
String token = getBearerToken(oauth, client, scopeName);
|
||||
|
||||
try {
|
||||
withCausePropagation(() -> {
|
||||
testingClient.server(TEST_REALM_NAME).run((session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = //
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator =
|
||||
new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
|
||||
// Prepare the issue endpoint with no credential builders.
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator, Map.of());
|
||||
|
||||
CredentialRequest credentialRequest = //
|
||||
CredentialRequest credentialRequest =
|
||||
new CredentialRequest().setCredentialConfigurationId(credentialConfigurationId);
|
||||
issuerEndpoint.requestCredential(credentialRequest);
|
||||
}));
|
||||
});
|
||||
Assert.fail("Should have thrown an exception");
|
||||
}catch (Exception e) {
|
||||
} catch (Exception e) {
|
||||
Assert.assertTrue(e instanceof BadRequestException);
|
||||
Assert.assertEquals("No credential builder found for format jwt_vc", e.getMessage());
|
||||
}
|
||||
@@ -335,28 +343,28 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
.setCredentialIdentifier(scopeName);
|
||||
Response credentialResponse = issuerEndpoint.requestCredential(credentialRequest);
|
||||
assertEquals("The credential request should be answered successfully.",
|
||||
HttpStatus.SC_OK,
|
||||
credentialResponse.getStatus());
|
||||
HttpStatus.SC_OK,
|
||||
credentialResponse.getStatus());
|
||||
assertNotNull("A credential should be responded.", credentialResponse.getEntity());
|
||||
CredentialResponse credentialResponseVO = JsonSerialization.mapper
|
||||
.convertValue(credentialResponse.getEntity(),
|
||||
CredentialResponse.class);
|
||||
.convertValue(credentialResponse.getEntity(),
|
||||
CredentialResponse.class);
|
||||
JsonWebToken jsonWebToken = TokenVerifier.create((String) credentialResponseVO.getCredentials().get(0).getCredential(),
|
||||
JsonWebToken.class).getToken();
|
||||
JsonWebToken.class).getToken();
|
||||
|
||||
assertNotNull("A valid credential string should have been responded", jsonWebToken);
|
||||
assertNotNull("The credentials should be included at the vc-claim.",
|
||||
jsonWebToken.getOtherClaims().get("vc"));
|
||||
jsonWebToken.getOtherClaims().get("vc"));
|
||||
VerifiableCredential credential =
|
||||
JsonSerialization.mapper.convertValue(jsonWebToken.getOtherClaims().get("vc"),
|
||||
VerifiableCredential.class);
|
||||
VerifiableCredential.class);
|
||||
assertTrue("The static claim should be set.",
|
||||
credential.getCredentialSubject().getClaims().containsKey("scope-name"));
|
||||
credential.getCredentialSubject().getClaims().containsKey("scope-name"));
|
||||
assertEquals("The static claim should be set.",
|
||||
scopeName,
|
||||
credential.getCredentialSubject().getClaims().get("scope-name"));
|
||||
scopeName,
|
||||
credential.getCredentialSubject().getClaims().get("scope-name"));
|
||||
assertFalse("Only mappers supported for the requested type should have been evaluated.",
|
||||
credential.getCredentialSubject().getClaims().containsKey("AnotherCredentialType"));
|
||||
credential.getCredentialSubject().getClaims().containsKey("AnotherCredentialType"));
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -374,17 +382,304 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
.setCredentialIdentifier(scopeName);
|
||||
Response credentialResponse = issuerEndpoint.requestCredential(credentialRequest);
|
||||
assertEquals("The credential request should be answered successfully.",
|
||||
HttpStatus.SC_OK,
|
||||
credentialResponse.getStatus());
|
||||
HttpStatus.SC_OK,
|
||||
credentialResponse.getStatus());
|
||||
assertNotNull("A credential should be responded.", credentialResponse.getEntity());
|
||||
CredentialResponse credentialResponseVO = JsonSerialization.mapper
|
||||
.convertValue(credentialResponse.getEntity(),
|
||||
CredentialResponse.class);
|
||||
SdJwtVP sdJwtVP = SdJwtVP.of((String)credentialResponseVO.getCredentials().get(0).getCredential());
|
||||
.convertValue(credentialResponse.getEntity(),
|
||||
CredentialResponse.class);
|
||||
String credentialString = (String)credentialResponseVO.getCredentials().get(0).getCredential();
|
||||
SdJwtVP sdJwtVP = SdJwtVP.of(credentialString);
|
||||
assertNotNull("A valid credential string should have been responded", sdJwtVP);
|
||||
}));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestCredentialWithEncryption() {
|
||||
final String scopeName = jwtTypeCredentialClientScope.getName();
|
||||
String token = getBearerToken(oauth, client, scopeName);
|
||||
testingClient
|
||||
.server(TEST_REALM_NAME)
|
||||
.run((session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
|
||||
Map<String, Object> jwkPair;
|
||||
try {
|
||||
jwkPair = generateRsaJwkWithPrivateKey();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("Failed to generate JWK", e);
|
||||
}
|
||||
JWK jwk = (JWK) jwkPair.get("jwk");
|
||||
PrivateKey privateKey = (PrivateKey) jwkPair.get("privateKey");
|
||||
|
||||
CredentialRequest credentialRequest = new CredentialRequest()
|
||||
.setFormat(Format.JWT_VC)
|
||||
.setCredentialIdentifier(scopeName)
|
||||
.setCredentialResponseEncryption(
|
||||
new CredentialResponseEncryption()
|
||||
.setAlg("RSA-OAEP")
|
||||
.setEnc("A256GCM")
|
||||
.setJwk(jwk));
|
||||
|
||||
Response credentialResponse = issuerEndpoint.requestCredential(credentialRequest);
|
||||
|
||||
assertEquals("The credential request should be answered successfully.",
|
||||
HttpStatus.SC_OK, credentialResponse.getStatus());
|
||||
assertEquals("Response should be JWT type for encrypted responses",
|
||||
org.keycloak.utils.MediaType.APPLICATION_JWT, credentialResponse.getMediaType().toString());
|
||||
|
||||
String encryptedResponse = (String) credentialResponse.getEntity();
|
||||
CredentialResponse decryptedResponse;
|
||||
try {
|
||||
decryptedResponse = decryptJweResponse(encryptedResponse, privateKey);
|
||||
} catch (IOException | JWEException e) {
|
||||
Assert.fail("Failed to decrypt JWE response: " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify the decrypted payload
|
||||
assertNotNull("Decrypted response should contain a credential", decryptedResponse.getCredentials());
|
||||
JsonWebToken jsonWebToken;
|
||||
try {
|
||||
jsonWebToken = TokenVerifier.create((String) decryptedResponse.getCredentials().get(0).getCredential(), JsonWebToken.class).getToken();
|
||||
} catch (VerificationException e) {
|
||||
Assert.fail("Failed to verify JWT: " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
assertNotNull("A valid credential string should have been responded", jsonWebToken);
|
||||
VerifiableCredential credential = JsonSerialization.mapper.convertValue(
|
||||
jsonWebToken.getOtherClaims().get("vc"), VerifiableCredential.class);
|
||||
assertTrue("The static claim should be set.", credential.getCredentialSubject().getClaims().containsKey("scope-name"));
|
||||
}));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestCredentialWithIncompleteEncryptionParams() throws Throwable {
|
||||
String token = getBearerToken(oauth, client, jwtTypeCredentialClientScope.getName());
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
|
||||
// Missing enc parameter
|
||||
JWK jwk = JWKParser.create().parse("{\"kty\":\"RSA\",\"n\":\"test-n\",\"e\":\"AQAB\"}").getJwk();
|
||||
CredentialRequest credentialRequest = new CredentialRequest()
|
||||
.setFormat(Format.JWT_VC)
|
||||
.setCredentialIdentifier("test-credential")
|
||||
.setCredentialResponseEncryption(
|
||||
new CredentialResponseEncryption()
|
||||
.setAlg("RSA-OAEP")
|
||||
.setJwk(jwk));
|
||||
|
||||
try {
|
||||
issuerEndpoint.requestCredential(credentialRequest);
|
||||
Assert.fail("Expected BadRequestException due to missing encryption parameter 'enc'");
|
||||
} catch (BadRequestException e) {
|
||||
ErrorResponse error = (ErrorResponse) e.getResponse().getEntity();
|
||||
assertEquals(ErrorType.INVALID_ENCRYPTION_PARAMETERS, error.getError());
|
||||
assertTrue("Error message should specify missing parameters",
|
||||
error.getErrorDescription().contains("Missing required encryption parameters: enc"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCredentialIssuanceWithEncryption() throws Exception {
|
||||
// Integration test for the full credential issuance flow with encryption
|
||||
testCredentialIssuanceWithAuthZCodeFlow(jwtTypeCredentialClientScope,
|
||||
(testClientId, testScope) -> {
|
||||
String scopeName = jwtTypeCredentialClientScope.getName();
|
||||
return getBearerToken(oauth.clientId(testClientId).openid(false).scope(scopeName));
|
||||
},
|
||||
m -> {
|
||||
String accessToken = (String) m.get("accessToken");
|
||||
WebTarget credentialTarget = (WebTarget) m.get("credentialTarget");
|
||||
CredentialRequest credentialRequest = (CredentialRequest) m.get("credentialRequest");
|
||||
|
||||
Map<String, Object> jwkPair;
|
||||
try {
|
||||
jwkPair = generateRsaJwkWithPrivateKey();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("Failed to generate JWK", e);
|
||||
}
|
||||
JWK jwk = (JWK) jwkPair.get("jwk");
|
||||
PrivateKey privateKey = (PrivateKey) jwkPair.get("privateKey");
|
||||
|
||||
credentialRequest.setCredentialResponseEncryption(
|
||||
new CredentialResponseEncryption()
|
||||
.setAlg("RSA-OAEP")
|
||||
.setEnc("A256GCM")
|
||||
.setJwk(jwk));
|
||||
|
||||
try (Response response = credentialTarget.request()
|
||||
.header(HttpHeaders.AUTHORIZATION, "bearer " + accessToken)
|
||||
.post(Entity.json(credentialRequest))) {
|
||||
|
||||
assertEquals(200, response.getStatus());
|
||||
assertEquals("application/jwt", response.getMediaType().toString());
|
||||
|
||||
String encryptedResponse = response.readEntity(String.class);
|
||||
CredentialResponse decryptedResponse;
|
||||
try {
|
||||
decryptedResponse = decryptJweResponse(encryptedResponse, privateKey);
|
||||
} catch (IOException | JWEException e) {
|
||||
Assert.fail("Failed to decrypt JWE response: " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify the decrypted payload
|
||||
JsonWebToken jsonWebToken;
|
||||
try {
|
||||
jsonWebToken = TokenVerifier.create(
|
||||
(String) decryptedResponse.getCredentials().get(0).getCredential(),
|
||||
JsonWebToken.class
|
||||
).getToken();
|
||||
} catch (VerificationException e) {
|
||||
Assert.fail("Failed to verify JWT: " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
assertEquals("did:web:test.org", jsonWebToken.getIssuer());
|
||||
VerifiableCredential credential = JsonSerialization.mapper.convertValue(
|
||||
jsonWebToken.getOtherClaims().get("vc"),
|
||||
VerifiableCredential.class
|
||||
);
|
||||
assertEquals(List.of(jwtTypeCredentialClientScope.getName()), credential.getType());
|
||||
assertEquals(TEST_DID, credential.getIssuer());
|
||||
assertEquals("john@email.cz", credential.getCredentialSubject().getClaims().get("email"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestCredentialWithUnsupportedAlgorithms() throws Throwable {
|
||||
String token = getBearerToken(oauth, client, jwtTypeCredentialClientScope.getName());
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
|
||||
JWK jwk;
|
||||
try {
|
||||
jwk = generateRsaJwk();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("Failed to generate JWK", e);
|
||||
}
|
||||
|
||||
CredentialRequest credentialRequest = new CredentialRequest()
|
||||
.setFormat(Format.JWT_VC)
|
||||
.setCredentialIdentifier("test-credential")
|
||||
.setCredentialResponseEncryption(
|
||||
new CredentialResponseEncryption()
|
||||
.setAlg("UNSUPPORTED-ALG")
|
||||
.setEnc("A256GCM")
|
||||
.setJwk(jwk));
|
||||
|
||||
try {
|
||||
issuerEndpoint.requestCredential(credentialRequest);
|
||||
Assert.fail("Expected BadRequestException due to unsupported algorithm");
|
||||
} catch (BadRequestException e) {
|
||||
ErrorResponse error = (ErrorResponse) e.getResponse().getEntity();
|
||||
assertEquals(ErrorType.INVALID_ENCRYPTION_PARAMETERS, error.getError());
|
||||
assertTrue(error.getErrorDescription().contains("UNSUPPORTED-ALG"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestCredentialWithInvalidJWK() throws Throwable {
|
||||
String token = getBearerToken(oauth, client, jwtTypeCredentialClientScope.getName());
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
|
||||
// Invalid JWK (missing modulus)
|
||||
JWK jwk = JWKParser.create().parse("{\"kty\":\"RSA\",\"e\":\"AQAB\"}").getJwk();
|
||||
CredentialRequest credentialRequest = new CredentialRequest()
|
||||
.setFormat(Format.JWT_VC)
|
||||
.setCredentialIdentifier("test-credential")
|
||||
.setCredentialResponseEncryption(
|
||||
new CredentialResponseEncryption()
|
||||
.setAlg("RSA-OAEP")
|
||||
.setEnc("A256GCM")
|
||||
.setJwk(jwk));
|
||||
|
||||
try {
|
||||
issuerEndpoint.requestCredential(credentialRequest);
|
||||
Assert.fail("Expected BadRequestException due to invalid JWK missing modulus");
|
||||
} catch (BadRequestException e) {
|
||||
ErrorResponse error = (ErrorResponse) e.getResponse().getEntity();
|
||||
assertEquals(ErrorType.INVALID_ENCRYPTION_PARAMETERS, error.getError());
|
||||
assertTrue(error.getErrorDescription().contains("JWK"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestCredentialWithWrongKeyTypeJWK() throws Throwable {
|
||||
String token = getBearerToken(oauth, client, jwtTypeCredentialClientScope.getName());
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
|
||||
JWK jwk = JWKParser.create().parse("{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"test-x\",\"y\":\"test-y\"}").getJwk();
|
||||
CredentialRequest credentialRequest = new CredentialRequest()
|
||||
.setFormat(Format.JWT_VC)
|
||||
.setCredentialIdentifier("test-credential")
|
||||
.setCredentialResponseEncryption(
|
||||
new CredentialResponseEncryption()
|
||||
.setAlg("RSA-OAEP")
|
||||
.setEnc("A256GCM")
|
||||
.setJwk(jwk));
|
||||
|
||||
try {
|
||||
issuerEndpoint.requestCredential(credentialRequest);
|
||||
Assert.fail("Expected BadRequestException due to wrong JWK key type");
|
||||
} catch (BadRequestException e) {
|
||||
ErrorResponse error = (ErrorResponse) e.getResponse().getEntity();
|
||||
assertEquals(ErrorType.INVALID_ENCRYPTION_PARAMETERS, error.getError());
|
||||
assertTrue(error.getErrorDescription().contains("JWK"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestCredentialEncryptionRequiredButMissing() {
|
||||
String token = getBearerToken(oauth, client, jwtTypeCredentialClientScope.getName());
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
realm.setAttribute("oid4vci.encryption.required", "true");
|
||||
realm.setAttribute("oid4vci.encryption.algs", "RSA-OAEP");
|
||||
realm.setAttribute("oid4vci.encryption.encs", "A256GCM");
|
||||
|
||||
try {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
|
||||
CredentialRequest credentialRequest = new CredentialRequest()
|
||||
.setFormat(Format.JWT_VC)
|
||||
.setCredentialIdentifier("test-credential");
|
||||
|
||||
issuerEndpoint.requestCredential(credentialRequest);
|
||||
Assert.fail("Expected BadRequestException due to missing encryption parameters when required");
|
||||
} catch (BadRequestException e) {
|
||||
ErrorResponse error = (ErrorResponse) e.getResponse().getEntity();
|
||||
assertEquals(ErrorType.INVALID_ENCRYPTION_PARAMETERS, error.getError());
|
||||
assertEquals("Encryption is required by the Credential Issuer, but no encryption parameters were provided.", error.getErrorDescription());
|
||||
} finally {
|
||||
// Clean up realm attributes
|
||||
realm.removeAttribute("oid4vci.encryption.required");
|
||||
realm.removeAttribute("oid4vci.encryption.algs");
|
||||
realm.removeAttribute("oid4vci.encryption.encs");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Tests the complete flow from
|
||||
// 1. Retrieving the credential-offer-uri
|
||||
// 2. Using the uri to get the actual credential offer
|
||||
@@ -394,21 +689,20 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
// 6. Get the credential
|
||||
@Test
|
||||
public void testCredentialIssuance() throws Exception {
|
||||
|
||||
String token = getBearerToken(oauth, client, jwtTypeCredentialClientScope.getName());
|
||||
|
||||
// 1. Retrieving the credential-offer-uri
|
||||
final String credentialConfigurationId = jwtTypeCredentialClientScope.getAttributes()
|
||||
.get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
.get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
HttpGet getCredentialOfferURI = new HttpGet(getBasePath(TEST_REALM_NAME)
|
||||
+ "credential-offer-uri?credential_configuration_id="
|
||||
+ credentialConfigurationId);
|
||||
+ "credential-offer-uri?credential_configuration_id="
|
||||
+ credentialConfigurationId);
|
||||
getCredentialOfferURI.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
|
||||
CloseableHttpResponse credentialOfferURIResponse = httpClient.execute(getCredentialOfferURI);
|
||||
|
||||
assertEquals("A valid offer uri should be returned",
|
||||
HttpStatus.SC_OK,
|
||||
credentialOfferURIResponse.getStatusLine().getStatusCode());
|
||||
HttpStatus.SC_OK,
|
||||
credentialOfferURIResponse.getStatusLine().getStatusCode());
|
||||
String s = IOUtils.toString(credentialOfferURIResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
CredentialOfferURI credentialOfferURI = JsonSerialization.readValue(s, CredentialOfferURI.class);
|
||||
|
||||
@@ -456,10 +750,10 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
.forEach(supportedCredential -> {
|
||||
try {
|
||||
requestCredential(theToken,
|
||||
credentialIssuer.getCredentialEndpoint(),
|
||||
supportedCredential,
|
||||
new CredentialResponseHandler(),
|
||||
jwtTypeCredentialClientScope);
|
||||
credentialIssuer.getCredentialEndpoint(),
|
||||
supportedCredential,
|
||||
new CredentialResponseHandler(),
|
||||
jwtTypeCredentialClientScope);
|
||||
} catch (IOException e) {
|
||||
fail("Was not able to get the credential.");
|
||||
} catch (VerificationException e) {
|
||||
@@ -479,27 +773,27 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
WebTarget credentialTarget = (WebTarget) m.get("credentialTarget");
|
||||
CredentialRequest credentialRequest = (CredentialRequest) m.get("credentialRequest");
|
||||
assertEquals("Credential configuration id should match",
|
||||
jwtTypeCredentialClientScope.getAttributes().get(CredentialScopeModel.CONFIGURATION_ID),
|
||||
credentialRequest.getCredentialConfigurationId());
|
||||
jwtTypeCredentialClientScope.getAttributes().get(CredentialScopeModel.CONFIGURATION_ID),
|
||||
credentialRequest.getCredentialConfigurationId());
|
||||
|
||||
try (Response response = credentialTarget.request()
|
||||
.header(HttpHeaders.AUTHORIZATION, "bearer " + accessToken)
|
||||
.post(Entity.json(credentialRequest))) {
|
||||
.header(HttpHeaders.AUTHORIZATION, "bearer " + accessToken)
|
||||
.post(Entity.json(credentialRequest))) {
|
||||
if (response.getStatus() != 200) {
|
||||
String errorBody = response.readEntity(String.class);
|
||||
System.out.println("Error Response: " + errorBody);
|
||||
}
|
||||
assertEquals(200, response.getStatus());
|
||||
CredentialResponse credentialResponse = JsonSerialization.readValue(response.readEntity(String.class),
|
||||
CredentialResponse.class);
|
||||
CredentialResponse.class);
|
||||
|
||||
JsonWebToken jsonWebToken = TokenVerifier.create((String) credentialResponse.getCredentials().get(0).getCredential(),
|
||||
JsonWebToken.class).getToken();
|
||||
JsonWebToken.class).getToken();
|
||||
assertEquals(TEST_DID.toString(), jsonWebToken.getIssuer());
|
||||
|
||||
VerifiableCredential credential = JsonSerialization.mapper.convertValue(jsonWebToken.getOtherClaims()
|
||||
.get("vc"),
|
||||
VerifiableCredential.class);
|
||||
.get("vc"),
|
||||
VerifiableCredential.class);
|
||||
assertEquals(List.of(jwtTypeCredentialClientScope.getName()), credential.getType());
|
||||
assertEquals(TEST_DID, credential.getIssuer());
|
||||
assertEquals("john@email.cz", credential.getCredentialSubject().getClaims().get("email"));
|
||||
@@ -514,7 +808,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
@Test
|
||||
public void testCredentialIssuanceWithAuthZCodeWithScopeUnmatched() throws Exception {
|
||||
testCredentialIssuanceWithAuthZCodeFlow(sdJwtTypeCredentialClientScope, (testClientId, testScope) ->
|
||||
getBearerToken(oauth.clientId(testClientId).openid(false).scope("email")),// set registered different scope
|
||||
getBearerToken(oauth.clientId(testClientId).openid(false).scope("email")),// set registered different scope
|
||||
m -> {
|
||||
String accessToken = (String) m.get("accessToken");
|
||||
WebTarget credentialTarget = (WebTarget) m.get("credentialTarget");
|
||||
@@ -529,7 +823,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
@Test
|
||||
public void testCredentialIssuanceWithAuthZCodeSWithoutScope() throws Exception {
|
||||
testCredentialIssuanceWithAuthZCodeFlow(sdJwtTypeCredentialClientScope,
|
||||
(testClientId, testScope) -> getBearerToken(oauth.clientId(testClientId).openid(false).scope(null)),// no scope
|
||||
(testClientId, testScope) -> getBearerToken(oauth.clientId(testClientId).openid(false).scope(null)),// no scope
|
||||
m -> {
|
||||
String accessToken = (String) m.get("accessToken");
|
||||
WebTarget credentialTarget = (WebTarget) m.get("credentialTarget");
|
||||
@@ -557,13 +851,13 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
CredentialRequest credentialRequest = (CredentialRequest) m.get("credentialRequest");
|
||||
|
||||
try (Response response = credentialTarget.request()
|
||||
.header(HttpHeaders.AUTHORIZATION, "bearer " + accessToken)
|
||||
.post(Entity.json(credentialRequest))) {
|
||||
.header(HttpHeaders.AUTHORIZATION, "bearer " + accessToken)
|
||||
.post(Entity.json(credentialRequest))) {
|
||||
assertEquals(400, response.getStatus());
|
||||
String errorJson = response.readEntity(String.class);
|
||||
assertNotNull("Error response should not be null", errorJson);
|
||||
assertTrue("Error response should mention UNSUPPORTED_CREDENTIAL_TYPE or scope",
|
||||
errorJson.contains("UNSUPPORTED_CREDENTIAL_TYPE") || errorJson.contains("scope"));
|
||||
errorJson.contains("UNSUPPORTED_CREDENTIAL_TYPE") || errorJson.contains("scope"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -606,9 +900,9 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
public void testGetJwtVcConfigFromMetadata() {
|
||||
final String scopeName = jwtTypeCredentialClientScope.getName();
|
||||
final String credentialConfigurationId = jwtTypeCredentialClientScope.getAttributes()
|
||||
.get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
.get(CredentialScopeModel.CONFIGURATION_ID);
|
||||
final String verifiableCredentialType = jwtTypeCredentialClientScope.getAttributes()
|
||||
.get(CredentialScopeModel.VCT);
|
||||
.get(CredentialScopeModel.VCT);
|
||||
String expectedIssuer = suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/realms/" + TEST_REALM_NAME;
|
||||
String expectedCredentialsEndpoint = expectedIssuer + "/protocol/oid4vc/credential";
|
||||
String expectedNonceEndpoint = expectedIssuer + "/protocol/oid4vc/" + OID4VCIssuerEndpoint.NONCE_PATH;
|
||||
@@ -623,41 +917,41 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
assertEquals("The correct issuer should be included.", expectedIssuer, credentialIssuer.getCredentialIssuer());
|
||||
assertEquals("The correct credentials endpoint should be included.", expectedCredentialsEndpoint, credentialIssuer.getCredentialEndpoint());
|
||||
assertEquals("The correct nonce endpoint should be included.",
|
||||
expectedNonceEndpoint,
|
||||
credentialIssuer.getNonceEndpoint());
|
||||
expectedNonceEndpoint,
|
||||
credentialIssuer.getNonceEndpoint());
|
||||
assertEquals("Since the authorization server is equal to the issuer, just 1 should be returned.", 1, credentialIssuer.getAuthorizationServers().size());
|
||||
assertEquals("The expected server should have been returned.", expectedAuthorizationServer, credentialIssuer.getAuthorizationServers().get(0));
|
||||
|
||||
assertTrue("The jwt_vc-credential should be supported.",
|
||||
credentialIssuer.getCredentialsSupported()
|
||||
.containsKey(credentialConfigurationId));
|
||||
credentialIssuer.getCredentialsSupported()
|
||||
.containsKey(credentialConfigurationId));
|
||||
|
||||
SupportedCredentialConfiguration jwtVcConfig =
|
||||
credentialIssuer.getCredentialsSupported().get(credentialConfigurationId);
|
||||
assertEquals("The jwt_vc-credential should offer type test-credential",
|
||||
scopeName,
|
||||
jwtVcConfig.getScope());
|
||||
scopeName,
|
||||
jwtVcConfig.getScope());
|
||||
assertEquals("The jwt_vc-credential should be offered in the jwt_vc format.",
|
||||
Format.JWT_VC,
|
||||
jwtVcConfig.getFormat());
|
||||
Format.JWT_VC,
|
||||
jwtVcConfig.getFormat());
|
||||
|
||||
Claims jwtVcClaims = jwtVcConfig.getClaims();
|
||||
assertNotNull("The jwt_vc-credential can optionally provide a claims claim.",
|
||||
jwtVcClaims);
|
||||
jwtVcClaims);
|
||||
|
||||
assertEquals(5, jwtVcClaims.size());
|
||||
assertEquals(5, jwtVcClaims.size());
|
||||
{
|
||||
Claim claim = jwtVcClaims.get(0);
|
||||
assertEquals("The jwt_vc-credential claim credentialSubject.given_name is present.",
|
||||
Oid4VciConstants.CREDENTIAL_SUBJECT,
|
||||
claim.getPath().get(0));
|
||||
Oid4VciConstants.CREDENTIAL_SUBJECT,
|
||||
claim.getPath().get(0));
|
||||
assertEquals("The jwt_vc-credential claim credentialSubject.given_name is present.",
|
||||
"given_name",
|
||||
claim.getPath().get(1));
|
||||
"given_name",
|
||||
claim.getPath().get(1));
|
||||
assertFalse("The jwt_vc-credential claim credentialSubject.given_name is not mandatory.",
|
||||
claim.isMandatory());
|
||||
claim.isMandatory());
|
||||
assertNotNull("The jwt_vc-credential claim credentialSubject.given_name has display configured",
|
||||
claim.getDisplay());
|
||||
claim.getDisplay());
|
||||
assertEquals(15, claim.getDisplay().size());
|
||||
for (ClaimDisplay givenNameDisplay : claim.getDisplay()) {
|
||||
assertNotNull(givenNameDisplay.getName());
|
||||
@@ -667,15 +961,15 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
{
|
||||
Claim claim = jwtVcClaims.get(1);
|
||||
assertEquals("The jwt_vc-credential claim credentialSubject.family_name is present.",
|
||||
Oid4VciConstants.CREDENTIAL_SUBJECT,
|
||||
claim.getPath().get(0));
|
||||
Oid4VciConstants.CREDENTIAL_SUBJECT,
|
||||
claim.getPath().get(0));
|
||||
assertEquals("The jwt_vc-credential claim credentialSubject.family_name is present.",
|
||||
"family_name",
|
||||
claim.getPath().get(1));
|
||||
"family_name",
|
||||
claim.getPath().get(1));
|
||||
assertFalse("The jwt_vc-credential claim credentialSubject.family_name is not mandatory.",
|
||||
claim.isMandatory());
|
||||
claim.isMandatory());
|
||||
assertNotNull("The jwt_vc-credential claim credentialSubject.family_name has display configured",
|
||||
claim.getDisplay());
|
||||
claim.getDisplay());
|
||||
assertEquals(15, claim.getDisplay().size());
|
||||
for (ClaimDisplay familyNameDisplay : claim.getDisplay()) {
|
||||
assertNotNull(familyNameDisplay.getName());
|
||||
@@ -685,15 +979,15 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
{
|
||||
Claim claim = jwtVcClaims.get(2);
|
||||
assertEquals("The jwt_vc-credential claim credentialSubject.birthdate is present.",
|
||||
Oid4VciConstants.CREDENTIAL_SUBJECT,
|
||||
claim.getPath().get(0));
|
||||
Oid4VciConstants.CREDENTIAL_SUBJECT,
|
||||
claim.getPath().get(0));
|
||||
assertEquals("The jwt_vc-credential claim credentialSubject.birthdate is present.",
|
||||
"birthdate",
|
||||
claim.getPath().get(1));
|
||||
"birthdate",
|
||||
claim.getPath().get(1));
|
||||
assertFalse("The jwt_vc-credential claim credentialSubject.birthdate is not mandatory.",
|
||||
claim.isMandatory());
|
||||
claim.isMandatory());
|
||||
assertNotNull("The jwt_vc-credential claim credentialSubject.birthdate has display configured",
|
||||
claim.getDisplay());
|
||||
claim.getDisplay());
|
||||
assertEquals(15, claim.getDisplay().size());
|
||||
for (ClaimDisplay birthDateDisplay : claim.getDisplay()) {
|
||||
assertNotNull(birthDateDisplay.getName());
|
||||
@@ -703,15 +997,15 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
{
|
||||
Claim claim = jwtVcClaims.get(3);
|
||||
assertEquals("The jwt_vc-credential claim credentialSubject.email is present.",
|
||||
Oid4VciConstants.CREDENTIAL_SUBJECT,
|
||||
claim.getPath().get(0));
|
||||
Oid4VciConstants.CREDENTIAL_SUBJECT,
|
||||
claim.getPath().get(0));
|
||||
assertEquals("The jwt_vc-credential claim credentialSubject.email is present.",
|
||||
"email",
|
||||
claim.getPath().get(1));
|
||||
"email",
|
||||
claim.getPath().get(1));
|
||||
assertFalse("The jwt_vc-credential claim credentialSubject.email is not mandatory.",
|
||||
claim.isMandatory());
|
||||
claim.isMandatory());
|
||||
assertNotNull("The jwt_vc-credential claim credentialSubject.email has display configured",
|
||||
claim.getDisplay());
|
||||
claim.getDisplay());
|
||||
assertEquals(15, claim.getDisplay().size());
|
||||
for (ClaimDisplay birthDateDisplay : claim.getDisplay()) {
|
||||
assertNotNull(birthDateDisplay.getName());
|
||||
@@ -721,38 +1015,38 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
{
|
||||
Claim claim = jwtVcClaims.get(4);
|
||||
assertEquals("The jwt_vc-credential claim credentialSubject.scope-name is present.",
|
||||
Oid4VciConstants.CREDENTIAL_SUBJECT,
|
||||
claim.getPath().get(0));
|
||||
Oid4VciConstants.CREDENTIAL_SUBJECT,
|
||||
claim.getPath().get(0));
|
||||
assertEquals("The jwt_vc-credential claim credentialSubject.scope-name is present.",
|
||||
"scope-name",
|
||||
claim.getPath().get(1));
|
||||
"scope-name",
|
||||
claim.getPath().get(1));
|
||||
assertFalse("The jwt_vc-credential claim credentialSubject.scope-name is not mandatory.",
|
||||
claim.isMandatory());
|
||||
claim.isMandatory());
|
||||
assertNull("The jwt_vc-credential claim credentialSubject.scope-name has no display configured",
|
||||
claim.getDisplay());
|
||||
claim.getDisplay());
|
||||
}
|
||||
|
||||
assertEquals("The jwt_vc-credential should offer vct",
|
||||
verifiableCredentialType,
|
||||
jwtVcConfig.getVct());
|
||||
verifiableCredentialType,
|
||||
jwtVcConfig.getVct());
|
||||
|
||||
// We are offering key binding only for identity credential
|
||||
assertTrue("The jwt_vc-credential should contain a cryptographic binding method supported named jwk",
|
||||
jwtVcConfig.getCryptographicBindingMethodsSupported()
|
||||
.contains(CredentialScopeModel.CRYPTOGRAPHIC_BINDING_METHODS_DEFAULT));
|
||||
jwtVcConfig.getCryptographicBindingMethodsSupported()
|
||||
.contains(CredentialScopeModel.CRYPTOGRAPHIC_BINDING_METHODS_DEFAULT));
|
||||
assertTrue("The jwt_vc-credential should contain a credential signing algorithm named RS256",
|
||||
jwtVcConfig.getCredentialSigningAlgValuesSupported().contains("RS256"));
|
||||
jwtVcConfig.getCredentialSigningAlgValuesSupported().contains("RS256"));
|
||||
assertTrue("The jwt_vc-credential should support a proof of type jwt with signing algorithm RS256",
|
||||
credentialIssuer.getCredentialsSupported()
|
||||
.get(credentialConfigurationId)
|
||||
.getProofTypesSupported()
|
||||
.getSupportedProofTypes()
|
||||
.get("jwt")
|
||||
.getSigningAlgorithmsSupported()
|
||||
.contains("RS256"));
|
||||
credentialIssuer.getCredentialsSupported()
|
||||
.get(credentialConfigurationId)
|
||||
.getProofTypesSupported()
|
||||
.getSupportedProofTypes()
|
||||
.get("jwt")
|
||||
.getSigningAlgorithmsSupported()
|
||||
.contains("RS256"));
|
||||
assertEquals("The jwt_vc-credential should display as Test Credential",
|
||||
credentialConfigurationId,
|
||||
jwtVcConfig.getDisplay().get(0).getName());
|
||||
credentialConfigurationId,
|
||||
jwtVcConfig.getDisplay().get(0).getName());
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ public abstract class OID4VCTest extends AbstractTestRealmKeycloakTest {
|
||||
}
|
||||
|
||||
|
||||
public static KeyWrapper getRsaKey() {
|
||||
public static KeyWrapper getRsaKey(KeyUse keyUse, String algorithm, String keyName) {
|
||||
try {
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
|
||||
kpg.initialize(2048);
|
||||
@@ -185,16 +185,20 @@ public abstract class OID4VCTest extends AbstractTestRealmKeycloakTest {
|
||||
KeyWrapper kw = new KeyWrapper();
|
||||
kw.setPrivateKey(keyPair.getPrivate());
|
||||
kw.setPublicKey(keyPair.getPublic());
|
||||
kw.setUse(KeyUse.SIG);
|
||||
kw.setKid(KeyUtils.createKeyId(keyPair.getPublic()));
|
||||
kw.setUse(keyUse);
|
||||
kw.setKid(keyName != null ? keyName : KeyUtils.createKeyId(keyPair.getPublic()));
|
||||
kw.setType("RSA");
|
||||
kw.setAlgorithm("RS256");
|
||||
kw.setAlgorithm(algorithm);
|
||||
return kw;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static KeyWrapper getRsaKey() {
|
||||
return getRsaKey(KeyUse.SIG, "RS256", null);
|
||||
}
|
||||
|
||||
public static ComponentExportRepresentation getRsaKeyProvider(KeyWrapper keyWrapper) {
|
||||
ComponentExportRepresentation componentExportRepresentation = new ComponentExportRepresentation();
|
||||
componentExportRepresentation.setName("rsa-key-provider");
|
||||
@@ -211,12 +215,38 @@ public abstract class OID4VCTest extends AbstractTestRealmKeycloakTest {
|
||||
"active", List.of("true"),
|
||||
"priority", List.of("0"),
|
||||
"enabled", List.of("true"),
|
||||
"algorithm", List.of("RS256")
|
||||
"algorithm", List.of(keyWrapper.getAlgorithm()),
|
||||
"keyUse", List.of(keyWrapper.getUse().name())
|
||||
)
|
||||
));
|
||||
return componentExportRepresentation;
|
||||
}
|
||||
|
||||
public static ComponentExportRepresentation getRsaEncKeyProvider(String algorithm, String keyName) {
|
||||
KeyWrapper keyWrapper = getRsaKey(KeyUse.ENC, algorithm, keyName);
|
||||
ComponentExportRepresentation componentExportRepresentation = new ComponentExportRepresentation();
|
||||
componentExportRepresentation.setName(keyName);
|
||||
componentExportRepresentation.setId(UUID.randomUUID().toString());
|
||||
componentExportRepresentation.setProviderId("rsa");
|
||||
|
||||
Certificate certificate = CertificateUtils.generateV1SelfSignedCertificate(
|
||||
new KeyPair((PublicKey) keyWrapper.getPublicKey(), (PrivateKey) keyWrapper.getPrivateKey()), "TestKey");
|
||||
|
||||
componentExportRepresentation.setConfig(new MultivaluedHashMap<>(
|
||||
Map.of(
|
||||
"privateKey", List.of(PemUtils.encodeKey(keyWrapper.getPrivateKey())),
|
||||
"certificate", List.of(PemUtils.encodeCertificate(certificate)),
|
||||
"active", List.of("true"),
|
||||
"priority", List.of("100"),
|
||||
"enabled", List.of("true"),
|
||||
"algorithm", List.of(algorithm),
|
||||
"keyUse", List.of(KeyUse.ENC.name())
|
||||
)
|
||||
));
|
||||
return componentExportRepresentation;
|
||||
}
|
||||
|
||||
|
||||
protected ClientRepresentation getTestClient(String clientId) {
|
||||
ClientRepresentation clientRepresentation = new ClientRepresentation();
|
||||
clientRepresentation.setClientId(clientId);
|
||||
@@ -314,7 +344,7 @@ public abstract class OID4VCTest extends AbstractTestRealmKeycloakTest {
|
||||
protocolMapperRepresentation.setProtocolMapper("oid4vc-static-claim-mapper");
|
||||
protocolMapperRepresentation.setConfig(
|
||||
Map.of("claim.name", "scope-name",
|
||||
"staticValue", scopeName)
|
||||
"staticValue", scopeName)
|
||||
);
|
||||
return protocolMapperRepresentation;
|
||||
}
|
||||
@@ -467,7 +497,7 @@ public abstract class OID4VCTest extends AbstractTestRealmKeycloakTest {
|
||||
public static String getCNonce() {
|
||||
UriBuilder builder = UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT);
|
||||
URI oid4vcUri = RealmsResource.protocolUrl(builder).build(AbstractTestRealmKeycloakTest.TEST_REALM_NAME,
|
||||
OID4VCLoginProtocolFactory.PROTOCOL_ID);
|
||||
OID4VCLoginProtocolFactory.PROTOCOL_ID);
|
||||
String nonceUrl = String.format("%s/%s", oid4vcUri.toString(), OID4VCIssuerEndpoint.NONCE_PATH);
|
||||
|
||||
String nonceResponseString;
|
||||
@@ -476,10 +506,10 @@ public abstract class OID4VCTest extends AbstractTestRealmKeycloakTest {
|
||||
WebTarget nonceTarget = client.target(nonceUrl);
|
||||
// the nonce endpoint must be unprotected, and therefore it must be accessible without any authentication
|
||||
Invocation.Builder nonceInvocationBuilder = nonceTarget.request()
|
||||
// just making sure that no authentication is added
|
||||
// by interceptors or similar
|
||||
.header(HttpHeaders.AUTHORIZATION, null)
|
||||
.header(HttpHeaders.COOKIE, null);
|
||||
// just making sure that no authentication is added
|
||||
// by interceptors or similar
|
||||
.header(HttpHeaders.AUTHORIZATION, null)
|
||||
.header(HttpHeaders.COOKIE, null);
|
||||
|
||||
try (Response response = nonceInvocationBuilder.post(null)) {
|
||||
Assert.assertEquals(HttpStatus.SC_OK, response.getStatus());
|
||||
|
||||
Reference in New Issue
Block a user