From ffbca69a5e96cb5262dc2fe31bef80bff3d758db Mon Sep 17 00:00:00 2001 From: James Tarran Date: Tue, 14 Jul 2026 09:36:45 +0100 Subject: [PATCH] feat(auth-methods): enhance registration campaign configuration Expand the registration campaign feature with support for: - Selecting target authentication method (Microsoft Authenticator or Passkey/FIDO2) - Enforcing registration after limited snoozes - Include/exclude targeting by groups and users Adds a new dedicated Registration Campaign page with tabbed navigation alongside the existing Policies page. Updates the NudgeMFA standard schema to support the new options. --- src/data/standards.json | 56 +++- .../authentication-methods/index.js | 10 +- .../registration-campaign.js | 252 ++++++++++++++++++ .../authentication-methods/tabOptions.json | 12 + 4 files changed, 325 insertions(+), 5 deletions(-) create mode 100644 src/pages/tenant/administration/authentication-methods/registration-campaign.js create mode 100644 src/pages/tenant/administration/authentication-methods/tabOptions.json diff --git a/src/data/standards.json b/src/data/standards.json index 2eee70c7420b..6fd4e3d3b64e 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -1371,21 +1371,38 @@ "cat": "Entra (AAD) Standards", "tag": ["SMB1001 (2.5)"], "appliesToTest": ["SMB1001_2_5", "ZTNA21889"], - "helpText": "Sets the state of the registration campaign for the tenant", - "docsDescription": "Sets the state of the registration campaign for the tenant. If enabled nudges users to set up the Microsoft Authenticator during sign-in.", + "helpText": "Sets the state of the registration campaign for the tenant, including the targeted authentication method, snooze settings and include/exclude groups. Leave include/exclude blank to keep the groups currently configured in the tenant, or use 'AllUsers' to target all users.", + "docsDescription": "Sets the state of the registration campaign for the tenant. If enabled nudges users to set up the targeted authentication method (Microsoft Authenticator or a Passkey) during sign-in. Supports limiting the number of snoozes, and including or excluding specific groups (by display name).", "executiveText": "Prompts employees to set up multi-factor authentication during login, gradually improving the organization's security posture by encouraging adoption of stronger authentication methods. This helps achieve better security compliance without forcing immediate mandatory changes.", "addedComponent": [ { "type": "autoComplete", "multiple": false, "creatable": false, - "label": "Select value", + "label": "Registration campaign state", "name": "standards.NudgeMFA.state", "options": [ { "label": "Enabled", "value": "enabled" }, { "label": "Disabled", "value": "disabled" } ] }, + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "required": false, + "label": "Authentication method to nudge users to register (default is Microsoft Authenticator)", + "name": "standards.NudgeMFA.targetedAuthenticationMethod", + "options": [ + { "label": "Microsoft Authenticator", "value": "microsoftAuthenticator" }, + { "label": "Passkey (FIDO2)", "value": "fido2" } + ], + "condition": { + "field": "standards.NudgeMFA.state", + "compareType": "valueEq", + "compareValue": "enabled" + } + }, { "type": "number", "name": "standards.NudgeMFA.snoozeDurationInDays", @@ -1395,6 +1412,39 @@ "min": { "value": 0, "message": "Minimum value is 0" }, "max": { "value": 14, "message": "Maximum value is 14" } } + }, + { + "type": "switch", + "name": "standards.NudgeMFA.enforceRegistrationAfterAllowedSnoozes", + "label": "Limited number of snoozes (require registration after 3 snoozes)", + "defaultValue": true, + "condition": { + "field": "standards.NudgeMFA.state", + "compareType": "valueEq", + "compareValue": "enabled" + } + }, + { + "type": "textField", + "name": "standards.NudgeMFA.includeTargets", + "label": "Include groups (comma separated group names, 'AllUsers' for everyone, blank = keep current targets)", + "required": false, + "condition": { + "field": "standards.NudgeMFA.state", + "compareType": "valueEq", + "compareValue": "enabled" + } + }, + { + "type": "textField", + "name": "standards.NudgeMFA.excludeTargets", + "label": "Exclude groups (comma separated group names, blank = keep current exclusions)", + "required": false, + "condition": { + "field": "standards.NudgeMFA.state", + "compareType": "valueEq", + "compareValue": "enabled" + } } ], "label": "Sets the state for the request to setup Authenticator", diff --git a/src/pages/tenant/administration/authentication-methods/index.js b/src/pages/tenant/administration/authentication-methods/index.js index f865d6a0172b..2961fb8e4467 100644 --- a/src/pages/tenant/administration/authentication-methods/index.js +++ b/src/pages/tenant/administration/authentication-methods/index.js @@ -1,4 +1,6 @@ import { Layout as DashboardLayout } from "../../../../layouts/index.js"; +import { TabbedLayout } from "../../../../layouts/TabbedLayout"; +import tabOptions from "./tabOptions.json"; import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; import CippFormComponent from "../../../../components/CippComponents/CippFormComponent.jsx"; import { Box } from "@mui/material"; @@ -7,7 +9,7 @@ import { UserGroupIcon } from "@heroicons/react/24/outline"; import { useSettings } from "../../../../hooks/use-settings.js"; const Page = () => { - const pageTitle = "Auth Methods"; + const pageTitle = "Policies"; const tenant = useSettings().currentTenant; const apiUrl = "/api/ListGraphRequest"; @@ -357,6 +359,10 @@ const Page = () => { }; // Adding the layout for the dashboard -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => ( + + {page} + +); export default Page; diff --git a/src/pages/tenant/administration/authentication-methods/registration-campaign.js b/src/pages/tenant/administration/authentication-methods/registration-campaign.js new file mode 100644 index 000000000000..2658cba5dbb3 --- /dev/null +++ b/src/pages/tenant/administration/authentication-methods/registration-campaign.js @@ -0,0 +1,252 @@ +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { Alert, Typography } from "@mui/material"; +import { Grid } from "@mui/system"; +import { Layout as DashboardLayout } from "../../../../layouts/index.js"; +import { TabbedLayout } from "../../../../layouts/TabbedLayout"; +import tabOptions from "./tabOptions.json"; +import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; +import CippFormComponent from "../../../../components/CippComponents/CippFormComponent"; +import { ApiGetCall } from "../../../../api/ApiCall"; +import { useSettings } from "../../../../hooks/use-settings.js"; + +const stateOptions = [ + { label: "Microsoft managed", value: "default" }, + { label: "Enabled", value: "enabled" }, + { label: "Disabled", value: "disabled" }, +]; + +const methodOptions = [ + { label: "Microsoft Authenticator", value: "microsoftAuthenticator" }, + { label: "Passkey (FIDO2)", value: "fido2" }, +]; + +// Map campaign targets of one type to autocomplete options (all_users is handled by its own switch) +const targetsToOptions = (targets, targetType) => + (Array.isArray(targets) ? targets : []) + .filter((target) => target?.targetType === targetType && target?.id !== "all_users") + .map((target) => ({ label: target.id, value: target.id })); + +const toIdArray = (value) => + Array.isArray(value) ? value.map((item) => item.value).filter(Boolean) : []; + +const Page = () => { + const tenant = useSettings().currentTenant; + const queryKey = `RegistrationCampaign-${tenant}`; + + const formControl = useForm({ + mode: "onChange", + }); + + const campaignRequest = ApiGetCall({ + url: "/api/ListGraphRequest", + data: { + Endpoint: "authenticationMethodsPolicy", + tenantFilter: tenant, + }, + queryKey: queryKey, + }); + + const campaign = + campaignRequest.data?.Results?.[0]?.registrationEnforcement + ?.authenticationMethodsRegistrationCampaign; + + useEffect(() => { + if (campaignRequest.isSuccess && campaign) { + formControl.reset({ + state: stateOptions.find((option) => option.value === campaign.state) ?? stateOptions[0], + targetedAuthenticationMethod: + methodOptions.find( + (option) => option.value === campaign.includeTargets?.[0]?.targetedAuthenticationMethod, + ) ?? methodOptions[0], + snoozeDurationInDays: campaign.snoozeDurationInDays, + enforceRegistrationAfterAllowedSnoozes: !!campaign.enforceRegistrationAfterAllowedSnoozes, + includeAllUsers: (Array.isArray(campaign.includeTargets) + ? campaign.includeTargets + : [] + ).some((target) => target?.id === "all_users"), + includeGroups: targetsToOptions(campaign.includeTargets, "group"), + includeUsers: targetsToOptions(campaign.includeTargets, "user"), + excludeGroups: targetsToOptions(campaign.excludeTargets, "group"), + excludeUsers: targetsToOptions(campaign.excludeTargets, "user"), + }); + } + }, [campaignRequest.isSuccess, campaign]); + + const groupFieldApi = { + url: "/api/ListGraphRequest", + dataKey: "Results", + queryKey: `RegistrationCampaignGroups-${tenant}`, + labelField: (group) => (group.id ? `${group.displayName} (${group.id})` : group.displayName), + valueField: "id", + data: { + Endpoint: "groups", + manualPagination: true, + $select: "id,displayName", + $orderby: "displayName", + $top: 999, + $count: true, + }, + }; + + const userFieldApi = { + url: "/api/ListGraphRequest", + dataKey: "Results", + queryKey: `RegistrationCampaignUsers-${tenant}`, + labelField: (user) => `${user.displayName} (${user.userPrincipalName})`, + valueField: "id", + data: { + Endpoint: "users", + manualPagination: true, + $select: "id,displayName,userPrincipalName", + $orderby: "displayName", + $top: 999, + $count: true, + }, + }; + + return ( + ({ + tenantFilter: tenant, + state: values?.state?.value ?? values?.state, + targetedAuthenticationMethod: + values?.targetedAuthenticationMethod?.value ?? values?.targetedAuthenticationMethod, + snoozeDurationInDays: + values?.snoozeDurationInDays === "" || values?.snoozeDurationInDays === undefined + ? undefined + : Number(values?.snoozeDurationInDays), + enforceRegistrationAfterAllowedSnoozes: !!values?.enforceRegistrationAfterAllowedSnoozes, + includeAllUsers: !!values?.includeAllUsers, + includeGroups: toIdArray(values?.includeGroups), + includeUsers: toIdArray(values?.includeUsers), + excludeGroups: toIdArray(values?.excludeGroups), + excludeUsers: toIdArray(values?.excludeUsers), + })} + > + + + + Nudge users to set up Microsoft Authenticator or a passkey during sign-in. Users are + prompted after completing MFA and can snooze the prompt for the configured number of + days. + + + {campaignRequest.isError && ( + + + Failed to load the current registration campaign settings for this tenant. + + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; diff --git a/src/pages/tenant/administration/authentication-methods/tabOptions.json b/src/pages/tenant/administration/authentication-methods/tabOptions.json new file mode 100644 index 000000000000..7133119254a2 --- /dev/null +++ b/src/pages/tenant/administration/authentication-methods/tabOptions.json @@ -0,0 +1,12 @@ +[ + { + "label": "Policies", + "path": "/tenant/administration/authentication-methods", + "icon": "Key" + }, + { + "label": "Registration Campaign", + "path": "/tenant/administration/authentication-methods/registration-campaign", + "icon": "Notifications" + } +]