Skip to content
Closed
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 @@ -3587,7 +3587,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
Expand Down
180 changes: 174 additions & 6 deletions js/apps/admin-ui/src/client-scopes/details/ScopeForm.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 {
Expand All @@ -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;
Expand All @@ -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"),
Expand All @@ -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";
Expand Down Expand Up @@ -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,
}}
/>
</>
)}
</>
)}

Expand Down
Loading