Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4a0f566
Merge pull request #137 from KelvinTegelaar/dev
pull[bot] Jun 30, 2026
9923cab
Merge pull request #138 from KelvinTegelaar/dev
pull[bot] Jun 30, 2026
72a9176
Merge pull request #139 from KelvinTegelaar/dev
pull[bot] Jun 30, 2026
d5bc826
Merge pull request #140 from KelvinTegelaar/dev
pull[bot] Jul 1, 2026
cc36c87
Merge pull request #141 from KelvinTegelaar/dev
pull[bot] Jul 1, 2026
d5ef5ea
Merge pull request #142 from KelvinTegelaar/dev
pull[bot] Jul 1, 2026
1ba5ffb
Merge pull request #143 from KelvinTegelaar/dev
pull[bot] Jul 2, 2026
4a47d55
Merge pull request #144 from KelvinTegelaar/dev
pull[bot] Jul 2, 2026
32bf765
Merge pull request #145 from KelvinTegelaar/dev
pull[bot] Jul 2, 2026
3a991a4
Merge pull request #146 from KelvinTegelaar/dev
pull[bot] Jul 2, 2026
f18a736
Merge pull request #147 from KelvinTegelaar/dev
pull[bot] Jul 3, 2026
ed0b34e
Merge pull request #148 from KelvinTegelaar/dev
pull[bot] Jul 6, 2026
5d63a60
Merge pull request #149 from KelvinTegelaar/dev
pull[bot] Jul 6, 2026
260d394
Merge pull request #150 from KelvinTegelaar/dev
pull[bot] Jul 7, 2026
225d6cd
Merge pull request #151 from KelvinTegelaar/dev
pull[bot] Jul 7, 2026
141dee6
Merge pull request #152 from KelvinTegelaar/dev
pull[bot] Jul 7, 2026
e7fbaeb
Merge pull request #153 from KelvinTegelaar/dev
pull[bot] Jul 8, 2026
75089a8
Merge pull request #154 from KelvinTegelaar/dev
pull[bot] Jul 9, 2026
b6e6e6b
Merge pull request #155 from KelvinTegelaar/dev
pull[bot] Jul 9, 2026
b365b4e
Merge pull request #156 from KelvinTegelaar/dev
pull[bot] Jul 9, 2026
f756eba
Merge pull request #157 from KelvinTegelaar/dev
pull[bot] Jul 10, 2026
a073395
Merge pull request #158 from KelvinTegelaar/dev
pull[bot] Jul 10, 2026
1dce247
Merge pull request #160 from KelvinTegelaar/dev
pull[bot] Jul 10, 2026
da668a5
Merge pull request #161 from KelvinTegelaar/dev
pull[bot] Jul 10, 2026
54ae975
Merge pull request #162 from KelvinTegelaar/dev
pull[bot] Jul 13, 2026
9820048
Merge pull request #163 from KelvinTegelaar/dev
pull[bot] Jul 13, 2026
e81ae1d
Merge pull request #165 from KelvinTegelaar/dev
pull[bot] Jul 13, 2026
ffbca69
feat(auth-methods): enhance registration campaign configuration
TecharyJames Jul 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 53 additions & 3 deletions src/data/standards.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
10 changes: 8 additions & 2 deletions src/pages/tenant/administration/authentication-methods/index.js
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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";

Expand Down Expand Up @@ -357,6 +359,10 @@ const Page = () => {
};

// Adding the layout for the dashboard
Page.getLayout = (page) => <DashboardLayout>{page}</DashboardLayout>;
Page.getLayout = (page) => (
<DashboardLayout>
<TabbedLayout tabOptions={tabOptions}>{page}</TabbedLayout>
</DashboardLayout>
);

export default Page;
Original file line number Diff line number Diff line change
@@ -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 (
<CippFormPage
title="Registration Campaign"
hidePageType={true}
hideBackButton={true}
formControl={formControl}
resetForm={false}
postUrl="/api/ExecRegistrationCampaign"
queryKey={queryKey}
customDataformatter={(values) => ({
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),
})}
>
<Grid container spacing={2}>
<Grid size={{ xs: 12 }}>
<Typography variant="body2" color="text.secondary">
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.
</Typography>
</Grid>
{campaignRequest.isError && (
<Grid size={{ xs: 12 }}>
<Alert severity="error">
Failed to load the current registration campaign settings for this tenant.
</Alert>
</Grid>
)}
<Grid size={{ xs: 12, md: 6 }}>
<CippFormComponent
type="select"
name="state"
label="Campaign state"
creatable={false}
options={stateOptions}
formControl={formControl}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<CippFormComponent
type="select"
name="targetedAuthenticationMethod"
label="Authentication method to nudge users to register"
creatable={false}
options={methodOptions}
formControl={formControl}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<CippFormComponent
type="number"
name="snoozeDurationInDays"
label="Days allowed to snooze (0-14)"
validators={{
min: { value: 0, message: "Minimum value is 0" },
max: { value: 14, message: "Maximum value is 14" },
}}
formControl={formControl}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<CippFormComponent
type="switch"
name="enforceRegistrationAfterAllowedSnoozes"
label="Limited number of snoozes (require registration after 3 snoozes)"
formControl={formControl}
/>
</Grid>
<Grid size={{ xs: 12 }}>
<CippFormComponent
type="switch"
name="includeAllUsers"
label="Include all users"
formControl={formControl}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<CippFormComponent
type="autoComplete"
name="includeGroups"
label="Include group(s)"
multiple={true}
creatable={false}
api={groupFieldApi}
formControl={formControl}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<CippFormComponent
type="autoComplete"
name="includeUsers"
label="Include user(s)"
multiple={true}
creatable={false}
api={userFieldApi}
formControl={formControl}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<CippFormComponent
type="autoComplete"
name="excludeGroups"
label="Exclude group(s)"
multiple={true}
creatable={false}
api={groupFieldApi}
formControl={formControl}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<CippFormComponent
type="autoComplete"
name="excludeUsers"
label="Exclude user(s)"
multiple={true}
creatable={false}
api={userFieldApi}
formControl={formControl}
/>
</Grid>
</Grid>
</CippFormPage>
);
};

Page.getLayout = (page) => (
<DashboardLayout>
<TabbedLayout tabOptions={tabOptions}>{page}</TabbedLayout>
</DashboardLayout>
);

export default Page;
Original file line number Diff line number Diff line change
@@ -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"
}
]