Add CIBA policy tab to authentication policies (#4300)
This commit is contained in:
parent
5600b5fb1c
commit
b4f9544b4e
5 changed files with 292 additions and 5 deletions
|
@ -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."
|
||||||
}
|
}
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
262
apps/admin-ui/src/authentication/policies/CibaPolicy.tsx
Normal file
262
apps/admin-ui/src/authentication/policies/CibaPolicy.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue