diff --git a/apps/admin-ui/public/resources/en/authentication-help.json b/apps/admin-ui/public/resources/en/authentication-help.json index 3215a283d2..d1c739d143 100644 --- a/apps/admin-ui/public/resources/en/authentication-help.json +++ b/apps/admin-ui/public/resources/en/authentication-help.json @@ -42,5 +42,9 @@ "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.", "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." } \ No newline at end of file diff --git a/apps/admin-ui/public/resources/en/authentication.json b/apps/admin-ui/public/resources/en/authentication.json index e393d594eb..465de2c451 100644 --- a/apps/admin-ui/public/resources/en/authentication.json +++ b/apps/admin-ui/public/resources/en/authentication.json @@ -35,6 +35,17 @@ }, "updateOtpSuccess": "OTP policy successfully updated", "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", "webAuthnPolicyRpId": "Relying party ID", "webAuthnPolicyAttestationConveyancePreference": "Attestation conveyance preference", diff --git a/apps/admin-ui/public/resources/en/common.json b/apps/admin-ui/public/resources/en/common.json index 4d3ff1cf3e..c03abeb20a 100644 --- a/apps/admin-ui/public/resources/en/common.json +++ b/apps/admin-ui/public/resources/en/common.json @@ -143,6 +143,8 @@ "details": "Details", "required": "Required field", "maxLength": "Max length {{length}}", + "lessThan": "Must be less than {{value}}", + "greaterThan": "Must be greater than {{value}}", "createRealm": "Create Realm", "recent": "Recent", "jumpToSection": "Jump to section", diff --git a/apps/admin-ui/src/authentication/policies/CibaPolicy.tsx b/apps/admin-ui/src/authentication/policies/CibaPolicy.tsx new file mode 100644 index 0000000000..f7e857b123 --- /dev/null +++ b/apps/admin-ui/src/authentication/policies/CibaPolicy.tsx @@ -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({ 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 ( + + + + } + > + ( + + )} + /> + + + } + validated={errors.attributes?.cibaExpiresIn ? "error" : "default"} + helperTextInvalid={errors.attributes?.cibaExpiresIn?.message} + isRequired + > + + + + {t("common:times:seconds")} + + + + + } + validated={errors.attributes?.cibaInterval ? "error" : "default"} + helperTextInvalid={errors.attributes?.cibaInterval?.message} + isRequired + > + + + + {t("common:times:seconds")} + + + + + } + > + + + + + + + + + ); +}; diff --git a/apps/admin-ui/src/authentication/policies/Policies.tsx b/apps/admin-ui/src/authentication/policies/Policies.tsx index 0693d42437..1d8e5f9d36 100644 --- a/apps/admin-ui/src/authentication/policies/Policies.tsx +++ b/apps/admin-ui/src/authentication/policies/Policies.tsx @@ -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 { 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 { useRealm } from "../../context/realm-context/RealmContext"; -import { PasswordPolicy } from "./PasswordPolicy"; +import { CibaPolicy } from "./CibaPolicy"; import { OtpPolicy } from "./OtpPolicy"; +import { PasswordPolicy } from "./PasswordPolicy"; import { WebauthnPolicy } from "./WebauthnPolicy"; -import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner"; export const Policies = () => { const { t } = useTranslation("authentication"); @@ -70,6 +71,13 @@ export const Policies = () => { > + {t("cibaPolicy")}} + > + + ); };