8000 [OID4VCI] Fix OID4VCI token typ defaults by credential format by forkimenjeckayang · Pull Request #47779 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ Create a JSON file (e.g., `client-scopes.json`) with the following content:
"vc.issuer_did": "did:web:vc.example.com",
"vc.credential_configuration_id": "my-credential-configuration-id",
"vc.credential_identifier": "my-credential-identifier",
"vc.format": "jwt_vc",
"vc.format": "jwt_vc_json",
"vc.expiry_in_seconds": 31536000,
"vc.verifiable_credential_type": "my-vct",
"vc.supported_credential_types": "credential-type-1,credential-type-2",
Expand All @@ -255,7 +255,7 @@ Create a JSON file (e.g., `client-scopes.json`) with the following content:
"vc.sd_jwt.number_of_decoys": "2",
"vc.credential_build_config.sd_jwt.visible_claims": "iat,jti,nbf,exp,given_name",
"vc.credential_build_config.hash_algorithm": "SHA-256",
"vc.credential_build_config.token_jws_type": "JWS",
"vc.credential_build_config.token_jws_type": "vc+jwt",
"vc.include_in_metadata": "true"
},
"protocolMappers": [
Expand Down Expand Up @@ -396,7 +396,7 @@ _Default_: `SHA-256`
| `vc.credential_build_config.token_jws_type`
| optional
| JWT type written into the `typ` header of the token. +
_Default_: `JWS`
_Default_: format-specific (`dc+sd-jwt` for `dc+sd-jwt`, `vc+jwt` for `jwt_vc_json`)

| `vc.expiry_in_s`
| optional
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ If an H2 JDBC URL is provided on the command line or in a configuration file, an

If the connection factory for the H2 database is initialized outside Keycloak, that initialization needs to take care of adding the `NON_KEYWORDS` attribute.

See the H2 documentation http://www.h2database.com/html/commands.html#set_non_keywords[on the `NON_KEYWORDS` attribute] for details.
See the H2 documentation https://www.h2database.com/html/commands.html#set_non_keywords[on the `NON_KEYWORDS` attribute] for details.

== Upgrade of H2 database files

H2 database base files created with H2 version 1.x should not be used with version 2.x.

Purge existing H2 database files to start with an empty database, export and import the realms using Keycloak's export and import functionality, or refer to the http://www.h2database.com/html/migration-to-v2.html[migration notes on the H2 database project's website] for details on how to migrate H2 database contents.
Purge existing H2 database files to start with an empty database, export and import the realms using Keycloak's export and import functionality, or refer to the https://www.h2database.com/html/migration-to-v2.html[migration notes on the H2 database project's website] for details on how to migrate H2 database contents.

= Breaking changes in the new version of Keycloak Operator

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3712,7 +3712,8 @@ supportedCredentialTypesHelp=Comma-separated list of credential types (e.g., "Ve
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.
tokenJwsTypeHelp=The type value written into the typ header of the JWT. If empty, a format-specific default is used ("dc+sd-jwt" for SD-JWT VC and "vc+jwt" for JWT VC). Can be set to custom values if required by the wallet or system.
tokenJwsTypeFormatWarning=The configured Token JWS Type does not match the recommended value for the selected credential format. Recommended value: {{recommended}}.
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
Expand Down
42 changes: 40 additions & 2 deletions js/apps/admin-ui/src/client-scopes/details/ScopeForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
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 {
ActionGroup,
Button,
FormHelperText,
HelperText,
HelperTextItem,
} from "@patternfly/react-core";
import { useEffect, useMemo, useState } from "react";
import { FormProvider, useForm, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
Expand Down Expand Up @@ -34,6 +40,8 @@ import { removeEmptyOid4vcAttributes } from "./oid4vciAttributes";
const OID4VC_PROTOCOL = "oid4vc";
const VC_FORMAT_JWT_VC = "jwt_vc_json";
const VC_FORMAT_SD_JWT = "dc+sd-jwt";
const VC_FORMAT_JWT_VC_TYP = "vc+jwt";
const VC_FORMAT_SD_JWT_TYP = "dc+sd-jwt";

// Allowed values for OID4VCI cryptographic binding methods and proof types.
// Keep these in sync with server-side support in CredentialScopeModel / ProofType.
Expand Down Expand Up @@ -157,12 +165,31 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
),
defaultValue: clientScope?.attributes?.["vc.format"] ?? VC_FORMAT_SD_JWT,
});
const selectedTokenJwsType = useWatch({
control,
name: convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
"attributes.vc.credential_build_config.token_jws_type",
),
defaultValue:
clientScope?.attributes?.["vc.credential_build_config.token_jws_type"] ??
"",
});

