mirror of
https://github.com/keycloak/keycloak.git
synced 2026-06-03 13:58:36 -04:00
[OID4VCI] Migrate OID4VCJWTIssuerEndpointTest
Closes #46925 Signed-off-by: Giuseppe Graziano <g.graziano94@gmail.com>
This commit is contained in:
parent
529c1a9009
commit
5db69aec7d
7 changed files with 2358 additions and 1608 deletions
|
|
@ -132,6 +132,11 @@ public class ClientConfigBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
public ClientConfigBuilder optionalClientScopes(String... optionalClientScopes) {
|
||||
rep.setOptionalClientScopes(Collections.combine(rep.getOptionalClientScopes(), optionalClientScopes));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClientConfigBuilder protocolMappers(List<ProtocolMapperRepresentation> mappers) {
|
||||
rep.setProtocolMappers(Collections.combine(rep.getProtocolMappers(), mappers));
|
||||
return this;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package org.keycloak.testframework.realm;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
|
@ -11,6 +12,7 @@ import org.keycloak.representations.idm.ClientPolicyRepresentation;
|
|||
import org.keycloak.representations.idm.ClientProfileRepresentation;
|
||||
import org.keycloak.representations.idm.ClientProfilesRepresentation;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
||||
import org.keycloak.representations.idm.GroupRepresentation;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
|
|
@ -70,6 +72,11 @@ public class RealmConfigBuilder {
|
|||
return UserConfigBuilder.update(user).enabled(true).username(username);
|
||||
}
|
||||
|
||||
public UserConfigBuilder addUser(UserRepresentation user) {
|
||||
rep.setUsers(Collections.combine(rep.getUsers(), user));
|
||||
return UserConfigBuilder.update(user);
|
||||
}
|
||||
|
||||
public GroupConfigBuilder addGroup(String name) {
|
||||
GroupRepresentation group = new GroupRepresentation();
|
||||
rep.setGroups(Collections.combine(rep.getGroups(), group));
|
||||
|
|
@ -77,13 +84,18 @@ public class RealmConfigBuilder {
|
|||
}
|
||||
|
||||
public RoleConfigBuilder addRole(String name) {
|
||||
RoleRepresentation role = new RoleRepresentation();
|
||||
role.setName(name);
|
||||
return addRole(role);
|
||||
}
|
||||
|
||||
public RoleConfigBuilder addRole(RoleRepresentation roleRepresentation) {
|
||||
if (rep.getRoles() == null) {
|
||||
rep.setRoles(new RolesRepresentation());
|
||||
}
|
||||
|
||||
RoleRepresentation role = new RoleRepresentation();
|
||||
rep.getRoles().setRealm(Collections.combine(rep.getRoles().getRealm(), role));
|
||||
return RoleConfigBuilder.update(role).name(name);
|
||||
rep.getRoles().setRealm(Collections.combine(rep.getRoles().getRealm(), roleRepresentation));
|
||||
return RoleConfigBuilder.update(roleRepresentation).name(roleRepresentation.getName());
|
||||
}
|
||||
|
||||
public RoleConfigBuilder addClientRole(String clientName, String roleName) {
|
||||
|
|
@ -446,6 +458,24 @@ public class RealmConfigBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
public void verifiableCredentialsEnabled(Boolean verifiableCredentialsEnabled) {
|
||||
rep.setVerifiableCredentialsEnabled(verifiableCredentialsEnabled);
|
||||
}
|
||||
|
||||
public void attribute(String key, String value) {
|
||||
if (rep.getAttributes() == null) {
|
||||
rep.setAttributes(new HashMap<>());
|
||||
}
|
||||
rep.getAttributes().put(key, value);
|
||||
}
|
||||
|
||||
public void addClientScope(ClientScopeRepresentation clientScope) {
|
||||
if (rep.getClientScopes() == null) {
|
||||
rep.setClientScopes(new ArrayList<>());
|
||||
}
|
||||
rep.getClientScopes().add(clientScope);
|
||||
}
|
||||
|
||||
/**
|
||||
* Best practice is to use other convenience methods when configuring a realm, but while the framework is under
|
||||
* active development there may not be a way to perform all updates required. In these cases this method allows
|
||||
|
|
|
|||
|
|
@ -16,31 +16,13 @@
|
|||
*/
|
||||
package org.keycloak.tests.oid4vc;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.cert.Certificate;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.keycloak.admin.client.resource.ComponentsResource;
|
||||
import org.keycloak.common.util.CertificateUtils;
|
||||
import org.keycloak.common.util.KeyUtils;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
import org.keycloak.common.util.SecretGenerator;
|
||||
import org.keycloak.crypto.Algorithm;
|
||||
import org.keycloak.crypto.KeyUse;
|
||||
import org.keycloak.crypto.KeyWrapper;
|
||||
import org.keycloak.keys.KeyProvider;
|
||||
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerWellKnownProvider;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialIssuer;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialResponseEncryptionMetadata;
|
||||
import org.keycloak.representations.idm.ComponentRepresentation;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.annotations.TestSetup;
|
||||
import org.keycloak.testframework.remote.runonserver.InjectRunOnServer;
|
||||
|
|
@ -99,100 +81,4 @@ public class OID4VCIWellKnownProviderTest extends OID4VCIssuerTestBase {
|
|||
assertTrue(algValuesSupported.contains(RSA_OAEP_256), "The algorithm of the configured asymmetric encryption key should be provided.");
|
||||
});
|
||||
}
|
||||
|
||||
ComponentRepresentation getAesKeyProvider(String algorithm, String keyName, String keyUse, String providerId) {
|
||||
// Generate a random AES key (default length: 256 bits)
|
||||
byte[] secret = SecretGenerator.getInstance().randomBytes(32); // 32 bytes = 256 bits
|
||||
|
||||
String secretBase64 = Base64.getEncoder().encodeToString(secret);
|
||||
|
||||
ComponentRepresentation component = new ComponentRepresentation();
|
||||
component.setProviderType(KeyProvider.class.getName());
|
||||
component.setName(keyName);
|
||||
component.setId(UUID.randomUUID().toString());
|
||||
component.setProviderId(providerId);
|
||||
|
||||
component.setConfig(new MultivaluedHashMap<>(
|
||||
Map.of(
|
||||
"secret", List.of(secretBase64),
|
||||
"active", List.of("true"),
|
||||
"priority", List.of(String.valueOf(100)),
|
||||
"enabled", List.of("true"),
|
||||
"algorithm", List.of(algorithm),
|
||||
"keyUse", List.of(keyUse) // encryption usage
|
||||
)
|
||||
));
|
||||
return component;
|
||||
}
|
||||
|
||||
ComponentRepresentation getRsaKeyProvider(KeyWrapper keyWrapper) {
|
||||
ComponentRepresentation component = new ComponentRepresentation();
|
||||
component.setProviderType(KeyProvider.class.getName());
|
||||
component.setName("rsa-key-provider");
|
||||
component.setId(UUID.randomUUID().toString());
|
||||
component.setProviderId("rsa");
|
||||
|
||||
Certificate certificate = CertificateUtils.generateV1SelfSignedCertificate(
|
||||
new KeyPair((PublicKey) keyWrapper.getPublicKey(), (PrivateKey) keyWrapper.getPrivateKey()), "TestKey");
|
||||
|
||||
component.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("0"),
|
||||
"enabled", List.of("true"),
|
||||
"algorithm", List.of(keyWrapper.getAlgorithm()),
|
||||
"keyUse", List.of(keyWrapper.getUse().name())
|
||||
)
|
||||
));
|
||||
return component;
|
||||
}
|
||||
|
||||
ComponentRepresentation getRsaEncKeyProvider(String algorithm, String keyName, int priority) {
|
||||
ComponentRepresentation component = new ComponentRepresentation();
|
||||
component.setProviderType(KeyProvider.class.getName());
|
||||
component.setName(keyName);
|
||||
component.setId(UUID.randomUUID().toString());
|
||||
component.setProviderId("rsa");
|
||||
|
||||
KeyWrapper keyWrapper = getRsaKey(KeyUse.ENC, algorithm, keyName);
|
||||
Certificate certificate = CertificateUtils.generateV1SelfSignedCertificate(
|
||||
new KeyPair((PublicKey) keyWrapper.getPublicKey(), (PrivateKey) keyWrapper.getPrivateKey()), "TestKey");
|
||||
|
||||
component.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(String.valueOf(priority)),
|
||||
"enabled", List.of("true"),
|
||||
"algorithm", List.of(algorithm),
|
||||
"keyUse", List.of(KeyUse.ENC.name())
|
||||
)
|
||||
));
|
||||
return component;
|
||||
}
|
||||
|
||||
KeyWrapper getRsaKey_Default() {
|
||||
return getRsaKey(KeyUse.SIG, "RS256", null);
|
||||
}
|
||||
|
||||
KeyWrapper getRsaKey(KeyUse keyUse, String algorithm, String keyName) {
|
||||
try {
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
|
||||
kpg.initialize(2048);
|
||||
var keyPair = kpg.generateKeyPair();
|
||||
KeyWrapper kw = new KeyWrapper();
|
||||
kw.setPrivateKey(keyPair.getPrivate());
|
||||
kw.setPublicKey(keyPair.getPublic());
|
||||
kw.setUse(keyUse);
|
||||
kw.setKid(keyName != null ? keyName : KeyUtils.createKeyId(keyPair.getPublic()));
|
||||
kw.setType("RSA");
|
||||
kw.setAlgorithm(algorithm);
|
||||
return kw;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,556 @@
|
|||
/*
|
||||
* Copyright 2024 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.tests.oid4vc;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
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.keycloak.OAuth2Constants;
|
||||
import org.keycloak.TokenVerifier;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.admin.client.resource.ComponentsResource;
|
||||
import org.keycloak.common.VerificationException;
|
||||
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.jws.JWSInput;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.oid4vci.CredentialScopeModel;
|
||||
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint;
|
||||
import org.keycloak.protocol.oid4vc.issuance.TimeProvider;
|
||||
import org.keycloak.protocol.oid4vc.issuance.credentialbuilder.CredentialBuilder;
|
||||
import org.keycloak.protocol.oid4vc.issuance.credentialbuilder.JwtCredentialBuilder;
|
||||
import org.keycloak.protocol.oid4vc.issuance.credentialbuilder.SdJwtCredentialBuilder;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialIssuer;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialRequest;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialResponse;
|
||||
import org.keycloak.protocol.oid4vc.model.DisplayObject;
|
||||
import org.keycloak.protocol.oid4vc.model.OID4VCAuthorizationDetail;
|
||||
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
|
||||
import org.keycloak.protocol.oidc.utils.OAuth2Code;
|
||||
import org.keycloak.protocol.oidc.utils.OAuth2CodeParser;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
||||
import org.keycloak.representations.idm.ComponentRepresentation;
|
||||
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||
import org.keycloak.representations.userprofile.config.UPAttribute;
|
||||
import org.keycloak.representations.userprofile.config.UPAttributePermissions;
|
||||
import org.keycloak.representations.userprofile.config.UPConfig;
|
||||
import org.keycloak.services.managers.AppAuthManager;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.testframework.annotations.InjectKeycloakUrls;
|
||||
import org.keycloak.testframework.remote.providers.runonserver.RunOnServerException;
|
||||
import org.keycloak.testframework.server.KeycloakUrls;
|
||||
import org.keycloak.testframework.util.ApiUtil;
|
||||
import org.keycloak.testsuite.util.oauth.oid4vc.CredentialIssuerMetadataResponse;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
import org.keycloak.userprofile.config.UPConfigUtils;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.validate.validators.PatternValidator;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.ContentType;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
||||
import static org.keycloak.OID4VCConstants.CLAIM_NAME_VC;
|
||||
import static org.keycloak.jose.jwe.JWEConstants.RSA_OAEP_256;
|
||||
import static org.keycloak.models.oid4vci.CredentialScopeModel.VC_CONFIGURATION_ID;
|
||||
import static org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint.CREDENTIAL_OFFER_URI_CODE_SCOPE;
|
||||
import static org.keycloak.userprofile.DeclarativeUserProfileProvider.UP_COMPONENT_CONFIG_KEY;
|
||||
import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_ADMIN;
|
||||
import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_USER;
|
||||
import static org.keycloak.util.JsonSerialization.valueAsPrettyString;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.aMapWithSize;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
public abstract class OID4VCIssuerEndpointTest extends OID4VCIssuerTestBase {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(OID4VCIssuerEndpointTest.class);
|
||||
|
||||
protected static final TimeProvider TIME_PROVIDER = new StaticTimeProvider(1000);
|
||||
|
||||
protected ClientScopeRepresentation sdJwtTypeCredentialClientScope;
|
||||
protected ClientScopeRepresentation jwtTypeCredentialClientScope;
|
||||
protected ClientScopeRepresentation minimalJwtTypeCredentialClientScope;
|
||||
|
||||
protected CloseableHttpClient httpClient;
|
||||
protected ClientRepresentation client;
|
||||
|
||||
@InjectKeycloakUrls
|
||||
KeycloakUrls keycloakUrls;
|
||||
|
||||
record OAuth2CodeEntry(String key, OAuth2Code code) {}
|
||||
|
||||
@Override
|
||||
public void configureTestRealm() {
|
||||
super.configureTestRealm();
|
||||
|
||||
ComponentsResource components = testRealm.admin().components();
|
||||
components.add(getRsaKeyProvider(getRsaKey_Default())).close();
|
||||
components.add(getRsaEncKeyProvider(RSA_OAEP_256, "enc-key-oaep256", 100)).close();
|
||||
components.add(getUserProfileProvider());
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
httpClient = HttpClientBuilder.create().build();
|
||||
client = requireExistingClient(OID4VCI_CLIENT_ID);
|
||||
sdJwtTypeCredentialClientScope = requireExistingClientScope(sdJwtTypeCredentialScopeName);
|
||||
jwtTypeCredentialClientScope = requireExistingClientScope(jwtTypeCredentialScopeName);
|
||||
minimalJwtTypeCredentialClientScope = requireExistingClientScope(minimalJwtTypeCredentialScopeName);
|
||||
}
|
||||
|
||||
protected static OAuth2CodeEntry prepareSessionCode(
|
||||
KeycloakSession session,
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator,
|
||||
String note) {
|
||||
|
||||
AuthenticationManager.AuthResult authResult = authenticator.authenticate();
|
||||
UserSessionModel userSessionModel = authResult.session();
|
||||
AuthenticatedClientSessionModel authenticatedClientSessionModel =
|
||||
userSessionModel.getAuthenticatedClientSessionByClient(authResult.client().getId());
|
||||
|
||||
OAuth2Code oauth2Code = new OAuth2Code(
|
||||
SecretGenerator.getInstance().randomString(),
|
||||
Time.currentTime() + 6000,
|
||||
SecretGenerator.getInstance().randomString(),
|
||||
CREDENTIAL_OFFER_URI_CODE_SCOPE,
|
||||
authenticatedClientSessionModel.getUserSession().getId()
|
||||
);
|
||||
|
||||
String nonce = OAuth2CodeParser.persistCode(session, authenticatedClientSessionModel, oauth2Code);
|
||||
authenticatedClientSessionModel.setNote(nonce, note);
|
||||
|
||||
return new OAuth2CodeEntry(nonce, oauth2Code);
|
||||
}
|
||||
|
||||
protected static OID4VCIssuerEndpoint prepareIssuerEndpoint(
|
||||
KeycloakSession session,
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator) {
|
||||
|
||||
JwtCredentialBuilder jwtCredentialBuilder = new JwtCredentialBuilder(TIME_PROVIDER, session);
|
||||
SdJwtCredentialBuilder sdJwtCredentialBuilder = new SdJwtCredentialBuilder();
|
||||
|
||||
Map<String, CredentialBuilder> credentialBuilders = Map.of(
|
||||
jwtCredentialBuilder.getSupportedFormat(), jwtCredentialBuilder,
|
||||
sdJwtCredentialBuilder.getSupportedFormat(), sdJwtCredentialBuilder
|
||||
);
|
||||
|
||||
return prepareIssuerEndpoint(session, authenticator, credentialBuilders);
|
||||
}
|
||||
|
||||
protected static OID4VCIssuerEndpoint prepareIssuerEndpoint(
|
||||
KeycloakSession session,
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator,
|
||||
Map<String, CredentialBuilder> credentialBuilders) {
|
||||
|
||||
return new OID4VCIssuerEndpoint(
|
||||
session,
|
||||
credentialBuilders,
|
||||
authenticator,
|
||||
TIME_PROVIDER,
|
||||
30
|
||||
);
|
||||
}
|
||||
|
||||
protected ClientScopeRepresentation createOptionalClientScope(
|
||||
String scopeName,
|
||||
String issuerDid,
|
||||
String credentialConfigurationId,
|
||||
String credentialIdentifier,
|
||||
String vct,
|
||||
String format,
|
||||
String protocolMapperReferenceFile,
|
||||
List<String> acceptedKeyAttestationValues) {
|
||||
|
||||
ClientScopeRepresentation clientScope = new ClientScopeRepresentation();
|
||||
clientScope.setName(scopeName);
|
||||
clientScope.setProtocol(OID4VCIConstants.OID4VC_PROTOCOL);
|
||||
|
||||
Map<String, String> attributes = new HashMap<>();
|
||||
attributes.put(ClientScopeModel.INCLUDE_IN_TOKEN_SCOPE, "true");
|
||||
attributes.put(CredentialScopeModel.VC_EXPIRY_IN_SECONDS, "15");
|
||||
|
||||
if (issuerDid != null) {
|
||||
attributes.put(CredentialScopeModel.VC_ISSUER_DID, issuerDid);
|
||||
}
|
||||
if (credentialConfigurationId != null) {
|
||||
attributes.put(CredentialScopeModel.VC_CONFIGURATION_ID, credentialConfigurationId);
|
||||
}
|
||||
if (credentialIdentifier != null) {
|
||||
attributes.put(CredentialScopeModel.VC_IDENTIFIER, credentialIdentifier);
|
||||
}
|
||||
if (format != null) {
|
||||
attributes.put(CredentialScopeModel.VC_FORMAT, format);
|
||||
}
|
||||
|
||||
attributes.put(CredentialScopeModel.VCT, Optional.ofNullable(vct).orElse(credentialIdentifier));
|
||||
|
||||
if (credentialConfigurationId != null) {
|
||||
try {
|
||||
String vcDisplay = JsonSerialization.writeValueAsString(List.of(
|
||||
new DisplayObject().setName(credentialConfigurationId).setLocale("en-EN"),
|
||||
new DisplayObject().setName(credentialConfigurationId).setLocale("de-DE")
|
||||
));
|
||||
attributes.put(CredentialScopeModel.VC_DISPLAY, vcDisplay);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (acceptedKeyAttestationValues != null) {
|
||||
attributes.put(CredentialScopeModel.VC_KEY_ATTESTATION_REQUIRED, "true");
|
||||
if (!acceptedKeyAttestationValues.isEmpty()) {
|
||||
String joinedValues = String.join(",", acceptedKeyAttestationValues);
|
||||
attributes.put(CredentialScopeModel.VC_KEY_ATTESTATION_REQUIRED_KEY_STORAGE, joinedValues);
|
||||
attributes.put(CredentialScopeModel.VC_KEY_ATTESTATION_REQUIRED_USER_AUTH, joinedValues);
|
||||
}
|
||||
}
|
||||
clientScope.setAttributes(attributes);
|
||||
|
||||
List<ProtocolMapperRepresentation> protocolMappers;
|
||||
if (protocolMapperReferenceFile == null) {
|
||||
protocolMappers = getProtocolMappers(scopeName);
|
||||
} else {
|
||||
protocolMappers = resolveProtocolMappers(protocolMapperReferenceFile);
|
||||
protocolMappers.add(getStaticClaimMapper(scopeName));
|
||||
}
|
||||
|
||||
clientScope.setProtocolMappers(protocolMappers);
|
||||
return clientScope;
|
||||
}
|
||||
|
||||
protected ClientScopeRepresentation registerOptionalClientScope(ClientScopeRepresentation clientScope) {
|
||||
// Automatically removed when a test method is finished.
|
||||
try (Response res = testRealm.admin().clientScopes().create(clientScope)) {
|
||||
String scopeId = ApiUtil.getCreatedId(res);
|
||||
testRealm.cleanup().add(realm -> realm.clientScopes().get(scopeId).remove());
|
||||
clientScope.setId(scopeId);
|
||||
}
|
||||
return clientScope;
|
||||
}
|
||||
|
||||
protected ClientRepresentation requireExistingClient(String clientId) {
|
||||
return testRealm.admin().clients().findByClientId(clientId).stream()
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("No such client: " + clientId));
|
||||
}
|
||||
|
||||
protected ClientScopeRepresentation requireExistingClientScope(String scopeName) {
|
||||
// Check if the client scope already exists
|
||||
return testRealm.admin().clientScopes().findAll().stream()
|
||||
.filter(scope -> scope.getName().equals(scopeName))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("No such client scope: " + scopeName));
|
||||
}
|
||||
|
||||
public static List<ProtocolMapperRepresentation> resolveProtocolMappers(String protocolMapperReferenceFile) {
|
||||
if (protocolMapperReferenceFile == null) {
|
||||
return null;
|
||||
}
|
||||
try (InputStream inputStream = OID4VCIssuerEndpointTest.class.getResourceAsStream(protocolMapperReferenceFile)) {
|
||||
return JsonSerialization.mapper.readValue(inputStream, ClientScopeRepresentation.class).getProtocolMappers();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the AuthZCode complete flow without scope from
|
||||
// 1. Get authorization code without scope specified by wallet
|
||||
// 2. Using the code to get access token
|
||||
// 3. Get the credential configuration id from issuer metadata at .wellKnown
|
||||
// 4. With the access token, get the credential
|
||||
protected void testCredentialIssuanceWithAuthZCodeFlow(ClientScopeRepresentation credScope,
|
||||
Function<String, String> f,
|
||||
Consumer<Map<String, Object>> c) {
|
||||
|
||||
// Use credential_identifier if available, otherwise use configuration_id for error testing
|
||||
String testCredentialConfigurationId = credScope.getAttributes().get(VC_CONFIGURATION_ID);
|
||||
testCredentialIssuanceWithAuthZCodeFlow(credScope, f, c, (credentialIdentifier) -> {
|
||||
CredentialRequest request = new CredentialRequest();
|
||||
if (credentialIdentifier != null) {
|
||||
request.setCredentialIdentifier(credentialIdentifier);
|
||||
} else {
|
||||
request.setCredentialConfigurationId(testCredentialConfigurationId);
|
||||
}
|
||||
return request;
|
||||
});
|
||||
}
|
||||
|
||||
protected void testCredentialIssuanceWithAuthZCodeFlow(
|
||||
ClientScopeRepresentation clientScope,
|
||||
Function<String, String> f,
|
||||
Consumer<Map<String, Object>> c,
|
||||
Function<String, CredentialRequest> crf) {
|
||||
|
||||
String testScope = clientScope.getName();
|
||||
String testFormat = clientScope.getAttributes().get(CredentialScopeModel.VC_FORMAT);
|
||||
String testCredentialConfigurationId = clientScope.getAttributes().get(CredentialScopeModel.VC_CONFIGURATION_ID);
|
||||
|
||||
// Use credential_identifier if available, otherwise use configuration_id for error testing
|
||||
if (crf == null) {
|
||||
crf = (credentialIdentifier) -> {
|
||||
CredentialRequest request = new CredentialRequest();
|
||||
if (credentialIdentifier != null) {
|
||||
request.setCredentialIdentifier(credentialIdentifier);
|
||||
} else {
|
||||
request.setCredentialConfigurationId(testCredentialConfigurationId);
|
||||
}
|
||||
return request;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
try (Client restClient = Keycloak.getClientProvider().newRestEasyClient(null, null, true)) {
|
||||
String metadataUrl = getRealmMetadataPath(testRealm.getName());
|
||||
WebTarget oid4vciDiscoveryTarget = restClient.target(metadataUrl);
|
||||
|
||||
// 1. Get authorization code without scope specified by wallet
|
||||
// 2. Using the code to get the AccessToken
|
||||
String token = f.apply(testScope);
|
||||
|
||||
// Extract credential_identifier from the token (client-side parsing)
|
||||
String credentialIdentifier = null;
|
||||
try {
|
||||
JsonWebToken jwt = new JWSInput(token).readJsonContent(JsonWebToken.class);
|
||||
Object authDetails = jwt.getOtherClaims().get(OAuth2Constants.AUTHORIZATION_DETAILS);
|
||||
if (authDetails != null) {
|
||||
List<OID4VCAuthorizationDetail> authDetailsResponse = JsonSerialization.readValue(
|
||||
JsonSerialization.writeValueAsString(authDetails),
|
||||
new TypeReference<>() {}
|
||||
);
|
||||
if (!authDetailsResponse.isEmpty() &&
|
||||
authDetailsResponse.get(0).getCredentialIdentifiers() != null &&
|
||||
!authDetailsResponse.get(0).getCredentialIdentifiers().isEmpty()) {
|
||||
credentialIdentifier = authDetailsResponse.get(0).getCredentialIdentifiers().get(0);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to extract credential_identifier from token", e);
|
||||
}
|
||||
|
||||
// 3. Get the credential configuration id from issuer metadata at .wellKnown
|
||||
try (Response discoveryResponse = oid4vciDiscoveryTarget.request().get()) {
|
||||
CredentialIssuer oid4vciIssuerConfig = JsonSerialization.readValue(
|
||||
discoveryResponse.readEntity(String.class),
|
||||
CredentialIssuer.class
|
||||
);
|
||||
|
||||
assertEquals(200, discoveryResponse.getStatus());
|
||||
assertEquals(getRealmPath(testRealm.getName()), oid4vciIssuerConfig.getCredentialIssuer());
|
||||
assertEquals(getBasePath(testRealm.getName()) + "credential", oid4vciIssuerConfig.getCredentialEndpoint());
|
||||
|
||||
// 4. With the access token, get the credential
|
||||
try (Client clientForCredentialRequest = Keycloak.getClientProvider().newRestEasyClient(null, null, true)) {
|
||||
UriBuilder credentialUriBuilder = UriBuilder.fromUri(oid4vciIssuerConfig.getCredentialEndpoint());
|
||||
URI credentialUri = credentialUriBuilder.build();
|
||||
WebTarget credentialTarget = clientForCredentialRequest.target(credentialUri);
|
||||
|
||||
CredentialRequest request = crf.apply(credentialIdentifier);
|
||||
|
||||
assertEquals(testFormat, oid4vciIssuerConfig.getCredentialsSupported().get(testCredentialConfigurationId).getFormat());
|
||||
assertEquals(testCredentialConfigurationId, oid4vciIssuerConfig.getCredentialsSupported().get(testCredentialConfigurationId).getId());
|
||||
|
||||
c.accept(Map.of(
|
||||
"accessToken", token,
|
||||
"credentialTarget", credentialTarget,
|
||||
"credentialRequest", request
|
||||
));
|
||||
}
|
||||
}
|
||||
} catch (IOException | AssertionError e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected String getBasePath(String realm) {
|
||||
return getRealmPath(realm) + "/protocol/oid4vc/";
|
||||
}
|
||||
|
||||
protected String getRealmPath(String realm) {
|
||||
return keycloakUrls.getBaseUrl() + "/realms/" + realm;
|
||||
}
|
||||
|
||||
protected String getRealmMetadataPath(String realm) {
|
||||
return keycloakUrls.getBaseUrl() + "/.well-known/openid-credential-issuer/realms/" + realm;
|
||||
}
|
||||
|
||||
protected void requestCredentialWithIdentifier(
|
||||
String token,
|
||||
String credentialEndpoint,
|
||||
String credentialIdentifier,
|
||||
CredentialResponseHandler responseHandler,
|
||||
ClientScopeRepresentation expectedClientScope) throws IOException, VerificationException {
|
||||
|
||||
CredentialRequest request = new CredentialRequest();
|
||||
request.setCredentialIdentifier(credentialIdentifier);
|
||||
|
||||
StringEntity stringEntity = new StringEntity(JsonSerialization.writeValueAsString(request), ContentType.APPLICATION_JSON);
|
||||
|
||||
HttpPost postCredential = new HttpPost(credentialEndpoint);
|
||||
postCredential.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
|
||||
postCredential.setEntity(stringEntity);
|
||||
|
||||
CredentialResponse credentialResponse;
|
||||
try (CloseableHttpResponse credentialRequestResponse = httpClient.execute(postCredential)) {
|
||||
assertEquals(HttpStatus.SC_OK, credentialRequestResponse.getStatusLine().getStatusCode());
|
||||
String s = IOUtils.toString(credentialRequestResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
credentialResponse = JsonSerialization.readValue(s, CredentialResponse.class);
|
||||
}
|
||||
|
||||
// Use response handler to customize checks based on formats.
|
||||
responseHandler.handleCredentialResponse(credentialResponse, expectedClientScope);
|
||||
}
|
||||
|
||||
public CredentialIssuer getCredentialIssuerMetadata() {
|
||||
CredentialIssuerMetadataResponse metadataResponse = oauth.oid4vc().doIssuerMetadataRequest();
|
||||
return metadataResponse.getMetadata();
|
||||
}
|
||||
|
||||
private ComponentRepresentation getUserProfileProvider() {
|
||||
// Add the User DID attribute, with the same logic as in DeclarativeUserProfileProviderFactory
|
||||
UPConfig profileConfig = UPConfigUtils.parseSystemDefaultConfig();
|
||||
|
||||
if (profileConfig.getAttribute(UserModel.DID) == null) {
|
||||
UPAttribute attr = new UPAttribute(UserModel.DID);
|
||||
attr.setDisplayName("${did}");
|
||||
attr.setPermissions(new UPAttributePermissions(Set.of(ROLE_ADMIN, ROLE_USER), Set.of(ROLE_ADMIN, ROLE_USER)));
|
||||
attr.setValidations(Map.of(
|
||||
PatternValidator.ID, Map.of(
|
||||
"pattern", "^did:.+:.+$",
|
||||
"error-message", "Value must start with 'did:scheme:'"
|
||||
)
|
||||
));
|
||||
profileConfig.addOrReplaceAttribute(attr);
|
||||
}
|
||||
|
||||
ComponentRepresentation componentRepresentation = new ComponentRepresentation();
|
||||
componentRepresentation.setId(UUID.randomUUID().toString());
|
||||
componentRepresentation.setProviderId("declarative-user-profile");
|
||||
componentRepresentation.setProviderType(UserProfileProvider.class.getName());
|
||||
componentRepresentation.setConfig(new MultivaluedHashMap<>(
|
||||
Map.of(UP_COMPONENT_CONFIG_KEY, List.of(JsonSerialization.valueAsString(profileConfig)))
|
||||
));
|
||||
|
||||
return componentRepresentation;
|
||||
}
|
||||
|
||||
protected void withCausePropagation(Runnable r) throws Throwable {
|
||||
try {
|
||||
r.run();
|
||||
} catch (Exception e) {
|
||||
if (e instanceof RunOnServerException) {
|
||||
throw e.getCause();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
protected static class CredentialResponseHandler {
|
||||
final Logger log = Logger.getLogger(OID4VCIssuerEndpointTest.class);
|
||||
|
||||
protected void handleCredentialResponse(CredentialResponse credentialResponse, ClientScopeRepresentation clientScope) throws VerificationException {
|
||||
assertNotNull(credentialResponse.getCredentials(), "The credentials array should be present in the response.");
|
||||
assertFalse(credentialResponse.getCredentials().isEmpty(), "The credentials array should not be empty.");
|
||||
|
||||
// Get the first credential from the array (maintaining compatibility with single credential tests)
|
||||
CredentialResponse.Credential credentialObj = credentialResponse.getCredentials().get(0);
|
||||
assertNotNull(credentialObj, "The first credential in the array should not be null.");
|
||||
|
||||
JsonWebToken jsonWebToken = TokenVerifier.create((String) credentialObj.getCredential(), JsonWebToken.class).getToken();
|
||||
Map<String, Object> otherClaims = jsonWebToken.getOtherClaims();
|
||||
|
||||
log.infof("JsonWebToken: %s", valueAsPrettyString(jsonWebToken));
|
||||
assertNotNull(jsonWebToken.getId(), "Expected jti claim");
|
||||
assertNotNull(jsonWebToken.getExp(), "Expected exp claim");
|
||||
assertNotNull(jsonWebToken.getNbf(), "Expected nbf claim");
|
||||
assertNotNull(jsonWebToken.getIssuer(), "Expected iss claim");
|
||||
assertNotNull(jsonWebToken.getSubject(), "Expected sub claim");
|
||||
|
||||
assertNull(jsonWebToken.getAudience(), "Unexpected aud claim");
|
||||
assertNull(jsonWebToken.getIat(), "Unexpected iat claim");
|
||||
|
||||
assertEquals("did:web:test.org", jsonWebToken.getIssuer());
|
||||
assertEquals(Set.of(CLAIM_NAME_VC), otherClaims.keySet());
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, ?> vc = (Map<String, ?>) otherClaims.get("vc");
|
||||
VerifiableCredential credential = JsonSerialization.mapper.convertValue(vc, VerifiableCredential.class);
|
||||
Map<String, ?> subClaims = credential.getCredentialSubject().getClaims();
|
||||
|
||||
assertNotNull(credential.getIssuanceDate(), "Expected vc.issuanceDate claim");
|
||||
assertNotNull(credential.getExpirationDate(), "Expected vc.expirationDate claim");
|
||||
assertNotNull(credential.getContext(), "Expected vc.@context claim");
|
||||
|
||||
assertEquals(List.of(clientScope.getName()), credential.getType(), "vc.type mapped correctly");
|
||||
assertEquals("did:web:test.org", jsonWebToken.getIssuer(), "iss mapped correctly");
|
||||
assertEquals(URI.create("did:web:test.org"), credential.getIssuer(), "vc.issuer mapped correctly");
|
||||
assertEquals(jsonWebToken.getSubject(), subClaims.get("id"), "vc.credentialSubject.id mapped correctly");
|
||||
assertEquals("John", subClaims.get("given_name"), "vc.credentialSubject.given_name mapped correctly");
|
||||
assertEquals("john@email.cz", subClaims.get("email"), "vc.credentialSubject.email mapped correctly");
|
||||
assertEquals(clientScope.getName(), subClaims.get("scope-name"), "vc.credentialSubject.scope-name mapped correctly");
|
||||
|
||||
assertThat("vc.credentialSubject.address is parent claim for nested claims", subClaims.get("address"), instanceOf(Map.class));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, ?> nestedAddressClaim = (Map<String, ?>) subClaims.get("address");
|
||||
|
||||
assertThat("vc.credentialSubject.address contains two nested claims", nestedAddressClaim, aMapWithSize(2));
|
||||
assertEquals("221B Baker Street", nestedAddressClaim.get("street_address"), "vc.credentialSubject.address.street_address mapped correctly");
|
||||
assertEquals("London", nestedAddressClaim.get("locality"), "vc.credentialSubject.address.locality mapped correctly");
|
||||
|
||||
assertFalse(subClaims.containsKey("AnotherCredentialType"), "Unexpected other claim");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,16 @@ package org.keycloak.tests.oid4vc;
|
|||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.cert.Certificate;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
|
@ -11,38 +20,46 @@ import java.util.Map;
|
|||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import org.keycloak.OID4VCConstants;
|
||||
import org.keycloak.VCFormat;
|
||||
import org.keycloak.admin.client.CreatedResponseUtil;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.admin.client.resource.ClientResource;
|
||||
import org.keycloak.admin.client.resource.ClientScopesResource;
|
||||
import org.keycloak.admin.client.resource.ClientsResource;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.admin.client.resource.UsersResource;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.crypto.CryptoIntegration;
|
||||
import org.keycloak.common.util.CertificateUtils;
|
||||
import org.keycloak.common.util.KeyUtils;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
import org.keycloak.common.util.SecretGenerator;
|
||||
import org.keycloak.constants.OID4VCIConstants;
|
||||
import org.keycloak.crypto.KeyUse;
|
||||
import org.keycloak.crypto.KeyWrapper;
|
||||
import org.keycloak.keys.KeyProvider;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailsParser;
|
||||
import org.keycloak.protocol.oid4vc.issuance.TimeProvider;
|
||||
import org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCGeneratedIdMapper;
|
||||
import org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCIssuedAtTimeClaimMapper;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialScopeRepresentation;
|
||||
import org.keycloak.protocol.oid4vc.model.DisplayObject;
|
||||
import org.keycloak.protocol.oid4vc.model.OID4VCAuthorizationDetail;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
||||
import org.keycloak.representations.idm.ComponentRepresentation;
|
||||
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.userprofile.config.UPConfig;
|
||||
import org.keycloak.testframework.annotations.InjectAdminClient;
|
||||
import org.keycloak.testframework.annotations.InjectDependency;
|
||||
import org.keycloak.testframework.annotations.InjectClient;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.TestSetup;
|
||||
import org.keycloak.testframework.oauth.OAuthClient;
|
||||
import org.keycloak.testframework.oauth.annotations.InjectOAuthClient;
|
||||
import org.keycloak.testframework.realm.ClientConfig;
|
||||
import org.keycloak.testframework.realm.ClientConfigBuilder;
|
||||
import org.keycloak.testframework.realm.ManagedClient;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.realm.RealmConfig;
|
||||
import org.keycloak.testframework.realm.RealmConfigBuilder;
|
||||
|
|
@ -51,9 +68,10 @@ import org.keycloak.testframework.remote.timeoffset.InjectTimeOffSet;
|
|||
import org.keycloak.testframework.remote.timeoffset.TimeOffSet;
|
||||
import org.keycloak.testframework.server.KeycloakServerConfig;
|
||||
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
|
||||
import org.keycloak.testframework.server.KeycloakUrls;
|
||||
import org.keycloak.testframework.ui.annotations.InjectWebDriver;
|
||||
import org.keycloak.testframework.ui.webdriver.ManagedWebDriver;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenRequest;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
import org.keycloak.util.AuthorizationDetailsParser;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
|
|
@ -64,6 +82,8 @@ import static org.keycloak.OID4VCConstants.CLAIM_NAME_SUBJECT_ID;
|
|||
import static org.keycloak.OID4VCConstants.OID4VCI_ENABLED_ATTRIBUTE_KEY;
|
||||
import static org.keycloak.OID4VCConstants.OPENID_CREDENTIAL;
|
||||
import static org.keycloak.constants.OID4VCIConstants.CREDENTIAL_OFFER_CREATE;
|
||||
import static org.keycloak.models.Constants.CREATE_DEFAULT_CLIENT_SCOPES;
|
||||
import static org.keycloak.models.oid4vci.CredentialScopeModel.VC_FORMAT_DEFAULT;
|
||||
|
||||
/**
|
||||
* Abstract base class for OID4VCI Testing
|
||||
|
|
@ -73,23 +93,30 @@ import static org.keycloak.constants.OID4VCIConstants.CREDENTIAL_OFFER_CREATE;
|
|||
*/
|
||||
public abstract class OID4VCIssuerTestBase {
|
||||
|
||||
final Logger log = Logger.getLogger(getClass());
|
||||
protected final Logger log = Logger.getLogger(getClass());
|
||||
|
||||
public static final String OID4VCI_CLIENT_ID = "oid4vci-client";
|
||||
static final URI ISSUER_DID = URI.create("did:web:test.org");
|
||||
static final String TEST_CREDENTIAL_MAPPERS_FILE = "/oid4vc/test-credential-mappers.json";
|
||||
|
||||
protected static final String sdJwtCredentialVct = "https://credentials.example.com/SD-JWT-Credential";
|
||||
protected static final String sdJwtTypeCredentialScopeName = "sd-jwt-credential";
|
||||
protected static final String sdJwtTypeCredentialConfigurationIdName = "sd-jwt-credential-config-id";
|
||||
|
||||
static final String jwtTypeCredentialScopeName = "jwt-credential";
|
||||
static final String jwtTypeCredentialConfigurationIdName = "jwt-credential-config-id";
|
||||
static final String minimalJwtTypeCredentialScopeName = "vc-with-minimal-config";
|
||||
static final String minimalJwtTypeCredentialScopeIdName = "vc-with-minimal-config-id";
|
||||
|
||||
CredentialScopeRepresentation jwtTypeCredentialScope;
|
||||
CredentialScopeRepresentation minimalJwtTypeCredentialScope;
|
||||
CredentialScopeRepresentation jwtTypeCredentialScope;
|
||||
CredentialScopeRepresentation sdJwtTypeCredentialScope;
|
||||
|
||||
@InjectRealm(config = VCTestRealmConfig.class)
|
||||
ManagedRealm testRealm;
|
||||
|
||||
@InjectAdminClient
|
||||
Keycloak keycloak;
|
||||
@InjectClient(ref = "oid4vci-client", config = OID4VCIClient.class)
|
||||
ManagedClient managedClient;
|
||||
|
||||
@InjectOAuthClient
|
||||
OAuthClient oauth;
|
||||
|
|
@ -100,114 +127,32 @@ public abstract class OID4VCIssuerTestBase {
|
|||
@InjectWebDriver
|
||||
ManagedWebDriver driver;
|
||||
|
||||
String clientId = "test-app";
|
||||
|
||||
ClientRepresentation client;
|
||||
|
||||
@InjectAdminClient
|
||||
Keycloak keycloak;
|
||||
|
||||
@TestSetup
|
||||
public void configureTestRealm() {
|
||||
|
||||
// Enable OID4VCI on the test realm
|
||||
//
|
||||
RealmResource realmResource = testRealm.admin();
|
||||
RealmRepresentation realm = realmResource.toRepresentation();
|
||||
realm.setVerifiableCredentialsEnabled(shouldEnableOid4vci(realm));
|
||||
realmResource.update(realm);
|
||||
|
||||
// Add a user representations
|
||||
//
|
||||
UsersResource usersResource = realmResource.users();
|
||||
for (UserRepresentation user : List.of(
|
||||
getUserRepresentation("John Doe", Map.of("did", "did:key:1234"), List.of(CREDENTIAL_OFFER_CREATE.getName()), Map.of()),
|
||||
getUserRepresentation("Alice Wonderland", Map.of("did", "did:key:5678"), List.of(), Map.of()),
|
||||
getUserRepresentation("Bob Baumeister", Map.of("did", "did:key:789"), List.of(), Map.of()))) {
|
||||
|
||||
try (Response res = usersResource.create(user)) {
|
||||
|
||||
String userId = CreatedResponseUtil.getCreatedId(res);
|
||||
UserResource userResource = usersResource.get(userId);
|
||||
|
||||
List<RoleRepresentation> userRealmRoles = testRealm.admin().roles().list().stream()
|
||||
.filter(it -> user.getRealmRoles().contains(it.getName()))
|
||||
.toList();
|
||||
|
||||
userResource.roles().realmLevel().add(userRealmRoles);
|
||||
}
|
||||
}
|
||||
|
||||
// Register Credential Scopes
|
||||
//
|
||||
ClientScopesResource clientScopesResource = realmResource.clientScopes();
|
||||
clientScopesResource.create(createCredentialScope(jwtTypeCredentialScopeName,
|
||||
ISSUER_DID.toString(),
|
||||
jwtTypeCredentialConfigurationIdName,
|
||||
jwtTypeCredentialScopeName,
|
||||
null,
|
||||
VCFormat.JWT_VC,
|
||||
TEST_CREDENTIAL_MAPPERS_FILE,
|
||||
Collections.emptyList())
|
||||
).close();
|
||||
clientScopesResource.create(createCredentialScope(minimalJwtTypeCredentialScopeName,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null)
|
||||
).close();
|
||||
|
||||
jwtTypeCredentialScope = requireExistingCredentialScope(jwtTypeCredentialScopeName);
|
||||
minimalJwtTypeCredentialScope = requireExistingCredentialScope(minimalJwtTypeCredentialScopeName);
|
||||
|
||||
// Update the test clients
|
||||
//
|
||||
ClientsResource clientsResource = realmResource.clients();
|
||||
for (String cid : List.of(clientId)) {
|
||||
ClientRepresentation client = clientsResource.findByClientId(cid).get(0);
|
||||
ClientResource clientResource = clientsResource.get(client.getId());
|
||||
|
||||
// Enable OID4VCI
|
||||
setOid4vciEnabled(client, shouldEnableOid4vci(client));
|
||||
clientResource.update(client);
|
||||
|
||||
// Assign optional client scopes
|
||||
clientResource.addOptionalClientScope(jwtTypeCredentialScope.getId());
|
||||
clientResource.addOptionalClientScope(minimalJwtTypeCredentialScope.getId());
|
||||
}
|
||||
|
||||
// Fetch the test client
|
||||
client = clientsResource.findByClientId(clientId).get(0);
|
||||
UPConfig upConfig = realmResource.users().userProfile().getConfiguration();
|
||||
upConfig.setUnmanagedAttributePolicy(UPConfig.UnmanagedAttributePolicy.ADMIN_EDIT);
|
||||
realmResource.users().userProfile().update(upConfig);
|
||||
|
||||
AuthorizationDetailsParser.registerParser(OPENID_CREDENTIAL, new OID4VCAuthorizationDetailsParser());
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void beforeEachInternal() {
|
||||
client = testRealm.admin().clients().findByClientId(clientId).get(0);
|
||||
client = managedClient.admin().toRepresentation();
|
||||
jwtTypeCredentialScope = requireExistingCredentialScope(jwtTypeCredentialScopeName);
|
||||
minimalJwtTypeCredentialScope = requireExistingCredentialScope(minimalJwtTypeCredentialScopeName);
|
||||
sdJwtTypeCredentialScope = requireExistingCredentialScope(sdJwtTypeCredentialScopeName);
|
||||
oauth.client(OID4VCI_CLIENT_ID, "test-secret");
|
||||
}
|
||||
|
||||
protected boolean shouldEnableOid4vci(RealmRepresentation realm) {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean shouldEnableOid4vci(ClientRepresentation client) {
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean isOid4vciEnabled(ClientRepresentation client) {
|
||||
Map<String, String> attributes = Optional.ofNullable(client.getAttributes()).orElse(new HashMap<>());
|
||||
return Boolean.parseBoolean(attributes.get(OID4VCI_ENABLED_ATTRIBUTE_KEY));
|
||||
}
|
||||
|
||||
void setOid4vciEnabled(ClientRepresentation client, boolean enable) {
|
||||
Map<String, String> attributes = Optional.ofNullable(client.getAttributes()).orElse(new HashMap<>());
|
||||
attributes.put(OID4VCI_ENABLED_ATTRIBUTE_KEY, String.valueOf(enable));
|
||||
client.setAttributes(attributes);
|
||||
}
|
||||
|
||||
CredentialScopeRepresentation createCredentialScope(
|
||||
public static CredentialScopeRepresentation createCredentialScope(
|
||||
String scopeName,
|
||||
String issuerDid,
|
||||
String credentialConfigurationId,
|
||||
|
|
@ -229,7 +174,8 @@ public abstract class OID4VCIssuerTestBase {
|
|||
if (credentialConfigurationId != null) {
|
||||
List<DisplayObject> displayObjects = List.of(
|
||||
new DisplayObject().setName(credentialConfigurationId).setLocale("en-EN"),
|
||||
new DisplayObject().setName(credentialConfigurationId).setLocale("de-DE"));
|
||||
new DisplayObject().setName(credentialConfigurationId).setLocale("de-DE")
|
||||
);
|
||||
cs.setDisplay(JsonSerialization.valueAsString(displayObjects));
|
||||
}
|
||||
|
||||
|
|
@ -244,7 +190,7 @@ public abstract class OID4VCIssuerTestBase {
|
|||
if (protocolMapperReferenceFile == null) {
|
||||
cs.setProtocolMappers(getProtocolMappers(scopeName));
|
||||
} else {
|
||||
List<ProtocolMapperRepresentation> protocolMappers = resolveProtocolMappers(protocolMapperReferenceFile);
|
||||
List<ProtocolMapperRepresentation> protocolMappers = new ArrayList<>(resolveProtocolMappers(protocolMapperReferenceFile));
|
||||
protocolMappers.add(getStaticClaimMapper(scopeName));
|
||||
cs.setProtocolMappers(protocolMappers);
|
||||
}
|
||||
|
|
@ -256,7 +202,8 @@ public abstract class OID4VCIssuerTestBase {
|
|||
return testRealm.admin().clientScopes().findAll().stream()
|
||||
.filter(it -> scopeName.equals(it.getName()))
|
||||
.map(CredentialScopeRepresentation::new)
|
||||
.findFirst().orElse(null);
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
CredentialScopeRepresentation requireExistingCredentialScope(String scopeName) {
|
||||
|
|
@ -264,7 +211,7 @@ public abstract class OID4VCIssuerTestBase {
|
|||
.orElseThrow(() -> new IllegalStateException("No such credential scope: " + scopeName));
|
||||
}
|
||||
|
||||
ProtocolMapperRepresentation getIssuedAtTimeMapper(String subjectProperty, String truncateToTimeUnit, String valueSource) {
|
||||
public static ProtocolMapperRepresentation getIssuedAtTimeMapper(String subjectProperty, String truncateToTimeUnit, String valueSource) {
|
||||
ProtocolMapperRepresentation protocolMapperRepresentation = new ProtocolMapperRepresentation();
|
||||
protocolMapperRepresentation.setName(subjectProperty + "-oid4vc-issued-at-time-claim-mapper");
|
||||
protocolMapperRepresentation.setProtocol(OID4VCIConstants.OID4VC_PROTOCOL);
|
||||
|
|
@ -283,7 +230,7 @@ public abstract class OID4VCIssuerTestBase {
|
|||
return protocolMapperRepresentation;
|
||||
}
|
||||
|
||||
ProtocolMapperRepresentation getJtiGeneratedIdMapper() {
|
||||
public static ProtocolMapperRepresentation getJtiGeneratedIdMapper() {
|
||||
ProtocolMapperRepresentation protocolMapperRepresentation = new ProtocolMapperRepresentation();
|
||||
protocolMapperRepresentation.setName("generated-id-mapper");
|
||||
protocolMapperRepresentation.setProtocol(OID4VCIConstants.OID4VC_PROTOCOL);
|
||||
|
|
@ -293,7 +240,7 @@ public abstract class OID4VCIssuerTestBase {
|
|||
return protocolMapperRepresentation;
|
||||
}
|
||||
|
||||
List<ProtocolMapperRepresentation> getProtocolMappers(String scopeName) {
|
||||
public static List<ProtocolMapperRepresentation> getProtocolMappers(String scopeName) {
|
||||
return List.of(
|
||||
getSubjectIdMapper(CLAIM_NAME_SUBJECT_ID, UserModel.DID),
|
||||
getUserAttributeMapper("email", "email"),
|
||||
|
|
@ -307,7 +254,7 @@ public abstract class OID4VCIssuerTestBase {
|
|||
getIssuedAtTimeMapper("nbf", null, "COMPUTE"));
|
||||
}
|
||||
|
||||
ProtocolMapperRepresentation getSubjectIdMapper(String subjectProperty, String attributeName) {
|
||||
public static ProtocolMapperRepresentation getSubjectIdMapper(String subjectProperty, String attributeName) {
|
||||
ProtocolMapperRepresentation protocolMapperRepresentation = new ProtocolMapperRepresentation();
|
||||
protocolMapperRepresentation.setName(attributeName + "-mapper");
|
||||
protocolMapperRepresentation.setProtocol(OID4VCIConstants.OID4VC_PROTOCOL);
|
||||
|
|
@ -319,7 +266,7 @@ public abstract class OID4VCIssuerTestBase {
|
|||
return protocolMapperRepresentation;
|
||||
}
|
||||
|
||||
ProtocolMapperRepresentation getStaticClaimMapper(String scopeName) {
|
||||
public static ProtocolMapperRepresentation getStaticClaimMapper(String scopeName) {
|
||||
ProtocolMapperRepresentation protocolMapperRepresentation = new ProtocolMapperRepresentation();
|
||||
protocolMapperRepresentation.setName(UUID.randomUUID().toString());
|
||||
protocolMapperRepresentation.setProtocol(OID4VCIConstants.OID4VC_PROTOCOL);
|
||||
|
|
@ -332,7 +279,7 @@ public abstract class OID4VCIssuerTestBase {
|
|||
return protocolMapperRepresentation;
|
||||
}
|
||||
|
||||
ProtocolMapperRepresentation getUserAttributeMapper(String subjectProperty, String attributeName) {
|
||||
public static ProtocolMapperRepresentation getUserAttributeMapper(String subjectProperty, String attributeName) {
|
||||
ProtocolMapperRepresentation protocolMapperRepresentation = new ProtocolMapperRepresentation();
|
||||
protocolMapperRepresentation.setName(attributeName + "-mapper");
|
||||
protocolMapperRepresentation.setProtocol(OID4VCIConstants.OID4VC_PROTOCOL);
|
||||
|
|
@ -346,16 +293,17 @@ public abstract class OID4VCIssuerTestBase {
|
|||
return protocolMapperRepresentation;
|
||||
}
|
||||
|
||||
UserRepresentation getUserRepresentation(
|
||||
public static UserRepresentation getUserRepresentation(
|
||||
String fullName,
|
||||
Map<String, String> attributes,
|
||||
List<String> realmRoles,
|
||||
Map<String, List<String>> clientRoles
|
||||
) {
|
||||
Map<String, List<String>> clientRoles) {
|
||||
|
||||
String[] nameToks = fullName.split("\\s");
|
||||
String firstName = nameToks[0];
|
||||
String lastName = nameToks[1];
|
||||
String lastName = nameToks.length > 1 ? nameToks[1] : "";
|
||||
String username = firstName.toLowerCase();
|
||||
|
||||
UserConfigBuilder userBuilder = UserConfigBuilder.create()
|
||||
.id(KeycloakModelUtils.generateId())
|
||||
.username(username)
|
||||
|
|
@ -372,27 +320,171 @@ public abstract class OID4VCIssuerTestBase {
|
|||
attributes.forEach(userBuilder::attribute);
|
||||
|
||||
// When Keycloak issues a token for a user and client:
|
||||
//
|
||||
// 1. It looks up all effective realm roles and all effective client roles assigned to the user.
|
||||
// 2. The token includes only those roles that the user actually has.
|
||||
//
|
||||
realmRoles.forEach(userBuilder::roles);
|
||||
clientRoles.forEach((cid, roles) -> roles.forEach(role -> userBuilder.roles(cid, role)));
|
||||
|
||||
return userBuilder.build();
|
||||
}
|
||||
|
||||
List<ProtocolMapperRepresentation> resolveProtocolMappers(String protocolMapperReferenceFile) {
|
||||
public static List<ProtocolMapperRepresentation> resolveProtocolMappers(String protocolMapperReferenceFile) {
|
||||
if (protocolMapperReferenceFile == null) {
|
||||
return null;
|
||||
}
|
||||
try (InputStream inputStream = getClass().getResourceAsStream(protocolMapperReferenceFile)) {
|
||||
return JsonSerialization.mapper.readValue(inputStream,
|
||||
ClientScopeRepresentation.class).getProtocolMappers();
|
||||
try (InputStream inputStream = OID4VCIssuerTestBase.class.getResourceAsStream(protocolMapperReferenceFile)) {
|
||||
return JsonSerialization.mapper.readValue(inputStream, ClientScopeRepresentation.class).getProtocolMappers();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class StaticTimeProvider implements TimeProvider {
|
||||
private final int currentTimeInS;
|
||||
|
||||
public StaticTimeProvider(int currentTimeInS) {
|
||||
this.currentTimeInS = currentTimeInS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int currentTimeSeconds() {
|
||||
return currentTimeInS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long currentTimeMillis() {
|
||||
return currentTimeInS * 1000L;
|
||||
}
|
||||
}
|
||||
|
||||
public static KeyWrapper getRsaKey(KeyUse keyUse, String algorithm, String keyName) {
|
||||
try {
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
|
||||
kpg.initialize(2048);
|
||||
var keyPair = kpg.generateKeyPair();
|
||||
|
||||
KeyWrapper kw = new KeyWrapper();
|
||||
kw.setPrivateKey(keyPair.getPrivate());
|
||||
kw.setPublicKey(keyPair.getPublic());
|
||||
kw.setUse(keyUse);
|
||||
kw.setKid(keyName != null ? keyName : KeyUtils.createKeyId(keyPair.getPublic()));
|
||||
kw.setType("RSA");
|
||||
kw.setAlgorithm(algorithm);
|
||||
|
||||
return kw;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static ComponentRepresentation getRsaKeyProvider(KeyWrapper keyWrapper) {
|
||||
return createRsaKeyProviderComponent(keyWrapper, "rsa-key-provider", 0);
|
||||
}
|
||||
|
||||
public static ComponentRepresentation getRsaEncKeyProvider(String algorithm, String keyName, int priority) {
|
||||
KeyWrapper keyWrapper = getRsaKey(KeyUse.ENC, algorithm, keyName);
|
||||
return createRsaKeyProviderComponent(keyWrapper, keyName, priority);
|
||||
}
|
||||
|
||||
private static ComponentRepresentation createRsaKeyProviderComponent(KeyWrapper keyWrapper, String name, int priority) {
|
||||
ComponentRepresentation component = new ComponentRepresentation();
|
||||
component.setProviderType(KeyProvider.class.getName());
|
||||
component.setName(name);
|
||||
component.setId(UUID.randomUUID().toString());
|
||||
component.setProviderId("rsa");
|
||||
|
||||
Certificate certificate = CertificateUtils.generateV1SelfSignedCertificate(
|
||||
new KeyPair((PublicKey) keyWrapper.getPublicKey(), (PrivateKey) keyWrapper.getPrivateKey()), "TestKey");
|
||||
|
||||
component.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(String.valueOf(priority)),
|
||||
"enabled", List.of("true"),
|
||||
"algorithm", List.of(keyWrapper.getAlgorithm()),
|
||||
"keyUse", List.of(keyWrapper.getUse().name())
|
||||
)));
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
public static KeyWrapper getRsaKey_Default() {
|
||||
return getRsaKey(KeyUse.SIG, "RS256", null);
|
||||
}
|
||||
|
||||
public static ComponentRepresentation getAesKeyProvider(String algorithm, String keyName, String keyUse, String providerId) {
|
||||
// Generate a random AES key (default length: 256 bits)
|
||||
byte[] secret = SecretGenerator.getInstance().randomBytes(32); // 32 bytes = 256 bits
|
||||
String secretBase64 = Base64.getEncoder().encodeToString(secret);
|
||||
|
||||
ComponentRepresentation component = new ComponentRepresentation();
|
||||
component.setProviderType(KeyProvider.class.getName());
|
||||
component.setName(keyName);
|
||||
component.setId(UUID.randomUUID().toString());
|
||||
component.setProviderId(providerId);
|
||||
|
||||
component.setConfig(new MultivaluedHashMap<>(Map.of(
|
||||
"secret", List.of(secretBase64),
|
||||
"active", List.of("true"),
|
||||
"priority", List.of(String.valueOf(100)),
|
||||
"enabled", List.of("true"),
|
||||
"algorithm", List.of(algorithm),
|
||||
"keyUse", List.of(keyUse) // encryption usage
|
||||
)));
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
protected String getBearerToken(OAuthClient oauthClient) {
|
||||
return getBearerToken(oauthClient, null);
|
||||
}
|
||||
|
||||
protected String getBearerToken(OAuthClient oauthClient, ClientRepresentation client) {
|
||||
return getBearerToken(oauthClient, client, null);
|
||||
}
|
||||
|
||||
protected String getBearerToken(OAuthClient oauthClient, ClientRepresentation client, String scope) {
|
||||
return getBearerToken(oauthClient, client, "john", scope);
|
||||
}
|
||||
|
||||
protected String getBearerToken(OAuthClient oauthClient, ClientRepresentation client, String username, String scope) {
|
||||
return getBearerTokenCodeFlow(oauthClient, client, username, scope).getAccessToken();
|
||||
}
|
||||
|
||||
protected AccessTokenResponse getBearerTokenCodeFlow(OAuthClient oauthClient, ClientRepresentation client, String username, String scope) {
|
||||
var authCode = getAuthorizationCode(oauthClient, client, username, scope);
|
||||
return oauthClient.accessTokenRequest(authCode).send();
|
||||
}
|
||||
|
||||
protected String getAuthorizationCode(OAuthClient oAuthClient, ClientRepresentation client, String username, String scope) {
|
||||
if (client != null) {
|
||||
if (client.getSecret() != null) {
|
||||
oAuthClient.client(client.getClientId(), client.getSecret());
|
||||
}
|
||||
else {
|
||||
oAuthClient.client(client.getClientId());
|
||||
}
|
||||
}
|
||||
if (scope != null) {
|
||||
oAuthClient.scope(scope);
|
||||
}
|
||||
var authorizationEndpointResponse = oAuthClient.doLogin(username, "password");
|
||||
return authorizationEndpointResponse.getCode();
|
||||
}
|
||||
|
||||
protected AccessTokenResponse getBearerToken(OAuthClient oauthClient, String authCode, OID4VCAuthorizationDetail... authDetail) {
|
||||
AccessTokenRequest accessTokenRequest = oauthClient.accessTokenRequest(authCode);
|
||||
if (authDetail != null && authDetail.length > 0) {
|
||||
accessTokenRequest.authorizationDetails(Arrays.asList(authDetail));
|
||||
}
|
||||
AccessTokenResponse tokenResponse = accessTokenRequest.send();
|
||||
if (!tokenResponse.isSuccess()) {
|
||||
throw new IllegalStateException(tokenResponse.getErrorDescription());
|
||||
}
|
||||
return tokenResponse;
|
||||
}
|
||||
|
||||
// Config ----------------------------------------------------------------------------------------------------------
|
||||
|
||||
static class VCTestServerConfig implements KeycloakServerConfig {
|
||||
|
|
@ -406,14 +498,85 @@ public abstract class OID4VCIssuerTestBase {
|
|||
|
||||
public static final String TEST_REALM_NAME = "test";
|
||||
|
||||
@InjectDependency
|
||||
KeycloakUrls keycloakUrls;
|
||||
|
||||
@Override
|
||||
public RealmConfigBuilder configure(RealmConfigBuilder realm) {
|
||||
realm.name(TEST_REALM_NAME);
|
||||
|
||||
CryptoIntegration.init(this.getClass().getClassLoader());
|
||||
realm.verifiableCredentialsEnabled(true);
|
||||
realm.addRole(CREDENTIAL_OFFER_CREATE);
|
||||
|
||||
// Allow the default client scopes to be added as well
|
||||
realm.attribute(CREATE_DEFAULT_CLIENT_SCOPES, String.valueOf(true));
|
||||
|
||||
realm.addClientScope(createCredentialScope(
|
||||
sdJwtTypeCredentialScopeName,
|
||||
null,
|
||||
sdJwtTypeCredentialConfigurationIdName,
|
||||
sdJwtTypeCredentialScopeName,
|
||||
sdJwtCredentialVct,
|
||||
VCFormat.SD_JWT_VC,
|
||||
null,
|
||||
List.of(OID4VCConstants.KeyAttestationResistanceLevels.HIGH, OID4VCConstants.KeyAttestationResistanceLevels.MODERATE))
|
||||
);
|
||||
|
||||
realm.addClientScope(createCredentialScope(
|
||||
jwtTypeCredentialScopeName,
|
||||
ISSUER_DID.toString(),
|
||||
jwtTypeCredentialConfigurationIdName,
|
||||
jwtTypeCredentialScopeName,
|
||||
null,
|
||||
VCFormat.JWT_VC,
|
||||
TEST_CREDENTIAL_MAPPERS_FILE,
|
||||
Collections.emptyList()
|
||||
));
|
||||
|
||||
|
||||
realm.addClientScope(createCredentialScope(
|
||||
minimalJwtTypeCredentialScopeName,
|
||||
null,
|
||||
minimalJwtTypeCredentialScopeIdName,
|
||||
null,
|
||||
minimalJwtTypeCredentialScopeName,
|
||||
VC_FORMAT_DEFAULT,
|
||||
null,
|
||||
null
|
||||
));
|
||||
|
||||
realm.addUser(getUserRepresentation("John Doe", Map.of("did", "did:key:1234"), List.of(CREDENTIAL_OFFER_CREATE.getName()), Collections.emptyMap()));
|
||||
realm.addUser(getUserRepresentation("Alice Wonderland", Map.of("did", "did:key:5678"), List.of(), Map.of()));
|
||||
|
||||
return realm;
|
||||
}
|
||||
}
|
||||
|
||||
public static class OID4VCIClient implements ClientConfig {
|
||||
|
||||
@Override
|
||||
public ClientConfigBuilder configure(ClientConfigBuilder client) {
|
||||
client.clientId(OID4VCI_CLIENT_ID)
|
||||
.serviceAccountsEnabled(true)
|
||||
.directAccessGrantsEnabled(true)
|
||||
.attribute(OID4VCI_ENABLED_ATTRIBUTE_KEY, "true")
|
||||
.defaultClientScopes("basic", "profile", "roles")
|
||||
.optionalClientScopes(jwtTypeCredentialScopeName, minimalJwtTypeCredentialScopeName, sdJwtTypeCredentialScopeName, "email")
|
||||
.redirectUris("*")
|
||||
.secret("test-secret");
|
||||
|
||||
return client;
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean shouldEnableOid4vci(RealmRepresentation realm) {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean shouldEnableOid4vci(ClientRepresentation client) {
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean isOid4vciEnabled(ClientRepresentation client) {
|
||||
Map<String, String> attributes = Optional.ofNullable(client.getAttributes()).orElse(new HashMap<>());
|
||||
return Boolean.parseBoolean(attributes.get(OID4VCI_ENABLED_ATTRIBUTE_KEY));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue