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).

Settings for a conditional access policy.
Figure 1: Settings for a conditional access policy

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.

About the Author

Tony Redmond

Tony Redmond has written thousands of articles about Microsoft technology since 1996. He is the lead author for the Office 365 for IT Pros eBook, the only book covering Office 365 that is updated monthly to keep pace with change in the cloud. Apart from contributing to Practical365.com, Tony also writes at Office365itpros.com to support the development of the eBook. He has been a Microsoft MVP since 2004.

Comments

  1. K.M

    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)

    1. Avatar photo
      Tony Redmond

      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.

  2. SV

    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

    1. Avatar photo
      Tony Redmond

      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!

  3. AnonymousCyberProfessional

    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.

Leave a Reply