const isOid4vcProtocol = selectedProtocol === OID4VC_PROTOCOL;
const isOid4vcEnabled =
isFeatureEnabled(Feature.OpenId4VCI) &&
realmRepresentation?.verifiableCredentialsEnabled;
const isNotSaml = selectedProtocol != "saml";
const recommendedTokenJwsType =
selectedFormat === VC_FORMAT_SD_JWT
? VC_FORMAT_SD_JWT_TYP
: selectedFormat === VC_FORMAT_JWT_VC
? VC_FORMAT_JWT_VC_TYP
: undefined;
const showTokenJwsTypeWarning =
Boolean(selectedTokenJwsType?.trim()) &&
Boolean(recommendedTokenJwsType) &&
selectedTokenJwsType.trim() !== recommendedTokenJwsType;

const bindingRequired = useWatch({
control,
Expand Down Expand Up @@ -415,9 +442,20 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
defaultValue={
clientScope?.attributes?.[
"vc.credential_build_config.token_jws_type"
] ?? "JWS"
] ?? ""
}
/>
{showTokenJwsTypeWarning && (
<FormHelperText>
<HelperText>
<HelperTextItem variant="warning">
{t("tokenJwsTypeFormatWarning", {
recommended: recommendedTokenJwsType,
})}
</HelperTextItem>
</HelperText>
</FormHelperText>
)}
{realmKeys && realmKeys.length > 0 && (
<SelectControl
id="kc-signing-key-id"
Expand Down
57 changes: 57 additions & 0 deletions js/apps/admin-ui/test/client-scope/oid4vci-client-scope.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ const TEST_VALUES = {
SUPPORTED_CREDENTIAL_TYPES: "VerifiableCredential,UniversityDegreeCredential",
VERIFIABLE_CREDENTIAL_TYPE: "TestCredentialType",
} as const;
const TOKEN_JWS_TYPE_WARNING_PREFIX =
"The configured Token JWS Type does not match the recommended value for the selected credential format.";

test.describe("OID4VCI Client Scope Functionality", () => {
test("should display OID4VCI fields when protocol is selected", async ({
Expand Down Expand Up @@ -549,6 +551,61 @@ test.describe("OID4VCI Client Scope Functionality", () => {
await expect(page.getByTestId(OID4VCI_FIELDS.TOKEN_JWS_TYPE)).toBeVisible();
});

test("should show warning for mismatched token_jws_type based on selected format", async ({
page,
}) => {
await using testBed = await createTestBed({
verifiableCredentialsEnabled: true,
});
await createClientScopeAndSelectProtocolAndFormat(
page,
testBed,
"JWT VC (jwt_vc_json)",
);

const tokenJwsType = page.getByTestId(OID4VCI_FIELDS.TOKEN_JWS_TYPE);
await tokenJwsType.fill("dc+sd-jwt");

await expect(page.getByText(TOKEN_JWS_TYPE_WARNING_PREFIX)).toBeVisible();
await expect(page.getByText("Recommended value: vc+jwt.")).toBeVisible();

await tokenJwsType.fill("vc+jwt");
await expect(page.getByText(TOKEN_JWS_TYPE_WARNING_PREFIX)).toHaveCount(0);

await selectItem(page, "#kc-vc-format", "SD-JWT VC (dc+sd-jwt)");
await page.waitForLoadState("domcontentloaded");

await tokenJwsType.fill("vc+jwt");
await expect(page.getByText(TOKEN_JWS_TYPE_WARNING_PREFIX)).toBeVisible();
await expect(page.getByText("Recommended value: dc+sd-jwt.")).toBeVisible();

await tokenJwsType.fill("dc+sd-jwt");
await expect(page.getByText(TOKEN_JWS_TYPE_WARNING_PREFIX)).toHaveCount(0);
});

test("should not show token_jws_type warning when the value is empty", async ({
page,
}) => {
await using testBed = await createTestBed({
verifiableCredentialsEnabled: true,
});
await createClientScopeAndSelectProtocolAndFormat(
page,
testBed,
"SD-JWT VC (dc+sd-jwt)",
);

const tokenJwsType = page.getByTestId(OID4VCI_FIELDS.TOKEN_JWS_TYPE);
await tokenJwsType.fill("");
await expect(page.getByText(TOKEN_JWS_TYPE_WARNING_PREFIX)).toHaveCount(0);

await selectItem(page, "#kc-vc-format", "JWT VC (jwt_vc_json)");
await page.waitForLoadState("domcontentloaded");

await tokenJwsType.fill("");
await expect(page.getByText(TOKEN_JWS_TYPE_WARNING_PREFIX)).toHaveCount(0);
});

test("should display signing algorithm dropdown with available algorithms", async ({
page,
}) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.utils.StringUtil;

import static org.keycloak.constants.OID4VCIConstants.OID4VC_PROTOCOL;

Expand All @@ -45,7 +46,8 @@ public class CredentialScopeModel implements ClientScopeModel {

public static final String VC_BUILD_CONFIG_HASH_ALGORITHM_DEFAULT = "SHA-256";
public static final String VC_BUILD_CONFIG_SD_JWT_VISIBLE_CLAIMS_DEFAULT = "id,iat,nbf,exp,jti";
public static final String VC_BUILD_CONFIG_TOKEN_JWS_TYPE_DEFAULT = "JWS";
public static final String VC_BUILD_CONFIG_TOKEN_JWS_TYPE_DEFAULT_SD_JWT_VC = "dc+sd-jwt";
public static final String VC_BUILD_CONFIG_TOKEN_JWS_TYPE_DEFAULT_JWT_VC = "vc+jwt";
public static final Integer VC_EXPIRY_IN_SECONDS_DEFAULT = 31536000; // 1 year
public static final String VC_FORMAT_DEFAULT = VCFormat.SD_JWT_VC;
public static final Integer VC_SD_JWT_NUMBER_OF_DECOYS_DEFAULT = 10;
Expand Down Expand Up @@ -232,13 +234,24 @@ public void setVct(String vct) {

public String getBuildConfigTokenJwsType() {
return Optional.ofNullable(clientScope.getAttribute(VC_BUILD_CONFIG_TOKEN_JWS_TYPE))
.orElse(VC_BUILD_CONFIG_TOKEN_JWS_TYPE_DEFAULT);
.filter(StringUtil::isNotBlank)
.orElse(getDefaultTokenJwsTypeForFormat(getFormat()));
}

public void setBuildConfigTokenJwsType(String tokenJwsType) {
clientScope.setAttribute(VC_BUILD_CONFIG_TOKEN_JWS_TYPE, tokenJwsType);
}

public static String getDefaultTokenJwsTypeForFormat(String format) {
if (VCFormat.SD_JWT_VC.equals(format)) {
return VC_BUILD_CONFIG_TOKEN_JWS_TYPE_DEFAULT_SD_JWT_VC;
}
if (VCFormat.JWT_VC.equals(format)) {
return VC_BUILD_CONFIG_TOKEN_JWS_TYPE_DEFAULT_JWT_VC;
}
return null;
}

public String getSigningKeyId() {
return clientScope.getAttribute(VC_SIGNING_KEY_ID);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@
import static org.keycloak.models.oid4vci.CredentialScopeModel.VC_BUILD_CONFIG_SD_JWT_VISIBLE_CLAIMS;
import static org.keycloak.models.oid4vci.CredentialScopeModel.VC_BUILD_CONFIG_SD_JWT_VISIBLE_CLAIMS_DEFAULT;
import static org.keycloak.models.oid4vci.CredentialScopeModel.VC_BUILD_CONFIG_TOKEN_JWS_TYPE;
import static org.keycloak.models.oid4vci.CredentialScopeModel.VC_BUILD_CONFIG_TOKEN_JWS_TYPE_DEFAULT;
import static org.keycloak.models.oid4vci.CredentialScopeModel.VC_CONFIGURATION_ID;
import static org.keycloak.models.oid4vci.CredentialScopeModel.VC_CONTEXTS;
import static org.keycloak.models.oid4vci.CredentialScopeModel.VC_CRYPTOGRAPHIC_BINDING_METHODS;
Expand All @@ -71,6 +70,7 @@
import static org.keycloak.models.oid4vci.CredentialScopeModel.VC_SD_JWT_NUMBER_OF_DECOYS;
import static org.keycloak.models.oid4vci.CredentialScopeModel.VC_SD_JWT_NUMBER_OF_DECOYS_DEFAULT;
import static org.keycloak.models.oid4vci.CredentialScopeModel.VC_SUPPORTED_TYPES;
import static org.keycloak.models.oid4vci.CredentialScopeModel.getDefaultTokenJwsTypeForFormat;

/**
* Factory for creating all OID4VC related endpoints and the default mappers.
Expand Down Expand Up @@ -174,7 +174,7 @@ public void addClientScopeDefaults(ClientScopeRepresentation clientScope) {
clientScope.getAttributes().putIfAbsent(VCT, credentialType);
clientScope.getAttributes().putIfAbsent(VC_CRYPTOGRAPHIC_BINDING_METHODS, CRYPTOGRAPHIC_BINDING_METHODS_DEFAULT);
clientScope.getAttributes().putIfAbsent(VC_BUILD_CONFIG_HASH_ALGORITHM, VC_BUILD_CONFIG_HASH_ALGORITHM_DEFAULT);
clientScope.getAttributes().putIfAbsent(VC_BUILD_CONFIG_TOKEN_JWS_TYPE, VC_BUILD_CONFIG_TOKEN_JWS_TYPE_DEFAULT);
clientScope.getAttributes().putIfAbsent(VC_BUILD_CONFIG_TOKEN_JWS_TYPE, getDefaultTokenJwsTypeForFormat(format));
clientScope.getAttributes().putIfAbsent(VC_EXPIRY_IN_SECONDS, String.valueOf(VC_EXPIRY_IN_SECONDS_DEFAULT));

if (SD_JWT_VC.equals(format)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import static org.keycloak.models.oid4vci.CredentialScopeModel.VC_BUILD_CONFIG_SD_JWT_VISIBLE_CLAIMS;
import static org.keycloak.models.oid4vci.CredentialScopeModel.VC_BUILD_CONFIG_SD_JWT_VISIBLE_CLAIMS_DEFAULT;
import static org.keycloak.models.oid4vci.CredentialScopeModel.VC_BUILD_CONFIG_TOKEN_JWS_TYPE;
import static org.keycloak.models.oid4vci.CredentialScopeModel.VC_BUILD_CONFIG_TOKEN_JWS_TYPE_DEFAULT;
import static org.keycloak.models.oid4vci.CredentialScopeModel.VC_CONFIGURATION_ID;
import static org.keycloak.models.oid4vci.CredentialScopeModel.VC_CONTEXTS;
import static org.keycloak.models.oid4vci.CredentialScopeModel.VC_CRYPTOGRAPHIC_BINDING_METHODS;
Expand All @@ -37,6 +36,7 @@
import static org.keycloak.models.oid4vci.CredentialScopeModel.VC_SIGNING_ALG;
import static org.keycloak.models.oid4vci.CredentialScopeModel.VC_SIGNING_KEY_ID;
import static org.keycloak.models.oid4vci.CredentialScopeModel.VC_SUPPORTED_TYPES;
import static org.keycloak.models.oid4vci.CredentialScopeModel.getDefaultTokenJwsTypeForFormat;

/**
* An extension of {@link ClientScopeRepresentation} which and adds additional functionality for OID4VCI
Expand All @@ -48,12 +48,12 @@ public class CredentialScopeRepresentation extends ClientScopeRepresentation {
public CredentialScopeRepresentation(String name) {
this.name = name;
this.protocol = OID4VCIConstants.OID4VC_PROTOCOL;
setFormat(VC_FORMAT_DEFAULT);
setBuildConfigHashAlgorithm(VC_BUILD_CONFIG_HASH_ALGORITHM_DEFAULT);
setBuildConfigSdJwtVisibleClaims(VC_BUILD_CONFIG_SD_JWT_VISIBLE_CLAIMS_DEFAULT);
setBuildConfigTokenJwsType(VC_BUILD_CONFIG_TOKEN_JWS_TYPE_DEFAULT);
setBuildConfigTokenJwsType(getDefaultTokenJwsTypeForFormat(VC_FORMAT_DEFAULT));
setSdJwtNumberOfDecoys(VC_SD_JWT_NUMBER_OF_DECOYS_DEFAULT);
setExpiryInSeconds(VC_EXPIRY_IN_SECONDS_DEFAULT);
setFormat(VC_FORMAT_DEFAULT);
}

public CredentialScopeRepresentation(ClientScopeModel clientScope) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public void testDefaultOid4VciClientScopeAttributes() {
createdClientScope.getAttributes().get(CredentialScopeModel.VC_FORMAT));
Assertions.assertEquals(CredentialScopeModel.VC_BUILD_CONFIG_HASH_ALGORITHM_DEFAULT,
createdClientScope.getAttributes().get(CredentialScopeModel.VC_BUILD_CONFIG_HASH_ALGORITHM));
Assertions.assertEquals(CredentialScopeModel.VC_BUILD_CONFIG_TOKEN_JWS_TYPE_DEFAULT,
Assertions.assertEquals(CredentialScopeModel.VC_BUILD_CONFIG_TOKEN_JWS_TYPE_DEFAULT_SD_JWT_VC,
createdClientScope.getAttributes().get(CredentialScopeModel.VC_BUILD_CONFIG_TOKEN_JWS_TYPE));
Assertions.assertEquals(String.valueOf(CredentialScopeModel.VC_EXPIRY_IN_SECONDS_DEFAULT),
createdClientScope.getAttributes().get(CredentialScopeModel.VC_EXPIRY_IN_SECONDS));
Expand Down
Loading
Loading
0