[OID4VCI] Confine test realm setup to TestCase.configureTestRealm()

Signed-off-by: Thomas Diesler <tdiesler@ibm.com>
This commit is contained in:
Thomas Diesler
2026-02-05 15:12:47 +01:00
committed by Marek Posolda
parent 4036ddc837
commit 613e55d733
20 changed files with 268 additions and 321 deletions
@@ -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";
@@ -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";
}
@@ -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
@@ -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<String, String> 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)) {
@@ -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());
}
@@ -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";
@@ -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;
@@ -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: <credScope>
// 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<String> includeScopes,
List<String> excludeScopes,
@@ -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;
@@ -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<ClaimsDescription> 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);
@@ -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());
@@ -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)
@@ -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");
}
@@ -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<String, List<String>> clientRoles = Map.of(clientId, List.of("testRole"));
List<UserRepresentation> 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<String, String> 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<ClientScopeRepresentation> 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<String, String> 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<String> 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<String> 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<String> acceptedKeyAttestationValues) {
List<ClientScopeRepresentation> 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<String> 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<ProtocolMapperRepresentation> 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<ClientRepresentation> 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<ClientScopeRepresentation> 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<String, String> attributes = new HashMap<>(clientRepresentation.getAttributes() != null ? clientRepresentation.getAttributes() : Map.of());
attributes.put("oid4vci.enabled", String.valueOf(enabled));
clientRepresentation.setAttributes(attributes);
Map<String, String> 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<String, String, String> f,
Consumer<Map<String, Object>> 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<String, ClientRepresentation> 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<RoleRepresentation> mergedRoles = new ArrayList<>(existingRoles);
mergedRoles.addAll(newRoles);
return mergedRoles;
}
);
}
Map<String, List<String>> clientRoles = Map.of(clientId, List.of("testRole"));
List<UserRepresentation> 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
@@ -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;
}
/**
@@ -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 {
@@ -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
@@ -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);
@@ -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)
@@ -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