added otp policies screen (#1665)
This commit is contained in:
parent
0516c60839
commit
d4c50f6218
7 changed files with 438 additions and 1 deletions
|
@ -8,6 +8,7 @@ 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();
|
||||
|
@ -183,4 +184,21 @@ 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
export default class OTPPolicies {
|
||||
goToTab() {
|
||||
cy.get("#pf-tab-policies-policies")
|
||||
.click()
|
||||
.get("#pf-tab-2-otpPolicy")
|
||||
.click();
|
||||
return this;
|
||||
}
|
||||
|
||||
setPolicyType(type: string) {
|
||||
cy.findByTestId(type).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
increaseInitialCounter() {
|
||||
cy.get('#initialCounter > .pf-c-input-group > [aria-label="Plus"]').click();
|
||||
return this;
|
||||
}
|
||||
|
||||
checkSupportedActions(supportedActions: string) {
|
||||
cy.findByTestId("supportedActions").should("have.value", supportedActions);
|
||||
return this;
|
||||
}
|
||||
|
||||
save() {
|
||||
cy.findByTestId("save").click();
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -11,5 +11,16 @@ export default {
|
|||
addSubFlow:
|
||||
"Sub-Flows can be either generic or form. The form type is used to construct a sub-flow that generates a single flow for the user. Sub-flows are a special type of execution that evaluate as successful depending on how the executions they contain evaluate.",
|
||||
alias: "Name of the configuration",
|
||||
otpType:
|
||||
"totp is Time-Based One Time Password. 'hotp' is a counter base one time password in which the server keeps a counter to hash against.",
|
||||
otpHashAlgorithm:
|
||||
"What hashing algorithm should be used to generate the OTP.",
|
||||
otpPolicyDigits: "How many digits should the OTP have?",
|
||||
lookAhead:
|
||||
"How far ahead should the server look just in case the token generator and server are out of time sync or counter sync?",
|
||||
otpPolicyPeriod:
|
||||
"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",
|
||||
},
|
||||
};
|
||||
|
|
|
@ -15,6 +15,22 @@ export default {
|
|||
updatePasswordPolicyError:
|
||||
"Could not update the password policies: '{{error}}'",
|
||||
addPolicy: "Add policy",
|
||||
otpType: "OTP type",
|
||||
policyType: {
|
||||
totp: "Time based",
|
||||
hotp: "Counter based",
|
||||
},
|
||||
otpHashAlgorithm: "OTP hash algorithm",
|
||||
otpPolicyDigits: "Number of digits",
|
||||
lookAhead: "Look ahead window",
|
||||
otpPolicyPeriod: "OTP Token period",
|
||||
otpPolicyPeriodErrorHint:
|
||||
"Value needs to be between 1 second and 2 minutes",
|
||||
initialCounter: "Initial counter",
|
||||
initialCounterErrorHint: "Value needs to be between 1 and 120",
|
||||
supportedActions: "Supported actions",
|
||||
updateOtpSuccess: "OTP policy successfully updated",
|
||||
updateOtpError: "Could not update OTP policy: {{error}}",
|
||||
flowName: "Flow name",
|
||||
searchForFlow: "Search for flow",
|
||||
usedBy: "Used by",
|
||||
|
|
345
src/authentication/policies/OtpPolicy.tsx
Normal file
345
src/authentication/policies/OtpPolicy.tsx
Normal file
|
@ -0,0 +1,345 @@
|
|||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
import {
|
||||
PageSection,
|
||||
FormGroup,
|
||||
Radio,
|
||||
Select,
|
||||
SelectVariant,
|
||||
SelectOption,
|
||||
NumberInput,
|
||||
ActionGroup,
|
||||
Button,
|
||||
TextInput,
|
||||
ButtonVariant,
|
||||
AlertVariant,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
|
||||
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 { useRealm } from "../../context/realm-context/RealmContext";
|
||||
import { useAlerts } from "../../components/alert/Alerts";
|
||||
|
||||
import "./otp-policy.css";
|
||||
|
||||
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 = () => {
|
||||
const { t } = useTranslation("authentication");
|
||||
const {
|
||||
control,
|
||||
errors,
|
||||
reset,
|
||||
handleSubmit,
|
||||
formState: { isDirty },
|
||||
} = useForm({ mode: "onChange" });
|
||||
const adminClient = useAdminClient();
|
||||
const { realm: realmName } = useRealm();
|
||||
const { addAlert, addError } = useAlerts();
|
||||
|
||||
const [open, toggle] = useToggle();
|
||||
const [realm, setRealm] = useState<RealmRepresentation>();
|
||||
|
||||
const otpType = useWatch<typeof POLICY_TYPES[number]>({
|
||||
name: "otpPolicyType",
|
||||
control,
|
||||
defaultValue: POLICY_TYPES[0],
|
||||
});
|
||||
|
||||
useFetch(
|
||||
() => adminClient.realms.findOne({ realm: realmName }),
|
||||
(realm) => {
|
||||
setRealm(realm);
|
||||
reset({ ...realm });
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const save = async (realm: RealmRepresentation) => {
|
||||
try {
|
||||
await adminClient.realms.update({ realm: realmName }, realm);
|
||||
const updatedRealm = await adminClient.realms.findOne({
|
||||
realm: realmName,
|
||||
});
|
||||
setRealm(updatedRealm);
|
||||
reset({ ...updatedRealm });
|
||||
addAlert(t("updateOtpSuccess"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addError("authentication:updateOtpError", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageSection variant="light">
|
||||
<FormAccess
|
||||
role="manage-realm"
|
||||
isHorizontal
|
||||
onSubmit={handleSubmit(save)}
|
||||
className="keycloak__otp_policies_authentication__form"
|
||||
>
|
||||
<FormGroup
|
||||
label={t("otpType")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="authentication-help:otpType"
|
||||
forLabel={t("otpType")}
|
||||
forID={t(`common:helpLabel`, { label: t("otpType") })}
|
||||
/>
|
||||
}
|
||||
fieldId="otpType"
|
||||
hasNoPaddingTop
|
||||
>
|
||||
<Controller
|
||||
name="otpPolicyType"
|
||||
data-testid="otpPolicyType"
|
||||
defaultValue={POLICY_TYPES[0]}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<>
|
||||
{POLICY_TYPES.map((type) => (
|
||||
<Radio
|
||||
id={type}
|
||||
key={type}
|
||||
data-testid={type}
|
||||
isChecked={value === type}
|
||||
name="otpPolicyType"
|
||||
onChange={() => onChange(type)}
|
||||
label={t(`policyType.${type}`)}
|
||||
className="keycloak__otp_policies_authentication__policy-type"
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("otpHashAlgorithm")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="authentication-help:otpHashAlgorithm"
|
||||
forLabel={t("otpHashAlgorithm")}
|
||||
forID={t(`common:helpLabel`, { label: t("otpHashAlgorithm") })}
|
||||
/>
|
||||
}
|
||||
fieldId="otpHashAlgorithm"
|
||||
>
|
||||
<Controller
|
||||
name="otpPolicyAlgorithm"
|
||||
defaultValue={`Hmac${OTP_HASH_ALGORITHMS[0]}`}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId="otpHashAlgorithm"
|
||||
onToggle={toggle}
|
||||
onSelect={(_, value) => {
|
||||
onChange(value.toString());
|
||||
toggle();
|
||||
}}
|
||||
selections={value}
|
||||
variant={SelectVariant.single}
|
||||
aria-label={t("otpHashAlgorithm")}
|
||||
isOpen={open}
|
||||
>
|
||||
{OTP_HASH_ALGORITHMS.map((type) => (
|
||||
<SelectOption
|
||||
selected={`Hmac${type}` === value}
|
||||
key={type}
|
||||
value={`Hmac${type}`}
|
||||
>
|
||||
{type}
|
||||
</SelectOption>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("otpPolicyDigits")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="authentication-help:otpPolicyDigits"
|
||||
forLabel={t("otpPolicyDigits")}
|
||||
forID={t(`common:helpLabel`, { label: t("otpPolicyDigits") })}
|
||||
/>
|
||||
}
|
||||
fieldId="otpPolicyDigits"
|
||||
hasNoPaddingTop
|
||||
>
|
||||
<Controller
|
||||
name="otpPolicyDigits"
|
||||
data-testid="otpPolicyDigits"
|
||||
defaultValue={NUMBER_OF_DIGITS[0]}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<>
|
||||
{NUMBER_OF_DIGITS.map((type) => (
|
||||
<Radio
|
||||
id={`digit-${type}`}
|
||||
key={type}
|
||||
data-testid={`digit-${type}`}
|
||||
isChecked={value === type}
|
||||
name="otpPolicyDigits"
|
||||
onChange={() => onChange(type)}
|
||||
label={type}
|
||||
className="keycloak__otp_policies_authentication__number-of-digits"
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("lookAhead")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="authentication-help:lookAhead"
|
||||
forLabel={t("lookAhead")}
|
||||
forID={t(`common:helpLabel`, { label: t("lookAhead") })}
|
||||
/>
|
||||
}
|
||||
fieldId="lookAhead"
|
||||
>
|
||||
<Controller
|
||||
name="otpPolicyLookAheadWindow"
|
||||
defaultValue={1}
|
||||
control={control}
|
||||
render={({ onChange, value }) => {
|
||||
const MIN_VALUE = 0;
|
||||
const setValue = (newValue: number) =>
|
||||
onChange(Math.max(newValue, MIN_VALUE));
|
||||
|
||||
return (
|
||||
<NumberInput
|
||||
id="lookAhead"
|
||||
value={value}
|
||||
min={MIN_VALUE}
|
||||
onPlus={() => setValue(value + 1)}
|
||||
onMinus={() => setValue(value - 1)}
|
||||
onChange={(event) => {
|
||||
const newValue = Number(event.currentTarget.value);
|
||||
setValue(!isNaN(newValue) ? newValue : 0);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
{otpType === POLICY_TYPES[0] && (
|
||||
<FormGroup
|
||||
label={t("otpPolicyPeriod")}
|
||||
fieldId="otpPolicyPeriod"
|
||||
helperTextInvalid={t("otpPolicyPeriodErrorHint")}
|
||||
validated={errors.otpPolicyPeriod ? "error" : "default"}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="authentication-help:otpPolicyPeriod"
|
||||
forLabel={t("otpPolicyPeriod")}
|
||||
forID={t(`common:helpLabel`, { label: t("otpPolicyPeriod") })}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="otpPolicyPeriod"
|
||||
defaultValue={30}
|
||||
control={control}
|
||||
rules={{ min: 1, max: 120 }}
|
||||
render={({ onChange, value }) => (
|
||||
<TimeSelector
|
||||
data-testid="otpPolicyPeriod"
|
||||
aria-label={t("otpPolicyPeriod")}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
units={["seconds", "minutes"]}
|
||||
validated={errors.otpPolicyPeriod ? "error" : "default"}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
{otpType === POLICY_TYPES[1] && (
|
||||
<FormGroup
|
||||
label={t("initialCounter")}
|
||||
fieldId="initialCounter"
|
||||
helperTextInvalid={t("initialCounterErrorHint")}
|
||||
validated={errors.otpPolicyInitialCounter ? "error" : "default"}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="authentication-help:initialCounter"
|
||||
forLabel={t("initialCounter")}
|
||||
forID={t(`common:helpLabel`, { label: t("initialCounter") })}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="otpPolicyInitialCounter"
|
||||
defaultValue={30}
|
||||
control={control}
|
||||
rules={{ min: 1, max: 120 }}
|
||||
render={({ onChange, value }) => {
|
||||
const MIN_VALUE = 1;
|
||||
const setValue = (newValue: number) =>
|
||||
onChange(Math.max(newValue, MIN_VALUE));
|
||||
|
||||
return (
|
||||
<NumberInput
|
||||
id="initialCounter"
|
||||
value={value}
|
||||
min={MIN_VALUE}
|
||||
onPlus={() => setValue(value + 1)}
|
||||
onMinus={() => setValue(value - 1)}
|
||||
onChange={(event) => {
|
||||
const newValue = Number(event.currentTarget.value);
|
||||
setValue(!isNaN(newValue) ? newValue : 30);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
<FormGroup
|
||||
label={t("supportedActions")}
|
||||
fieldId="supportedActions"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="authentication-help:supportedActions"
|
||||
forLabel={t("supportedActions")}
|
||||
forID={t(`common:helpLabel`, { label: t("supportedActions") })}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TextInput
|
||||
id="supportedActions"
|
||||
data-testid="supportedActions"
|
||||
isReadOnly
|
||||
value={realm?.otpSupportedApplications?.join(", ")}
|
||||
/>
|
||||
</FormGroup>
|
||||
<ActionGroup>
|
||||
<Button
|
||||
data-testid="save"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
isDisabled={!isDirty}
|
||||
>
|
||||
{t("common:save")}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="reload"
|
||||
variant={ButtonVariant.link}
|
||||
onClick={() => reset({ ...realm })}
|
||||
>
|
||||
{t("common:reload")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</FormAccess>
|
||||
</PageSection>
|
||||
);
|
||||
};
|
|
@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
|
|||
import { Tab, Tabs, TabTitleText } from "@patternfly/react-core";
|
||||
|
||||
import { PasswordPolicy } from "./PasswordPolicy";
|
||||
import { OtpPolicy } from "./OtpPolicy";
|
||||
|
||||
export const Policies = () => {
|
||||
const { t } = useTranslation("authentication");
|
||||
|
@ -24,7 +25,9 @@ export const Policies = () => {
|
|||
id="otpPolicy"
|
||||
eventKey={2}
|
||||
title={<TabTitleText>{t("otpPolicy")}</TabTitleText>}
|
||||
></Tab>
|
||||
>
|
||||
<OtpPolicy />
|
||||
</Tab>
|
||||
<Tab
|
||||
id="webauthnPolicy"
|
||||
eventKey={3}
|
||||
|
|
15
src/authentication/policies/otp-policy.css
Normal file
15
src/authentication/policies/otp-policy.css
Normal file
|
@ -0,0 +1,15 @@
|
|||
.keycloak__otp_policies_authentication__policy-type {
|
||||
display: inline-grid;
|
||||
padding-right: var(--pf-global--spacer--lg);
|
||||
}
|
||||
|
||||
.keycloak__otp_policies_authentication__number-of-digits {
|
||||
display: inline-grid;
|
||||
padding-right: var(--pf-global--spacer--lg);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.keycloak__otp_policies_authentication__form .pf-c-form__group {
|
||||
--pf-c-form--m-horizontal__group-label--md--GridColumnWidth: 10rem;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue