mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 23:12:06 -03:30
[OID4VCI] Add Essential OID4VCI Client Scope Configuration Fields to Admin UI (#44389)
Closes: #43902 Signed-off-by: forkimenjeckayang <forkimenjeckayang@gmail.com>
This commit is contained in:
parent
9a6a7d98b1
commit
5ae0e0a645
@ -3616,7 +3616,20 @@ issuerDidHelp=The Decentralized Identifier (DID) of the credential issuer. This
|
||||
credentialLifetime=Credential Lifetime (seconds)
|
||||
credentialLifetimeHelp=The lifetime of the credential in seconds. After this time, the credential will expire and become invalid.
|
||||
supportedFormats=Supported Formats
|
||||
supportedFormatsHelp=The format of the verifiable credential. Currently supported formats: SD-JWT VC (dc+sd-jwt), JWT VC (jwt_vc_json).
|
||||
supportedFormatsHelp=The format of the verifiable credential. Currently supported formats: SD-JWT VC (dc+sd-jwt), JWT VC (jwt_vc).
|
||||
credentialDisplay=Credential Display
|
||||
credentialDisplayHelp=JSON array of objects containing display metadata for wallets (name, logo, colors, etc.). Example: [{"name": "IdentityCredential", "locale": "en-US", "logo": {"uri": "https://example.com/logo.png", "alt_text": "Logo"}, "background_color": "#12107c", "text_color": "#FFFFFF"}]
|
||||
supportedCredentialTypes=Supported Credential Types
|
||||
supportedCredentialTypesHelp=Comma-separated list of credential types (e.g., "VerifiableCredential,UniversityDegreeCredential"). Used in the credential definition for JWT VC and SD-JWT formats.
|
||||
verifiableCredentialType=Verifiable Credential Type (VCT)
|
||||
verifiableCredentialTypeHelp=The credential type identifier for SD-JWT format credentials. This value is used in the vct claim of the issued credential. Required for SD-JWT format.
|
||||
tokenJwsType=Token JWS Type
|
||||
tokenJwsTypeHelp=The type value written into the typ header of the JWT. Defaults to "JWS". Can be set to custom values like "dc+sd-jwt" if required by the wallet or system.
|
||||
visibleClaims=Visible Claims
|
||||
visibleClaimsHelp=Comma-separated list of claims that are always disclosed in the SD-JWT body (e.g., "id,iat,nbf,exp,jti,given_name"). Defaults to "id,iat,nbf,exp,jti". Only applicable for SD-JWT format.
|
||||
signingKeyId=Signing Key ID
|
||||
signingKeyIdHelp=Optional. The ID of the realm key used to sign the credential. If not specified, the realm's active signing key will be used automatically.
|
||||
useDefaultKey=Use default (realm's active signing key)
|
||||
# Workflows
|
||||
workflows=Workflows
|
||||
titleWorkflows=Workflows
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation";
|
||||
import type { KeyMetadataRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/keyMetadataRepresentation";
|
||||
import { ActionGroup, Button } from "@patternfly/react-core";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { FormProvider, useForm, useWatch } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
@ -9,8 +10,10 @@ import {
|
||||
SelectControl,
|
||||
TextAreaControl,
|
||||
TextControl,
|
||||
useFetch,
|
||||
} from "@keycloak/keycloak-ui-shared";
|
||||
|
||||
import { useAdminClient } from "../../admin-client";
|
||||
import { getProtocolName } from "../../clients/utils";
|
||||
import { DefaultSwitchControl } from "../../components/SwitchControl";
|
||||
import {
|
||||
@ -19,12 +22,33 @@ import {
|
||||
} from "../../components/client-scope/ClientScopeTypes";
|
||||
import { FormAccess } from "../../components/form/FormAccess";
|
||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||
import { useLoginProviders } from "../../context/server-info/ServerInfoProvider";
|
||||
import {
|
||||
useLoginProviders,
|
||||
useServerInfo,
|
||||
} from "../../context/server-info/ServerInfoProvider";
|
||||
import { convertAttributeNameToForm, convertToFormValues } from "../../util";
|
||||
import useIsFeatureEnabled, { Feature } from "../../utils/useIsFeatureEnabled";
|
||||
import { toClientScopes } from "../routes/ClientScopes";
|
||||
|
||||
const OID4VC_PROTOCOL = "oid4vc";
|
||||
const VC_FORMAT_JWT_VC = "jwt_vc";
|
||||
const VC_FORMAT_SD_JWT = "dc+sd-jwt";
|
||||
|
||||
// Validation function for comma-separated lists
|
||||
const validateCommaSeparatedList = (value: string | undefined) => {
|
||||
if (!value || value.trim() === "") {
|
||||
return true;
|
||||
}
|
||||
if (value.includes(", ") || value.includes(" ,")) {
|
||||
return "Comma-separated list must not contain spaces around commas";
|
||||
}
|
||||
const entries = value.split(",");
|
||||
const hasEmptyEntries = entries.some((entry) => entry.trim() === "");
|
||||
if (hasEmptyEntries) {
|
||||
return "Comma-separated list contains empty entries";
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
type ScopeFormProps = {
|
||||
clientScope?: ClientScopeRepresentation;
|
||||
@ -33,15 +57,60 @@ type ScopeFormProps = {
|
||||
|
||||
export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { adminClient } = useAdminClient();
|
||||
const form = useForm<ClientScopeDefaultOptionalType>({ mode: "onChange" });
|
||||
const { control, handleSubmit, setValue, formState } = form;
|
||||
const { isDirty, isValid } = formState;
|
||||
const { realm } = useRealm();
|
||||
|
||||
const providers = useLoginProviders();
|
||||
const serverInfo = useServerInfo();
|
||||
const isFeatureEnabled = useIsFeatureEnabled();
|
||||
const isDynamicScopesEnabled = isFeatureEnabled(Feature.DynamicScopes);
|
||||
|
||||
// Get available signature algorithms from server info
|
||||
const signatureAlgorithms = useMemo(
|
||||
() =>
|
||||
serverInfo?.providers?.signature?.providers
|
||||
? Object.keys(serverInfo.providers.signature.providers)
|
||||
: [],
|
||||
[serverInfo],
|
||||
);
|
||||
|
||||
// Fetch realm keys for signing_key_id dropdown
|
||||
const [realmKeys, setRealmKeys] = useState<KeyMetadataRepresentation[]>([]);
|
||||
|
||||
useFetch(
|
||||
async () => {
|
||||
const keysMetadata = await adminClient.realms.getKeys({ realm });
|
||||
return keysMetadata.keys || [];
|
||||
},
|
||||
setRealmKeys,
|
||||
[],
|
||||
);
|
||||
|
||||
// Prepare key options for SelectControl
|
||||
// Filter only active keys suitable for signing credentials
|
||||
const keyOptions = useMemo(() => {
|
||||
const options = [{ key: "", value: t("useDefaultKey") }];
|
||||
if (realmKeys && realmKeys.length > 0) {
|
||||
const keyOptions = realmKeys
|
||||
.filter(
|
||||
(key) =>
|
||||
key.kid &&
|
||||
key.status === "ACTIVE" &&
|
||||
key.algorithm &&
|
||||
signatureAlgorithms.includes(key.algorithm),
|
||||
)
|
||||
.map((key) => ({
|
||||
key: key.kid!,
|
||||
value: `${key.kid} (${key.algorithm})`,
|
||||
}));
|
||||
options.push(...keyOptions);
|
||||
}
|
||||
return options;
|
||||
}, [realmKeys, signatureAlgorithms, t]);
|
||||
|
||||
const displayOnConsentScreen: string = useWatch({
|
||||
control,
|
||||
name: convertAttributeNameToForm("attributes.display.on.consent.screen"),
|
||||
@ -62,6 +131,14 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
|
||||
name: "protocol",
|
||||
});
|
||||
|
||||
const selectedFormat = useWatch({
|
||||
control,
|
||||
name: convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
|
||||
"attributes.vc.format",
|
||||
),
|
||||
defaultValue: clientScope?.attributes?.["vc.format"] ?? VC_FORMAT_SD_JWT,
|
||||
});
|
||||
|
||||
const isOid4vcProtocol = selectedProtocol === OID4VC_PROTOCOL;
|
||||
const isOid4vcEnabled = isFeatureEnabled(Feature.OpenId4VCI);
|
||||
const isNotSaml = selectedProtocol != "saml";
|
||||
@ -251,13 +328,104 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
|
||||
)}
|
||||
label={t("supportedFormats")}
|
||||
labelIcon={t("supportedFormatsHelp")}
|
||||
controller={{ defaultValue: "dc+sd-jwt" }}
|
||||
controller={{ defaultValue: VC_FORMAT_SD_JWT }}
|
||||
options={[
|
||||
{ key: "dc+sd-jwt", value: "SD-JWT VC (dc+sd-jwt)" },
|
||||
{ key: "jwt_vc", value: "JWT VC (jwt_vc)" },
|
||||
{ key: "ldp_vc", value: "LDP VC (ldp_vc)" },
|
||||
{
|
||||
key: VC_FORMAT_SD_JWT,
|
||||
value: `SD-JWT VC (${VC_FORMAT_SD_JWT})`,
|
||||
},
|
||||
{
|
||||
key: VC_FORMAT_JWT_VC,
|
||||
value: `JWT VC (${VC_FORMAT_JWT_VC})`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<TextControl
|
||||
name={convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
|
||||
"attributes.vc.credential_build_config.token_jws_type",
|
||||
)}
|
||||
label={t("tokenJwsType")}
|
||||
labelIcon={t("tokenJwsTypeHelp")}
|
||||
defaultValue={
|
||||
clientScope?.attributes?.[
|
||||
"vc.credential_build_config.token_jws_type"
|
||||
] ?? "JWS"
|
||||
}
|
||||
/>
|
||||
{realmKeys && realmKeys.length > 0 && (
|
||||
<SelectControl
|
||||
id="kc-signing-key-id"
|
||||
name={convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
|
||||
"attributes.vc.signing_key_id",
|
||||
)}
|
||||
label={t("signingKeyId")}
|
||||
labelIcon={t("signingKeyIdHelp")}
|
||||
controller={{
|
||||
defaultValue:
|
||||
clientScope?.attributes?.["vc.signing_key_id"] ?? "",
|
||||
}}
|
||||
options={keyOptions}
|
||||
/>
|
||||
)}
|
||||
<TextAreaControl
|
||||
name={convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
|
||||
"attributes.vc.display",
|
||||
)}
|
||||
label={t("credentialDisplay")}
|
||||
labelIcon={t("credentialDisplayHelp")}
|
||||
rules={{
|
||||
validate: (value: string | undefined) => {
|
||||
if (!value || value.trim() === "") {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return true;
|
||||
} catch {
|
||||
return "Invalid JSON format";
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{(selectedFormat === VC_FORMAT_JWT_VC ||
|
||||
selectedFormat === VC_FORMAT_SD_JWT) && (
|
||||
<TextControl
|
||||
name={convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
|
||||
"attributes.vc.supported_credential_types",
|
||||
)}
|
||||
label={t("supportedCredentialTypes")}
|
||||
labelIcon={t("supportedCredentialTypesHelp")}
|
||||
rules={{
|
||||
validate: validateCommaSeparatedList,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{selectedFormat === VC_FORMAT_SD_JWT && (
|
||||
<>
|
||||
<TextControl
|
||||
name={convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
|
||||
"attributes.vc.verifiable_credential_type",
|
||||
)}
|
||||
label={t("verifiableCredentialType")}
|
||||
labelIcon={t("verifiableCredentialTypeHelp")}
|
||||
/>
|
||||
<TextControl
|
||||
name={convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
|
||||
"attributes.vc.credential_build_config.sd_jwt.visible_claims",
|
||||
)}
|
||||
label={t("visibleClaims")}
|
||||
labelIcon={t("visibleClaimsHelp")}
|
||||
defaultValue={
|
||||
clientScope?.attributes?.[
|
||||
"vc.credential_build_config.sd_jwt.visible_claims"
|
||||
] ?? "id,iat,nbf,exp,jti"
|
||||
}
|
||||
rules={{
|
||||
validate: validateCommaSeparatedList,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@ -1,11 +1,63 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import type { Page } from "@playwright/test";
|
||||
import { createTestBed } from "../support/testbed.ts";
|
||||
import { goToClientScopes } from "../utils/sidebar.ts";
|
||||
import { clickSaveButton } from "../utils/form.ts";
|
||||
import { clickSaveButton, selectItem } from "../utils/form.ts";
|
||||
import { clickTableRowItem, clickTableToolbarItem } from "../utils/table.ts";
|
||||
import { login } from "../utils/login.ts";
|
||||
import { toClientScopes } from "../../src/client-scopes/routes/ClientScopes.tsx";
|
||||
|
||||
// Helper function to create client scope (without selecting protocol)
|
||||
async function createClientScope(
|
||||
page: Page,
|
||||
testBed: Awaited<ReturnType<typeof createTestBed>>,
|
||||
) {
|
||||
await login(page, { to: toClientScopes({ realm: testBed.realm }) });
|
||||
|
||||
await goToClientScopes(page);
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await clickTableToolbarItem(page, "Create client scope");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
}
|
||||
|
||||
// Helper function to create client scope and select protocol/format
|
||||
async function createClientScopeAndSelectProtocolAndFormat(
|
||||
page: Page,
|
||||
testBed: Awaited<ReturnType<typeof createTestBed>>,
|
||||
format?: "SD-JWT VC (dc+sd-jwt)" | "JWT VC (jwt_vc)",
|
||||
) {
|
||||
await createClientScope(page, testBed);
|
||||
|
||||
await selectItem(page, "#kc-protocol", "OpenID for Verifiable Credentials");
|
||||
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
if (format) {
|
||||
await selectItem(page, "#kc-vc-format", format);
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to navigate back to client scope and verify saved values
|
||||
async function navigateBackAndVerifyClientScope(
|
||||
page: Page,
|
||||
testBed: Awaited<ReturnType<typeof createTestBed>>,
|
||||
clientScopeName: string,
|
||||
) {
|
||||
const currentUrl = page.url();
|
||||
const baseUrl = currentUrl.split("#")[0];
|
||||
await page.goto(
|
||||
`${baseUrl}#${toClientScopes({ realm: testBed.realm }).pathname!}`,
|
||||
);
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await page.getByPlaceholder("Search for client scope").fill(clientScopeName);
|
||||
|
||||
await clickTableRowItem(page, clientScopeName);
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
}
|
||||
|
||||
// OID4VCI field selectors
|
||||
const OID4VCI_FIELDS = {
|
||||
CREDENTIAL_CONFIGURATION_ID: "attributes.vc🍺credential_configuration_id",
|
||||
@ -13,6 +65,13 @@ const OID4VCI_FIELDS = {
|
||||
ISSUER_DID: "attributes.vc🍺issuer_did",
|
||||
EXPIRY_IN_SECONDS: "attributes.vc🍺expiry_in_seconds",
|
||||
FORMAT: "#kc-vc-format",
|
||||
TOKEN_JWS_TYPE: "attributes.vc🍺credential_build_config🍺token_jws_type",
|
||||
SIGNING_KEY_ID: "#kc-signing-key-id",
|
||||
DISPLAY: "attributes.vc🍺display",
|
||||
SUPPORTED_CREDENTIAL_TYPES: "attributes.vc🍺supported_credential_types",
|
||||
VERIFIABLE_CREDENTIAL_TYPE: "attributes.vc🍺verifiable_credential_type",
|
||||
VISIBLE_CLAIMS:
|
||||
"attributes.vc🍺credential_build_config🍺sd_jwt🍺visible_claims",
|
||||
} as const;
|
||||
|
||||
// Test values
|
||||
@ -21,7 +80,12 @@ const TEST_VALUES = {
|
||||
CREDENTIAL_ID: "test-cred-identifier",
|
||||
ISSUER_DID: "did:key:test123",
|
||||
EXPIRY_SECONDS: "86400",
|
||||
FORMAT: "jwt_vc",
|
||||
TOKEN_JWS_TYPE: "dc+sd-jwt",
|
||||
VISIBLE_CLAIMS: "id,iat,nbf,exp,jti,given_name",
|
||||
DISPLAY:
|
||||
'[{"name": "Test Credential", "locale": "en-US", "logo": {"uri": "https://example.com/logo.png", "alt_text": "Logo"}, "background_color": "#12107c", "text_color": "#FFFFFF"}]',
|
||||
SUPPORTED_CREDENTIAL_TYPES: "VerifiableCredential,UniversityDegreeCredential",
|
||||
VERIFIABLE_CREDENTIAL_TYPE: "TestCredentialType",
|
||||
} as const;
|
||||
|
||||
test.describe("OID4VCI Client Scope Functionality", () => {
|
||||
@ -29,13 +93,7 @@ test.describe("OID4VCI Client Scope Functionality", () => {
|
||||
page,
|
||||
}) => {
|
||||
await using testBed = await createTestBed();
|
||||
await login(page, { to: toClientScopes({ realm: testBed.realm }) });
|
||||
|
||||
await goToClientScopes(page);
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await clickTableToolbarItem(page, "Create client scope");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
await createClientScope(page, testBed);
|
||||
|
||||
await expect(page.locator("#kc-protocol")).toBeVisible();
|
||||
|
||||
@ -65,26 +123,19 @@ test.describe("OID4VCI Client Scope Functionality", () => {
|
||||
page.getByTestId(OID4VCI_FIELDS.EXPIRY_IN_SECONDS),
|
||||
).toBeVisible();
|
||||
await expect(page.locator(OID4VCI_FIELDS.FORMAT)).toBeVisible();
|
||||
await expect(page.getByTestId(OID4VCI_FIELDS.TOKEN_JWS_TYPE)).toBeVisible();
|
||||
await expect(page.getByTestId(OID4VCI_FIELDS.DISPLAY)).toBeVisible();
|
||||
});
|
||||
|
||||
test("should save and persist OID4VCI field values", async ({ page }) => {
|
||||
await using testBed = await createTestBed();
|
||||
const testClientScopeName = `oid4vci-test-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
||||
|
||||
await login(page, { to: toClientScopes({ realm: testBed.realm }) });
|
||||
|
||||
await goToClientScopes(page);
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await clickTableToolbarItem(page, "Create client scope");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await expect(page.locator("#kc-protocol")).toBeVisible();
|
||||
|
||||
const { selectItem } = await import("../utils/form.ts");
|
||||
await selectItem(page, "#kc-protocol", "OpenID for Verifiable Credentials");
|
||||
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
await createClientScopeAndSelectProtocolAndFormat(
|
||||
page,
|
||||
testBed,
|
||||
"JWT VC (jwt_vc)",
|
||||
);
|
||||
|
||||
await page
|
||||
.getByTestId(OID4VCI_FIELDS.CREDENTIAL_CONFIGURATION_ID)
|
||||
@ -99,26 +150,21 @@ test.describe("OID4VCI Client Scope Functionality", () => {
|
||||
.getByTestId(OID4VCI_FIELDS.EXPIRY_IN_SECONDS)
|
||||
.fill(TEST_VALUES.EXPIRY_SECONDS);
|
||||
|
||||
await selectItem(page, "#kc-vc-format", "JWT VC (jwt_vc)");
|
||||
await page
|
||||
.getByTestId(OID4VCI_FIELDS.TOKEN_JWS_TYPE)
|
||||
.fill(TEST_VALUES.TOKEN_JWS_TYPE);
|
||||
|
||||
await page.getByTestId(OID4VCI_FIELDS.DISPLAY).fill(TEST_VALUES.DISPLAY);
|
||||
await page
|
||||
.getByTestId(OID4VCI_FIELDS.SUPPORTED_CREDENTIAL_TYPES)
|
||||
.fill(TEST_VALUES.SUPPORTED_CREDENTIAL_TYPES);
|
||||
|
||||
await page.getByTestId("name").fill(testClientScopeName);
|
||||
|
||||
await clickSaveButton(page);
|
||||
await expect(page.getByText("Client scope created")).toBeVisible();
|
||||
|
||||
const currentUrl = page.url();
|
||||
const baseUrl = currentUrl.split("#")[0];
|
||||
await page.goto(
|
||||
`${baseUrl}#${toClientScopes({ realm: testBed.realm }).pathname!}`,
|
||||
);
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await page
|
||||
.getByPlaceholder("Search for client scope")
|
||||
.fill(testClientScopeName);
|
||||
|
||||
await clickTableRowItem(page, testClientScopeName);
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
await navigateBackAndVerifyClientScope(page, testBed, testClientScopeName);
|
||||
|
||||
await expect(
|
||||
page.getByTestId(OID4VCI_FIELDS.CREDENTIAL_CONFIGURATION_ID),
|
||||
@ -135,20 +181,22 @@ test.describe("OID4VCI Client Scope Functionality", () => {
|
||||
await expect(page.locator("#kc-vc-format")).toContainText(
|
||||
"JWT VC (jwt_vc)",
|
||||
);
|
||||
await expect(page.getByTestId(OID4VCI_FIELDS.DISPLAY)).toHaveValue(
|
||||
TEST_VALUES.DISPLAY,
|
||||
);
|
||||
await expect(
|
||||
page.getByTestId(OID4VCI_FIELDS.SUPPORTED_CREDENTIAL_TYPES),
|
||||
).toHaveValue(TEST_VALUES.SUPPORTED_CREDENTIAL_TYPES);
|
||||
await expect(page.getByTestId(OID4VCI_FIELDS.TOKEN_JWS_TYPE)).toHaveValue(
|
||||
TEST_VALUES.TOKEN_JWS_TYPE,
|
||||
);
|
||||
});
|
||||
|
||||
test("should show OID4VCI protocol when global feature is enabled", async ({
|
||||
page,
|
||||
}) => {
|
||||
await using testBed = await createTestBed();
|
||||
|
||||
await login(page, { to: toClientScopes({ realm: testBed.realm }) });
|
||||
|
||||
await goToClientScopes(page);
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await clickTableToolbarItem(page, "Create client scope");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
await createClientScope(page, testBed);
|
||||
|
||||
await expect(page.locator("#kc-protocol")).toBeVisible();
|
||||
|
||||
@ -163,13 +211,7 @@ test.describe("OID4VCI Client Scope Functionality", () => {
|
||||
page,
|
||||
}) => {
|
||||
await using testBed = await createTestBed();
|
||||
await login(page, { to: toClientScopes({ realm: testBed.realm }) });
|
||||
|
||||
await goToClientScopes(page);
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await clickTableToolbarItem(page, "Create client scope");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
await createClientScope(page, testBed);
|
||||
|
||||
await expect(page.locator("#kc-protocol")).toBeVisible();
|
||||
|
||||
@ -195,19 +237,14 @@ test.describe("OID4VCI Client Scope Functionality", () => {
|
||||
page.getByTestId(OID4VCI_FIELDS.EXPIRY_IN_SECONDS),
|
||||
).toBeHidden();
|
||||
await expect(page.locator(OID4VCI_FIELDS.FORMAT)).toBeHidden();
|
||||
await expect(page.getByTestId(OID4VCI_FIELDS.DISPLAY)).toBeHidden();
|
||||
});
|
||||
|
||||
test("should handle OID4VCI protocol selection correctly", async ({
|
||||
page,
|
||||
}) => {
|
||||
await using testBed = await createTestBed();
|
||||
await login(page, { to: toClientScopes({ realm: testBed.realm }) });
|
||||
|
||||
await goToClientScopes(page);
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await clickTableToolbarItem(page, "Create client scope");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
await createClientScope(page, testBed);
|
||||
|
||||
await expect(page.locator("#kc-protocol")).toBeVisible();
|
||||
|
||||
@ -236,4 +273,185 @@ test.describe("OID4VCI Client Scope Functionality", () => {
|
||||
page.getByTestId(OID4VCI_FIELDS.CREDENTIAL_CONFIGURATION_ID),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("should only show supported format options (dc+sd-jwt and jwt_vc)", async ({
|
||||
page,
|
||||
}) => {
|
||||
await using testBed = await createTestBed();
|
||||
await createClientScopeAndSelectProtocolAndFormat(page, testBed);
|
||||
|
||||
await page.locator("#kc-vc-format").click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("option", { name: "SD-JWT VC (dc+sd-jwt)" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("option", { name: "JWT VC (jwt_vc)" }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole("option", { name: "LDP VC (ldp_vc)" }),
|
||||
).toBeHidden();
|
||||
});
|
||||
|
||||
test("should show format-specific fields for SD-JWT format", async ({
|
||||
page,
|
||||
}) => {
|
||||
await using testBed = await createTestBed();
|
||||
await createClientScopeAndSelectProtocolAndFormat(
|
||||
page,
|
||||
testBed,
|
||||
"SD-JWT VC (dc+sd-jwt)",
|
||||
);
|
||||
|
||||
await expect(page.getByTestId(OID4VCI_FIELDS.DISPLAY)).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId(OID4VCI_FIELDS.SUPPORTED_CREDENTIAL_TYPES),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByTestId(OID4VCI_FIELDS.VERIFIABLE_CREDENTIAL_TYPE),
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId(OID4VCI_FIELDS.VISIBLE_CLAIMS)).toBeVisible();
|
||||
});
|
||||
|
||||
test("should show format-specific fields for JWT VC format", async ({
|
||||
page,
|
||||
}) => {
|
||||
await using testBed = await createTestBed();
|
||||
await createClientScopeAndSelectProtocolAndFormat(
|
||||
page,
|
||||
testBed,
|
||||
"JWT VC (jwt_vc)",
|
||||
);
|
||||
|
||||
await expect(page.getByTestId(OID4VCI_FIELDS.DISPLAY)).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId(OID4VCI_FIELDS.SUPPORTED_CREDENTIAL_TYPES),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByTestId(OID4VCI_FIELDS.VERIFIABLE_CREDENTIAL_TYPE),
|
||||
).toBeHidden();
|
||||
await expect(page.getByTestId(OID4VCI_FIELDS.VISIBLE_CLAIMS)).toBeHidden();
|
||||
});
|
||||
|
||||
test("should save and persist new OID4VCI field values for SD-JWT format", async ({
|
||||
page,
|
||||
}) => {
|
||||
await using testBed = await createTestBed();
|
||||
const testClientScopeName = `oid4vci-sdjwt-test-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
||||
|
||||
await createClientScopeAndSelectProtocolAndFormat(
|
||||
page,
|
||||
testBed,
|
||||
"SD-JWT VC (dc+sd-jwt)",
|
||||
);
|
||||
|
||||
await page
|
||||
.getByTestId(OID4VCI_FIELDS.CREDENTIAL_CONFIGURATION_ID)
|
||||
.fill(TEST_VALUES.CREDENTIAL_CONFIG);
|
||||
await page
|
||||
.getByTestId(OID4VCI_FIELDS.CREDENTIAL_IDENTIFIER)
|
||||
.fill(TEST_VALUES.CREDENTIAL_ID);
|
||||
await page.getByTestId(OID4VCI_FIELDS.DISPLAY).fill(TEST_VALUES.DISPLAY);
|
||||
await page
|
||||
.getByTestId(OID4VCI_FIELDS.SUPPORTED_CREDENTIAL_TYPES)
|
||||
.fill(TEST_VALUES.SUPPORTED_CREDENTIAL_TYPES);
|
||||
await page
|
||||
.getByTestId(OID4VCI_FIELDS.VERIFIABLE_CREDENTIAL_TYPE)
|
||||
.fill(TEST_VALUES.VERIFIABLE_CREDENTIAL_TYPE);
|
||||
await page
|
||||
.getByTestId(OID4VCI_FIELDS.VISIBLE_CLAIMS)
|
||||
.fill(TEST_VALUES.VISIBLE_CLAIMS);
|
||||
|
||||
await page.getByTestId("name").fill(testClientScopeName);
|
||||
|
||||
await clickSaveButton(page);
|
||||
await expect(page.getByText("Client scope created")).toBeVisible();
|
||||
|
||||
await navigateBackAndVerifyClientScope(page, testBed, testClientScopeName);
|
||||
|
||||
await expect(page.getByTestId(OID4VCI_FIELDS.DISPLAY)).toHaveValue(
|
||||
TEST_VALUES.DISPLAY,
|
||||
);
|
||||
await expect(
|
||||
page.getByTestId(OID4VCI_FIELDS.SUPPORTED_CREDENTIAL_TYPES),
|
||||
).toHaveValue(TEST_VALUES.SUPPORTED_CREDENTIAL_TYPES);
|
||||
await expect(
|
||||
page.getByTestId(OID4VCI_FIELDS.VERIFIABLE_CREDENTIAL_TYPE),
|
||||
).toHaveValue(TEST_VALUES.VERIFIABLE_CREDENTIAL_TYPE);
|
||||
await expect(page.getByTestId(OID4VCI_FIELDS.VISIBLE_CLAIMS)).toHaveValue(
|
||||
TEST_VALUES.VISIBLE_CLAIMS,
|
||||
);
|
||||
await expect(page.locator("#kc-vc-format")).toContainText(
|
||||
"SD-JWT VC (dc+sd-jwt)",
|
||||
);
|
||||
});
|
||||
|
||||
test("should conditionally show/hide fields when format changes", async ({
|
||||
page,
|
||||
}) => {
|
||||
await using testBed = await createTestBed();
|
||||
await createClientScopeAndSelectProtocolAndFormat(
|
||||
page,
|
||||
testBed,
|
||||
"SD-JWT VC (dc+sd-jwt)",
|
||||
);
|
||||
|
||||
await expect(
|
||||
page.getByTestId(OID4VCI_FIELDS.VERIFIABLE_CREDENTIAL_TYPE),
|
||||
).toBeVisible();
|
||||
|
||||
await selectItem(page, "#kc-vc-format", "JWT VC (jwt_vc)");
|
||||
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await expect(
|
||||
page.getByTestId(OID4VCI_FIELDS.VERIFIABLE_CREDENTIAL_TYPE),
|
||||
).toBeHidden();
|
||||
|
||||
await selectItem(page, "#kc-vc-format", "SD-JWT VC (dc+sd-jwt)");
|
||||
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await expect(
|
||||
page.getByTestId(OID4VCI_FIELDS.VERIFIABLE_CREDENTIAL_TYPE),
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId(OID4VCI_FIELDS.VISIBLE_CLAIMS)).toBeVisible();
|
||||
|
||||
await selectItem(page, "#kc-vc-format", "JWT VC (jwt_vc)");
|
||||
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await expect(
|
||||
page.getByTestId(OID4VCI_FIELDS.VERIFIABLE_CREDENTIAL_TYPE),
|
||||
).toBeHidden();
|
||||
await expect(page.getByTestId(OID4VCI_FIELDS.VISIBLE_CLAIMS)).toBeHidden();
|
||||
|
||||
await selectItem(page, "#kc-vc-format", "SD-JWT VC (dc+sd-jwt)");
|
||||
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await expect(
|
||||
page.getByTestId(OID4VCI_FIELDS.VERIFIABLE_CREDENTIAL_TYPE),
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId(OID4VCI_FIELDS.VISIBLE_CLAIMS)).toBeVisible();
|
||||
});
|
||||
|
||||
test("should show token_jws_type for all formats", async ({ page }) => {
|
||||
await using testBed = await createTestBed();
|
||||
await createClientScopeAndSelectProtocolAndFormat(
|
||||
page,
|
||||
testBed,
|
||||
"JWT VC (jwt_vc)",
|
||||
);
|
||||
|
||||
await expect(page.getByTestId(OID4VCI_FIELDS.TOKEN_JWS_TYPE)).toBeVisible();
|
||||
|
||||
await selectItem(page, "#kc-vc-format", "SD-JWT VC (dc+sd-jwt)");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await expect(page.getByTestId(OID4VCI_FIELDS.TOKEN_JWS_TYPE)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user