[OID4VCI] Disable OID4VCI functionality when Verified Credentials switch is off (#44995)

closes #44622


Signed-off-by: forkimenjeckayang <forkimenjeckayang@gmail.com>
Signed-off-by: mposolda <mposolda@gmail.com>
Co-authored-by: mposolda <mposolda@gmail.com>
This commit is contained in:
forkimenjeckayang 2026-01-19 14:09:42 +01:00 committed by GitHub
parent c8a41dea99
commit fa28ddddb2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 544 additions and 50 deletions

View file

@ -62,7 +62,7 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
const form = useForm<ClientScopeDefaultOptionalType>({ mode: "onChange" });
const { control, handleSubmit, setValue, formState } = form;
const { isDirty, isValid } = formState;
const { realm } = useRealm();
const { realm, realmRepresentation } = useRealm();
const providers = useLoginProviders();
const serverInfo = useServerInfo();
@ -152,7 +152,9 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
});
const isOid4vcProtocol = selectedProtocol === OID4VC_PROTOCOL;
const isOid4vcEnabled = isFeatureEnabled(Feature.OpenId4VCI);
const isOid4vcEnabled =
isFeatureEnabled(Feature.OpenId4VCI) &&
realmRepresentation?.verifiableCredentialsEnabled;
const isNotSaml = selectedProtocol != "saml";
const setDynamicRegex = (value: string, append: boolean) =>

View file

