mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 23:12:06 -03:30
[OID4VCI] Add UI support for vc.credential_signing_alg and vc.credential_build_config.hash_algorithm in OID4VCI client scopes (#44851)
Closes #44849 Signed-off-by: forkimenjeckayang <forkimenjeckayang@gmail.com>
This commit is contained in:
parent
cc3ab86544
commit
7a3fd3404c
@ -3650,6 +3650,10 @@ supportedFormats=Supported Formats
|
|||||||
supportedFormatsHelp=The format of the verifiable credential. Currently supported formats: SD-JWT VC (dc+sd-jwt), JWT VC (jwt_vc).
|
supportedFormatsHelp=The format of the verifiable credential. Currently supported formats: SD-JWT VC (dc+sd-jwt), JWT VC (jwt_vc).
|
||||||
credentialDisplay=Credential Display
|
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"}]
|
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"}]
|
||||||
|
credentialSigningAlgorithm=Credential Signing Algorithm
|
||||||
|
credentialSigningAlgorithmHelp=Signing algorithm used to sign credentials (for example "ES256"). Leave blank to use the realm defaults derived from available keys.
|
||||||
|
hashAlgorithm=Hash Algorithm
|
||||||
|
hashAlgorithmHelp=Hash algorithm used for SD-JWT credentials (for example "SHA-256"). Defaults to "SHA-256" if not specified.
|
||||||
supportedCredentialTypes=Supported Credential Types
|
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.
|
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)
|
verifiableCredentialType=Verifiable Credential Type (VCT)
|
||||||
|
|||||||
@ -77,6 +77,17 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
|
|||||||
[serverInfo],
|
[serverInfo],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Get available hash algorithms from server info
|
||||||
|
const hashAlgorithms = serverInfo?.providers?.hash?.providers
|
||||||
|
? Object.keys(serverInfo.providers.hash.providers)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// Get available asymmetric signature algorithms from server info
|
||||||
|
const asymmetricSigAlgOptions = useMemo(
|
||||||
|
() => serverInfo?.cryptoInfo?.clientSignatureAsymmetricAlgorithms ?? [],
|
||||||
|
[serverInfo],
|
||||||
|
);
|
||||||
|
|
||||||
// Fetch realm keys for signing_key_id dropdown
|
// Fetch realm keys for signing_key_id dropdown
|
||||||
const [realmKeys, setRealmKeys] = useState<KeyMetadataRepresentation[]>([]);
|
const [realmKeys, setRealmKeys] = useState<KeyMetadataRepresentation[]>([]);
|
||||||
|
|
||||||
@ -367,6 +378,45 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
|
|||||||
options={keyOptions}
|
options={keyOptions}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{asymmetricSigAlgOptions.length > 0 && (
|
||||||
|
<SelectControl
|
||||||
|
id="kc-credential-signing-alg"
|
||||||
|
name={convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
|
||||||
|
"attributes.vc.credential_signing_alg",
|
||||||
|
)}
|
||||||
|
label={t("credentialSigningAlgorithm")}
|
||||||
|
labelIcon={t("credentialSigningAlgorithmHelp")}
|
||||||
|
controller={{
|
||||||
|
defaultValue:
|
||||||
|
clientScope?.attributes?.["vc.credential_signing_alg"] ??
|
||||||
|
"",
|
||||||
|
}}
|
||||||
|
options={asymmetricSigAlgOptions.map((alg) => ({
|
||||||
|
key: alg,
|
||||||
|
value: alg,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{hashAlgorithms.length > 0 && (
|
||||||
|
<SelectControl
|
||||||
|
id="kc-hash-algorithm"
|
||||||
|
name={convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
|
||||||
|
"attributes.vc.credential_build_config.hash_algorithm",
|
||||||
|
)}
|
||||||
|
label={t("hashAlgorithm")}
|
||||||
|
labelIcon={t("hashAlgorithmHelp")}
|
||||||
|
controller={{
|
||||||
|
defaultValue:
|
||||||
|
clientScope?.attributes?.[
|
||||||
|
"vc.credential_build_config.hash_algorithm"
|
||||||
|
] ?? "SHA-256",
|
||||||
|
}}
|
||||||
|
options={hashAlgorithms.map((alg) => ({
|
||||||
|
key: alg,
|
||||||
|
value: alg,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<TextAreaControl
|
<TextAreaControl
|
||||||
name={convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
|
name={convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
|
||||||
"attributes.vc.display",
|
"attributes.vc.display",
|
||||||
|
|||||||
@ -67,6 +67,8 @@ const OID4VCI_FIELDS = {
|
|||||||
FORMAT: "#kc-vc-format",
|
FORMAT: "#kc-vc-format",
|
||||||
TOKEN_JWS_TYPE: "attributes.vc🍺credential_build_config🍺token_jws_type",
|
TOKEN_JWS_TYPE: "attributes.vc🍺credential_build_config🍺token_jws_type",
|
||||||
SIGNING_KEY_ID: "#kc-signing-key-id",
|
SIGNING_KEY_ID: "#kc-signing-key-id",
|
||||||
|
SIGNING_ALGORITHM: "#kc-credential-signing-alg",
|
||||||
|
HASH_ALGORITHM: "#kc-hash-algorithm",
|
||||||
DISPLAY: "attributes.vc🍺display",
|
DISPLAY: "attributes.vc🍺display",
|
||||||
SUPPORTED_CREDENTIAL_TYPES: "attributes.vc🍺supported_credential_types",
|
SUPPORTED_CREDENTIAL_TYPES: "attributes.vc🍺supported_credential_types",
|
||||||
VERIFIABLE_CREDENTIAL_TYPE: "attributes.vc🍺verifiable_credential_type",
|
VERIFIABLE_CREDENTIAL_TYPE: "attributes.vc🍺verifiable_credential_type",
|
||||||
@ -80,6 +82,8 @@ const TEST_VALUES = {
|
|||||||
CREDENTIAL_ID: "test-cred-identifier",
|
CREDENTIAL_ID: "test-cred-identifier",
|
||||||
ISSUER_DID: "did:key:test123",
|
ISSUER_DID: "did:key:test123",
|
||||||
EXPIRY_SECONDS: "86400",
|
EXPIRY_SECONDS: "86400",
|
||||||
|
SIGNING_ALG: "ES256",
|
||||||
|
HASH_ALGORITHM: "SHA-384",
|
||||||
TOKEN_JWS_TYPE: "dc+sd-jwt",
|
TOKEN_JWS_TYPE: "dc+sd-jwt",
|
||||||
VISIBLE_CLAIMS: "id,iat,nbf,exp,jti,given_name",
|
VISIBLE_CLAIMS: "id,iat,nbf,exp,jti,given_name",
|
||||||
DISPLAY:
|
DISPLAY:
|
||||||
@ -124,6 +128,8 @@ test.describe("OID4VCI Client Scope Functionality", () => {
|
|||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(page.locator(OID4VCI_FIELDS.FORMAT)).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.TOKEN_JWS_TYPE)).toBeVisible();
|
||||||
|
await expect(page.locator(OID4VCI_FIELDS.SIGNING_ALGORITHM)).toBeVisible();
|
||||||
|
await expect(page.locator(OID4VCI_FIELDS.HASH_ALGORITHM)).toBeVisible();
|
||||||
await expect(page.getByTestId(OID4VCI_FIELDS.DISPLAY)).toBeVisible();
|
await expect(page.getByTestId(OID4VCI_FIELDS.DISPLAY)).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -154,6 +160,18 @@ test.describe("OID4VCI Client Scope Functionality", () => {
|
|||||||
.getByTestId(OID4VCI_FIELDS.TOKEN_JWS_TYPE)
|
.getByTestId(OID4VCI_FIELDS.TOKEN_JWS_TYPE)
|
||||||
.fill(TEST_VALUES.TOKEN_JWS_TYPE);
|
.fill(TEST_VALUES.TOKEN_JWS_TYPE);
|
||||||
|
|
||||||
|
await selectItem(
|
||||||
|
page,
|
||||||
|
OID4VCI_FIELDS.SIGNING_ALGORITHM,
|
||||||
|
TEST_VALUES.SIGNING_ALG,
|
||||||
|
);
|
||||||
|
|
||||||
|
await selectItem(
|
||||||
|
page,
|
||||||
|
OID4VCI_FIELDS.HASH_ALGORITHM,
|
||||||
|
TEST_VALUES.HASH_ALGORITHM,
|
||||||
|
);
|
||||||
|
|
||||||
await page.getByTestId(OID4VCI_FIELDS.DISPLAY).fill(TEST_VALUES.DISPLAY);
|
await page.getByTestId(OID4VCI_FIELDS.DISPLAY).fill(TEST_VALUES.DISPLAY);
|
||||||
await page
|
await page
|
||||||
.getByTestId(OID4VCI_FIELDS.SUPPORTED_CREDENTIAL_TYPES)
|
.getByTestId(OID4VCI_FIELDS.SUPPORTED_CREDENTIAL_TYPES)
|
||||||
@ -181,6 +199,12 @@ test.describe("OID4VCI Client Scope Functionality", () => {
|
|||||||
await expect(page.locator("#kc-vc-format")).toContainText(
|
await expect(page.locator("#kc-vc-format")).toContainText(
|
||||||
"JWT VC (jwt_vc)",
|
"JWT VC (jwt_vc)",
|
||||||
);
|
);
|
||||||
|
await expect(page.locator(OID4VCI_FIELDS.SIGNING_ALGORITHM)).toContainText(
|
||||||
|
TEST_VALUES.SIGNING_ALG,
|
||||||
|
);
|
||||||
|
await expect(page.locator(OID4VCI_FIELDS.HASH_ALGORITHM)).toContainText(
|
||||||
|
TEST_VALUES.HASH_ALGORITHM,
|
||||||
|
);
|
||||||
await expect(page.getByTestId(OID4VCI_FIELDS.DISPLAY)).toHaveValue(
|
await expect(page.getByTestId(OID4VCI_FIELDS.DISPLAY)).toHaveValue(
|
||||||
TEST_VALUES.DISPLAY,
|
TEST_VALUES.DISPLAY,
|
||||||
);
|
);
|
||||||
@ -237,6 +261,8 @@ test.describe("OID4VCI Client Scope Functionality", () => {
|
|||||||
page.getByTestId(OID4VCI_FIELDS.EXPIRY_IN_SECONDS),
|
page.getByTestId(OID4VCI_FIELDS.EXPIRY_IN_SECONDS),
|
||||||
).toBeHidden();
|
).toBeHidden();
|
||||||
await expect(page.locator(OID4VCI_FIELDS.FORMAT)).toBeHidden();
|
await expect(page.locator(OID4VCI_FIELDS.FORMAT)).toBeHidden();
|
||||||
|
await expect(page.locator(OID4VCI_FIELDS.SIGNING_ALGORITHM)).toBeHidden();
|
||||||
|
await expect(page.locator(OID4VCI_FIELDS.HASH_ALGORITHM)).toBeHidden();
|
||||||
await expect(page.getByTestId(OID4VCI_FIELDS.DISPLAY)).toBeHidden();
|
await expect(page.getByTestId(OID4VCI_FIELDS.DISPLAY)).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -354,6 +380,13 @@ test.describe("OID4VCI Client Scope Functionality", () => {
|
|||||||
await page
|
await page
|
||||||
.getByTestId(OID4VCI_FIELDS.CREDENTIAL_IDENTIFIER)
|
.getByTestId(OID4VCI_FIELDS.CREDENTIAL_IDENTIFIER)
|
||||||
.fill(TEST_VALUES.CREDENTIAL_ID);
|
.fill(TEST_VALUES.CREDENTIAL_ID);
|
||||||
|
|
||||||
|
await selectItem(
|
||||||
|
page,
|
||||||
|
OID4VCI_FIELDS.SIGNING_ALGORITHM,
|
||||||
|
TEST_VALUES.SIGNING_ALG,
|
||||||
|
);
|
||||||
|
|
||||||
await page.getByTestId(OID4VCI_FIELDS.DISPLAY).fill(TEST_VALUES.DISPLAY);
|
await page.getByTestId(OID4VCI_FIELDS.DISPLAY).fill(TEST_VALUES.DISPLAY);
|
||||||
await page
|
await page
|
||||||
.getByTestId(OID4VCI_FIELDS.SUPPORTED_CREDENTIAL_TYPES)
|
.getByTestId(OID4VCI_FIELDS.SUPPORTED_CREDENTIAL_TYPES)
|
||||||
@ -381,6 +414,9 @@ test.describe("OID4VCI Client Scope Functionality", () => {
|
|||||||
await expect(
|
await expect(
|
||||||
page.getByTestId(OID4VCI_FIELDS.VERIFIABLE_CREDENTIAL_TYPE),
|
page.getByTestId(OID4VCI_FIELDS.VERIFIABLE_CREDENTIAL_TYPE),
|
||||||
).toHaveValue(TEST_VALUES.VERIFIABLE_CREDENTIAL_TYPE);
|
).toHaveValue(TEST_VALUES.VERIFIABLE_CREDENTIAL_TYPE);
|
||||||
|
await expect(page.locator(OID4VCI_FIELDS.SIGNING_ALGORITHM)).toContainText(
|
||||||
|
TEST_VALUES.SIGNING_ALG,
|
||||||
|
);
|
||||||
await expect(page.getByTestId(OID4VCI_FIELDS.VISIBLE_CLAIMS)).toHaveValue(
|
await expect(page.getByTestId(OID4VCI_FIELDS.VISIBLE_CLAIMS)).toHaveValue(
|
||||||
TEST_VALUES.VISIBLE_CLAIMS,
|
TEST_VALUES.VISIBLE_CLAIMS,
|
||||||
);
|
);
|
||||||
@ -454,4 +490,99 @@ test.describe("OID4VCI Client Scope Functionality", () => {
|
|||||||
|
|
||||||
await expect(page.getByTestId(OID4VCI_FIELDS.TOKEN_JWS_TYPE)).toBeVisible();
|
await expect(page.getByTestId(OID4VCI_FIELDS.TOKEN_JWS_TYPE)).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should display signing algorithm dropdown with available algorithms", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await using testBed = await createTestBed();
|
||||||
|
await createClientScopeAndSelectProtocolAndFormat(
|
||||||
|
page,
|
||||||
|
testBed,
|
||||||
|
"SD-JWT VC (dc+sd-jwt)",
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(page.locator(OID4VCI_FIELDS.SIGNING_ALGORITHM)).toBeVisible();
|
||||||
|
|
||||||
|
await page.locator(OID4VCI_FIELDS.SIGNING_ALGORITHM).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole("option", { name: "RS256" })).toBeVisible();
|
||||||
|
await expect(page.getByRole("option", { name: "ES256" })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should display hash algorithm dropdown with available algorithms", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await using testBed = await createTestBed();
|
||||||
|
await createClientScopeAndSelectProtocolAndFormat(
|
||||||
|
page,
|
||||||
|
testBed,
|
||||||
|
"SD-JWT VC (dc+sd-jwt)",
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(page.locator(OID4VCI_FIELDS.HASH_ALGORITHM)).toBeVisible();
|
||||||
|
|
||||||
|
await page.locator(OID4VCI_FIELDS.HASH_ALGORITHM).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole("option", { name: "SHA-256" })).toBeVisible();
|
||||||
|
await expect(page.getByRole("option", { name: "SHA-384" })).toBeVisible();
|
||||||
|
await expect(page.getByRole("option", { name: "SHA-512" })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should save and persist hash algorithm value", async ({ page }) => {
|
||||||
|
await using testBed = await createTestBed();
|
||||||
|
const testClientScopeName = `oid4vci-hash-alg-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("name").fill(testClientScopeName);
|
||||||
|
|
||||||
|
await selectItem(
|
||||||
|
page,
|
||||||
|
OID4VCI_FIELDS.HASH_ALGORITHM,
|
||||||
|
TEST_VALUES.HASH_ALGORITHM,
|
||||||
|
);
|
||||||
|
|
||||||
|
await clickSaveButton(page);
|
||||||
|
await expect(page.getByText("Client scope created")).toBeVisible();
|
||||||
|
|
||||||
|
await navigateBackAndVerifyClientScope(page, testBed, testClientScopeName);
|
||||||
|
|
||||||
|
await expect(page.locator(OID4VCI_FIELDS.HASH_ALGORITHM)).toContainText(
|
||||||
|
TEST_VALUES.HASH_ALGORITHM,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should default to SHA-256 when hash algorithm is not set", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await using testBed = await createTestBed();
|
||||||
|
const testClientScopeName = `oid4vci-hash-default-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("name").fill(testClientScopeName);
|
||||||
|
|
||||||
|
await clickSaveButton(page);
|
||||||
|
await expect(page.getByText("Client scope created")).toBeVisible();
|
||||||
|
|
||||||
|
await navigateBackAndVerifyClientScope(page, testBed, testClientScopeName);
|
||||||
|
|
||||||
|
await expect(page.locator(OID4VCI_FIELDS.HASH_ALGORITHM)).toContainText(
|
||||||
|
"SHA-256",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user