From 613e55d733e79005ccaf4a5e8c682c9ff9e339e4 Mon Sep 17 00:00:00 2001 From: Thomas Diesler Date: Thu, 5 Feb 2026 15:12:47 +0100 Subject: [PATCH] [OID4VCI] Confine test realm setup to TestCase.configureTestRealm() Signed-off-by: Thomas Diesler --- .../java/org/keycloak/OID4VCConstants.java | 2 + .../java/org/keycloak/models/Constants.java | 4 + .../oid4vc/issuance/OID4VCIssuerEndpoint.java | 4 +- .../services/managers/RealmManager.java | 11 +- .../util/oauth/oid4vc/OID4VCClient.java | 4 + .../forms/PassThroughClientAuthenticator.java | 1 - .../AbstractTestRealmKeycloakTest.java | 4 +- .../OID4VCICredentialOfferMatrixTest.java | 22 +- .../mappers/OID4VCTargetRoleMapperTest.java | 2 - .../OID4VCAuthorizationCodeFlowTestBase.java | 26 +- ...ID4VCAuthorizationCodeFlowWithPARTest.java | 6 +- ...ID4VCAuthorizationDetailsFlowTestBase.java | 34 +- .../OID4VCCredentialOfferCorsTest.java | 5 - .../signing/OID4VCIssuerEndpointTest.java | 337 +++++++++--------- .../OID4VCJWTIssuerEndpointDisabledTest.java | 35 +- .../signing/OID4VCJWTIssuerEndpointTest.java | 34 +- ...ID4VCSdJwtIssuingEndpointDisabledTest.java | 38 +- .../OID4VCSdJwtIssuingEndpointTest.java | 12 +- ...4VCSdJwtPreInstalledNaturalPersonTest.java | 6 +- .../oid4vc/issuance/signing/OID4VCTest.java | 2 + 20 files changed, 268 insertions(+), 321 deletions(-) diff --git a/core/src/main/java/org/keycloak/OID4VCConstants.java b/core/src/main/java/org/keycloak/OID4VCConstants.java index f67eab8802d..9e67e39c560 100644 --- a/core/src/main/java/org/keycloak/OID4VCConstants.java +++ b/core/src/main/java/org/keycloak/OID4VCConstants.java @@ -5,6 +5,8 @@ package org.keycloak; */ public class OID4VCConstants { + public static final String OID4VCI_ENABLED_ATTRIBUTE_KEY = "oid4vci.enabled"; + // Sd-JWT constants public static final String SDJWT_DELIMITER = "~"; public static final String SD_HASH = "sd_hash"; diff --git a/server-spi-private/src/main/java/org/keycloak/models/Constants.java b/server-spi-private/src/main/java/org/keycloak/models/Constants.java index c331335b2c5..ce809b6cd37 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/Constants.java +++ b/server-spi-private/src/main/java/org/keycloak/models/Constants.java @@ -227,4 +227,8 @@ public final class Constants { // Internal note for storing authorization details response in client session context public static final String AUTHORIZATION_DETAILS_RESPONSE = "authorization_details_response"; + + // This attribute can be used in a realm import definition to signal that default client scopes should be created in addition to the client scopes defined by the realm import definition. + // When this attribute is omitted or set to false, the default client scopes are not created if at least one other client scope is defined by the realm import definition. + public static final String CREATE_DEFAULT_CLIENT_SCOPES = "CreateDefaultClientScopes"; } diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCIssuerEndpoint.java b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCIssuerEndpoint.java index ae0a66a35d9..eab19141fc5 100644 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCIssuerEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCIssuerEndpoint.java @@ -137,6 +137,7 @@ import org.apache.http.client.methods.HttpOptions; import org.apache.http.client.methods.HttpPost; import org.jboss.logging.Logger; +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.constants.OID4VCIConstants.OID4VC_PROTOCOL; @@ -191,9 +192,6 @@ public class OID4VCIssuerEndpoint { // lifespan of the preAuthorizedCodes in seconds private final int preAuthorizedCodeLifeSpan; - // constant for the OID4VCI enabled attribute key - public static final String OID4VCI_ENABLED_ATTRIBUTE_KEY = "oid4vci.enabled"; - /** * Credential builders are responsible for initiating the production of * credentials in a specific format. Their output is an appropriate credential diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java index 695b147f7bb..2c3c637898d 100755 --- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java +++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java @@ -19,6 +19,7 @@ package org.keycloak.services.managers; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Optional; import jakarta.ws.rs.BadRequestException; @@ -79,6 +80,7 @@ import org.keycloak.utils.SMTPUtil; import org.keycloak.utils.StringUtil; import static org.keycloak.constants.OID4VCIConstants.CREDENTIAL_OFFER_CREATE; +import static org.keycloak.models.Constants.CREATE_DEFAULT_CLIENT_SCOPES; /** * Per request object @@ -647,8 +649,7 @@ public class RealmManager { setupOfflineTokens(realm, rep); } - - if (rep.getClientScopes() == null) { + if (isCreateDefaultClientScopes(rep)) { createDefaultClientScopes(realm); } @@ -719,6 +720,12 @@ public class RealmManager { return realm; } + private boolean isCreateDefaultClientScopes(RealmRepresentation rep) { + Map attributes = rep.getAttributesOrEmpty(); + String createDefaultClientScopes = attributes.remove(CREATE_DEFAULT_CLIENT_SCOPES); + return rep.getClientScopes() == null || Boolean.parseBoolean(createDefaultClientScopes); + } + private String determineDefaultRoleName(RealmRepresentation rep) { String defaultRoleName = Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + rep.getRealm().toLowerCase(); if (! hasRealmRole(rep, defaultRoleName)) { diff --git a/tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/oid4vc/OID4VCClient.java b/tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/oid4vc/OID4VCClient.java index 1c46b6f50ef..801961a40ee 100644 --- a/tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/oid4vc/OID4VCClient.java +++ b/tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/oid4vc/OID4VCClient.java @@ -33,6 +33,10 @@ public class OID4VCClient { return new CredentialOfferRequest(client, nonce); } + public CredentialOfferResponse doCredentialOfferRequest(CredentialOfferURI credOfferUri) { + return credentialOfferRequest(credOfferUri).send(); + } + public Oid4vcCredentialRequest credentialRequest() { return credentialRequest(new CredentialRequest()); } diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/PassThroughClientAuthenticator.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/PassThroughClientAuthenticator.java index 5561919dae1..98e9234042a 100755 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/PassThroughClientAuthenticator.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/PassThroughClientAuthenticator.java @@ -41,7 +41,6 @@ public class PassThroughClientAuthenticator extends AbstractClientAuthenticator public static final String PROVIDER_ID = "testsuite-client-passthrough"; public static String clientId = "test-app"; - public static String namedClientId = "named-test-app"; // If this parameter is present in the HTTP request, the error will be thrown during authentication public static final String TEST_ERROR_PARAM = "test_error_param"; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractTestRealmKeycloakTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractTestRealmKeycloakTest.java index 33e510cc037..1c6b479e1a1 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractTestRealmKeycloakTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractTestRealmKeycloakTest.java @@ -42,7 +42,9 @@ import static org.keycloak.testsuite.AbstractAdminTest.loadJson; * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. */ public abstract class AbstractTestRealmKeycloakTest extends AbstractKeycloakTest { + public static final String TEST_REALM_NAME = "test"; + public static final String clientId = "test-app"; protected RealmResource testRealm() { return adminClient.realm(TEST_REALM_NAME); @@ -60,7 +62,7 @@ public abstract class AbstractTestRealmKeycloakTest extends AbstractKeycloakTest protected ClientRepresentation findTestApp(RealmRepresentation testRealm) { for (ClientRepresentation client : testRealm.getClients()) { - if (client.getClientId().equals("test-app")) return client; + if (client.getClientId().equals(clientId)) return client; } return null; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/OID4VCICredentialOfferMatrixTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/OID4VCICredentialOfferMatrixTest.java index 5a2c89fca6c..935adbe25da 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/OID4VCICredentialOfferMatrixTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/OID4VCICredentialOfferMatrixTest.java @@ -54,11 +54,8 @@ import org.junit.Test; import static org.keycloak.OAuth2Constants.SCOPE_OPENID; import static org.keycloak.OID4VCConstants.OPENID_CREDENTIAL; -import static org.keycloak.constants.OID4VCIConstants.CREDENTIAL_OFFER_CREATE; import static org.keycloak.protocol.oid4vc.model.ErrorType.INVALID_CREDENTIAL_OFFER_REQUEST; import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsernameId; -import static org.keycloak.testsuite.forms.PassThroughClientAuthenticator.clientId; -import static org.keycloak.testsuite.forms.PassThroughClientAuthenticator.namedClientId; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -86,6 +83,8 @@ import static org.junit.Assert.fail; */ public class OID4VCICredentialOfferMatrixTest extends OID4VCIssuerEndpointTest { + String namedClientId = "named-test-app"; + String issUsername = "john"; String issClientId = clientId; @@ -116,6 +115,13 @@ public class OID4VCICredentialOfferMatrixTest extends OID4VCIssuerEndpointTest { } } + @Override + protected void afterAbstractKeycloakTestRealmImport() { + ClientRepresentation namedClient = requireExistingClient(namedClientId); + assignOptionalClientScope(namedClient, credScopeName); + setOid4vciEnabled(namedClient, true); + } + @Test public void testCredentialWithoutOffer() throws Exception { var ctx = new TestContext(false, null, appUsername); @@ -238,9 +244,10 @@ public class OID4VCICredentialOfferMatrixTest extends OID4VCIssuerEndpointTest { // Exclude scope: // Require role: credential-offer-create - verifyTokenJwt(ctx, issToken, + // [TODO] Require role: credential-offer-create + verifyTokenJwt(issToken, List.of(), List.of(ctx.credentialConfiguration.getScope()), - List.of(CREDENTIAL_OFFER_CREATE.getName()), List.of()); + List.of(), List.of()); // Retrieving the credential-offer-uri // @@ -397,9 +404,7 @@ public class OID4VCICredentialOfferMatrixTest extends OID4VCIssuerEndpointTest { } private CredentialsOffer getCredentialsOffer(TestContext ctx, CredentialOfferURI credOfferUri) throws Exception { - CredentialOfferResponse credentialOfferResponse = oauth.oid4vc() - .credentialOfferRequest(credOfferUri) - .send(); + CredentialOfferResponse credentialOfferResponse = oauth.oid4vc().doCredentialOfferRequest(credOfferUri); CredentialsOffer credOffer = credentialOfferResponse.getCredentialsOffer(); assertEquals(List.of(ctx.credentialConfiguration.getId()), credOffer.getCredentialConfigurationIds()); return credOffer; @@ -478,7 +483,6 @@ public class OID4VCICredentialOfferMatrixTest extends OID4VCIssuerEndpointTest { } private void verifyTokenJwt( - TestContext ctx, String token, List includeScopes, List excludeScopes, diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/mappers/OID4VCTargetRoleMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/mappers/OID4VCTargetRoleMapperTest.java index d0958b420b0..5282fc91f91 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/mappers/OID4VCTargetRoleMapperTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/mappers/OID4VCTargetRoleMapperTest.java @@ -40,8 +40,6 @@ import org.keycloak.testsuite.runonserver.RunOnServerException; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.Test; -import static org.keycloak.testsuite.forms.PassThroughClientAuthenticator.clientId; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationCodeFlowTestBase.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationCodeFlowTestBase.java index e2c09afb3e4..1898802aeb5 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationCodeFlowTestBase.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationCodeFlowTestBase.java @@ -147,7 +147,6 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn // ===== STEP 2: Second login - Regular SSO (should NOT return authorization_details) ===== // Second login WITHOUT OID4VCI scope and WITHOUT authorization_details. - oauth.client(client.getClientId(), "password"); oauth.scope(OAuth2Constants.SCOPE_OPENID); oauth.openLoginForm(); @@ -157,7 +156,6 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn // Exchange second code for tokens WITHOUT authorization_details using OAuthClient AccessTokenResponse secondTokenResponse = oauth.accessTokenRequest(secondCode) .endpoint(ctx.openidConfig.getTokenEndpoint()) - .client(client.getClientId(), "password") .send(); assertEquals("Second token exchange should succeed", HttpStatus.SC_OK, secondTokenResponse.getStatusCode()); @@ -496,7 +494,6 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn // First token exchange - should succeed AccessTokenResponse tokenResponse = oauth.accessTokenRequest(code) .endpoint(ctx.openidConfig.getTokenEndpoint()) - .client(client.getClientId(), "password") .send(); assertEquals(HttpStatus.SC_OK, tokenResponse.getStatusCode()); @@ -506,7 +503,6 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn // Second token exchange with same code - should fail AccessTokenResponse errorResponse = oauth.accessTokenRequest(code) .endpoint(ctx.openidConfig.getTokenEndpoint()) - .client(client.getClientId(), "password") .send(); assertEquals(HttpStatus.SC_BAD_REQUEST, errorResponse.getStatusCode()); @@ -517,7 +513,7 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn // Verify error event was fired // Note: When code is reused, user is null but session from first successful use may still exist events.expect(EventType.CODE_TO_TOKEN_ERROR) - .client(client.getClientId()) + .client(clientId) .user((String) null) .session(AssertEvents.isSessionId()) .error(Errors.INVALID_CODE) @@ -536,7 +532,6 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn AccessTokenResponse errorResponse = oauth.accessTokenRequest("invalid-code-12345") .endpoint(ctx.openidConfig.getTokenEndpoint()) - .client(client.getClientId(), "password") .send(); assertEquals(HttpStatus.SC_BAD_REQUEST, errorResponse.getStatusCode()); @@ -547,7 +542,7 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn // Verify error event was fired // Note: When code is invalid (never valid), there is no session because authentication never occurred events.expect(EventType.CODE_TO_TOKEN_ERROR) - .client(client.getClientId()) + .client(clientId) .user((String) null) .session((String) null) .error(Errors.INVALID_CODE) @@ -566,7 +561,6 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn AccessTokenResponse tokenResponse = oauth.accessTokenRequest(code) .endpoint(ctx.openidConfig.getTokenEndpoint()) - .client(client.getClientId(), "password") .send(); assertEquals("Token exchange should succeed without authorization_details (it's optional)", @@ -596,7 +590,6 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn AccessTokenResponse errorResponse = oauth.accessTokenRequest(code) .endpoint(ctx.openidConfig.getTokenEndpoint()) - .client(client.getClientId(), "password") .send(); assertEquals(HttpStatus.SC_BAD_REQUEST, errorResponse.getStatusCode()); @@ -626,7 +619,7 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn AccessTokenResponse errorResponse = new InvalidTokenRequest(code, oauth) .endpoint(ctx.openidConfig.getTokenEndpoint()) - .withClientId(client.getClientId()) + .withClientId(clientId) .withClientSecret("password") // redirect_uri is intentionally omitted .send(); @@ -657,7 +650,7 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn AccessTokenResponse errorResponse = new InvalidTokenRequest(code, oauth) .endpoint(ctx.openidConfig.getTokenEndpoint()) - .withClientId(client.getClientId()) + .withClientId(clientId) .withClientSecret("password") .withRedirectUri("http://invalid-redirect-uri") .send(); @@ -736,7 +729,7 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn AccessTokenResponse errorResponse = oauth.accessTokenRequest(code) .endpoint(ctx.openidConfig.getTokenEndpoint()) - .client(client.getClientId(), "wrong-secret") + .client(clientId, "wrong-secret") .send(); assertEquals(HttpStatus.SC_UNAUTHORIZED, errorResponse.getStatusCode()); @@ -783,7 +776,6 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn Oid4vcTestContext ctx = prepareOid4vcTestContext(); // Perform authorization code flow with malformed authorization_details in the authorization request. - oauth.client(client.getClientId()); oauth.scope(getCredentialClientScope().getName()); oauth.loginForm() .param(OAuth2Constants.AUTHORIZATION_DETAILS, "invalid-json") @@ -796,7 +788,6 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn AccessTokenResponse errorResponse = oauth.accessTokenRequest(code) .endpoint(ctx.openidConfig.getTokenEndpoint()) - .client(client.getClientId(), "password") .send(); assertEquals(HttpStatus.SC_BAD_REQUEST, errorResponse.getStatusCode()); @@ -825,7 +816,6 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn AccessTokenResponse errorResponse = oauth.accessTokenRequest(code) .endpoint(ctx.openidConfig.getTokenEndpoint()) - .client(client.getClientId(), "password") .authorizationDetails(differentAuthDetails) .send(); @@ -960,7 +950,6 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn // Successful authorization_code flow private AccessTokenResponse authzCodeFlow(Oid4vcTestContext ctx, List claimsForAuthorizationDetailsParameter, boolean expectUserAlreadyAuthenticated) throws Exception { // Perform authorization code flow to get authorization code - oauth.client(client.getClientId(), "password"); oauth.scope(getCredentialClientScope().getName()); // Add the credential scope if (expectUserAlreadyAuthenticated) { oauth.openLoginForm(); @@ -982,7 +971,6 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn // Exchange authorization code for tokens with authorization_details return oauth.accessTokenRequest(code) .endpoint(ctx.openidConfig.getTokenEndpoint()) - .client(client.getClientId(), "password") .authorizationDetails(authDetails) .send(); } @@ -1095,7 +1083,6 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn * @return the authorization code */ protected String performAuthorizationCodeLogin() { - oauth.client(client.getClientId()); oauth.scope(getCredentialClientScope().getName()); oauth.loginForm().doLogin("john", "password"); String code = oauth.parseLoginResponse().getCode(); @@ -1110,7 +1097,6 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn * @return the authorization code */ protected String performAuthorizationCodeLoginWithAuthorizationDetails(String authorizationDetailsJson) { - oauth.client(client.getClientId()); oauth.scope(getCredentialClientScope().getName()); oauth.loginForm() // Encode JSON so UriBuilder does not treat '{' or '}' as URI template characters @@ -1129,7 +1115,7 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn */ protected AssertEvents.ExpectedEvent expectCredentialRequestError() { return events.expect(EventType.VERIFIABLE_CREDENTIAL_REQUEST_ERROR) - .client(client.getClientId()) + .client(clientId) .user(AssertEvents.isUUID()) .session(AssertEvents.isSessionId()) .error(Errors.INVALID_REQUEST); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationCodeFlowWithPARTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationCodeFlowWithPARTest.java index a98e4165b23..ed55d1b9b1f 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationCodeFlowWithPARTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationCodeFlowWithPARTest.java @@ -136,7 +136,7 @@ public class OID4VCAuthorizationCodeFlowWithPARTest extends OID4VCIssuerEndpoint assertNotNull("Request URI should not be null", requestUri); // Step 2: Perform authorization with PAR - oauth.client(client.getClientId()); + oauth.client(clientId); oauth.scope(getCredentialClientScope().getName()); oauth.loginForm().requestUri(requestUri).doLogin("john", "password"); @@ -235,7 +235,7 @@ public class OID4VCAuthorizationCodeFlowWithPARTest extends OID4VCIssuerEndpoint assertNotNull("Request URI should not be null", requestUri); // Step 2: Perform authorization with PAR - oauth.client(client.getClientId()); + oauth.client(clientId); oauth.scope(getCredentialClientScope().getName()); oauth.loginForm().requestUri(requestUri).doLogin("john", "password"); @@ -272,7 +272,6 @@ public class OID4VCAuthorizationCodeFlowWithPARTest extends OID4VCIssuerEndpoint assertNotNull("Request URI should not be null", requestUri); // Step 2: Perform authorization with PAR - oauth.client(client.getClientId()); oauth.scope(getCredentialClientScope().getName()); oauth.loginForm().requestUri(requestUri).doLogin("john", "password"); @@ -282,7 +281,6 @@ public class OID4VCAuthorizationCodeFlowWithPARTest extends OID4VCIssuerEndpoint // Step 3: Exchange authorization code for tokens AccessTokenResponse tokenResponse = oauth.accessTokenRequest(code) .endpoint(ctx.openidConfig.getTokenEndpoint()) - .client(oauth.getClientId(), "password") .send(); assertEquals(HttpStatus.SC_OK, tokenResponse.getStatusCode()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationDetailsFlowTestBase.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationDetailsFlowTestBase.java index 76ab85e1e38..20ed25a9b0b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationDetailsFlowTestBase.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCAuthorizationDetailsFlowTestBase.java @@ -38,7 +38,9 @@ import org.keycloak.protocol.oid4vc.model.CredentialsOffer; import org.keycloak.protocol.oid4vc.model.OID4VCAuthorizationDetail; import org.keycloak.protocol.oid4vc.model.PreAuthorizedCode; import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation; +import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientScopeRepresentation; +import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.util.ClientManager; import org.keycloak.testsuite.util.oauth.AccessTokenResponse; @@ -126,11 +128,11 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue .username("john") .send(); assertEquals(HttpStatus.SC_OK, credentialOfferURIResponse.getStatusCode()); - CredentialOfferURI credentialOfferURI = credentialOfferURIResponse.getCredentialOfferURI(); + CredentialOfferURI credOfferUri = credentialOfferURIResponse.getCredentialOfferURI(); // Verify CREDENTIAL_OFFER_REQUEST event was fired events.expect(EventType.VERIFIABLE_CREDENTIAL_OFFER_REQUEST) - .client(client.getClientId()) + .client(clientId) .user(AssertEvents.isUUID()) .session(AssertEvents.isSessionId()) .detail(Details.USERNAME, "john") @@ -140,15 +142,13 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue // Clear events before credential offer request events.clear(); - CredentialOfferResponse credentialOfferResponse = oauth.oid4vc() - .credentialOfferRequest(credentialOfferURI) - .send(); + CredentialOfferResponse credentialOfferResponse = oauth.oid4vc().doCredentialOfferRequest(credOfferUri); assertEquals(HttpStatus.SC_OK, credentialOfferResponse.getStatusCode()); ctx.credentialsOffer = credentialOfferResponse.getCredentialsOffer(); // Verify CREDENTIAL_OFFER_REQUEST event was fired (unauthenticated endpoint) events.expect(EventType.VERIFIABLE_CREDENTIAL_OFFER_REQUEST) - .client(client.getClientId()) + .client(clientId) .user(AssertEvents.isUUID()) .session((String) null) .detail(Details.CREDENTIAL_TYPE, credentialConfigurationId) @@ -660,7 +660,7 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue // Verify CREDENTIAL_REQUEST event was fired events.expect(EventType.VERIFIABLE_CREDENTIAL_REQUEST) - .client(client.getClientId()) + .client(clientId) .user(AssertEvents.isUUID()) .session(AssertEvents.isSessionId()) .detail(Details.USERNAME, "john") @@ -750,10 +750,20 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue // which has an "UNKNOWN" grant type context, is ALLOWED (backward compatibility). // This ensures the fail-closed logic doesn't accidentally block standard Keycloak flows. + ClientRepresentation accountClient = testRealm().clients().findByClientId("account").stream().findFirst().orElse(null); + assertNotNull("Has account client", accountClient); + + ClientRepresentation testClient = testRealm().clients().findByClientId("test-app").stream().findFirst().orElse(null); + assertNotNull("Has test-app", testClient); + assertEquals(testClient.getClientId(), oauth.getClientId()); + + UserRepresentation testUser = testRealm().users().search("test-user@localhost").stream().findFirst().orElse(null); + assertNotNull("Has test-user", testUser); + + log.debugf(JsonSerialization.valueAsPrettyString(testClient)); + // 1. Get standard token - oauth.realm("test"); - oauth.client("test-app", "password"); - org.keycloak.testsuite.util.oauth.AccessTokenResponse response = oauth.doPasswordGrantRequest("test-user@localhost", "password"); + AccessTokenResponse response = oauth.doPasswordGrantRequest("test-user@localhost", "password"); String accessToken = response.getAccessToken(); // 2. Use at Account API (which would be restricted if it were a pre-authorized token) @@ -892,7 +902,7 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue // Verify CREDENTIAL_REQUEST event was fired events.expect(EventType.VERIFIABLE_CREDENTIAL_REQUEST) - .client(client.getClientId()) + .client(clientId) .user(AssertEvents.isUUID()) .session(AssertEvents.isSessionId()) .detail(Details.USERNAME, "john") @@ -960,7 +970,7 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue // Verify VERIFIABLE_CREDENTIAL_REQUEST_ERROR event was fired events.expect(EventType.VERIFIABLE_CREDENTIAL_REQUEST_ERROR) - .client(client.getClientId()) + .client(clientId) .user(AssertEvents.isUUID()) .session(AssertEvents.isSessionId()) .error(Errors.INVALID_REQUEST) diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCCredentialOfferCorsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCCredentialOfferCorsTest.java index 30f38679abb..6a473967d30 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCCredentialOfferCorsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCCredentialOfferCorsTest.java @@ -55,8 +55,6 @@ import org.hamcrest.Matchers; import org.junit.Rule; import org.junit.Test; -import static org.keycloak.testsuite.forms.PassThroughClientAuthenticator.clientId; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -370,9 +368,6 @@ public class OID4VCCredentialOfferCorsTest extends OID4VCIssuerEndpointTest { // Helper methods private AccessTokenResponse getAccessToken() throws Exception { - oauth.realm("test"); - oauth.client(client.getClientId(), client.getSecret()); - return oauth.doPasswordGrantRequest("john", "password"); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCIssuerEndpointTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCIssuerEndpointTest.java index 68e7155178d..2343e60fad7 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCIssuerEndpointTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCIssuerEndpointTest.java @@ -37,8 +37,6 @@ import java.util.Set; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Consumer; -import java.util.function.Function; -import java.util.stream.Collectors; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; @@ -52,7 +50,6 @@ import org.keycloak.OID4VCConstants.KeyAttestationResistanceLevels; import org.keycloak.TokenVerifier; import org.keycloak.VCFormat; import org.keycloak.admin.client.resource.ClientResource; -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; @@ -96,7 +93,6 @@ import org.keycloak.representations.idm.ClientScopeRepresentation; import org.keycloak.representations.idm.ComponentExportRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.RealmRepresentation; -import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.RolesRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.userprofile.config.UPAttribute; @@ -128,14 +124,14 @@ import org.jboss.logging.Logger; import org.junit.Before; import static org.keycloak.OID4VCConstants.CLAIM_NAME_VC; +import static org.keycloak.OID4VCConstants.OID4VCI_ENABLED_ATTRIBUTE_KEY; import static org.keycloak.constants.OID4VCIConstants.CREDENTIAL_OFFER_CREATE; 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.models.Constants.CREATE_DEFAULT_CLIENT_SCOPES; import static org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint.CREDENTIAL_OFFER_URI_CODE_SCOPE; import static org.keycloak.protocol.oid4vc.model.ProofType.JWT; -import static org.keycloak.testsuite.forms.PassThroughClientAuthenticator.clientId; -import static org.keycloak.testsuite.forms.PassThroughClientAuthenticator.namedClientId; 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; @@ -155,29 +151,138 @@ import static org.junit.Assert.fail; */ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest { + private static final Logger LOGGER = Logger.getLogger(OID4VCIssuerEndpointTest.class); + protected static final TimeProvider TIME_PROVIDER = new OID4VCTest.StaticTimeProvider(1000); protected static final String sdJwtCredentialVct = "https://credentials.example.com/SD-JWT-Credential"; - private static final Logger LOGGER = Logger.getLogger(OID4VCIssuerEndpointTest.class); - - protected static ClientScopeRepresentation jwtTypeNaturalPersonClientScope; - protected static ClientScopeRepresentation sdJwtTypeNaturalPersonClientScope; - - protected static ClientScopeRepresentation jwtTypeCredentialClientScope; - protected static ClientScopeRepresentation sdJwtTypeCredentialClientScope; - protected static ClientScopeRepresentation minimalJwtTypeCredentialClientScope; + protected ClientScopeRepresentation sdJwtTypeCredentialClientScope; + protected ClientScopeRepresentation jwtTypeCredentialClientScope; + protected ClientScopeRepresentation minimalJwtTypeCredentialClientScope; protected CloseableHttpClient httpClient; - protected ClientRepresentation client; - protected ClientRepresentation namedClient; record OAuth2CodeEntry(String key, OAuth2Code code) {} - protected boolean shouldEnableOid4vci() { + protected boolean shouldEnableOid4vci(RealmRepresentation testRealm) { return true; } + protected boolean shouldEnableOid4vci(ClientRepresentation testClient) { + return true; + } + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + CryptoIntegration.init(this.getClass().getClassLoader()); + + testRealm.setVerifiableCredentialsEnabled(shouldEnableOid4vci(testRealm)); + + if (testRealm.getComponents() == null) { + testRealm.setComponents(new MultivaluedHashMap<>()); + } + + // Add key providers + testRealm.getComponents().add("org.keycloak.keys.KeyProvider", + getKeyProvider()); + testRealm.getComponents().add("org.keycloak.keys.KeyProvider", + getRsaEncKeyProvider(RSA_OAEP_256, "enc-key-oaep256", 100)); + testRealm.getComponents().add("org.keycloak.keys.KeyProvider", + getRsaEncKeyProvider(RSA_OAEP, "enc-key-oaep", 101)); + + // Add Did attribute to the user profile + testRealm.getComponents().add("org.keycloak.userprofile.UserProfileProvider", + getUserProfileProvider()); + + // Add a role representations + // + RolesRepresentation realmRoles = testRealm.getRoles(); + realmRoles.getRealm().add(CREDENTIAL_OFFER_CREATE); + realmRoles.getClient().get(clientId).add(getRoleRepresentation("testRole", clientId)); + + // Add user representations + // + Map> clientRoles = Map.of(clientId, List.of("testRole")); + List realmUsers = Optional.ofNullable(testRealm.getUsers()).map(ArrayList::new).orElse(new ArrayList<>()); + realmUsers.add(getUserRepresentation("John Doe", Map.of("did", "did:key:1234"), List.of(CREDENTIAL_OFFER_CREATE.getName()), clientRoles)); + realmUsers.add(getUserRepresentation("Alice Wonderland", Map.of("did", "did:key:5678"), List.of(), Map.of())); + testRealm.setUsers(realmUsers); + + // Allow the default client scopes to be added as well + Map realmAttributes = Optional.ofNullable(testRealm.getAttributes()).orElse(new HashMap<>()); + realmAttributes.put(CREATE_DEFAULT_CLIENT_SCOPES, String.valueOf(true)); + testRealm.setAttributes(realmAttributes); + + // Add additional client scopes + // + List clientScopes = Optional.ofNullable(testRealm.getClientScopes()).orElse(new ArrayList<>()); + clientScopes.add(createOptionalClientScope(sdJwtTypeCredentialScopeName, + null, + sdJwtTypeCredentialConfigurationIdName, + sdJwtTypeCredentialScopeName, + sdJwtCredentialVct, + VCFormat.SD_JWT_VC, + null, + List.of(KeyAttestationResistanceLevels.HIGH, KeyAttestationResistanceLevels.MODERATE)) + ); + clientScopes.add(createOptionalClientScope(jwtTypeCredentialScopeName, + TEST_DID.toString(), + jwtTypeCredentialConfigurationIdName, + jwtTypeCredentialScopeName, + null, + VCFormat.JWT_VC, + TEST_CREDENTIAL_MAPPERS_FILE, + Collections.emptyList()) + ); + clientScopes.add(createOptionalClientScope(minimalJwtTypeCredentialScopeName, + null, + null, + null, + null, + null, + null, null) + ); + testRealm.setClientScopes(clientScopes); + + // Enable oid4vci in test clients + for (String cid : List.of(clientId)) { + + ClientRepresentation testClient = testRealm.getClients().stream() + .filter(c -> c.getClientId().equals(cid)) + .findFirst().orElseThrow(() -> new IllegalStateException("Client with clientId=" + cid + " not found in realm")); + + // Enable oid4vci on the client + Map attributes = Optional.ofNullable(testClient.getAttributes()).orElse(new HashMap<>()); + attributes.put(OID4VCI_ENABLED_ATTRIBUTE_KEY, String.valueOf(shouldEnableOid4vci(testClient))); + testClient.setAttributes(attributes); + + // Assign default client scopes + List defaultClientScopes = new ArrayList<>(Optional.ofNullable(testClient.getDefaultClientScopes()).orElse(List.of())); + defaultClientScopes.addAll(List.of("web-origins", "acr", "roles", "profile", "basic", "email")); + testClient.setDefaultClientScopes(defaultClientScopes); + + // Assign optional client scopes + List optionalClientScopes = new ArrayList<>(Optional.ofNullable(testClient.getOptionalClientScopes()).orElse(List.of())); + // Realm import does not assign the default optional scopes + // optionalClientScopes.addAll(List.of("address", "phone", "offline_access", "organization", "microprofile-jwt")); + optionalClientScopes.addAll(clientScopes.stream().map(ClientScopeRepresentation::getName).toList()); + optionalClientScopes.addAll(List.of(jwtTypeNaturalPersonScopeName, sdJwtTypeNaturalPersonScopeName)); + testClient.setOptionalClientScopes(optionalClientScopes); + } + } + + @Before + public void setup() { + httpClient = HttpClientBuilder.create().build(); + client = requireExistingClient(clientId); + + // Lookup additional client scopes + 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(); @@ -222,85 +327,14 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest { 30); } - @Before - public void setup() { - CryptoIntegration.init(this.getClass().getClassLoader()); - httpClient = HttpClientBuilder.create().build(); - client = testRealm().clients().findByClientId(clientId).get(0); - namedClient = testRealm().clients().findByClientId(namedClientId).get(0); - - // Enable OID4VCI at realm level (required before assigning OID4VCI scopes) - RealmRepresentation realmRep = testRealm().toRepresentation(); - realmRep.setVerifiableCredentialsEnabled(true); - testRealm().update(realmRep); - - // Lookup the pre-installed oid4vc_natural_person client scope - jwtTypeNaturalPersonClientScope = requireExistingClientScope(jwtTypeNaturalPersonScopeName); - sdJwtTypeNaturalPersonClientScope = requireExistingClientScope(sdJwtTypeNaturalPersonScopeName); - - // Register the optional client scopes - sdJwtTypeCredentialClientScope = registerOptionalClientScope(sdJwtTypeCredentialScopeName, - null, - sdJwtTypeCredentialConfigurationIdName, - sdJwtTypeCredentialScopeName, - sdJwtCredentialVct, - VCFormat.SD_JWT_VC, - null, - List.of(KeyAttestationResistanceLevels.HIGH, KeyAttestationResistanceLevels.MODERATE)); - jwtTypeCredentialClientScope = registerOptionalClientScope(jwtTypeCredentialScopeName, - TEST_DID.toString(), - jwtTypeCredentialConfigurationIdName, - jwtTypeCredentialScopeName, - null, - VCFormat.JWT_VC, - TEST_CREDENTIAL_MAPPERS_FILE, - Collections.emptyList()); - minimalJwtTypeCredentialClientScope = registerOptionalClientScope("vc-with-minimal-config", - null, - null, - null, - null, - null, - null, null); - - List.of(client, namedClient).forEach(client -> { - String clientId = client.getClientId(); - - // Assign the registered optional client scopes to the client - assignOptionalClientScopeToClient(jwtTypeNaturalPersonClientScope.getId(), clientId); - assignOptionalClientScopeToClient(sdJwtTypeNaturalPersonClientScope.getId(), clientId); - assignOptionalClientScopeToClient(sdJwtTypeCredentialClientScope.getId(), clientId); - assignOptionalClientScopeToClient(jwtTypeCredentialClientScope.getId(), clientId); - assignOptionalClientScopeToClient(minimalJwtTypeCredentialClientScope.getId(), clientId); - - // Enable OID4VCI for the client by default, but allow tests to override - setClientOid4vciEnabled(clientId, shouldEnableOid4vci()); - }); - } - - private ClientResource findClientByClientId(RealmResource realm, String clientId) { - for (ClientRepresentation c : realm.clients().findAll()) { - if (clientId.equals(c.getClientId())) { - return realm.clients().get(c.getId()); - } - } - return null; - } - - protected ClientScopeRepresentation registerOptionalClientScope(String scopeName, - String issuerDid, - String credentialConfigurationId, - String credentialIdentifier, - String vct, - String format, - String protocolMapperReferenceFile, - List acceptedKeyAttestationValues) { - List existingScopes = testRealm().clientScopes().findAll(); - for (ClientScopeRepresentation existingScope : existingScopes) { - if (existingScope.getName().equals(scopeName)) { - return existingScope; - } - } + protected ClientScopeRepresentation createOptionalClientScope(String scopeName, + String issuerDid, + String credentialConfigurationId, + String credentialIdentifier, + String vct, + String format, + String protocolMapperReferenceFile, + List acceptedKeyAttestationValues) { ClientScopeRepresentation clientScope = new ClientScopeRepresentation(); clientScope.setName(scopeName); @@ -334,34 +368,41 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest { attributes.put(CredentialScopeModel.KEY_ATTESTATION_REQUIRED, "true"); if (!acceptedKeyAttestationValues.isEmpty()) { attributes.put(CredentialScopeModel.KEY_ATTESTATION_REQUIRED_KEY_STORAGE, - String.join(",", acceptedKeyAttestationValues)); + String.join(",", acceptedKeyAttestationValues)); attributes.put(CredentialScopeModel.KEY_ATTESTATION_REQUIRED_USER_AUTH, - String.join(",", acceptedKeyAttestationValues)); + String.join(",", acceptedKeyAttestationValues)); } } clientScope.setAttributes(attributes); - Response res = testRealm().clientScopes().create(clientScope); - String scopeId = ApiUtil.getCreatedId(res); - getCleanup().addClientScopeId(scopeId); // Automatically removed when a test method is finished. - res.close(); - - clientScope.setId(scopeId); - List protocolMappers; if (protocolMapperReferenceFile == null) { protocolMappers = getProtocolMappers(scopeName); - addProtocolMappersToClientScope(clientScope, protocolMappers); } else { protocolMappers = resolveProtocolMappers(protocolMapperReferenceFile); protocolMappers.add(getStaticClaimMapper(scopeName)); - addProtocolMappersToClientScope(clientScope, protocolMappers); } clientScope.setProtocolMappers(protocolMappers); return clientScope; } - private ClientScopeRepresentation requireExistingClientScope(String scopeName) { + protected ClientScopeRepresentation registerOptionalClientScope(ClientScopeRepresentation clientScope) { + // Automatically removed when a test method is finished. + try (Response res = testRealm().clientScopes().create(clientScope)) { + String scopeId = ApiUtil.getCreatedId(res); + getCleanup().addClientScopeId(scopeId); + clientScope.setId(scopeId); + } + return clientScope; + } + + protected ClientRepresentation requireExistingClient(String clientId) { + List clientRepresentations = testRealm().clients().findByClientId(clientId); + assertFalse("No such client", clientRepresentations.isEmpty()); + return clientRepresentations.get(0); + } + + protected ClientScopeRepresentation requireExistingClientScope(String scopeName) { // Check if the client scope already exists List existingScopes = testRealm().clientScopes().findAll(); @@ -386,9 +427,14 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest { } } - private void assignOptionalClientScopeToClient(String scopeId, String clientId) { - ClientResource clientResource = findClientByClientId(testRealm(), clientId); - clientResource.addOptionalClientScope(scopeId); + protected void assignOptionalClientScope(ClientRepresentation testClient, String scopeName) { + ClientScopeRepresentation clientScope = requireExistingClientScope(scopeName); + assignOptionalClientScope(testClient, clientScope); + } + + protected void assignOptionalClientScope(ClientRepresentation client, ClientScopeRepresentation clientScope) { + ClientResource clientResource = testRealm().clients().get(client.getId()); + clientResource.addOptionalClientScope(clientScope.getId()); } protected void logoutUser(String username) { @@ -493,15 +539,14 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest { return out.toByteArray(); } - 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()); + protected void setOid4vciEnabled(ClientRepresentation testClient, boolean enabled) { + ClientResource clientResource = testRealm().clients().get(testClient.getId()); - Map attributes = new HashMap<>(clientRepresentation.getAttributes() != null ? clientRepresentation.getAttributes() : Map.of()); - attributes.put("oid4vci.enabled", String.valueOf(enabled)); - clientRepresentation.setAttributes(attributes); + Map attributes = Optional.ofNullable(testClient.getAttributes()).orElse(new HashMap<>()); + attributes.put(OID4VCI_ENABLED_ATTRIBUTE_KEY, String.valueOf(enabled)); + testClient.setAttributes(attributes); - clientResource.update(clientRepresentation); + clientResource.update(testClient); } // Tests the AuthZCode complete flow without scope from @@ -512,7 +557,6 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest { protected void testCredentialIssuanceWithAuthZCodeFlow(ClientScopeRepresentation clientScope, BiFunction f, Consumer> c) { - String testClientId = client.getClientId(); String testScope = clientScope.getName(); String testFormat = clientScope.getAttributes().get(CredentialScopeModel.FORMAT); String testCredentialConfigurationId = clientScope.getAttributes().get(CredentialScopeModel.CONFIGURATION_ID); @@ -522,8 +566,8 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest { WebTarget oid4vciDiscoveryTarget = client.target(metadataUrl); // 1. Get authoriZation code without scope specified by wallet - // 2. Using the code to get accesstoken - String token = f.apply(testClientId, testScope); + // 2. Using the code to get the AccessToken + String token = f.apply(clientId, testScope); // Extract credential_identifier from the token (client-side parsing) String credentialIdentifier = null; @@ -645,12 +689,8 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest { } public CredentialIssuer getCredentialIssuerMetadata() { - final String endpoint = getRealmMetadataPath(TEST_REALM_NAME); CredentialIssuerMetadataResponse metadataResponse = oauth.oid4vc() - .issuerMetadataRequest() - .endpoint(endpoint) - .send(); - assertEquals(HttpStatus.SC_OK, metadataResponse.getStatusCode()); + .doIssuerMetadataRequest(); return metadataResponse.getMetadata(); } @@ -669,51 +709,6 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest { return result; } - @Override - public void configureTestRealm(RealmRepresentation testRealm) { - testRealm.setVerifiableCredentialsEnabled(true); - - if (testRealm.getComponents() == null) { - testRealm.setComponents(new MultivaluedHashMap<>()); - } - - // Add key providers - testRealm.getComponents().add("org.keycloak.keys.KeyProvider", getKeyProvider()); - testRealm.getComponents().add("org.keycloak.keys.KeyProvider", - getRsaEncKeyProvider(RSA_OAEP_256, "enc-key-oaep256", 100)); - testRealm.getComponents().add("org.keycloak.keys.KeyProvider", - getRsaEncKeyProvider(RSA_OAEP, "enc-key-oaep", 101)); - - // Add Did attribute to the user profile - testRealm.getComponents().add("org.keycloak.userprofile.UserProfileProvider", getUserProfileProvider()); - - // Find existing client representation - Map realmClients = testRealm.getClients().stream() - .collect(Collectors.toMap(ClientRepresentation::getClientId, Function.identity())); - ClientRepresentation existingClient = Optional.ofNullable(realmClients.get(clientId)) - .orElseThrow(() -> new IllegalStateException("Client with ID " + clientId + " not found in realm")); - - // Add a role to an existing client - RolesRepresentation realmRoles = testRealm.getRoles(); - if (realmRoles != null) { - realmRoles.getClient().merge( - existingClient.getClientId(), - List.of(getRoleRepresentation("testRole", existingClient.getClientId())), - (existingRoles, newRoles) -> { - List mergedRoles = new ArrayList<>(existingRoles); - mergedRoles.addAll(newRoles); - return mergedRoles; - } - ); - } - - Map> clientRoles = Map.of(clientId, List.of("testRole")); - List realmUsers = Optional.ofNullable(testRealm.getUsers()).map(ArrayList::new).orElse(new ArrayList<>()); - realmUsers.add(getUserRepresentation("John Doe", Map.of("did", "did:key:1234"), List.of(CREDENTIAL_OFFER_CREATE.getName()), clientRoles)); - realmUsers.add(getUserRepresentation("Alice Wonderland", Map.of("did", "did:key:5678"), List.of(), Map.of())); - testRealm.setUsers(realmUsers); - } - private ComponentExportRepresentation getUserProfileProvider() { // Add the User DID attribute, with the same logic as in DeclarativeUserProfileProviderFactory diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCJWTIssuerEndpointDisabledTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCJWTIssuerEndpointDisabledTest.java index 2dcf9163c2b..0eb4b9b886a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCJWTIssuerEndpointDisabledTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCJWTIssuerEndpointDisabledTest.java @@ -1,13 +1,12 @@ package org.keycloak.testsuite.oid4vc.issuance.signing; -import java.util.List; import java.util.function.Consumer; import jakarta.ws.rs.core.Response; -import org.keycloak.common.crypto.CryptoIntegration; import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint; import org.keycloak.protocol.oid4vc.model.CredentialRequest; +import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.services.CorsErrorResponseException; import org.keycloak.services.managers.AppAuthManager; @@ -15,13 +14,8 @@ import org.keycloak.testsuite.Assert; import org.keycloak.util.JsonSerialization; import com.fasterxml.jackson.core.JsonProcessingException; -import org.apache.http.impl.client.HttpClientBuilder; -import org.junit.Before; import org.junit.Test; -import static org.keycloak.testsuite.forms.PassThroughClientAuthenticator.clientId; -import static org.keycloak.testsuite.forms.PassThroughClientAuthenticator.namedClientId; - import static org.junit.Assert.assertEquals; /** @@ -30,34 +24,13 @@ import static org.junit.Assert.assertEquals; public class OID4VCJWTIssuerEndpointDisabledTest extends OID4VCIssuerEndpointTest { @Override - protected boolean shouldEnableOid4vci() { + protected boolean shouldEnableOid4vci(RealmRepresentation testRealm) { return false; } @Override - public void configureTestRealm(RealmRepresentation testRealm) { - super.configureTestRealm(testRealm); - testRealm.setVerifiableCredentialsEnabled(false); - } - - /** - * Override setup to skip creating oid4vc client scopes when verifiable credentials is disabled. - * The parent setup() tries to create client scopes with oid4vc protocol, which will fail - * with the new validation that prevents creating oid4vc scopes when VC is disabled. - */ - @Override - @Before - public void setup() { - CryptoIntegration.init(this.getClass().getClassLoader()); - httpClient = HttpClientBuilder.create().build(); - client = testRealm().clients().findByClientId(clientId).get(0); - namedClient = testRealm().clients().findByClientId(namedClientId).get(0); - - List.of(client, namedClient).forEach(client -> { - String clientId = client.getClientId(); - // Enable OID4VCI for the client by default, but allow tests to override - setClientOid4vciEnabled(clientId, shouldEnableOid4vci()); - }); + protected boolean shouldEnableOid4vci(ClientRepresentation testClient) { + return false; } /** diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCJWTIssuerEndpointTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCJWTIssuerEndpointTest.java index b1ed865a3cc..cdefc4142f4 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCJWTIssuerEndpointTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCJWTIssuerEndpointTest.java @@ -450,7 +450,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest { // 1. Retrieving the credential-offer-uri final String credentialConfigurationId = jwtTypeCredentialClientScope.getAttributes() .get(CredentialScopeModel.CONFIGURATION_ID); - CredentialOfferURI credentialOfferURI = oauth.oid4vc() + CredentialOfferURI credOfferUri = oauth.oid4vc() .credentialOfferUriRequest(credentialConfigurationId) .preAuthorized(true) .username("john") @@ -458,14 +458,11 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest { .send() .getCredentialOfferURI(); - assertNotNull("A valid offer uri should be returned", credentialOfferURI); + assertNotNull("A valid offer uri should be returned", credOfferUri); // 2. Using the uri to get the actual credential offer - CredentialsOffer credentialsOffer = oauth.oid4vc() - .credentialOfferRequest(credentialOfferURI) - .bearerToken(token) - .send() - .getCredentialsOffer(); + CredentialOfferResponse credentialOfferResponse = oauth.oid4vc().doCredentialOfferRequest(credOfferUri); + CredentialsOffer credentialsOffer = credentialOfferResponse.getCredentialsOffer(); assertNotNull("A valid offer should be returned", credentialsOffer); @@ -1059,15 +1056,16 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest { */ @Test public void testCredentialRequestWithOptionalClientScope() { - ClientScopeRepresentation optionalScope = registerOptionalClientScope( + ClientScopeRepresentation optionalScope = createOptionalClientScope( "optional-jwt-credential", TEST_DID.toString(), "optional-jwt-credential-config-id", null, null, VCFormat.JWT_VC, null, null); - - ClientRepresentation testClient = testRealm().clients().findByClientId(client.getClientId()).get(0); + + optionalScope = registerOptionalClientScope(optionalScope); + ClientRepresentation testClient = testRealm().clients().findByClientId(clientId).get(0); testRealm().clients().get(testClient.getId()).addOptionalClientScope(optionalScope.getId()); // Extract serializable data before lambda @@ -1114,15 +1112,16 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest { */ @Test public void testCannotAssignOid4vciScopeAsDefaultToClient() { - ClientScopeRepresentation oid4vciScope = registerOptionalClientScope( + ClientScopeRepresentation oid4vciScope = createOptionalClientScope( "test-oid4vci-scope", TEST_DID.toString(), "test-oid4vci-config-id", null, null, VCFormat.JWT_VC, null, null); - - ClientRepresentation testClient = testRealm().clients().findByClientId(client.getClientId()).get(0); + + oid4vciScope = registerOptionalClientScope(oid4vciScope); + ClientRepresentation testClient = testRealm().clients().findByClientId(clientId).get(0); ClientResource clientResource = testRealm().clients().get(testClient.getId()); try { @@ -1137,14 +1136,14 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest { @Test public void testCannotAssignOid4vciScopeAsDefaultToRealm() { - ClientScopeRepresentation oid4vciScope = registerOptionalClientScope( + ClientScopeRepresentation oid4vciScope = createOptionalClientScope( "test-oid4vci-realm-scope", TEST_DID.toString(), "test-oid4vci-realm-config-id", null, null, VCFormat.JWT_VC, null, null); - + oid4vciScope = registerOptionalClientScope(oid4vciScope); try { testRealm().addDefaultDefaultClientScope(oid4vciScope.getId()); Assert.fail("Expected BadRequestException when trying to assign OID4VCI scope as realm Default"); @@ -1160,13 +1159,14 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest { */ @Test public void testCannotAssignOid4vciScopeWhenRealmDisabled() { - ClientScopeRepresentation oid4vciScope = registerOptionalClientScope( + ClientScopeRepresentation oid4vciScope = createOptionalClientScope( "test-oid4vci-disabled-scope", TEST_DID.toString(), "test-oid4vci-disabled-config-id", null, null, VCFormat.JWT_VC, null, null); + oid4vciScope = registerOptionalClientScope(oid4vciScope); testingClient.server(TEST_REALM_NAME).run(session -> { RealmModel realm = session.getContext().getRealm(); @@ -1174,7 +1174,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest { }); try { - ClientRepresentation testClient = testRealm().clients().findByClientId(client.getClientId()).get(0); + ClientRepresentation testClient = testRealm().clients().findByClientId(clientId).get(0); ClientResource clientResource = testRealm().clients().get(testClient.getId()); try { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCSdJwtIssuingEndpointDisabledTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCSdJwtIssuingEndpointDisabledTest.java index 65a4c909306..442f8fcac50 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCSdJwtIssuingEndpointDisabledTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCSdJwtIssuingEndpointDisabledTest.java @@ -1,13 +1,12 @@ package org.keycloak.testsuite.oid4vc.issuance.signing; -import java.util.List; import java.util.function.Consumer; import jakarta.ws.rs.core.Response; -import org.keycloak.common.crypto.CryptoIntegration; import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint; import org.keycloak.protocol.oid4vc.model.CredentialRequest; +import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.services.CorsErrorResponseException; import org.keycloak.services.managers.AppAuthManager; @@ -15,13 +14,8 @@ import org.keycloak.testsuite.Assert; import org.keycloak.util.JsonSerialization; import com.fasterxml.jackson.core.JsonProcessingException; -import org.apache.http.impl.client.HttpClientBuilder; -import org.junit.Before; import org.junit.Test; -import static org.keycloak.testsuite.forms.PassThroughClientAuthenticator.clientId; -import static org.keycloak.testsuite.forms.PassThroughClientAuthenticator.namedClientId; - import static org.junit.Assert.assertEquals; /** @@ -30,37 +24,13 @@ import static org.junit.Assert.assertEquals; public class OID4VCSdJwtIssuingEndpointDisabledTest extends OID4VCIssuerEndpointTest { @Override - protected boolean shouldEnableOid4vci() { + protected boolean shouldEnableOid4vci(RealmRepresentation testRealm) { return false; } @Override - public void configureTestRealm(RealmRepresentation testRealm) { - super.configureTestRealm(testRealm); - testRealm.setVerifiableCredentialsEnabled(false); - } - - /** - * Override setup to skip creating oid4vc client scopes when verifiable credentials is disabled. - * The parent setup() tries to create client scopes with oid4vc protocol, which will fail - * with the new validation that prevents creating oid4vc scopes when VC is disabled. - */ - @Override - @Before - public void setup() { - CryptoIntegration.init(this.getClass().getClassLoader()); - httpClient = HttpClientBuilder.create().build(); - client = testRealm().clients().findByClientId(clientId).get(0); - namedClient = testRealm().clients().findByClientId(namedClientId).get(0); - - // Skip creating oid4vc client scopes when VC is disabled - they cannot be created - // and are not needed for these tests which verify that endpoints reject calls - - List.of(client, namedClient).forEach(client -> { - String clientId = client.getClientId(); - // Enable OID4VCI for the client by default, but allow tests to override - setClientOid4vciEnabled(clientId, shouldEnableOid4vci()); - }); + protected boolean shouldEnableOid4vci(ClientRepresentation testClient) { + return false; } @Test diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCSdJwtIssuingEndpointTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCSdJwtIssuingEndpointTest.java index 9be981c67dc..89e82537364 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCSdJwtIssuingEndpointTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCSdJwtIssuingEndpointTest.java @@ -60,6 +60,7 @@ import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.sdjwt.vp.SdJwtVP; import org.keycloak.services.managers.AppAuthManager; import org.keycloak.testsuite.util.oauth.AccessTokenResponse; +import org.keycloak.testsuite.util.oauth.oid4vc.CredentialOfferResponse; import org.keycloak.util.JsonSerialization; import com.fasterxml.jackson.databind.JsonNode; @@ -373,7 +374,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest { // 1. Retrieving the credential-offer-uri final String credentialConfigurationId = clientScope.getAttributes().get(CredentialScopeModel.CONFIGURATION_ID); - CredentialOfferURI credentialOfferURI = oauth.oid4vc() + CredentialOfferURI credOfferUri = oauth.oid4vc() .credentialOfferUriRequest(credentialConfigurationId) .preAuthorized(true) .username("john") @@ -381,14 +382,11 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest { .send() .getCredentialOfferURI(); - assertNotNull("A valid offer uri should be returned", credentialOfferURI); + assertNotNull("A valid offer uri should be returned", credOfferUri); // 2. Using the uri to get the actual credential offer - CredentialsOffer credentialsOffer = oauth.oid4vc() - .credentialOfferRequest(credentialOfferURI) - .bearerToken(token) - .send() - .getCredentialsOffer(); + CredentialOfferResponse credentialOfferResponse = oauth.oid4vc().doCredentialOfferRequest(credOfferUri); + CredentialsOffer credentialsOffer = credentialOfferResponse.getCredentialsOffer(); assertNotNull("A valid offer should be returned", credentialsOffer); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCSdJwtPreInstalledNaturalPersonTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCSdJwtPreInstalledNaturalPersonTest.java index 9e5a80d6dbb..3342a4cf918 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCSdJwtPreInstalledNaturalPersonTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCSdJwtPreInstalledNaturalPersonTest.java @@ -22,6 +22,7 @@ import java.util.stream.Collectors; import org.keycloak.VCFormat; import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerWellKnownProvider; import org.keycloak.protocol.oid4vc.model.Claim; +import org.keycloak.representations.idm.ClientScopeRepresentation; import org.junit.Test; import org.slf4j.LoggerFactory; @@ -43,8 +44,9 @@ public class OID4VCSdJwtPreInstalledNaturalPersonTest extends OID4VCIssuerEndpoi */ @Test public void testGetSdJwtConfigFromMetadata() { - final String scopeName = sdJwtTypeNaturalPersonClientScope.getName(); - final String credentialConfigurationId = sdJwtTypeNaturalPersonClientScope.getAttributes().get(CONFIGURATION_ID); + String scopeName = sdJwtTypeNaturalPersonScopeName; + ClientScopeRepresentation clientScope = requireExistingClientScope(scopeName); + String credentialConfigurationId = clientScope.getAttributes().get(CONFIGURATION_ID); String expectedIssuer = suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/" + TEST_REALM_NAME; testingClient .server(TEST_REALM_NAME) diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCTest.java index 2481ea74d44..fb8c0fcbc95 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCTest.java @@ -144,6 +144,8 @@ public abstract class OID4VCTest extends AbstractTestRealmKeycloakTest { protected static final String jwtTypeCredentialScopeName = "jwt-credential"; protected static final String jwtTypeCredentialConfigurationIdName = "jwt-credential-config-id"; + protected static final String minimalJwtTypeCredentialScopeName = "vc-with-minimal-config"; + protected static final String TEST_CREDENTIAL_MAPPERS_FILE = "/oid4vc/test-credential-mappers.json"; @BeforeClass