Add CIBA policy tab to authentication policies (#4300)

This commit is contained in:
Jon Koops 2023-02-02 15:17:09 +01:00 committed by GitHub
parent 5600b5fb1c
commit b4f9544b4e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 292 additions and 5 deletions

View file

@ -42,5 +42,9 @@
"digits": "The number of numerical digits required in the password string.", "digits": "The number of numerical digits required in the password string.",
"hashAlgorithm": "Applies a hashing algorithm to passwords, so they are not stored in clear text.", "hashAlgorithm": "Applies a hashing algorithm to passwords, so they are not stored in clear text.",
"maxLength": "The maximum number of characters allowed in the password." "maxLength": "The maximum number of characters allowed in the password."
} },
"cibaBackchannelTokenDeliveryMode": "Specifies how the CD (Consumption Device) gets the authentication result and related tokens. This mode will be used by default for the CIBA clients, which do not have other mode explicitly set.",
"cibaExpiresIn": "The expiration time of the \"auth_req_id\" in seconds since the authentication request was received.",
"cibaInterval": "The minimum amount of time in seconds that the CD (Consumption Device) must wait between polling requests to the token endpoint. If set to 0, the CD must use 5 as the default value according to the CIBA specification.",
"cibaAuthRequestedUserHint": "The way of identifying the end-user for whom authentication is being requested. Currently only \"login_hint\" is supported."
} }

View file

@ -35,6 +35,17 @@
}, },
"updateOtpSuccess": "OTP policy successfully updated", "updateOtpSuccess": "OTP policy successfully updated",
"updateOtpError": "Could not update OTP policy: {{error}}", "updateOtpError": "Could not update OTP policy: {{error}}",
"cibaPolicy": "CIBA Policy",
"cibaBackchannelTokenDeliveryMode": "Backchannel Token Delivery Mode",
"cibaBackhannelTokenDeliveryModes": {
"poll": "Poll",
"ping": "Ping"
},
"cibaExpiresIn": "Expires In",
"cibaInterval": "Interval",
"cibaAuthRequestedUserHint": "Authentication Requested User Hint",
"updateCibaSuccess": "CIBA policy successfully updated",
"updateCibaError": "Could not update CIBA policy: {{error}}",
"webAuthnPolicySignatureAlgorithms": "Signature algorithms", "webAuthnPolicySignatureAlgorithms": "Signature algorithms",
"webAuthnPolicyRpId": "Relying party ID", "webAuthnPolicyRpId": "Relying party ID",
"webAuthnPolicyAttestationConveyancePreference": "Attestation conveyance preference", "webAuthnPolicyAttestationConveyancePreference": "Attestation conveyance preference",

View file

@ -143,6 +143,8 @@
"details": "Details", "details": "Details",
"required": "Required field", "required": "Required field",
"maxLength": "Max length {{length}}", "maxLength": "Max length {{length}}",
"lessThan": "Must be less than {{value}}",
"greaterThan": "Must be greater than {{value}}",
"createRealm": "Create Realm", "createRealm": "Create Realm",
"recent": "Recent", "recent": "Recent",
"jumpToSection": "Jump to section", "jumpToSection": "Jump to section",

View file

@ -0,0 +1,262 @@
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
import {
ActionGroup,
AlertVariant,
Button,
ButtonVariant,
FormGroup,
InputGroup,
InputGroupText,
PageSection,
Select,
SelectOption,
SelectVariant,
} from "@patternfly/react-core";
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useAlerts } from "../../components/alert/Alerts";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
import { useAdminClient } from "../../context/auth/AdminClient";
import { useRealm } from "../../context/realm-context/RealmContext";
import { convertFormValuesToObject, convertToFormValues } from "../../util";
const CIBA_BACKHANNEL_TOKEN_DELIVERY_MODES = ["poll", "ping"] as const;
const CIBA_EXPIRES_IN_MIN = 10;
const CIBA_EXPIRES_IN_MAX = 600;
const CIBA_INTERVAL_MIN = 0;
const CIBA_INTERVAL_MAX = 600;
type CibaPolicyProps = {
realm: RealmRepresentation;
realmUpdated: (realm: RealmRepresentation) => void;
};
type FormFields = Omit<
RealmRepresentation,
"clients" | "components" | "groups"
>;
export const CibaPolicy = ({ realm, realmUpdated }: CibaPolicyProps) => {
const { t } = useTranslation("authentication");
const {
control,
register,
handleSubmit,
setValue,
formState: { errors, isValid, isDirty },
} = useForm<FormFields>({ mode: "onChange" });
const { adminClient } = useAdminClient();
const { realm: realmName } = useRealm();
const { addAlert, addError } = useAlerts();
const [
backchannelTokenDeliveryModeOpen,
setBackchannelTokenDeliveryModeOpen,
] = useState(false);
const [authRequestedUserHintOpen, setAuthRequestedUserHintOpen] =
useState(false);
const setupForm = (realm: RealmRepresentation) =>
convertToFormValues(realm, setValue);
useEffect(() => setupForm(realm), []);
const onSubmit = async (formValues: FormFields) => {
try {
await adminClient.realms.update(
{ realm: realmName },
convertFormValuesToObject(formValues)
);
const updatedRealm = await adminClient.realms.findOne({
realm: realmName,
});
realmUpdated(updatedRealm!);
setupForm(updatedRealm!);
addAlert(t("updateCibaSuccess"), AlertVariant.success);
} catch (error) {
addError("authentication:updateCibaError", error);
}
};
return (
<PageSection variant="light">
<FormAccess
role="manage-realm"
isHorizontal
onSubmit={handleSubmit(onSubmit)}
>
<FormGroup
fieldId="cibaBackchannelTokenDeliveryMode"
label={t("cibaBackchannelTokenDeliveryMode")}
labelIcon={
<HelpItem
helpText="authentication-help:cibaBackchannelTokenDeliveryMode"
fieldLabelId="authentication:cibaBackchannelTokenDeliveryMode"
/>
}
>
<Controller
name="attributes.cibaBackchannelTokenDeliveryMode"
defaultValue={CIBA_BACKHANNEL_TOKEN_DELIVERY_MODES[0]}
control={control}
render={({ field }) => (
<Select
toggleId="cibaBackchannelTokenDeliveryMode"
onSelect={(_, value) => {
setBackchannelTokenDeliveryModeOpen(false);
field.onChange(value.toString());
}}
selections={field.value}
variant={SelectVariant.single}
isOpen={backchannelTokenDeliveryModeOpen}
onToggle={(isExpanded) =>
setBackchannelTokenDeliveryModeOpen(isExpanded)
}
>
{CIBA_BACKHANNEL_TOKEN_DELIVERY_MODES.map((value) => (
<SelectOption
key={value}
value={value}
selected={value === field.value}
>
{t(`cibaBackhannelTokenDeliveryModes.${value}`)}
</SelectOption>
))}
</Select>
)}
/>
</FormGroup>
<FormGroup
fieldId="cibaExpiresIn"
label={t("cibaExpiresIn")}
labelIcon={
<HelpItem
helpText="authentication-help:cibaExpiresIn"
fieldLabelId="authentication:cibaExpiresIn"
/>
}
validated={errors.attributes?.cibaExpiresIn ? "error" : "default"}
helperTextInvalid={errors.attributes?.cibaExpiresIn?.message}
isRequired
>
<InputGroup>
<KeycloakTextInput
id="cibaExpiresIn"
type="number"
min={CIBA_EXPIRES_IN_MIN}
max={CIBA_EXPIRES_IN_MAX}
{...register("attributes.cibaExpiresIn", {
min: {
value: CIBA_EXPIRES_IN_MIN,
message: t("common:greaterThan", {
value: CIBA_EXPIRES_IN_MIN,
}),
},
max: {
value: CIBA_EXPIRES_IN_MAX,
message: t("common:lessThan", { value: CIBA_EXPIRES_IN_MAX }),
},
required: {
value: true,
message: t("common:required"),
},
})}
validated={errors.attributes?.cibaExpiresIn ? "error" : "default"}
/>
<InputGroupText variant="plain">
{t("common:times:seconds")}
</InputGroupText>
</InputGroup>
</FormGroup>
<FormGroup
fieldId="cibaInterval"
label={t("cibaInterval")}
labelIcon={
<HelpItem
helpText="authentication-help:cibaInterval"
fieldLabelId="authentication:cibaInterval"
/>
}
validated={errors.attributes?.cibaInterval ? "error" : "default"}
helperTextInvalid={errors.attributes?.cibaInterval?.message}
isRequired
>
<InputGroup>
<KeycloakTextInput
id="cibaInterval"
type="number"
min={CIBA_INTERVAL_MIN}
max={CIBA_INTERVAL_MAX}
{...register("attributes.cibaInterval", {
min: {
value: CIBA_INTERVAL_MIN,
message: t("common:greaterThan", {
value: CIBA_INTERVAL_MIN,
}),
},
max: {
value: CIBA_INTERVAL_MAX,
message: t("common:lessThan", { value: CIBA_INTERVAL_MAX }),
},
required: {
value: true,
message: t("common:required"),
},
})}
validated={errors.attributes?.cibaInterval ? "error" : "default"}
/>
<InputGroupText variant="plain">
{t("common:times:seconds")}
</InputGroupText>
</InputGroup>
</FormGroup>
<FormGroup
fieldId="cibaAuthRequestedUserHint"
label={t("cibaAuthRequestedUserHint")}
labelIcon={
<HelpItem
helpText="authentication-help:cibaAuthRequestedUserHint"
fieldLabelId="authentication:cibaAuthRequestedUserHint"
/>
}
>
<Select
toggleId="cibaAuthRequestedUserHint"
selections="login_hint"
isOpen={authRequestedUserHintOpen}
onToggle={(isExpanded) => setAuthRequestedUserHintOpen(isExpanded)}
isDisabled
>
<SelectOption value="login_hint">login_hint</SelectOption>
<SelectOption value="id_token_hint">id_token_hint</SelectOption>
<SelectOption value="login_hint_token">
login_hint_token
</SelectOption>
</Select>
</FormGroup>
<ActionGroup>
<Button
data-testid="save"
variant="primary"
type="submit"
isDisabled={!isValid || !isDirty}
>
{t("common:save")}
</Button>
<Button
data-testid="reload"
variant={ButtonVariant.link}
onClick={() => setupForm({ ...realm })}
>
{t("common:reload")}
</Button>
</ActionGroup>
</FormAccess>
</PageSection>
);
};

