mirror of
https://github.com/keycloak/keycloak.git
synced 2026-02-03 20:39:33 -05:00
[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:
parent
c8a41dea99
commit
fa28ddddb2
34 changed files with 544 additions and 50 deletions
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}))}
|
||||
|
|
|
|||
|
|
@ -667,7 +667,9 @@ export const RealmSettingsTokensTab = ({
|
|||
},
|
||||
{
|
||||
title: t("oid4vciAttributes"),
|
||||
isHidden: !isFeatureEnabled(Feature.OpenId4VCI),
|
||||
isHidden:
|
||||
!isFeatureEnabled(Feature.OpenId4VCI) ||
|
||||
!realm.verifiableCredentialsEnabled,
|
||||
panel: (
|
||||
<FormAccess
|
||||
isHorizontal
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 }) });
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 = //
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -46,5 +46,6 @@ public abstract class CredentialBuilderTest extends OID4VCTest {
|
|||
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
testRealm.setVerifiableCredentialsEnabled(true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<>());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) -> {
|
||||
|
|
|
|||
|
|
@ -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) -> {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue