Finish the policy pages of the authentication section (#1713)

* added WebauthnPolicy tab to Plicies

* change for passwordless

* added tests

* removed use of useToggle
This commit is contained in:
Erik Jan de Wit 2022-01-05 17:18:49 +01:00 committed by GitHub
parent 23b11eb879
commit 33a1769c39
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 634 additions and 62 deletions

View file

@ -0,0 +1,68 @@
import { keycloakBefore } from "../support/util/keycloak_before";
import Masthead from "../support/pages/admin_console/Masthead";
import LoginPage from "../support/pages/LoginPage";
import SidebarPage from "../support/pages/admin_console/SidebarPage";
import OTPPolicies from "../support/pages/admin_console/manage/authentication/OTPPolicies";
import WebAuthnPolicies from "../support/pages/admin_console/manage/authentication/WebAuthnPolicies";
describe("Policies", () => {
const masthead = new Masthead();
const loginPage = new LoginPage();
const sidebarPage = new SidebarPage();
describe("OTP policies tab", () => {
const otpPoliciesPage = new OTPPolicies();
beforeEach(() => {
keycloakBefore();
loginPage.logIn();
sidebarPage.goToAuthentication();
otpPoliciesPage.goToTab();
});
it("should change to hotp", () => {
otpPoliciesPage.checkSupportedActions("FreeOTP, Google Authenticator");
otpPoliciesPage.setPolicyType("hotp").increaseInitialCounter().save();
masthead.checkNotificationMessage("OTP policy successfully updated");
otpPoliciesPage.checkSupportedActions("FreeOTP");
});
});
describe("Webauthn policies tabs", () => {
const webauthnPage = new WebAuthnPolicies();
beforeEach(() => {
keycloakBefore();
loginPage.logIn();
sidebarPage.goToAuthentication();
});
it("should fill webauthn settings", () => {
webauthnPage.goToTab();
webauthnPage.fillSelects({
webAuthnPolicyAttestationConveyancePreference: "Indirect",
webAuthnPolicyRequireResidentKey: "Yes",
webAuthnPolicyUserVerificationRequirement: "Preferred",
});
webauthnPage.webAuthnPolicyCreateTimeout(30).save();
masthead.checkNotificationMessage(
"Updated webauthn policies successfully"
);
});
it("should fill webauthn passwordless settings", () => {
webauthnPage.goToPasswordlessTab();
webauthnPage
.fillSelects(
{
webAuthnPolicyAttestationConveyancePreference: "Indirect",
webAuthnPolicyRequireResidentKey: "Yes",
webAuthnPolicyUserVerificationRequirement: "Preferred",
},
true
)
.save();
masthead.checkNotificationMessage(
"Updated webauthn policies successfully"
);
});
});
});

View file

@ -8,7 +8,6 @@ import FlowDetails from "../support/pages/admin_console/manage/authentication/Fl
import RequiredActions from "../support/pages/admin_console/manage/authentication/RequiredActions";
import AdminClient from "../support/util/AdminClient";
import PasswordPolicies from "../support/pages/admin_console/manage/authentication/PasswordPolicies";
import OTPPolicies from "../support/pages/admin_console/manage/authentication/OTPPolicies";
describe("Authentication test", () => {
const loginPage = new LoginPage();
@ -184,21 +183,4 @@ describe("Authentication test", () => {
passwordPoliciesPage.shouldShowEmptyState();
});
});
describe("OTP policies tab", () => {
const otpPoliciesPage = new OTPPolicies();
beforeEach(() => {
keycloakBefore();
loginPage.logIn();
sidebarPage.goToAuthentication();
otpPoliciesPage.goToTab();
});
it("should change to hotp", () => {
otpPoliciesPage.checkSupportedActions("FreeOTP, Google Authenticator");
otpPoliciesPage.setPolicyType("hotp").increaseInitialCounter().save();
masthead.checkNotificationMessage("OTP policy successfully updated");
otpPoliciesPage.checkSupportedActions("FreeOTP");
});
});
});

View file

@ -0,0 +1,41 @@
export default class WebAuthnPolicies {
webAuthnPolicyCreateTimeout(value: number) {
cy.findByTestId("webAuthnPolicyCreateTimeout").type(String(value));
return this;
}
goToTab() {
cy.get("#pf-tab-policies-policies")
.click()
.get("#pf-tab-3-webauthnPolicy")
.click();
return this;
}
goToPasswordlessTab() {
cy.get("#pf-tab-policies-policies")
.click()
.get("#pf-tab-4-webauthnPasswordlessPolicy")
.click();
return this;
}
fillSelects(data: Record<string, string>, isPasswordLess: boolean = false) {
for (const prop of Object.keys(data)) {
cy.get(
`#${
isPasswordLess ? prop.replace("Policy", "PolicyPasswordless") : prop
}`
)
.click()
.parent()
.contains(data[prop])
.click();
}
return this;
}
save() {
cy.findByTestId("save").click();
return this;
}
}

View file

@ -24,5 +24,28 @@ export default {
"How many seconds should an OTP token be valid? Defaults to 30 seconds.",
supportedActions:
"Applications that are known to work with the current OTP policy",
webauthnIntro: "What is this form used for?",
webAuthnPolicyFormHelp:
"Policy for WebAuthn authentication. This one will be used by 'WebAuthn Register' required action and 'WebAuthn Authenticator' authenticator. Typical usage is, when WebAuthn will be used for the two-factor authentication.",
webAuthnPolicyPasswordlessFormHelp:
"Policy for passwordless WebAuthn authentication. This one will be used by 'Webauthn Register Passwordless' required action and 'WebAuthn Passwordless Authenticator' authenticator. Typical usage is, when WebAuthn will be used as first-factor authentication. Having both 'WebAuthn Policy' and 'WebAuthn Passwordless Policy' allows to use WebAuthn as both first factor and second factor authenticator in the same realm.",
webAuthnPolicySignatureAlgorithms:
"What signature algorithms should be used for Authentication Assertion.",
webAuthnPolicyRpId:
"This is ID as WebAuthn Relying Party. It must be origin's effective domain.",
webAuthnPolicyAttestationConveyancePreference:
"Communicates to an authenticator the preference of how to generate an attestation statement.",
webAuthnPolicyAuthenticatorAttachment:
"Communicates to an authenticator an acceptable attachment pattern.",
webAuthnPolicyRequireResidentKey:
"It tells an authenticator create a public key credential as Resident Key or not.",
webAuthnPolicyUserVerificationRequirement:
"Communicates to an authenticator to confirm actually verifying a user.",
webAuthnPolicyCreateTimeout:
"Timeout value for creating user's public key credential in seconds. if set to 0, this timeout option is not adapted.",
webAuthnPolicyAvoidSameAuthenticatorRegister:
"Avoid registering the authenticator that has already been registered.",
webAuthnPolicyAcceptableAaguids:
"The list of AAGUID of which an authenticator can be registered.",
},
};

View file

@ -33,6 +33,44 @@ export default {
supportedActions: "Supported actions",
updateOtpSuccess: "OTP policy successfully updated",
updateOtpError: "Could not update OTP policy: {{error}}",
webAuthnPolicySignatureAlgorithms: "Signature algorithms",
webAuthnPolicyRpId: "Relying party ID",
webAuthnPolicyAttestationConveyancePreference:
"Attestation conveyance preference",
attestationPreference: {
"not specified": "Not specified",
none: "None",
indirect: "Indirect",
direct: "Direct",
},
webAuthnPolicyAuthenticatorAttachment: "Authenticator Attachment",
authenticatorAttachment: {
"not specified": "Not specified",
platform: "Platform",
"cross-platform": "Cross platform",
},
webAuthnPolicyRequireResidentKey: "Require resident key",
residentKey: {
"not specified": "Not specified",
Yes: "Yes",
No: "No",
},
webAuthnPolicyUserVerificationRequirement: "User verification requirement",
userVerify: {
"not specified": "Not specified",
required: "Required",
preferred: "Preferred",
discouraged: "Discouraged",
},
webAuthnPolicyCreateTimeout: "Timeout",
webAuthnPolicyCreateTimeoutHint:
"Timeout needs to be between 0 seconds and 8 hours",
webAuthnPolicyAvoidSameAuthenticatorRegister:
"Avoid same authenticator registration",
webAuthnPolicyAcceptableAaguids: "Acceptable AAGUIDs",
addAaguids: "Add AAGUID",
webAuthnUpdateSuccess: "Updated webauthn policies successfully",
webAuthnUpdateError: "Could not update webauthn policies due to {{error}}",
flowName: "Flow name",
searchForFlow: "Search for flow",
usedBy: "Used by",

View file

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Controller, useForm, useWatch } from "react-hook-form";
import {
@ -21,7 +21,7 @@ import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import useToggle from "../../utils/useToggle";
import { TimeSelector } from "../../components/time-selector/TimeSelector";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import { useAdminClient } from "../../context/auth/AdminClient";
import { useRealm } from "../../context/realm-context/RealmContext";
import { useAlerts } from "../../components/alert/Alerts";
@ -31,10 +31,16 @@ const POLICY_TYPES = ["totp", "hotp"] as const;
const OTP_HASH_ALGORITHMS = ["SHA1", "SHA256", "SHA512"] as const;
const NUMBER_OF_DIGITS = [6, 8] as const;
export const OtpPolicy = () => {
type OtpPolicyProps = {
realm: RealmRepresentation;
realmUpdated: (realm: RealmRepresentation) => void;
};
export const OtpPolicy = ({ realm, realmUpdated }: OtpPolicyProps) => {
const { t } = useTranslation("authentication");
const {
control,
register,
errors,
reset,
handleSubmit,
@ -45,7 +51,6 @@ export const OtpPolicy = () => {
const { addAlert, addError } = useAlerts();
const [open, toggle] = useToggle();
const [realm, setRealm] = useState<RealmRepresentation>();
const otpType = useWatch<typeof POLICY_TYPES[number]>({
name: "otpPolicyType",
@ -53,14 +58,13 @@ export const OtpPolicy = () => {
defaultValue: POLICY_TYPES[0],
});
useFetch(
() => adminClient.realms.findOne({ realm: realmName }),
(realm) => {
setRealm(realm);
reset({ ...realm });
},
[]
);
const setupForm = (realm: RealmRepresentation) =>
reset({
...realm,
otpSupportedApplications: realm.otpSupportedApplications?.join(", "),
});
useEffect(() => setupForm(realm), []);
const save = async (realm: RealmRepresentation) => {
try {
@ -68,8 +72,8 @@ export const OtpPolicy = () => {
const updatedRealm = await adminClient.realms.findOne({
realm: realmName,
});
setRealm(updatedRealm);
reset({ ...updatedRealm });
realmUpdated(updatedRealm!);
setupForm(updatedRealm!);
addAlert(t("updateOtpSuccess"), AlertVariant.success);
} catch (error) {
addError("authentication:updateOtpError", error);
@ -310,9 +314,12 @@ export const OtpPolicy = () => {
>
<TextInput
id="supportedActions"
name="otpSupportedApplications"
ref={register({
setValueAs: (value) => value.split(", "),
})}
data-testid="supportedActions"
isReadOnly
value={realm?.otpSupportedApplications?.join(", ")}
/>
</FormGroup>
<ActionGroup>

View file

@ -1,4 +1,4 @@
import React, { useMemo, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import {
@ -23,10 +23,9 @@ import { PlusCircleIcon } from "@patternfly/react-icons";
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
import type PasswordPolicyTypeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/passwordPolicyTypeRepresentation";
import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner";
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
import { FormAccess } from "../../components/form-access/FormAccess";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import { useAdminClient } from "../../context/auth/AdminClient";
import { useRealm } from "../../context/realm-context/RealmContext";
import { useAlerts } from "../../components/alert/Alerts";
import { parsePolicy, SubmittedValues } from "./util";
@ -72,15 +71,22 @@ const PolicySelect = ({ onSelect, selectedPolicies }: PolicySelectProps) => {
);
};
export const PasswordPolicy = () => {
type PasswordPolicyProps = {
realm: RealmRepresentation;
realmUpdated: (realm: RealmRepresentation) => void;
};
export const PasswordPolicy = ({
realm,
realmUpdated,
}: PasswordPolicyProps) => {
const { t } = useTranslation("authentication");
const { passwordPolicies } = useServerInfo();
const adminClient = useAdminClient();
const { realm: realmName } = useRealm();
const { addAlert, addError } = useAlerts();
const { realm: realmName } = useRealm();
const [realm, setRealm] = useState<RealmRepresentation>();
const [rows, setRows] = useState<PasswordPolicyTypeRepresentation[]>([]);
const onSelect = (row: PasswordPolicyTypeRepresentation) =>
setRows([...rows, row]);
@ -96,20 +102,7 @@ export const PasswordPolicy = () => {
setRows(values);
};
useFetch(
async () => {
const realm = await adminClient.realms.findOne({ realm: realmName });
if (!realm) {
throw new Error(t("common:notFound"));
}
return realm;
},
(realm) => {
setRealm(realm);
setupForm(realm);
},
[]
);
useEffect(() => setupForm(realm), []);
const save = async (values: SubmittedValues) => {
const updatedRealm = {
@ -118,17 +111,14 @@ export const PasswordPolicy = () => {
};
try {
await adminClient.realms.update({ realm: realmName }, updatedRealm);
setRealm(updatedRealm);
realmUpdated(updatedRealm);
setupForm(updatedRealm);
addAlert(t("updatePasswordPolicySuccess"), AlertVariant.success);
} catch (error: any) {
addError("authentication:updatePasswordPolicyError", error);
}
};
if (!realm) {
return <KeycloakSpinner />;
}
return (
<PageSection variant="light" className="pf-u-p-0">
{(rows.length !== 0 || realm.passwordPolicy) && (

View file

@ -2,12 +2,39 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Tab, Tabs, TabTitleText } from "@patternfly/react-core";
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import { useRealm } from "../../context/realm-context/RealmContext";
import { PasswordPolicy } from "./PasswordPolicy";
import { OtpPolicy } from "./OtpPolicy";
import { WebauthnPolicy } from "./WebauthnPolicy";
import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner";
export const Policies = () => {
const { t } = useTranslation("authentication");
const [subTab, setSubTab] = useState(1);
const adminClient = useAdminClient();
const { realm: realmName } = useRealm();
const [realm, setRealm] = useState<RealmRepresentation>();
useFetch(
async () => {
const realm = await adminClient.realms.findOne({ realm: realmName });
if (!realm) {
throw new Error(t("common:notFound"));
}
return realm;
},
(realm) => {
setRealm(realm);
},
[]
);
if (!realm) {
return <KeycloakSpinner />;
}
return (
<Tabs
activeKey={subTab}
@ -19,25 +46,29 @@ export const Policies = () => {
eventKey={1}
title={<TabTitleText>{t("passwordPolicy")}</TabTitleText>}
>
<PasswordPolicy />
<PasswordPolicy realm={realm} realmUpdated={setRealm} />
</Tab>
<Tab
id="otpPolicy"
eventKey={2}
title={<TabTitleText>{t("otpPolicy")}</TabTitleText>}
>
<OtpPolicy />
<OtpPolicy realm={realm} realmUpdated={setRealm} />
</Tab>
<Tab
id="webauthnPolicy"
eventKey={3}
title={<TabTitleText>{t("webauthnPolicy")}</TabTitleText>}
></Tab>
>
<WebauthnPolicy realm={realm} realmUpdated={setRealm} />
</Tab>
<Tab
id="webauthnPasswordlessPolicy"
eventKey={4}
title={<TabTitleText>{t("webauthnPasswordlessPolicy")}</TabTitleText>}
></Tab>
>
<WebauthnPolicy realm={realm} realmUpdated={setRealm} isPasswordLess />
</Tab>
</Tabs>
);
};

View file

@ -0,0 +1,381 @@
import React, { useEffect, useState } from "react";
import {
Controller,
FormProvider,
useForm,
useFormContext,
} from "react-hook-form";
import { useTranslation } from "react-i18next";
import {
ActionGroup,
AlertVariant,
Button,
ButtonVariant,
FormGroup,
PageSection,
Popover,
Select,
SelectOption,
SelectVariant,
Switch,
Text,
TextContent,
TextInput,
} from "@patternfly/react-core";
import { QuestionCircleIcon } from "@patternfly/react-icons";
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
import { convertFormValuesToObject, convertToFormValues } from "../../util";
import { useAdminClient } from "../../context/auth/AdminClient";
import { useRealm } from "../../context/realm-context/RealmContext";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { useHelp } from "../../components/help-enabler/HelpHeader";
import { useAlerts } from "../../components/alert/Alerts";
import { TimeSelector } from "../../components/time-selector/TimeSelector";
import { MultiLineInput } from "../../components/multi-line-input/MultiLineInput";
import "./webauthn-policy.css";
const SIGNATURE_ALGORITHMS = [
"ES256",
"ES384",
"ES512",
"RS256",
"RS384",
"RS512",
"RS1",
] as const;
const ATTESTATION_PREFERENCE = [
"not specified",
"none",
"indirect",
"direct",
] as const;
const AUTHENTICATOR_ATTACHMENT = [
"not specified",
"platform",
"cross-platform",
] as const;
const RESIDENT_KEY_OPTIONS = ["not specified", "Yes", "No"] as const;
const USER_VERIFY = [
"not specified",
"required",
"preferred",
"discouraged",
] as const;
type WeauthnSelectProps = {
name: string;
label: string;
options: readonly string[];
labelPrefix?: string;
isMultiSelect?: boolean;
};
const WebauthnSelect = ({
name,
label,
options,
labelPrefix,
isMultiSelect = false,
}: WeauthnSelectProps) => {
const { t } = useTranslation("authentication");
const { control } = useFormContext();
const [open, toggle] = useState(false);
return (
<FormGroup
label={t(label)}
labelIcon={
<HelpItem
helpText={`authentication-help:${label}`}
fieldLabelId={`authentication:${label}`}
/>
}
fieldId={name}
>
<Controller
name={name}
defaultValue={options[0]}
control={control}
render={({ onChange, value }) => (
<Select
toggleId={name}
onToggle={toggle}
onSelect={(_, selectedValue) => {
if (isMultiSelect) {
const changedValue = value.find(
(item: string) => item === selectedValue
)
? value.filter((item: string) => item !== selectedValue)
: [...value, selectedValue];
onChange(changedValue);
} else {
onChange(selectedValue.toString());
toggle(false);
}
}}
selections={labelPrefix ? t(`${labelPrefix}.${value}`) : value}
variant={
isMultiSelect
? SelectVariant.typeaheadMulti
: SelectVariant.single
}
aria-label={t(name)}
isOpen={open}
>
{options.map((option) => (
<SelectOption
selected={option === value}
key={option}
value={option}
>
{labelPrefix ? t(`${labelPrefix}.${option}`) : option}
</SelectOption>
))}
</Select>
)}
/>
</FormGroup>
);
};
const MULTILINE_INPUTS = [
"webAuthnPolicyAcceptableAaguids",
"webAuthnPolicyPasswordlessAcceptableAaguids",
];
type WebauthnPolicyProps = {
realm: RealmRepresentation;
realmUpdated: (realm: RealmRepresentation) => void;
isPasswordLess?: boolean;
};
export const WebauthnPolicy = ({
realm,
realmUpdated,
isPasswordLess = false,
}: WebauthnPolicyProps) => {
const { t } = useTranslation("authentication");
const adminClient = useAdminClient();
const { addAlert, addError } = useAlerts();
const { realm: realmName } = useRealm();
const { enabled } = useHelp();
const form = useForm({ mode: "onChange", shouldUnregister: false });
const {
control,
register,
setValue,
errors,
handleSubmit,
formState: { isDirty },
} = form;
const namePrefix = isPasswordLess
? "webAuthnPolicyPasswordless"
: "webAuthnPolicy";
const setupForm = (realm: RealmRepresentation) =>
convertToFormValues(realm, setValue, MULTILINE_INPUTS);
useEffect(() => setupForm(realm), []);
const save = async (realm: RealmRepresentation) => {
const submittedRealm = convertFormValuesToObject(realm, MULTILINE_INPUTS);
try {
await adminClient.realms.update({ realm: realmName }, submittedRealm);
realmUpdated(submittedRealm);
setupForm(submittedRealm);
addAlert(t("webAuthnUpdateSuccess"), AlertVariant.success);
} catch (error) {
addError("authentication:webAuthnUpdateError", error);
}
};
return (
<PageSection variant="light">
{enabled && (
<Popover bodyContent={t(`authentication-help:${namePrefix}FormHelp`)}>
<TextContent className="keycloak__webauthn_policies__intro">
<Text>
<QuestionCircleIcon /> {t("authentication-help:webauthnIntro")}
</Text>
</TextContent>
</Popover>
)}
<FormAccess
role="manage-realm"
isHorizontal
onSubmit={handleSubmit(save)}
className="keycloak__webauthn_policies_authentication__form"
>
<FormGroup
label={t("webAuthnPolicyRpEntityName")}
fieldId="webAuthnPolicyRpEntityName"
helperTextInvalid={t("common:required")}
validated={errors.webAuthnPolicyRpEntityName ? "error" : "default"}
isRequired
labelIcon={
<HelpItem
helpText="authentication-help:webAuthnPolicyRpEntityName"
fieldLabelId="authentication:webAuthnPolicyRpEntityName"
/>
}
>
<TextInput
ref={register({ required: true })}
name={`${namePrefix}RpEntityName`}
id="webAuthnPolicyRpEntityName"
data-testid="webAuthnPolicyRpEntityName"
validated={errors.webAuthnPolicyRpEntityName ? "error" : "default"}
/>
</FormGroup>
<FormProvider {...form}>
<WebauthnSelect
name={`${namePrefix}SignatureAlgorithms`}
label="webAuthnPolicySignatureAlgorithms"
options={SIGNATURE_ALGORITHMS}
isMultiSelect
/>
<FormGroup
label={t("webAuthnPolicyRpId")}
labelIcon={
<HelpItem
helpText="authentication-help:webAuthnPolicyRpId"
fieldLabelId="authentication:webAuthnPolicyRpId"
/>
}
fieldId="webAuthnPolicyRpId"
>
<TextInput
id="webAuthnPolicyRpId"
name={`${namePrefix}RpId`}
ref={register()}
data-testid="webAuthnPolicyRpId
"
/>
</FormGroup>
<WebauthnSelect
name={`${namePrefix}AttestationConveyancePreference`}
label="webAuthnPolicyAttestationConveyancePreference"
options={ATTESTATION_PREFERENCE}
labelPrefix="attestationPreference"
/>
<WebauthnSelect
name={`${namePrefix}AuthenticatorAttachment`}
label="webAuthnPolicyAuthenticatorAttachment"
options={AUTHENTICATOR_ATTACHMENT}
labelPrefix="authenticatorAttachment"
/>
<WebauthnSelect
name={`${namePrefix}RequireResidentKey`}
label="webAuthnPolicyRequireResidentKey"
options={RESIDENT_KEY_OPTIONS}
labelPrefix="residentKey"
/>
<WebauthnSelect
name={`${namePrefix}UserVerificationRequirement`}
label="webAuthnPolicyUserVerificationRequirement"
options={USER_VERIFY}
labelPrefix="userVerify"
/>
<FormGroup
label={t("webAuthnPolicyCreateTimeout")}
fieldId="webAuthnPolicyCreateTimeout"
helperTextInvalid={t("webAuthnPolicyCreateTimeoutHint")}
validated={errors.webAuthnPolicyCreateTimeout ? "error" : "default"}
labelIcon={
<HelpItem
helpText="authentication-help:webAuthnPolicyCreateTimeout"
fieldLabelId="authentication:webAuthnPolicyCreateTimeout"
/>
}
>
<Controller
name={`${namePrefix}CreateTimeout`}
defaultValue={0}
control={control}
rules={{ min: 0, max: 31536 }}
render={({ onChange, value }) => (
<TimeSelector
data-testid="webAuthnPolicyCreateTimeout"
aria-label={t("webAuthnPolicyCreateTimeout")}
value={value}
onChange={onChange}
units={["seconds", "minutes", "hours"]}
validated={
errors.webAuthnPolicyCreateTimeout ? "error" : "default"
}
/>
)}
/>
</FormGroup>
<FormGroup
label={t("webAuthnPolicyAvoidSameAuthenticatorRegister")}
fieldId="webAuthnPolicyAvoidSameAuthenticatorRegister"
labelIcon={
<HelpItem
helpText="authentication-help:webAuthnPolicyAvoidSameAuthenticatorRegister"
fieldLabelId="authentication:webAuthnPolicyAvoidSameAuthenticatorRegister"
/>
}
>
<Controller
name={`${namePrefix}AvoidSameAuthenticatorRegister`}
defaultValue={false}
control={control}
render={({ onChange, value }) => (
<Switch
id="webAuthnPolicyAvoidSameAuthenticatorRegister"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value}
onChange={onChange}
/>
)}
/>
</FormGroup>
<FormGroup
label={t("webAuthnPolicyAcceptableAaguids")}
fieldId="webAuthnPolicyAcceptableAaguids"
labelIcon={
<HelpItem
helpText="authentication-help:webAuthnPolicyAcceptableAaguids"
fieldLabelId="authentication:webAuthnPolicyAcceptableAaguids"
/>
}
>
<MultiLineInput
name={`${namePrefix}AcceptableAaguids`}
aria-label={t("webAuthnPolicyAcceptableAaguids")}
addButtonLabel="authentication:addAaguids"
/>
</FormGroup>
</FormProvider>
<ActionGroup>
<Button
data-testid="save"
variant="primary"
type="submit"
isDisabled={!isDirty}
>
{t("common:save")}
</Button>
<Button
data-testid="reload"
variant={ButtonVariant.link}
onClick={() => setupForm(realm)}
>
{t("common:reload")}
</Button>
</ActionGroup>
</FormAccess>
</PageSection>
);
};

View file

@ -0,0 +1,11 @@
.keycloak__webauthn_policies__intro {
padding: var(--pf-global--spacer--md) 0 var(--pf-global--spacer--lg);
color: var(--pf-global--primary-color--100);
width: fit-content;
}
@media (min-width: 768px) {
.keycloak__webauthn_policies_authentication__form .pf-c-form__group {
--pf-c-form--m-horizontal__group-label--md--GridColumnWidth: 10rem;
}
}