Example how to create Azure AD access reviews using Microsoft Graph app permissions with PowerShell

Microsoft

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.

  1. Log into the Azure portal as a global administrator.
  2. 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.
  3. Navigate to Azure Active Directory, and then click Identity Governance on the left hand side.
  4. 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.

 

  1. Log into the Azure portal as a global administrator.
  2. In the Azure portal, go to Azure Active Directory, and then click App registrations on the left.
  3. Click New registration. Give your app a name, and then click Register.
  4. Copy and save for later the application (client) ID that appears after the app is registered.
  5. On the left, click API permissions.
  6. Click Add a permission, click Microsoft Graph, and then click Application permissions.

 

 
  1. In the Select permissions list, expand AccessReview and select AccessReview.ReadWrite.Membership.

 

 

 

 

  1. 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,
  2. Click Add permissions.
  3. 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.

 

 

 

  1. On the left, click Certificates & secrets.
  2. Click New client secret and then for Expires select Never.
  3. Click Add.
  4. 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.

  1. Save the PowerShell below to a file named sample-ar-app-permissions.psm1.
  2. Open a new PowerShell window, change to the directory where the file is located and type Import-Module .\sample-ar-app-permissions.psm1

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

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


 

 

9 Replies

@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?

@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

 

This is a great start, thanks @Mike Crowley 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!

For me it says ##[error]Invalid schedule recurrence type provided : never

Update:
I had to change to this to make it work:

$RecurrenceSettings = @{
            RecurrenceType = "onetime"
            RecurrenceEndType = "endBy"
            DurationInDays = 30
            RecurrenceCount = 180
            }

Hi @Mark Wahl - Any way we can create an access review via PowerShell for multiple groups at the same time ?

@ilik0, those types are imported with the modules, sorry I forgot to mention this.
Thanks, Mike. Do you know how to add an AAD group to the review created by the code above?
Hi @Optimus5430

Have you got answer? How to create access review for multiple groups in Entra using powershell script?
You will have to run the above as a foreach loop.