View file

@ -1,14 +1,15 @@
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
import { Tab, Tabs, TabTitleText } from "@patternfly/react-core";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; 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 { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient"; import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import { useRealm } from "../../context/realm-context/RealmContext"; import { useRealm } from "../../context/realm-context/RealmContext";
import { PasswordPolicy } from "./PasswordPolicy"; import { CibaPolicy } from "./CibaPolicy";
import { OtpPolicy } from "./OtpPolicy"; import { OtpPolicy } from "./OtpPolicy";
import { PasswordPolicy } from "./PasswordPolicy";
import { WebauthnPolicy } from "./WebauthnPolicy"; import { WebauthnPolicy } from "./WebauthnPolicy";
import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner";
export const Policies = () => { export const Policies = () => {
const { t } = useTranslation("authentication"); const { t } = useTranslation("authentication");
@ -70,6 +71,13 @@ export const Policies = () => {
> >
<WebauthnPolicy realm={realm} realmUpdated={setRealm} isPasswordLess /> <WebauthnPolicy realm={realm} realmUpdated={setRealm} isPasswordLess />
</Tab> </Tab>
<Tab
id="cibaPolicy"
eventKey={5}
title={<TabTitleText>{t("cibaPolicy")}</TabTitleText>}
>
<CibaPolicy realm={realm} realmUpdated={setRealm} />
</Tab>
</Tabs> </Tabs>
); );
}; };