Forum Discussion
Example how to create Azure AD access reviews using Microsoft Graph app permissions with PowerShell
The Azure AD access reviews feature is part of Microsoft Graph, with a list of methods at https://docs.microsoft.com/en-us/graph/api/resources/accessreviews-root?view=graph-rest-beta. An earlier blog post included an example of how a user, such as a Security Reader, could retrieve all the programs, controls and access reviews via Microsoft Graph, in PowerShell.
In this post, I'll show an example PowerShell script that uses the new application permission AccessReview.ReadWrite.Membership. . Application permissions don’t need the app to have a logged in user to call Graph, so you can use this to automatically to create and retrieve access reviews from scheduled jobs or as part of your existing automation.
Code Sample Prerequisite #1: Azure AD PowerShell
To set up this code sample, you’ll need an Azure AD tenant where you’re a global administrator. In this example, as with the previous blog post, the sample code to use the API leverages the ADAL library to retrieve an access token used by Microsoft Graph. The ADAL library is automatically installed when you install Azure AD PowerShell cmdlets. So before you begin, ensure that you have PowerShell 3.0 or later,.NET Framework 4.5 and either the Azure AD PowerShell v2 General Availability or Preview modules installed on your computer. If you don’t have Azure AD PowerShell installed yet, see https://docs.microsoft.com/en-us/powershell/azure/active-directory/install-adv2?view=azureadps-2.0. Try the Connect-AzureAD command to ensure that you can authenticate to Azure AD as a global administrator. Also try Get-AzureADUser to make sure you can retrieve users, as you'll need a user ID later on.
Code Sample Prerequisite #2: Azure AD access reviews
This example assumes you have already onboarded Azure AD access reviews in your tenant directory. If you have already done so, then skip to the next section “Register an Azure AD application”. Otherwise, continue with these steps to ensure the feature is onboarded so the APIs will allow access reviews to be created and return some data.
- Log into the Azure portal as a global administrator.
- Ensure that your organization has Azure AD Premium P2 or EMS E5 subscription active. If not, click https://portal.azure.com/#blade/Microsoft_AAD_IAM/TryBuyProductBlade and activate a trial of Enterprise Mobility + Security E5. Otherwise, if your organization has an active subscription, continue at the next step.
- Navigate to Azure Active Directory, and then click Identity Governance on the left hand side.
- Click Access reviews. If no one has onboarded Azure AD access reviews in your organization, onboard it now.
Register an Azure AD application
Next, you'll need to create an app registration with the new permission, to allow your application to call the access reviews API in Microsoft Graph. If you haven’t created an app registration lately, the user interface in the Azure portal has changed. Here’s how to create an app with the updated UI.
- Log into the Azure portal as a global administrator.
- In the Azure portal, go to Azure Active Directory, and then click App registrations on the left.
- Click New registration. Give your app a name, and then click Register.
- Copy and save for later the application (client) ID that appears after the app is registered.
- On the left, click API permissions.
- Click Add a permission, click Microsoft Graph, and then click Application permissions.
- In the Select permissions list, expand AccessReview and select AccessReview.ReadWrite.Membership.
- While here, though not required for this sample, you might want to expand Group and give the app the permission Group.Read.All, and expand User and give the app the User.Read.All permission,
- Click Add permissions.
- Click to Grant admin consent for <your tenant> and then click Yes. The status for each permission the app needs should change to a green checkmark, indicating consent was granted.
- On the left, click Certificates & secrets.
- Click New client secret and then for Expires select Never.
- Click Add.
- Copy and save locally the value of the secret that appears- you won’t see it again after you leave this part of the UI.
At this point, you’ll have a client app ID and a client secret. In real life you'd probably want to store the secret in Azure Automation, Azure Key Vault, or similar.
Create and retrieve access reviews using Graph
Next, here's how to try out Microsoft Graph API requests when authenticated as an application, using a PowerShell script to be your application. I'll assume you have Azure AD v2 PowerShell cmdlets already installed - the script uses the Azure AD library included in those modules for authentication.
- Save the PowerShell below to a file named sample-ar-app-permissions.psm1.
- Open a new PowerShell window, change to the directory where the file is located and type Import-Module .\sample-ar-app-permissions.psm1
Type Connect-AzureADMSARSample. This obtains a token needed for the service principal to call Graph. You’ll be prompted to provide the following information:
- ClientApplicationId
- ClientSecret
- TenantDomain (e.g. demo….onmicrosoft.com)
- To create a new access review, use the command New-AzureADMSARSampleAccessReview. To try out this command, you’ll need to have an Azure AD group with members and owners – the owners will be the reviewers. You’ll be prompted to provide the following information:
- DisplayName: (a display name for the access review)
- ReviewedEntityId: (the object ID of a group whose members are to be reviewed)
- OwnerUserId: (the object ID of a user such as an admin who will be listed as the owner of a review – since apps can’t own access reviews)
If successful, the command will return the ID of the new access review. If you see an error about permissions, please note that app registration may take a few minutes to be set up in the directory – if the Graph calls don’t work right away, try again an hour later.
- Type Get-AzureADMSARSampleAccessReview to see the access review that you've created.
I expect we’ll bring commands to create and query access reviews into the Azure AD PowerShell module in the future, which will remove the need for Connect-AzureADMSARSample. Please let us know about any other feedback/suggestions you have for more code samples. Thanks, Mark Wahl
# Example for creating and retrieving the results of an Azure AD access review via Microsoft Graph using application permissions
#
# This material is provided "AS-IS" and has no warranty.
#
# Last updated August 2019
#
# This example is adapted from the documentation example located at
# https://docs.microsoft.com/en-us/intune/intune-graph-apis
#
#
# the following functions are from Intune graph API samples, adapted for service principal authentication
function Get-GraphExampleAuthTokenServicePrincipal {
[cmdletbinding()]
param
(
[Parameter(Mandatory = $true)]
$ClientId,
[Parameter(Mandatory = $true)]
$ClientSecret,
[Parameter(Mandatory = $true)]
$TenantDomain
)
$tenant = $TenantDomain
Write-Verbose "Checking for AzureAD module..."
$AadModule = Get-Module -Name "AzureAD" -ListAvailable
if ($AadModule -eq $null) {
write-verbose "AzureAD PowerShell module not found, looking for AzureADPreview"
$AadModule = Get-Module -Name "AzureADPreview" -ListAvailable
}
if ($AadModule -eq $null) {
write-output
write-error "AzureAD Powershell module not installed..."
write-output "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt"
write-output "Script can't continue..."
write-output
return ""
}
# Getting path to ActiveDirectory Assemblies
# If the module count is greater than 1 find the latest version
if ($AadModule.count -gt 1) {
write-verbose "multiple module versions"
$Latest_Version = ($AadModule | select version | Sort-Object)[-1]
$aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version }
$adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll"
$adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll"
}
else {
write-verbose "single module version"
$adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll"
$adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll"
}
Write-verbose "loading $adal and $adalforms"
[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null
[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null
write-verbose "DLLs loaded"
# $redirectUri = "urn:ietf:wg:oauth:2.0:oob"
$resourceAppIdURI = "https://graph.microsoft.com"
$authority = "https://login.microsoftonline.com/$Tenant"
try {
write-verbose "instantiating ADAL objects for $authority"
$authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority
write-verbose "client $ClientId $clientSecret"
$clientCredential = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.ClientCredential" -ArgumentList ($ClientId,$ClientSecret)
write-verbose "acquiring token for $resourceAppIdURI"
# AuthenticationResult authResult = await authContext.AcquireTokenAsync(BatchResourceUri, new ClientCredential(ClientId, ClientKey));
# if you get an error about PowerShell not being able to find this method with 2 parameters, it means there is another version of ADAL DLL already in the process space of your PowerShell environment.
$authResult = $authContext.AcquireTokenAsync($resourceAppIdURI, $clientCredential).Result
# If the accesstoken is valid then create the authentication header
if ($authResult.AccessToken) {
write-verbose "acquired token"
# Creating header for Authorization token
$authHeader = @{
'Content-Type' = 'application/json'
'Authorization' = "Bearer " + $authResult.AccessToken
'ExpiresOn' = $authResult.ExpiresOn
}
return $authHeader
}
else {
write-output ""
write-output "Authorization Access Token is null, please re-run authentication..."
write-output ""
break
}
}
catch {
write-output $_.Exception.Message
write-output $_.Exception.ItemName
write-output ""
break
}
}
$_SampleInternalAuthNHeaders = @()
# exported module member
function Connect-AzureADMSARSample {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[ValidateScript({
try {
[System.Guid]::Parse($_) | Out-Null
$true
} catch {
throw "$_ is not a valid GUID"
}
})]
[string]$ClientApplicationId,
[Parameter(Mandatory=$true)]
[string]$ClientSecret, # base64 client secret. Note this as a command line parameter is for testing purposes only
[Parameter(Mandatory=$true)]
[string]$TenantDomain # e.g., microsoft.onmicrosoft.com
)
$script:_SampleInternalAuthNHeaders = @()
$authHeaders = Get-GraphExampleAuthTokenServicePrincipal -ClientId $ClientApplicationId -ClientSecret $ClientSecret -TenantDomain $TenantDomain
$script:_SampleInternalAuthNHeaders = $authHeaders
}
function Get-InternalAuthNHeaders {
[CmdletBinding()]
param()
try {
$authResult = $script:_SampleInternalAuthNHeaders
if ($authResult.Length -eq @()) {
Throw "Connect-AzureADMSARSample must be called first"
}
} catch {
Throw # "Connect-AzureADMSControls must be called first"
}
return $authResult
}
function New-GraphExampleAccessReview($authHeaders,$displayName,$reviewedObjectId,$reviewerType,$businessFlowTemplateId,$description,$durationInDays,$ownerUserId) {
$recurrenceSettings = @{
recurrenceType = "onetime";
recurrenceEndType = "endBy";
durationInDays = 0;
recurrenceCount = 0;
}
$autoReviewSettings = @{
notReviewedResult = "Approve" # also use "Deny" or "Recommendation"
}
$settings = @{
mailNotificationsEnabled = $true;
remindersEnabled = $true;
justificationRequiredOnApproval = $false;
autoReviewEnabled = $true;
activityDurationInDays = 30;
autoApplyReviewResultsEnabled = $false;
accessRecommendationsEnabled = $true;
recurrenceSettings = $recurrenceSettings;
autoReviewSettings = $autoReviewSettings;
}
$reviewedEntity = [pscustomobject]@{
id = $reviewedObjectId
}
$owner = [pscustomobject]@{
id = $ownerUserId
}
$now = Get-Date
$ts = Get-Date $now.ToUniversalTime() -format "s"
$startDate = $ts + "Z"
$ts = Get-Date $now.AddDays($durationInDays).ToUniversalTime() -format "s"
$endDate = $ts + "Z"
$bodyObj = [pscustomobject]@{
displayName = $displayName;
startDateTime = $startDate;
endDateTime = $endDate;
reviewedEntity = $reviewedEntity;
reviewerType = $reviewerType;
businessFlowTemplateId = $businessFlowTemplateId;
description = $description;
settings = $settings;
createdBy = $owner;
}
#
$body = ConvertTo-Json $bodyObj -compress
# ensure it contains "Content-Type" = "application/json";
$requestHeadersp = @{
"Content-Length" = $body.Length
}
$requestHeadersp += $authHeaders
$uri1 = "https://graph.microsoft.com/beta/accessReviews"
$resp1 = Invoke-WebRequest -UseBasicParsing -Headers $requestHeadersp -Uri $uri1 -Method Post -Body $body
if ($resp1 -eq $null -or $resp1.Content -eq $null) {
throw "error repsonse from $uri1"
}
$val1 = ConvertFrom-Json $resp1.Content
return $val1.id
}
function New-AzureADMSARSampleAccessReview {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[string]$DisplayName, # of the review to create
[Parameter()]
[string]$Description = "", # of the review to create
[Parameter()]
[int]$DurationInDays=30,
[Parameter(Mandatory=$true)]
[ValidateScript({
try {
[System.Guid]::Parse($_) | Out-Null
$true
} catch {
throw "$_ is not a valid GUID"
}
})]
[string]$ReviewedEntityId,
[Parameter()]
[ValidateScript({
try {
[System.Guid]::Parse($_) | Out-Null
$true
} catch {
throw "$_ is not a valid GUID"
}
})]
[string]$BusinessFlowTemplateId = "6e4f3d20-c5c3-407f-9695-8460952bcc68",
#business flow template 6e4f3d20-c5c3-407f-9695-8460952bcc68 for Access reviews of memberships of a group
[Parameter()]
[string]$ReviewerType = "entityOwners",
[Parameter(Mandatory=$true)]
[ValidateScript({
try {
[System.Guid]::Parse($_) | Out-Null
$true
} catch {
throw "$_ is not a valid GUID"
}
})]
[string]$OwnerUserId # a user who has an email address
)
$authHeaders = Get-InternalAuthNHeaders
$reviewId = New-GraphExampleAccessReview $authHeaders $DisplayName $ReviewedEntityId $ReviewerType $BusinessFlowTemplateId $Description $durationInDays $OwnerUserId
return $reviewId
}
function Get-GraphExampleAccessReviews($authHeaders,$businessFlowTemplateId)
{
$uri1 = 'https://graph.microsoft.com/beta/accessReviews?$filter=businessFlowTemplateId%20eq%20' + "'" + $businessFlowTemplateId + "'"
$results = @()
do {
$resp1 = Invoke-WebRequest -UseBasicParsing -headers $authHeaders -Uri $uri1 -Method Get
if ($resp1 -eq $null -or $resp1.Content -eq $null) {
throw "error response from $uri1"
}
$val1 = ConvertFrom-Json $resp1.Content
foreach ($i in $val1.value) {
$results += $i
}
$uri1 = $val1.'@odata.nextLink'
# Odata list may have more
} while ($uri1 -ne $null)
return $results
}
function Get-GraphExampleAccessReview($authHeaders,$reviewId)
{
$uri1 = "https://graph.microsoft.com/beta/accessReviews/" + $reviewId
$resp1 = Invoke-WebRequest -UseBasicParsing -headers $authHeaders -Uri $uri1 -Method Get
if ($resp1 -eq $null -or $resp1.Content -eq $null) {
throw "error response from $uri1"
}
$val1 = ConvertFrom-Json $resp1.Content
return $val1
}
function Get-AzureADMSARSampleAccessReview {
[CmdletBinding()]
param(
[Parameter()]
[ValidateScript({
try {
[System.Guid]::Parse($_) | Out-Null
$true
} catch {
throw "$_ is not a valid GUID"
}
})]
[string]$ReviewId,
[Parameter()]
[ValidateScript({
try {
[System.Guid]::Parse($_) | Out-Null
$true
} catch {
throw "$_ is not a valid GUID"
}
})]
[string]$BusinessFlowTemplateId = "6e4f3d20-c5c3-407f-9695-8460952bcc68"
)
#business flow template 842169fe-e1b7-4ce9-98b6-6a9db02eec6b for Access reviews of guest user memberships of a group
#business flow template 7fbc909b-efe1-4c72-8ae6-99cb30b882de for Access reviews of guest user assignments to an application
#business flow template 50839a84-e23c-44a7-a8cc-16e162afc656 for Access reviews of assignments to an application
#business flow template 6e4f3d20-c5c3-407f-9695-8460952bcc68 for Access reviews of memberships of a group
$authHeaders = Get-InternalAuthNHeaders
if ($ReviewId -ne $null) {
if ($ReviewId.Length -ge 1) {
$reviewObj = Get-GraphExampleAccessReview $authHeaders $ReviewId
$reviews = @()
$reviews += $reviewObj
return $reviewObj
}
}
$res = Get-GraphExampleAccessReviews $authHeaders $BusinessFlowTemplateId
return $res
}
###
export-modulemember -function Connect-AzureADMSARSample
export-modulemember -function Get-AzureADMSARSampleAccessReview
export-modulemember -function New-AzureADMSARSampleAccessReview
- Roger WilliamsCopper Contributor
Mark_WahlThanks for providing this script. It is a big help in our project to automate the creation of Group Access Reviews.
Would it be possible for you to update this example using MSAL instead of ADAL since ADAL is going away?
- MikeCrowleyIron Contributor
Roger Williams I just came across this post and wanted to share an approach if anyone else has the same question:
Connect-MgGraph -TenantId mytenant.onmicrosoft.com -Scopes AccessReview.ReadWrite.All Select-MgProfile -Name beta Import-Module Microsoft.Graph.Identity.Governance $AccessReviewTemplate = Get-MgBusinessFlowTemplate | Where DisplayName -eq 'Access reviews of memberships of a group' $AccessReviewTemplate.Id $AutoReviewSettings = [Microsoft.Graph.PowerShell.Models.MicrosoftGraphAutoReviewSettings]@{ NotReviewedResult = "None" } $RecurrenceSettings = [Microsoft.Graph.PowerShell.Models.MicrosoftGraphAccessReviewRecurrenceSettings]@{ DurationInDays = 1 RecurrenceCount = 0 RecurrenceEndType = "never" RecurrenceType = "weekly" } $ReviewSettings = [Microsoft.Graph.PowerShell.Models.MicrosoftGraphAccessReviewSettings]@{ AccessRecommendationsEnabled = $true ActivityDurationInDays = 0 AutoApplyReviewResultsEnabled = $false AutoReviewEnabled = $false AutoReviewSettings = $AutoReviewSettings JustificationRequiredOnApproval = $true MailNotificationsEnabled = $true RecurrenceSettings = $RecurrenceSettings RemindersEnabled = $true } $ReviewedEntity = [Microsoft.Graph.PowerShell.Models.MicrosoftGraphIdentity]@{ DisplayName = "Group2" Id = "00000001-c59e-48c1-86e9-14ee6daef724" # AAD ObjectId } $NewAccessReview = @{ DisplayName = "Group2" BusinessFlowTemplateId = $AccessReviewTemplate.Id Description = "review2 description!" Settings = $ReviewSettings StartDateTime = (get-date) ReviewedEntity = $ReviewedEntity ReviewerType = "entityOwners" } # https://docs.microsoft.com/en-us/powershell/module/microsoft.graph.identity.governance/new-mgaccessreview?view=graph-powershell-beta New-MgAccessReview @NewAccessReview
- vmovsessianCopper Contributor
This is a great start, thanks MikeCrowley and Mark_Wahl!
Any suggestions on how we can set the reviewers to be the manager of the user?
The docs article for New-MgAccessReview says that it needs to be "one of self, delegated or entityOwners". I've tried all three of those and the access review doesn't get created with "managers" as the reviewers like it does when the reviews are manually created.
The Microsoft Graph API reference says to include this in the body of the web request:
"reviewers": [ { "query": "./manager", "queryType": "MicrosoftGraph", "queryRoot": "decisions" } ]
I've adapted Mark Wahl's original script to literally send that exact string as part of the web request body and the Graph API responds back with "(400) Bad Request".
Any help would greatly be appreciated, thanks again!
- Optimus5430Copper Contributor
Hi Mark_Wahl - Any way we can create an access review via PowerShell for multiple groups at the same time ?
- spacex9Copper ContributorHi Optimus5430
Have you got answer? How to create access review for multiple groups in Entra using powershell script?- TrynaDoStuffCopper ContributorYou will have to run the above as a foreach loop.
- frenjdCopper ContributorWhen I use this script, the owner of the access review is set as [].
The access review is created, but the reviewer is never notified via email and if the login to the access review portal, they do not see the access review that has been created.
I have tried many variations to set the Access Review owner, but none of them work. Any Ideas?
Thanks- TrynaDoStuffCopper Contributor
frenjd You cannot set the owner of the Access Review in this method, as it is created by a Service Principal/App registration, not by a user. The App cannot then sign into Entra, as it is not a user. There is no way to manually set it via API or PowerShell.
If you setup the Review to ask the Group Owners to review, that will work, and they will get the notificatin to renew/approve members. You can also setup "fallback reviewers" in case the owners are gone.