@ -16,6 +16,7 @@ import { FineGrainOpenIdConnect } from "./advanced/FineGrainOpenIdConnect";
import { FineGrainSamlEndpointConfig } from "./advanced/FineGrainSamlEndpointConfig";
import { OpenIdConnectCompatibilityModes } from "./advanced/OpenIdConnectCompatibilityModes";
import { OpenIdVerifiableCredentials } from "./advanced/OpenIdVerifiableCredentials";
import { useRealm } from "../context/realm-context/RealmContext";
import { PROTOCOL_OIDC, PROTOCOL_OID4VC } from "./constants";
export const parseResult = (
@ -53,6 +54,7 @@ export type AdvancedProps = {
export const AdvancedTab = ({ save, client }: AdvancedProps) => {
const { t } = useTranslation();
const { realmRepresentation } = useRealm();
const isFeatureEnabled = useIsFeatureEnabled();
const { setValue } = useFormContext();
@ -207,7 +209,8 @@ export const AdvancedTab = ({ save, client }: AdvancedProps) => {
title: t("openIdVerifiableCredentials"),
isHidden:
(protocol !== PROTOCOL_OIDC && protocol !== PROTOCOL_OID4VC) ||
!isFeatureEnabled(Feature.OpenId4VCI),
!isFeatureEnabled(Feature.OpenId4VCI) ||
!realmRepresentation?.verifiableCredentialsEnabled,
panel: (
<>
<Text className="pf-v5-u-pb-lg">

View file

@ -2,13 +2,20 @@ import { useTranslation } from "react-i18next";
import { SelectControl } from "@keycloak/keycloak-ui-shared";
import { FormAccess } from "../../components/form/FormAccess";
import { useLoginProviders } from "../../context/server-info/ServerInfoProvider";
import { useRealm } from "../../context/realm-context/RealmContext";
import { ClientDescription } from "../ClientDescription";
import { getProtocolName } from "../utils";
import { PROTOCOL_OID4VC } from "../constants";
export const GeneralSettings = () => {
const { t } = useTranslation();
const { realmRepresentation } = useRealm();
const providers = useLoginProviders();
const filteredProviders = realmRepresentation?.verifiableCredentialsEnabled
? providers
: providers.filter((provider) => provider !== PROTOCOL_OID4VC);
return (
<FormAccess isHorizontal role="manage-clients">
<SelectControl
@ -18,7 +25,7 @@ export const GeneralSettings = () => {
controller={{
defaultValue: "",
}}
options={providers.map((option) => ({
options={filteredProviders.map((option) => ({
key: option,
value: getProtocolName(t, option),
}))}

View file

@ -667,7 +667,9 @@ export const RealmSettingsTokensTab = ({
},
{
title: t("oid4vciAttributes"),
isHidden: !isFeatureEnabled(Feature.OpenId4VCI),
isHidden:
!isFeatureEnabled(Feature.OpenId4VCI) ||
!realm.verifiableCredentialsEnabled,
panel: (
<FormAccess
isHorizontal

View file

@ -1,6 +1,7 @@
import { expect, test } from "@playwright/test";
import type { Page } from "@playwright/test";
import { createTestBed } from "../support/testbed.ts";
import adminClient from "../utils/AdminClient.js";
import { goToClientScopes } from "../utils/sidebar.ts";
import { clickSaveButton, selectItem } from "../utils/form.ts";
import { clickTableRowItem, clickTableToolbarItem } from "../utils/table.ts";
@ -96,7 +97,9 @@ test.describe("OID4VCI Client Scope Functionality", () => {
test("should display OID4VCI fields when protocol is selected", async ({
page,
}) => {
await using testBed = await createTestBed();
await using testBed = await createTestBed({
verifiableCredentialsEnabled: true,
});
await createClientScope(page, testBed);
await expect(page.locator("#kc-protocol")).toBeVisible();
@ -134,7 +137,9 @@ test.describe("OID4VCI Client Scope Functionality", () => {
});
test("should save and persist OID4VCI field values", async ({ page }) => {
await using testBed = await createTestBed();
await using testBed = await createTestBed({
verifiableCredentialsEnabled: true,
});
const testClientScopeName = `oid4vci-test-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
await createClientScopeAndSelectProtocolAndFormat(
@ -218,7 +223,9 @@ test.describe("OID4VCI Client Scope Functionality", () => {
test("should show OID4VCI protocol when global feature is enabled", async ({
page,
}) => {
await using testBed = await createTestBed();
await using testBed = await createTestBed({
verifiableCredentialsEnabled: true,
});
await createClientScope(page, testBed);
await expect(page.locator("#kc-protocol")).toBeVisible();
@ -268,7 +275,9 @@ test.describe("OID4VCI Client Scope Functionality", () => {
test("should handle OID4VCI protocol selection correctly", async ({
page,
}) => {
await using testBed = await createTestBed();
await using testBed = await createTestBed({
verifiableCredentialsEnabled: true,
});
await createClientScope(page, testBed);
await expect(page.locator("#kc-protocol")).toBeVisible();
@ -302,7 +311,9 @@ test.describe("OID4VCI Client Scope Functionality", () => {
test("should only show supported format options (dc+sd-jwt and jwt_vc)", async ({
page,
}) => {
await using testBed = await createTestBed();
await using testBed = await createTestBed({
verifiableCredentialsEnabled: true,
});
await createClientScopeAndSelectProtocolAndFormat(page, testBed);
await page.locator("#kc-vc-format").click();
@ -322,7 +333,9 @@ test.describe("OID4VCI Client Scope Functionality", () => {
test("should show format-specific fields for SD-JWT format", async ({
page,
}) => {
await using testBed = await createTestBed();
await using testBed = await createTestBed({
verifiableCredentialsEnabled: true,
});
await createClientScopeAndSelectProtocolAndFormat(
page,
testBed,
@ -343,7 +356,9 @@ test.describe("OID4VCI Client Scope Functionality", () => {
test("should show format-specific fields for JWT VC format", async ({
page,
}) => {
await using testBed = await createTestBed();
await using testBed = await createTestBed({
verifiableCredentialsEnabled: true,
});
await createClientScopeAndSelectProtocolAndFormat(
page,
testBed,
@ -364,7 +379,9 @@ test.describe("OID4VCI Client Scope Functionality", () => {
test("should save and persist new OID4VCI field values for SD-JWT format", async ({
page,
}) => {
await using testBed = await createTestBed();
await using testBed = await createTestBed({
verifiableCredentialsEnabled: true,
});
const testClientScopeName = `oid4vci-sdjwt-test-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
await createClientScopeAndSelectProtocolAndFormat(
@ -427,7 +444,9 @@ test.describe("OID4VCI Client Scope Functionality", () => {
test("should omit optional OID4VCI fields when left blank", async ({
page,
}) => {
await using testBed = await createTestBed();
await using testBed = await createTestBed({
verifiableCredentialsEnabled: true,
});
const testClientScopeName = `oid4vci-blank-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
await createClientScopeAndSelectProtocolAndFormat(
@ -454,7 +473,9 @@ test.describe("OID4VCI Client Scope Functionality", () => {
test("should conditionally show/hide fields when format changes", async ({
page,
}) => {
await using testBed = await createTestBed();
await using testBed = await createTestBed({
verifiableCredentialsEnabled: true,
});
await createClientScopeAndSelectProtocolAndFormat(
page,
testBed,
@ -502,7 +523,9 @@ test.describe("OID4VCI Client Scope Functionality", () => {
});
test("should show token_jws_type for all formats", async ({ page }) => {
await using testBed = await createTestBed();
await using testBed = await createTestBed({
verifiableCredentialsEnabled: true,
});
await createClientScopeAndSelectProtocolAndFormat(
page,
testBed,
@ -520,7 +543,9 @@ test.describe("OID4VCI Client Scope Functionality", () => {
test("should display signing algorithm dropdown with available algorithms", async ({
page,
}) => {
await using testBed = await createTestBed();
await using testBed = await createTestBed({
verifiableCredentialsEnabled: true,
});
await createClientScopeAndSelectProtocolAndFormat(
page,
testBed,
@ -538,7 +563,9 @@ test.describe("OID4VCI Client Scope Functionality", () => {
test("should display hash algorithm dropdown with available algorithms", async ({
page,
}) => {
await using testBed = await createTestBed();
await using testBed = await createTestBed({
verifiableCredentialsEnabled: true,
});
await createClientScopeAndSelectProtocolAndFormat(
page,
testBed,
@ -555,7 +582,9 @@ test.describe("OID4VCI Client Scope Functionality", () => {
});
test("should save and persist hash algorithm value", async ({ page }) => {
await using testBed = await createTestBed();
await using testBed = await createTestBed({
verifiableCredentialsEnabled: true,
});
const testClientScopeName = `oid4vci-hash-alg-test-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
await createClientScopeAndSelectProtocolAndFormat(
@ -588,7 +617,9 @@ test.describe("OID4VCI Client Scope Functionality", () => {
test("should default to SHA-256 when hash algorithm is not set", async ({
page,
}) => {
await using testBed = await createTestBed();
await using testBed = await createTestBed({
verifiableCredentialsEnabled: true,
});
const testClientScopeName = `oid4vci-hash-default-test-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
await createClientScopeAndSelectProtocolAndFormat(
@ -611,4 +642,32 @@ test.describe("OID4VCI Client Scope Functionality", () => {
"SHA-256",
);
});
test("should not offer OID4VCI protocol when verifiable credentials are disabled for the realm", async ({
page,
}) => {
await using testBed = await createTestBed();
await adminClient.updateRealm(testBed.realm, {
verifiableCredentialsEnabled: false,
});
try {
await createClientScope(page, testBed);
await expect(page.locator("#kc-protocol")).toBeVisible();
await page.locator("#kc-protocol").click();
await expect(
page.getByRole("option", {
name: "OpenID for Verifiable Credentials",
}),
).toHaveCount(0);
} finally {
// Re-enable verifiable credentials so other tests see the default behavior
await adminClient.updateRealm(testBed.realm, {
verifiableCredentialsEnabled: true,
});
}
});
});

View file

@ -86,7 +86,7 @@ test.describe("OID4VCI Protocol Mapper Configuration", () => {
let testBed: Awaited<ReturnType<typeof createTestBed>>;
test.beforeEach(async ({ page }) => {
testBed = await createTestBed();
testBed = await createTestBed({ verifiableCredentialsEnabled: true });
await login(page, { to: toClientScopes({ realm: testBed.realm }) });
});

View file

@ -138,7 +138,9 @@ test.describe.serial("OpenID for Verifiable Credentials", () => {
const realmName = `oid4vci-test-${uuidv4()}`;
const clientIdOpenIdConnect = `client-oidc-${uuidv4()}`;
test.beforeAll(async () => {
await adminClient.createRealm(realmName, {});
await adminClient.createRealm(realmName, {
verifiableCredentialsEnabled: true,
});
await adminClient.createClient({
clientId: clientIdOpenIdConnect,
realm: realmName,
@ -183,5 +185,27 @@ test.describe.serial("OpenID for Verifiable Credentials", () => {
await expect(toggleSwitch).toBeHidden();
}
});
test("should hide OID4VC section when verifiable credentials are disabled for the realm", async ({
page,
}) => {
await adminClient.updateRealm(realmName, {
verifiableCredentialsEnabled: false,
});
await page.reload();
await page.waitForSelector('[data-testid="advancedTab"]', {
state: "visible",
timeout: 10000,
});
await page.getByTestId("advancedTab").click();
const toggleSwitch = page.locator("#attributes\\.oid4vci🍺enabled");
await expect(toggleSwitch).toBeHidden();
await adminClient.updateRealm(realmName, {
verifiableCredentialsEnabled: true,
});
});
});
});

View file

@ -9,7 +9,9 @@ import { toClients } from "../../src/clients/routes/Clients.tsx";
import { createClient, continueNext, save as saveClient } from "./utils.ts";
test("OIDC client can assign OID4VCI client scopes", async ({ page }) => {
await using testBed = await createTestBed();
await using testBed = await createTestBed({
verifiableCredentialsEnabled: true,
});
await login(page, { to: toClients({ realm: testBed.realm }) });
await goToRealm(page, testBed.realm);

View file

@ -7,10 +7,36 @@ import { SERVER_URL, ROOT_PATH } from "../utils/constants.ts";
import { login } from "../utils/login.js";
import { selectItem } from "../utils/form.ts";
test("OID4VCI section visibility and jump link in Tokens tab", async ({
test("OID4VCI section is hidden in Tokens tab when verifiable credentials are disabled", async ({
page,
}) => {
await using testBed = await createTestBed();
await adminClient.updateRealm(testBed.realm, {
verifiableCredentialsEnabled: false,
});
try {
await login(page, { to: toRealmSettings({ realm: testBed.realm }) });
const tokensTab = page.getByTestId("rs-tokens-tab");
await tokensTab.click();
const oid4vciJumpLink = page.getByTestId("jump-link-oid4vci-attributes");
await expect(oid4vciJumpLink).toBeHidden();
} finally {
await adminClient.updateRealm(testBed.realm, {
verifiableCredentialsEnabled: true,
});
}
});
test("OID4VCI section visibility and jump link in Tokens tab", async ({
page,
}) => {
await using testBed = await createTestBed({
verifiableCredentialsEnabled: true,
});
await login(page, { to: toRealmSettings({ realm: testBed.realm }) });
const tokensTab = page.getByTestId("rs-tokens-tab");
@ -29,7 +55,9 @@ test("OID4VCI section visibility and jump link in Tokens tab", async ({
test("should render fields and save values with correct attribute keys", async ({
page,
}) => {
await using testBed = await createTestBed();
await using testBed = await createTestBed({
verifiableCredentialsEnabled: true,
});
await login(page, { to: toRealmSettings({ realm: testBed.realm }) });
const tokensTab = page.getByTestId("rs-tokens-tab");
@ -63,7 +91,9 @@ test("should render fields and save values with correct attribute keys", async (
});
test("should persist values after page refresh", async ({ page }) => {
await using testBed = await createTestBed();
await using testBed = await createTestBed({
verifiableCredentialsEnabled: true,
});
await login(page, { to: toRealmSettings({ realm: testBed.realm }) });
const tokensTab = page.getByTestId("rs-tokens-tab");
@ -115,7 +145,9 @@ test("should persist values after page refresh", async ({ page }) => {
});
test("should validate form fields and save valid values", async ({ page }) => {
await using testBed = await createTestBed();
await using testBed = await createTestBed({
verifiableCredentialsEnabled: true,
});
await login(page, { to: toRealmSettings({ realm: testBed.realm }) });
const tokensTab = page.getByTestId("rs-tokens-tab");
@ -173,7 +205,9 @@ test("should validate form fields and save valid values", async ({ page }) => {
test("should show validation error for values below minimum threshold", async ({
page,
}) => {
await using testBed = await createTestBed();
await using testBed = await createTestBed({
verifiableCredentialsEnabled: true,
});
await login(page, { to: toRealmSettings({ realm: testBed.realm }) });
const tokensTab = page.getByTestId("rs-tokens-tab");
@ -205,7 +239,9 @@ test("should show validation error for values below minimum threshold", async ({
test("should save signed metadata, encryption, and batch issuance settings", async ({
page,
}) => {
await using testBed = await createTestBed();
await using testBed = await createTestBed({
verifiableCredentialsEnabled: true,
});
await login(page, { to: toRealmSettings({ realm: testBed.realm }) });
const tokensTab = page.getByTestId("rs-tokens-tab");
@ -262,7 +298,9 @@ test("should save signed metadata, encryption, and batch issuance settings", asy
test("should save time-based correlation mitigation settings", async ({
page,
}) => {
await using testBed = await createTestBed();
await using testBed = await createTestBed({
verifiableCredentialsEnabled: true,
});
await login(page, { to: toRealmSettings({ realm: testBed.realm }) });
const tokensTab = page.getByTestId("rs-tokens-tab");
@ -308,7 +346,9 @@ test("should save time-based correlation mitigation settings", async ({
});
test("should save Deflate Compression setting", async ({ page }) => {
await using testBed = await createTestBed();
await using testBed = await createTestBed({
verifiableCredentialsEnabled: true,
});
await login(page, { to: toRealmSettings({ realm: testBed.realm }) });
const tokensTab = page.getByTestId("rs-tokens-tab");
@ -352,7 +392,9 @@ test("should save Deflate Compression setting", async ({ page }) => {
test("should save Attestation Trust settings (Trusted Keys and IDs)", async ({
page,
}) => {
await using testBed = await createTestBed();
await using testBed = await createTestBed({
verifiableCredentialsEnabled: true,
});
await login(page, { to: toRealmSettings({ realm: testBed.realm }) });
const tokensTab = page.getByTestId("rs-tokens-tab");

View file

@ -16,7 +16,9 @@ import {
test.describe.serial("Realm Settings - Tokens", () => {
const realmName = `tokens-realm-settings-${uuid()}`;
test.beforeAll(() => adminClient.createRealm(realmName));
test.beforeAll(() =>
adminClient.createRealm(realmName, { verifiableCredentialsEnabled: true }),
);
test.afterAll(() => adminClient.deleteRealm(realmName));
test.beforeEach(async ({ page }) => {

View file

@ -19,8 +19,11 @@ package org.keycloak.protocol;
import java.util.Map;
import jakarta.ws.rs.WebApplicationException;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
@ -64,5 +67,22 @@ public interface LoginProtocolFactory extends ProviderFactory<LoginProtocol> {
/**
* Add default values to {@link ClientScopeRepresentation}s that refer to the specific login-protocol
*/
void addClientScopeDefaults(ClientScopeRepresentation clientModel);
void addClientScopeDefaults(ClientScopeRepresentation clientScope);
/**
* Invoked during client-scope creation or update to add additional validation hooks specific to target protocol. May throw errorResponseException in case
*
* @param session Keycloak session
* @param clientScope client scope to create or update
* @throws WebApplicationException or some of it's subclass if validation fails
*/
default void validateClientScope(KeycloakSession session, ClientScopeRepresentation clientScope) throws WebApplicationException {
}
/**
* Test if the clientScope is valid for particular client. Usually called during protocol requests
*/
default boolean isValidClientScope(KeycloakSession session, ClientModel client, ClientScopeModel clientScope) {
return true;
}
}

View file

@ -20,6 +20,8 @@ package org.keycloak.protocol.oid4vc;
import java.util.HashMap;
import java.util.Map;
import jakarta.ws.rs.core.Response;
import org.keycloak.Config;
import org.keycloak.constants.OID4VCIConstants;
import org.keycloak.events.EventBuilder;
@ -41,6 +43,8 @@ import org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCUserAttributeMapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ClientScopeRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.ErrorResponseException;
import org.jboss.logging.Logger;
@ -165,6 +169,23 @@ public class OID4VCLoginProtocolFactory implements LoginProtocolFactory, OID4VCE
clientScope.getAttributes().computeIfAbsent(EXPIRY_IN_SECONDS, k -> String.valueOf(EXPIRY_IN_SECONDS_DEFAULT));
}
@Override
public void validateClientScope(KeycloakSession session, ClientScopeRepresentation clientScope) throws ErrorResponseException {
LoginProtocolFactory.super.validateClientScope(session, clientScope);
RealmModel realm = session.getContext().getRealm();
if (!realm.isVerifiableCredentialsEnabled()) {
throw ErrorResponse.error(
"OID4VC protocol cannot be used when Verifiable Credentials is disabled for the realm",
Response.Status.BAD_REQUEST);
}
}
@Override
public boolean isValidClientScope(KeycloakSession session, ClientModel client, ClientScopeModel clientScope) {
return session.getContext().getRealm().isVerifiableCredentialsEnabled();
}
@Override
public LoginProtocol create(KeycloakSession session) {
return null;

View file

@ -248,6 +248,28 @@ public class OID4VCIssuerEndpoint {
credentialBuilder -> credentialBuilder));
}
/**
* Validates whether OID4VCI functionality is enabled for the realm.
* <p>
* If the realm setting is disabled, this method logs the status and throws a
* {@link CorsErrorResponseException} with an appropriate error message.
* </p>
*
* @throws CorsErrorResponseException if OID4VCI is disabled for the realm.
*/
private void checkIsOid4vciEnabled() {
RealmModel realm = session.getContext().getRealm();
if (!realm.isVerifiableCredentialsEnabled()) {
LOGGER.debugf("OID4VCI functionality is disabled for realm '%s'. Verifiable Credentials switch is off.", realm.getName());
throw new CorsErrorResponseException(
cors != null ? cors : Cors.builder().allowAllOrigins(),
Errors.INVALID_CLIENT,
"OID4VCI functionality is disabled for this realm",
Response.Status.FORBIDDEN
);
}
}
/**
* Validates whether the authenticated client is enabled for OID4VCI features.
* <p>
@ -287,6 +309,8 @@ public class OID4VCIssuerEndpoint {
@Produces({MediaType.APPLICATION_JSON})
@Path(NONCE_PATH)
public Response getCNonce() {
checkIsOid4vciEnabled();
RealmModel realm = session.getContext().getRealm();
EventBuilder eventBuilder = new EventBuilder(realm, session, session.getContext().getConnection());
eventBuilder.event(EventType.VERIFIABLE_CREDENTIAL_NONCE_REQUEST);
@ -385,6 +409,7 @@ public class OID4VCIssuerEndpoint {
@QueryParam("width") @DefaultValue("200") int width,
@QueryParam("height") @DefaultValue("200") int height
) {
checkIsOid4vciEnabled();
configureCors(true);
AuthenticatedClientSessionModel clientSession = getAuthenticatedClientSession();
@ -568,6 +593,7 @@ public class OID4VCIssuerEndpoint {
@Produces(MediaType.APPLICATION_JSON)
@Path(CREDENTIAL_OFFER_PATH + "{nonce}")
public Response getCredentialOffer(@PathParam("nonce") String nonce) {
checkIsOid4vciEnabled();
configureCors(false);
if (nonce == null) {
@ -655,6 +681,7 @@ public class OID4VCIssuerEndpoint {
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_JWT})
@Path(CREDENTIAL_PATH)
public Response requestCredential(String requestPayload) {
checkIsOid4vciEnabled();
LOGGER.debugf("Received credentials request with payload: %s", requestPayload);
RealmModel realm = session.getContext().getRealm();

View file

@ -24,6 +24,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.UriBuilder;
import jakarta.ws.rs.core.UriInfo;
@ -103,6 +104,11 @@ public class OID4VCIssuerWellKnownProvider implements WellKnownProvider {
@Override
public Object getConfig() {
RealmModel realm = keycloakSession.getContext().getRealm();
if (!realm.isVerifiableCredentialsEnabled()) {
LOGGER.debugf("OID4VCI functionality is disabled for realm '%s'. Verifiable Credentials switch is off.", realm.getName());
throw new NotFoundException("OID4VCI functionality is disabled for this realm");
}
CredentialIssuer issuer = getIssuerMetadata();
return getMetadataResponse(issuer, keycloakSession);
}

View file

@ -28,6 +28,7 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
@ -86,6 +87,8 @@ import org.keycloak.models.utils.RoleUtils;
import org.keycloak.models.utils.SessionExpirationUtils;
import org.keycloak.organization.protocol.mappers.oidc.OrganizationMembershipMapper;
import org.keycloak.organization.protocol.mappers.oidc.OrganizationScope;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocolFactory;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.protocol.oidc.encode.AccessTokenContext;
@ -697,8 +700,10 @@ public class TokenManager {
/**
* Check that all the ClientScopes that have been parsed into authorization_resources are actually in the requested scopes
* otherwise, the scope wasn't parsed correctly
* <p>
*
* @param scopes
* @param authorizationRequestContext
* @param authorizationRequestContext authorizationRequestContext. It is not null just if dynamic scopes feature is enabled
* @param client
* @return
*/
@ -727,11 +732,23 @@ public class TokenManager {
Set<String> clientScopes;
if (authorizationRequestContext == null) {
// only true when dynamic scopes feature is enabled
AtomicBoolean anyInvalid = new AtomicBoolean(false);
clientScopes = getRequestedClientScopes(session, scopes, client, user)
.filter(((Predicate<ClientScopeModel>) ClientModel.class::isInstance).negate())
.peek(clientScope -> {
LoginProtocolFactory factory = (LoginProtocolFactory) session.getKeycloakSessionFactory().getProviderFactory(LoginProtocol.class, clientScope.getProtocol());
if (factory != null && !factory.isValidClientScope(session, client, clientScope)) {
logger.debugf("Requested scope '%s' invalid for client '%s'", clientScope.getName(), client.getClientId());
anyInvalid.set(true);
}
})
.map(ClientScopeModel::getName)
.collect(Collectors.toSet());
if (anyInvalid.get()) {
return false;
}
} else {
List<AuthorizationDetails> details = Optional.ofNullable(authorizationRequestContext.getAuthorizationDetailEntries()).orElse(List.of());

View file

@ -60,6 +60,15 @@ public class PreAuthorizedCodeGrantType extends OAuth2GrantTypeBase {
LOGGER.debug("Process grant request for preauthorized.");
setContext(context);
// Check if OID4VCI functionality is enabled for the realm
if (!realm.isVerifiableCredentialsEnabled()) {
LOGGER.debugf("OID4VCI functionality is disabled for realm '%s'. Verifiable Credentials switch is off.", realm.getName());
event.error(Errors.INVALID_CLIENT);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_CLIENT,
"OID4VCI functionality is disabled for this realm",
Response.Status.FORBIDDEN);
}
// See: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-token-request
String code = formParams.getFirst(PreAuthorizedCodeGrantTypeFactory.CODE_REQUEST_PARAM);

View file

@ -33,6 +33,7 @@ import org.keycloak.common.util.Time;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelValidationException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserManager;
import org.keycloak.models.UserModel;
@ -85,6 +86,9 @@ public class ClientManager {
if (rep.getProtocol() != null) {
LoginProtocolFactory providerFactory = (LoginProtocolFactory) session.getKeycloakSessionFactory().getProviderFactory(LoginProtocol.class, rep.getProtocol());
if (providerFactory == null) {
throw new ModelValidationException("Invalid protocol: " + rep.getProtocol());
}
providerFactory.setupClientDefaults(rep, client);
}

View file

@ -45,7 +45,6 @@ import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocolFactory;
import org.keycloak.protocol.oid4vc.OID4VCLoginProtocolFactory;
import org.keycloak.representations.idm.ClientScopeRepresentation;
import org.keycloak.saml.common.util.StringUtil;
import org.keycloak.services.ErrorResponse;
@ -126,6 +125,7 @@ public class ClientScopeResource {
})
public Response update(final ClientScopeRepresentation rep) {
auth.clients().requireManageClientScopes();
ClientScopeResource.validateClientScope(session, rep);
validateDynamicScopeUpdate(rep);
try {
LoginProtocolFactory loginProtocolFactory = //
@ -247,14 +247,27 @@ public class ClientScopeResource {
.map(type -> (LoginProtocolFactory) type)
.map(LoginProtocolFactory::getId)
.collect(Collectors.toSet());
// the OID4VC protocol is not registered to prevent it from being displayed in the client-details ui
acceptedProtocols.add(OID4VCLoginProtocolFactory.PROTOCOL_ID);
if (protocol == null || !acceptedProtocols.contains(protocol)) {
throw ErrorResponse.error("Unexpected protocol", Response.Status.BAD_REQUEST);
}
}
/**
* Validates client scope during creation or update
*
* @param session the Keycloak session
* @param clientScope clientScope to validate
* @throws ErrorResponseException if error happens during client-scope validation
*/
public static void validateClientScope(KeycloakSession session, ClientScopeRepresentation clientScope)
throws ErrorResponseException {
LoginProtocolFactory factory = (LoginProtocolFactory) session.getKeycloakSessionFactory().getProviderFactory(LoginProtocol.class, clientScope.getProtocol());
if (factory != null) {
factory.validateClientScope(session, clientScope);
}
}
/**
* Makes sure that an update that makes a Client Scope Dynamic is rejected if the Client Scope is assigned to a client
* as a default scope.

View file

@ -122,6 +122,7 @@ public class ClientScopesResource {
auth.clients().requireManageClientScopes();
ClientScopeResource.validateClientScopeName(rep.getName());
ClientScopeResource.validateClientScopeProtocol(session, rep.getProtocol());
ClientScopeResource.validateClientScope(session, rep);
ClientScopeResource.validateDynamicClientScope(rep);
try {
LoginProtocolFactory loginProtocolFactory = //

View file

@ -744,7 +744,7 @@ public class ClientScopeTest extends AbstractClientScopeTest {
@DisplayName("Create ClientScope with protocol:")
@ParameterizedTest
@ValueSource(strings = {"openid-connect", "saml", "oid4vc"})
@ValueSource(strings = {"openid-connect", "saml"})
public void createClientScopeWithOpenIdProtocol(String protocol) {
createClientScope(protocol);
}

View file

@ -19,20 +19,24 @@
package org.keycloak.tests.admin.client;
import java.util.Map;
import java.util.Optional;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.core.Response;
import org.keycloak.admin.client.resource.ClientScopeResource;
import org.keycloak.common.Profile;
import org.keycloak.constants.OID4VCIConstants;
import org.keycloak.models.oid4vci.CredentialScopeModel;
import org.keycloak.representations.idm.ClientScopeRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.server.KeycloakServerConfig;
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
import org.keycloak.testframework.util.ApiUtil;
import org.junit.Assert;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@ -42,6 +46,13 @@ import org.junit.jupiter.api.Test;
@KeycloakIntegrationTest(config = ClientScopeTestOid4Vci.DefaultServerConfigWithOid4Vci.class)
public class ClientScopeTestOid4Vci extends AbstractClientScopeTest {
@BeforeEach
public void enableVerifiableCredentialsInRealm() {
// Enable verifiable credentials on the realm
// This is required in addition to the server feature being enabled
managedRealm.updateWithCleanup(r -> r.update(rep -> rep.setVerifiableCredentialsEnabled(true)));
}
@DisplayName("Verify default values are correctly set")
@Test
public void testDefaultOid4VciClientScopeAttributes() {
@ -54,11 +65,7 @@ public class ClientScopeTestOid4Vci extends AbstractClientScopeTest {
String clientScopeId = null;
try (Response response = clientScopes().create(clientScope)) {
Assertions.assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
String location = (String) Optional.ofNullable(response.getHeaders().get(HttpHeaders.LOCATION))
.map(list -> list.get(0))
.orElse(null);
Assertions.assertNotNull(location);
clientScopeId = location.substring(location.lastIndexOf("/") + 1);
clientScopeId = ApiUtil.getCreatedId(response);
ClientScopeRepresentation createdClientScope = clientScopes().get(clientScopeId).toRepresentation();
Assertions.assertNotNull(createdClientScope);
@ -85,9 +92,9 @@ public class ClientScopeTestOid4Vci extends AbstractClientScopeTest {
Assertions.assertEquals(clientScope.getName(),
createdClientScope.getAttributes().get(CredentialScopeModel.CONTEXTS));
Assertions.assertEquals(clientScope.getName(),
createdClientScope.getAttributes().get(CredentialScopeModel.VCT));
Assertions.assertEquals(clientScope.getName(),
createdClientScope.getAttributes().get(CredentialScopeModel.ISSUER_DID));
createdClientScope.getAttributes().get(CredentialScopeModel.VCT));
// Note: ISSUER_DID is intentionally not set by default, as there's no sensible default
// The implementation leaves it undefined so the realm's URL will be used as the Issuer's ID
} finally {
Assertions.assertNotNull(clientScopeId);
@ -96,6 +103,57 @@ public class ClientScopeTestOid4Vci extends AbstractClientScopeTest {
}
}
@DisplayName("Verify CRUD of clientScope when OID4VCI is disabled for the realm")
@Test
public void testCreateOid4vciClientScopeDisabledForTheRealm() {
RealmRepresentation realm = managedRealm.admin().toRepresentation();
try {
// Create clientScope1 successfully
String clientScopeId1;
ClientScopeRepresentation clientScope = new ClientScopeRepresentation();
clientScope.setName("test-client-scope1");
clientScope.setDescription("test-client-scope-description");
clientScope.setProtocol(OID4VCIConstants.OID4VC_PROTOCOL);
clientScope.setAttributes(Map.of("test-attribute", "test-value"));
try (Response response = clientScopes().create(clientScope)) {
Assertions.assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
clientScopeId1 = ApiUtil.getCreatedId(response);
}
// Disable OID4VCI for the realm
realm.setVerifiableCredentialsEnabled(false);
managedRealm.admin().update(realm);
// Test not possible to update existing oid4vci client-scope
ClientScopeResource clientScope1 = managedRealm.admin().clientScopes().get(clientScopeId1);
ClientScopeRepresentation clientScopeRep1 = clientScope1.toRepresentation();
clientScopeRep1.setDescription("Foo");
try {
clientScope1.update(clientScopeRep1);
Assert.fail("Not expected to update client scope");
} catch (BadRequestException bre) {
// expected
}
// Still possible to delete oid4vci clientScope
clientScope1.remove();
// Not possible to create new oid4vci clientScope
clientScope= new ClientScopeRepresentation();
clientScope.setName("test-client-scope2");
clientScope.setDescription("test-client-scope-description");
clientScope.setProtocol(OID4VCIConstants.OID4VC_PROTOCOL);
clientScope.setAttributes(Map.of("test-attribute", "test-value"));
try (Response response = clientScopes().create(clientScope)) {
Assertions.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus());
}
} finally {
// Revert
realm.setVerifiableCredentialsEnabled(true);
managedRealm.admin().update(realm);
}
}
public static class DefaultServerConfigWithOid4Vci implements KeycloakServerConfig {
@Override

View file

@ -206,6 +206,22 @@ public class ClientRegistrationTest extends AbstractClientRegistrationTest {
registerClient();
}
/**
* OID4VC protocol is not valid for clients. It can only be used for ClientScopes.
* Attempting to create a client with protocol "oid4vc" should be rejected.
*/
@Test
public void registerOid4vcClientShouldBeRejected() {
authManageClients();
ClientRepresentation client = buildClient();
client.setProtocol("oid4vc");
Response response = adminClient.realm(REALM_NAME).clients().create(client);
assertEquals("Creating a client with OID4VC protocol should be rejected as it is not a valid protocol for clients.",
400, response.getStatus());
}
@Test
public void registerClientAsAdminWithNoAccess() throws ClientRegistrationException {
authNoAccess();

View file

@ -97,6 +97,33 @@ public class PreAuthorizedGrantTest extends AbstractTestRealmKeycloakTest {
assertEquals("If no code is provided, no access token should be returned.", HttpStatus.SC_BAD_REQUEST, accessTokenResponse.getStatusCode());
}
/**
* When verifiable credentials are disabled for the realm, the pre-authorized code
* grant (used by OID4VCI flows) must be rejected with 403 Forbidden.
*/
@Test
public void testPreAuthorizedGrantRealmDisabled() throws Exception {
// Disable verifiable credentials for the test realm
RealmRepresentation realmRep = adminClient.realm(TEST_REALM_NAME).toRepresentation();
realmRep.setVerifiableCredentialsEnabled(false);
adminClient.realm(TEST_REALM_NAME).update(realmRep);
try {
String userSessionId = getUserSession();
String preAuthorizedCode = getTestingClient().testing()
.getPreAuthorizedCode(TEST_REALM_NAME, userSessionId, "test-app", Time.currentTime() + 30);
AccessTokenResponse accessTokenResponse = postCode(preAuthorizedCode);
assertEquals("Pre-authorized grant should be forbidden when verifiable credentials are disabled.",
HttpStatus.SC_FORBIDDEN, accessTokenResponse.getStatusCode());
} finally {
// Re-enable verifiable credentials so other tests see the default behavior
RealmRepresentation realmRepReset = adminClient.realm(TEST_REALM_NAME).toRepresentation();
realmRepReset.setVerifiableCredentialsEnabled(true);
adminClient.realm(TEST_REALM_NAME).update(realmRepReset);
}
}
private AccessTokenResponse postCode(String preAuthorizedCode) throws Exception {
HttpPost post = new HttpPost(getTokenEndpoint());
List<NameValuePair> parameters = new LinkedList<>();
@ -123,6 +150,8 @@ public class PreAuthorizedGrantTest extends AbstractTestRealmKeycloakTest {
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
testRealm.setVerifiableCredentialsEnabled(true);
UserRepresentation user = UserBuilder.create()
.id("user-id")
.username("john")

View file

@ -61,6 +61,8 @@ public class OID4VCIWellKnownProviderTest extends OID4VCTest {
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
testRealm.setVerifiableCredentialsEnabled(true);
if (testRealm.getComponents() != null) {
testRealm.getComponents().add("org.keycloak.keys.KeyProvider",
getRsaEncKeyProvider(RSA_OAEP_256, "enc-key-oaep256", 100));

View file

@ -46,5 +46,6 @@ public abstract class CredentialBuilderTest extends OID4VCTest {
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
testRealm.setVerifiableCredentialsEnabled(true);
}
}

View file

@ -82,6 +82,7 @@ public class OID4VCTargetRoleMapperTest extends OID4VCTest {
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
testRealm.setVerifiableCredentialsEnabled(true);
ClientRepresentation newClient = new ClientRepresentation();
newClient.setClientId("newClient");

View file

@ -69,6 +69,8 @@ import static org.junit.Assert.assertTrue;
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
testRealm.setVerifiableCredentialsEnabled(true);
if (testRealm.getComponents() != null) {
testRealm.getComponents().add("org.keycloak.keys.KeyProvider", getRsaKeyProvider(RSA_KEY));
} else {

View file

@ -243,6 +243,8 @@ public class JwtCredentialSignerTest extends OID4VCTest {
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
testRealm.setVerifiableCredentialsEnabled(true);
if (testRealm.getComponents() != null) {
testRealm.getComponents().add("org.keycloak.keys.KeyProvider", getRsaKeyProvider(rsaKey));
} else {

View file

@ -211,6 +211,8 @@ public class LDCredentialSignerTest extends OID4VCTest {
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
testRealm.setVerifiableCredentialsEnabled(true);
if (testRealm.getComponents() != null) {
testRealm.getComponents().add("org.keycloak.keys.KeyProvider", getEdDSAKeyProvider());
} else {

View file

@ -658,6 +658,8 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
testRealm.setVerifiableCredentialsEnabled(true);
if (testRealm.getComponents() == null) {
testRealm.setComponents(new MultivaluedHashMap<>());
}

View file

@ -771,6 +771,33 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
}));
}
/**
* When verifiable credentials are disabled for the realm, the OID4VCI well-known
* endpoint must not be exposed.
*/
@Test
public void testWellKnownEndpointDisabledWhenVerifiableCredentialsOff() {
try (Client client = AdminClientUtil.createResteasyClient()) {
// Disable verifiable credentials for the test realm
RealmRepresentation realmRep = adminClient.realm(TEST_REALM_NAME).toRepresentation();
realmRep.setVerifiableCredentialsEnabled(false);
adminClient.realm(TEST_REALM_NAME).update(realmRep);
String metadataUrl = getRealmMetadataPath(TEST_REALM_NAME);
WebTarget target = client.target(metadataUrl);
try (Response response = target.request().get()) {
assertEquals("OID4VCI well-known endpoint should be hidden when verifiable credentials are disabled",
Response.Status.NOT_FOUND.getStatusCode(), response.getStatus());
}
} finally {
// Re-enable verifiable credentials to avoid side effects on other tests
RealmRepresentation realmRep = adminClient.realm(TEST_REALM_NAME).toRepresentation();
realmRep.setVerifiableCredentialsEnabled(true);
adminClient.realm(TEST_REALM_NAME).update(realmRep);
}
}
@Test
public void testBatchCredentialIssuanceValidation() {
KeycloakTestingClient testingClient = this.testingClient;

View file

@ -1,19 +1,27 @@
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.RealmRepresentation;
import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.managers.AppAuthManager;
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;
/**
@ -26,6 +34,50 @@ public class OID4VCJWTIssuerEndpointDisabledTest extends OID4VCIssuerEndpointTes
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());
});
}
/**
* When verifiable credentials are disabled at the realm level, OID4VCI endpoints
* must reject calls regardless of the client configuration.
*/
@Test
public void testRealmDisabledEndpoints() {
testWithBearerToken(token -> testingClient.server(TEST_REALM_NAME).run((session) -> {
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
authenticator.setTokenString(token);
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
// Nonce endpoint should be forbidden when OID4VCI is disabled for the realm
CorsErrorResponseException nonceException = Assert.assertThrows(CorsErrorResponseException.class, issuerEndpoint::getCNonce);
assertEquals("Realm-disabled OID4VCI should return 403 for nonce endpoint",
Response.Status.FORBIDDEN.getStatusCode(), nonceException.getResponse().getStatus());
}));
}
@Test
public void testClientNotEnabled() {
testWithBearerToken(token -> testingClient.server(TEST_REALM_NAME).run((session) -> {

View file

@ -1,19 +1,27 @@
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.RealmRepresentation;
import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.managers.AppAuthManager;
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;
/**
@ -26,6 +34,35 @@ public class OID4VCSdJwtIssuingEndpointDisabledTest extends OID4VCIssuerEndpoint
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());
});
}
@Test
public void testClientNotEnabled() {
testWithBearerToken(token -> testingClient.server(TEST_REALM_NAME).run((session) -> {

View file

@ -369,6 +369,8 @@ public class SdJwtCredentialSignerTest extends OID4VCTest {
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
testRealm.setVerifiableCredentialsEnabled(true);
if (testRealm.getComponents() != null) {
testRealm.getComponents().add("org.keycloak.keys.KeyProvider", getRsaKeyProvider(rsaKey));
} else {