Need to Update Scripts that Manage Conditional Access Policies
In 2021, Damian Scoles wrote about managing conditional access (CA) policies with PowerShell. At the time, the relevant cmdlets came from the AzureAD or AzureADPreview modules, both of which Microsoft plans to deprecate on March 30, 2024. It’s time to update scripts based on these modules to use Graph API requests or the Microsoft Graph PowerShell SDK. This article explains how to use the SDK cmdlets to manage CA policies.
Before starting, let me say that I do not think many Microsoft 365 tenants will use PowerShell to create conditional access policies. As we’ll see, the New-MgIdentityConditionalAccessPolicy cmdlet certainly works, but I think it’s easier to create new conditional access policies through the Entra ID admin center. Another factor is that Microsoft tends to upgrade the UI to accommodate new policy capabilities first before it’s possible to manage the settings with PowerShell. Examples are using authentication contexts to identify specific SharePoint Online sites or insisting that sessions use a certain authentication strength for multi-factor authentication.
A delay invariably occurs between the appearance of a new feature and support in the PowerShell cmdlets. The delay is compounded by the need for Microsoft to upgrade the Graph API to support new features before the ‘auto generation’ process can run to bring that support forward into the SDK cmdlets.
Partners who manage customer tenants or organizations that run multiple tenants might find it convenient to deploy a set of policies to new tenants, and that’s where creating new conditional access policies with PowerShell is useful.
Connecting and Fetching Details of Conditional Access Policies
First, let’s connect to the Graph with the permission (scope) needed for read-write access to conditional access policies and use the Get-MgIdentityConditionalAccessPolicy cmdlet to return the set of policies in the tenant.
Connect-MgGraph -NoWelcome -Scopes Policy.ReadWrite.ConditionalAccess [array]$Policies = Get-MgIdentityConditionalAccessPolicy | Sort-Object DisplayName $Policies | Format-Table DisplayName, ModifiedDateTime, State DisplayName ModifiedDateTime State ----------- ---------------- ----- Allow Access to IT Apps 05/12/2023 16:51:13 enabled Authentication Strength 25/10/2022 09:45:00 enabledForReportingButNotEnforced Azure Information Protection CA policy disabled B2B Collaboration and Direct Connect Guests 05/12/2023 16:51:15 enabled Block Guest Access to PowerShell enabledForReportingButNotEnforced Block Weekend Access 08/05/2023 13:40:59 enabledForReportingButNotEnforced CA005: Require multi-factor authentication for guest access 05/12/2023 16:51:17 enabled Outlook Authentication Sessions 05/12/2023 19:08:25 enabled Remove Access for Legacy Email Clients 05/12/2023 16:51:19 enabled Require MFA access for Confidential Access SPO Sites 05/12/2023 16:51:21 enabled Restrict Access to Graph Explorer 05/12/2023 19:08:42 enabled
A conditional access policy can be in the following states:
- Enabled: Currently active and used by Entra ID to evaluate connection sessions.
- Disabled: Not currently used.
- EnabledForReportingButNotEnforced: Currently active but only for reporting purposes. Entra ID does not enforce conditions when evaluating connections.
Like most other Microsoft 365 objects, each policy also has a unique GUID (not shown) that cmdlets use to identify a policy. We can see the identifier by examining the properties of a policy:
$Policies[0] | Format-List Conditions : Microsoft.Graph.PowerShell.Models.MicrosoftGraphConditionalAccessConditionSet CreatedDateTime : 27/10/2022 20:19:01 Description : DisplayName : Allow Access to IT Apps GrantControls : Microsoft.Graph.PowerShell.Models.MicrosoftGraphConditionalAccessGrantControls Id : ead51ecc-0d37-412a-8b3f-aac6b61e6117 ModifiedDateTime : 05/12/2023 16:51:13 SessionControls : Microsoft.Graph.PowerShell.Models.MicrosoftGraphConditionalAccessSessionControls State : enabled TemplateId : AdditionalProperties : {}
Apart from the identifier (id), the most important properties are:
- Conditions: Dictates conditions used to evaluate a session, such as the device used, the location the session originates from, and the risk state of the user.
- GrantControls: Dictate the conditions under which a session is granted access, such as if multi-factor authentication is used.
- SessionControls: Dictate if Entra ID imposes limitations on a session, such as the sign-in frequency for users to prove that they have credentials (here’s why a frequent sign-in frequency is bad).
These properties correspond to the sections in the Entra ID UI used to configure conditional access settings (Figure 1).
From a PowerShell perspective, it’s critical to understand that many properties hold multiple other properties to store the policy settings. Here’s a view of each property and what lies beneath:
$Policies[0].Conditions | Format-List Applications : Microsoft.Graph.PowerShell.Models.MicrosoftGraphConditionalAccessApplications ClientAppTypes : {all} ClientApplications : Microsoft.Graph.PowerShell.Models.MicrosoftGraphConditionalAccessClientApplications Devices : Microsoft.Graph.PowerShell.Models.MicrosoftGraphConditionalAccessDevices Locations : Microsoft.Graph.PowerShell.Models.MicrosoftGraphConditionalAccessLocations Platforms : Microsoft.Graph.PowerShell.Models.MicrosoftGraphConditionalAccessPlatforms ServicePrincipalRiskLevels : {} SignInRiskLevels : {} UserRiskLevels : {} Users : Microsoft.Graph.PowerShell.Models.MicrosoftGraphConditionalAccessUsers AdditionalProperties : {} $Policies[0].GrantControls | Format-List AuthenticationStrength : Microsoft.Graph.PowerShell.Models.MicrosoftGraphAuthenticationStrengthPolicy BuiltInControls : {mfa} CustomAuthenticationFactors : {} Operator : OR TermsOfUse : {} AdditionalProperties : {[authenticationStrength@odata.context, https://graph.microsoft.com/v1.0/$metadata#identity/conditionalAccess/policies('ead51ecc-0d37-412a-8b3f-aac6b61e6117')/grantControls/authenticationStrength/$entity]} $Policies[0].SessionControls | Format-List ApplicationEnforcedRestrictions : Microsoft.Graph.PowerShell.Models.MicrosoftGraphApplicationEnforcedRestrictionsSessionControl CloudAppSecurity : Microsoft.Graph.PowerShell.Models.MicrosoftGraphCloudAppSecuritySessionControl DisableResilienceDefaults : PersistentBrowser : Microsoft.Graph.PowerShell.Models.MicrosoftGraphPersistentBrowserSessionControl SignInFrequency : Microsoft.Graph.PowerShell.Models.MicrosoftGraphSignInFrequencySessionControl AdditionalProperties : {}
Some properties have another level underneath. For example, to access details of the user accounts and groups that a policy applies to, we must look at the Users property of Conditions. Here we find a further set of properties. Because the IncludeUsers property is set to All, we know that the policy applies to all accounts except the two excluded user accounts defined in the ExcludeUsers property:
$Policies[0].Conditions.Users | fl ExcludeGroups : {} ExcludeGuestsOrExternalUsers : Microsoft.Graph.PowerShell.Models.MicrosoftGraphConditionalAccessGuestsOrExternalUsers ExcludeRoles : {} ExcludeUsers : {91813a30-f048-48f1-a0f2-fd7c72020515, b7289bc7-7e4e-44e2-ae1b-7e13e94e3749} IncludeGroups : {} IncludeGuestsOrExternalUsers : Microsoft.Graph.PowerShell.Models.MicrosoftGraphConditionalAccessGuestsOrExternalUsers IncludeRoles : {} IncludeUsers : {All} AdditionalProperties : {}
When you see a value like Microsoft.Graph.PowerShell.Models.MicrosoftGraphConditionalAccessGuestsOrExternalUsers, it means that one or more values are present in an array. You can extract the values like this:
$Values = $Conditions.Users | Select-Object -ExpandProperty ExcludeGuestsOrExternalUsers
What’s obvious from this description is that interacting with the settings of conditional access policies through SDK cmdlets requires some familiarity with the structure used to hold policy settings. In other words, take some time to poke around and find where individual policy settings surface.
Preparing to Create a New Conditional Access Policy
The documentation for the New-MgIdentityConditionalAccessPolicy cmdlet includes several useful examples that I do not intend to replicate here. Instead, I’ll create a relatively simple policy to illustrate the principle.
Before creating a policy, a script might need to gather some information, including:
Locations: A location defines where a session can originate from, such as a specific IPv4 or IPv6 range. Named locations are retrieved with the Get-MgIdentityConditionalAccessNamedLocation cmdlet. Conditional access policies use the identifier (id) for a named location.
Get-MgIdentityConditionalAccessNamedLocation Id CreatedDateTime DisplayName ModifiedDateTime -- --------------- ----------- ---------------- 6ff84108-eab2-4bba-bcea-c952075c2c9f 01/01/2021 16:01:22 IT Department 143f73a6-a30b-47b8-bad8-9381a04bbe65 24/01/2023 18:35:19 Ireland 24/01/2023 18:35:19 0053e3ac-90b5-4bd3-b51e-e2fae92564b0 24/01/2023 18:38:20 HQ 24/01/2023 18:38:20
Users, groups, and roles: Conditional access policies can set the scope for their application by including or excluding user accounts, groups, and administrative roles. For instance, it is good practice to exclude breakglass accounts from conditional access policies to ensure that there’s always a way to sign into a tenant. If you want to create or modify a policy with inclusions or exclusions, you’ll need to know the identifiers for the target user accounts, groups, or administrative roles. The identifiers for roles are available by running the Get-MgDirectoryRole or Get-MgDirectoryRoleTemplate cmdlets. The first returns the set of administrative roles used in the tenant; the second returns all available roles.
Applications: Many conditional access policies control access to applications like Exchange Online, or SharePoint Online. If you want to specify an application in a conditional access policy, you’ll need to know its identifier, which you can find by running the Get-MgServicePrincipal cmdlet and checking the output to find the correct app. For example, this code finds the identifier (AppId) for Exchange Online.
[array]$SPs = Get-MgServicePrincipal -All $Office365AppId = $SPs | Where-Object {$_.DisplayName -eq "Office 365 Exchange Online"} | Select-Object -ExpandProperty AppId
In addition, you’ll also need to know if the policy needs to implement any specific settings, such as if it covers specific application types or client platforms, or sets a sign-in frequency. If you use the latter setting, don’t set the frequency to less than a week unless absolutely necessary as forcing people to sign in frequently can impact their workflow.
Creating a New Conditional Access Policy
It would be possible to construct an exotic conditional access policy and use it as an example. A more practical example demonstrates how to create the well-known policy to enforce multifactor authentication for any user holding an administrative role (the policy covers fourteen selected roles). This policy should be used by all tenants
The first step is to create a parameter structure to hold the policy settings. In PowerShell terms, this is a hash table holding key/value pairs. Some of the values are other hash tables that hold arrays. For instance, the Users value holds the settings for accounts, groups, and roles included or excluded by the policy. Users is a hash table with arrays for included users, groups, roles, and so on. The only difference between the settings for this policy and those recommended by Microsoft is that the policy sets a 30-day sign-in frequency for administrators.
$PolicySettings = @{ displayName = "Require Multifactor authentication for admin roles" state = "enabled" conditions = @{ clientAppTypes = @( "all" ) applications = @{ includeApplications = @( "All" ) } users = @{ excludeUsers = @( "aff4cd58-1bb8-4899-94de-795f656b4a18" ) includeRoles = @( "62e90394-69f5-4237-9190-012177145e10" "194ae4cb-b126-40b2-bd5b-6091b380977d" "f28a1f50-f6e7-4571-818b-6a12f2af6b6c" "29232cdf-9323-42fd-ade2-1d097af3e4de" "b1be1c3e-b65d-4f19-8427-f6fa0d97feb9" "729827e3-9c14-49f7-bb1b-9608f156bbb8" "b0f54661-2d74-4c50-afa3-1ec803f12efe" "fe930be7-5e62-47db-91af-98c3a49a38b1" "c4e39bd9-1100-46d3-8c65-fb160da0071f" "9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3" "158c047a-c907-4556-b7ef-446551a6b5f7" "966707d0-3269-4727-9be2-8c3a10f19b9d" "7be44c8a-adaf-4e2a-84d6-ab2649e08a13" "e8611ab8-c189-46e8-94e1-60213ab1f814" ) } } grantControls = @{ operator = "OR" builtInControls = @( "mfa" ) } sessionControls = @{ signInFrequency = @{ value = 30 type = "days" isEnabled = $true } } }
To create the policy, run the New-MgDirectoryConditionalPolicy cmdlet:
New-MgIdentityConditionalAccessPolicy -BodyParameter $PolicySettings Id CreatedDateTime Description DisplayName -- --------------- ----------- ----------- b1e82588-96f9-4e3b-9881-b1fef352309c 17/12/2023 18:40:11 Require Multifactor authentication for admin roles
You’ll notice that the policy state is set to enabled, which means that Entra ID will start using the policy immediately. If you want to create a policy that has less of an impact on a tenant, consider limiting its scope to a selected group or individual user accounts. To do this, include an includeUsers or IncludeGroups array in the parameter structure.
As an example, let’s find the identifier for a group and use it to update the policy. First, use the Get-MgGroup cmdlet to retrieve the group identifier:
(Get-MgGroup -Filter "Displayname eq 'System Innovation'").Id 5c011293-7cc7-41c4-a0fc-3e3bb98db834
Now build a parameter structure that only updates the Users section of Conditions:
$UpdateSettings = @{ Conditions = @{ users = @{ includeGroups = @( "5c011293-7cc7-41c4-a0fc-3e3bb98db834" ) } } }
Finally, run the Update-MgIdentityConditionalAccessPolicy cmdlet after fetching the identifier for the conditional access policy to update:
$PolicyId = (Get-MgIdentityConditionalAccessPolicy -Filter "displayName eq 'Require Multifactor authentication for admin roles'").Id Update-MgIdentityConditionalAccessPolicy -BodyParameter $UpdateSettings -ConditionalAccessPolicyId $PolicyId
Only members of the selected group are now subject to the conditional access policy.
Takes a Little Time to Get Used to
Working with conditional access policies using Microsoft Graph PowerShell SDK cmdlets isn’t particularly difficult once you master how to pass values to update settings. If you’re not used to managing conditional access, I recommend that you consider creating and updating policies in a development tenant to make sure that everything works as expected before bringing work to your production environment. Entra ID is very good at following instructions from conditional access policies, so always make sure that you know what a setting does before changing it. If not, you might end up locking yourself and others out of the tenant.
Once you have created the policy, how can you run the report to see that it’s blocking the users in scope (many users not one at a time)
Check the sign in logs in the Entra admin center and look for entries for the people in scope for the policy. You can then see what CA policies were applied to the connection and the success/fail state.
Tony, thanks for a great article.
In our Azure environment, we’ve access packages being provisioned via auto-assignment policies. (Ex: Department=”Sales”)
I was looking for a way to retrieve each access package auto-assignment policy criteria using MS Graph API SDK. From what I could tell so far, it seems like the info I am looking for is stored in “MembershipRule” attribute of the policy but couldn’t find a correct API function that returns it.
I have tried the following API so far with no luck. Any input would be much appreciated!
Get-MgEntitlementManagementAccessPackage -ExpandProperty “assignmentpolicies”
Get-MgBetaEntitlementManagementAccessPackageAssignmentPolicy -AccessPackageAssignmentPolicyId $policy.Id
I haven’t tried to work with access packages through the Graph and I am traveling for the next three weeks so I can’t do anything for you… Sorry!
Really great article. Slowly trying to figure this out but seems to make sense. First time automating CA deployment. Would be nice if they just natively had export-import as there is no reason this shouldn’t all be in a nice simple JSON document.