diff --git a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties index d64b86acb08..8db32764833 100644 --- a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties +++ b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties @@ -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). 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"}] +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 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) diff --git a/js/apps/admin-ui/src/client-scopes/details/ScopeForm.tsx b/js/apps/admin-ui/src/client-scopes/details/ScopeForm.tsx index ab6d17c80f6..7601fd09fa3 100644 --- a/js/apps/admin-ui/src/client-scopes/details/ScopeForm.tsx +++ b/js/apps/admin-ui/src/client-scopes/details/ScopeForm.tsx @@ -77,6 +77,17 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => { [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 const [realmKeys, setRealmKeys] = useState([]); @@ -367,6 +378,45 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => { options={keyOptions} /> )} + {asymmetricSigAlgOptions.length > 0 && ( + ( + "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 && ( + ( + "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, + }))} + /> + )} ( "attributes.vc.display", diff --git a/js/apps/admin-ui/test/client-scope/oid4vci-client-scope.spec.ts b/js/apps/admin-ui/test/client-scope/oid4vci-client-scope.spec.ts index 3c8e3ba9c24..814a1cd9661 100644 --- a/js/apps/admin-ui/test/client-scope/oid4vci-client-scope.spec.ts +++ b/js/apps/admin-ui/test/client-scope/oid4vci-client-scope.spec.ts @@ -67,6 +67,8 @@ const OID4VCI_FIELDS = { FORMAT: "#kc-vc-format", TOKEN_JWS_TYPE: "attributes.vc🍺credential_build_config🍺token_jws_type", SIGNING_KEY_ID: "#kc-signing-key-id", + SIGNING_ALGORITHM: "#kc-credential-signing-alg", + HASH_ALGORITHM: "#kc-hash-algorithm", DISPLAY: "attributes.vc🍺display", SUPPORTED_CREDENTIAL_TYPES: "attributes.vc🍺supported_credential_types", VERIFIABLE_CREDENTIAL_TYPE: "attributes.vc🍺verifiable_credential_type", @@ -80,6 +82,8 @@ const TEST_VALUES = { CREDENTIAL_ID: "test-cred-identifier", ISSUER_DID: "did:key:test123", EXPIRY_SECONDS: "86400", + SIGNING_ALG: "ES256", + HASH_ALGORITHM: "SHA-384", TOKEN_JWS_TYPE: "dc+sd-jwt", VISIBLE_CLAIMS: "id,iat,nbf,exp,jti,given_name", DISPLAY: @@ -124,6 +128,8 @@ test.describe("OID4VCI Client Scope Functionality", () => { ).toBeVisible(); await expect(page.locator(OID4VCI_FIELDS.FORMAT)).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(); }); @@ -154,6 +160,18 @@ test.describe("OID4VCI Client Scope Functionality", () => { .getByTestId(OID4VCI_FIELDS.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.SUPPORTED_CREDENTIAL_TYPES) @@ -181,6 +199,12 @@ test.describe("OID4VCI Client Scope Functionality", () => { await expect(page.locator("#kc-vc-format")).toContainText( "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( TEST_VALUES.DISPLAY, ); @@ -237,6 +261,8 @@ 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.locator(OID4VCI_FIELDS.SIGNING_ALGORITHM)).toBeHidden(); + await expect(page.locator(OID4VCI_FIELDS.HASH_ALGORITHM)).toBeHidden(); await expect(page.getByTestId(OID4VCI_FIELDS.DISPLAY)).toBeHidden(); }); @@ -354,6 +380,13 @@ test.describe("OID4VCI Client Scope Functionality", () => { await page .getByTestId(OID4VCI_FIELDS.CREDENTIAL_IDENTIFIER) .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.SUPPORTED_CREDENTIAL_TYPES) @@ -381,6 +414,9 @@ test.describe("OID4VCI Client Scope Functionality", () => { await expect( page.getByTestId(OID4VCI_FIELDS.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( TEST_VALUES.VISIBLE_CLAIMS, ); @@ -454,4 +490,99 @@ test.describe("OID4VCI Client Scope Functionality", () => { 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", + ); + }); });