Automate API calls against the Microsoft Graph using PowerShell and Azure Active Directory Applications

 

Automate Azure AD App Creation and access Microsoft Graph

In this article, we’ll demonstrate how to script the creation and consent of an Azure AD Application. Our sample app will connect to the Microsoft Graph beta endpoints. It’ll collect the Office 365 Secure Score report for your tenant and export some results to a CSV. Once the application has collected the results, it will be removed.

Feel free to download and modify this script as needed. In our case, we have a version of this script set up to export the Secure Score Reports daily into Azure Cosmos DB via Azure Functions.

In the next post, I’ll demonstrate how to run this on all of your customer’s Office 365 tenants using your delegated administration credentials. That script will collect the Office 365 Secure Score info for all customer tenants and add them to a CSV. Using this method, we can run Microsoft Graph API calls against all customer tenants without having to individually configure and consent applications via the Azure Portal.

Application permissions vs Delegated permissions

Before you get started with this script, it’s important to understand the difference between Application permissions and Delegated permissions.

  • Application permissions allow an application in Azure Active Directory to act as it’s own entity, rather than on behalf of a specific user.
  • Delegated permissions allow an application in Azure Active Directory to perform actions on behalf of a particular user.

Which permissions should I use?

The Microsoft Graph documentation details which permission levels are required or accepted for each type of call. Some resources can only be reached using Application Permissions, others by Delegated permissions, and others by either of the two.

In our example, we’re accessing the Reports resources. Which can only be reached using Application permissions. Note that for some reason, the documentation for the Secure Score Reports API is not included on the Microsoft Graph site yet. However the permission required for any Reports call is Reports.Read.All. See here for an example: https://developer.microsoft.com/en-us/graph/docs/api-reference/beta/api/reportroot_getoffice365activationsuserdetail

Since we’re just using the Application permissions to access Office 365 reports, we don’t need the application to access private user data. But what if we wanted to script a process that requires Delegated user permissions?

First we’d need to grant consent for the application to access resources as the user. Once we have that consent, we can make API calls using an access token obtained using a grant type of password. See more about grant types below.

When using delegated permissions, consent needs to be granted by a user to allow the application to access their personal resources. However, it can also be granted by an admin, who can consent the application to access the resources for any user who uses the application.

To automate delegated calls against the Microsoft Graph, we’ll first need to automate this consent step. Providing consent for an application to use delegated user permissions is not something that can be performed via the Microsoft Graph at this time, instead we can use the Azure AD Graph API.

Grant Types and Access Tokens

To sum it up, if you want to make a successful call to the Microsoft Graph you need to have the following things:

  • An application registered in your Azure Active Directory, along with a list of permissions that it requires to access Microsoft Graph resources.
  • Consent granted to the application to access those resources, whether as a user (delegated permissions) or an application (application/app-only permissions)
  • An access token retrieved using application and/or user credentials

So once we’ve granted consent, how do we retrieve an access token?

When calling the Microsoft Graph, you need to authorise each call with an access token. Access tokens are typically valid for around an hour, and they include all the permissions that you have to call the Microsoft Graph.

Usually access tokens are retrieved using an interactive process, where a user or admin is prompted for their credentials, then to provide consent by clicking a button. However, this isn’t a great process if you’re looking to automate things.

Fortunately, there are two grant types that we can use to automate the retrieval of access tokens for calls that require Application or Delegated permissions. However they come with a few security considerations.

Grant Type – Client Credentials

The Client Credentials grant type allows you to request an access token for Application calls to the Microsoft Graph. The request for the access token includes the Client ID and Client Secret for the application. Treat this client ID and secret like a username and password. Anyone who has this information can run their own calls against the Microsoft Graph with all the permissions of your app.

Grant Type – Password

The password grant type allows you to request an access token for Delegated calls to the Microsoft Graph. The request for the access token includes the Client ID, Client Secret and the username and password of the user that is requesting the token.

That’s right – to automate delegated user calls against the Microsoft Graph using the password grant type, you need to include the username and password for the relevant user in your scripts. If you’re going to be using this method, you’ll need to take appropriate steps to secure these credentials too.

In this example, our application is only temporary, unless you set the variable at the top of the script for it not to be deleted. The application info, including the Client ID and Secret is exported to a CSV located at C:\temp\AzureADApps.csv. If you’re keeping your application around, be sure to retrieve the details from this CSV before deleting it. You could also consider creating a new Application Key and Secret in the Azure Portal and deleting the current one, just in case.

 

  1. Copy the script below into Visual Studio Code and save it as a .ps1 file.
  2. Modify the variables at the top of the script to suit your needs. There are some boolean values which should hopefully be self explanatory. This is a multipurpose script that can be used to create temporary or permanent applications that can access any resource on the Microsoft Graph.
  3. Run it by pressing F5
  4. Enter the credentials of an Office 365 Admin into the password prompt. This script supports MFA.
  5. If the Microsoft Graph service principal cannot be found in your tenant, you may be prompted to log in to an AzureRM PowerShell session using the same credentials. This will create the Microsoft Graph Service principal.
  6. If you didn’t make any modifications to the permissions or the required endpoints, the application will retrieve the secure score report from the Microsoft Graph and export it to a CSV.

PowerShell Script to automate creation and consent of Azure AD Applications to access the Microsoft Graph

<# This script will create a single Azure AD Application in your tenant, apply the appropriate permissions to it and execute a test call against a specified endpoint. Modify the values at the top of this script as required. #>
 
$applicationName = "GCITS Reporting"
 
# Modify the homePage, appIdURI and logoutURI values to whatever valid URI you like. They don't need to be actual addresses.
$homePage = "https://secure.gcits.com"
$appIdURI = "https://secure.gcits.com/reportingapp"
$logoutURI = "http://portal.office.com"
 
# Set this to false to keep the application in your tenant.
$removeApplicationWhenComplete = $true
 
# Set this to false to limit consent for delegated permissions to a single user ($UserForDelegatedPermissions).
$ConsentDelegatedPermissionsForAllUsers = $true
 
# If your initial test call required delegate permissions, set this to true. The script will retrieve an access token using the 'password' grant type instead.
$testCallRequiresDelegatePermissions = $false
 
# This will export information about the application to a CSV located at C:\temp\.
# The CSV will include the Client ID and Secret of the application, so keep it safe.
$exportApplicationInfoToCSV = $true
 
# These endpoints are called using GET method. Please modify the script below as required.
$URIForApplicationPermissionCall = "https://graph.microsoft.com/beta/reports/getTenantSecureScores(period=1)/content"
$URIForDelegatedPermissionCall = "https://graph.microsoft.com/v1.0/users"
 
# If using Delegated Permissions to execute a test call, you can specify username and password info here. 
# I strongly recommend securing these and not including them directly on the script. 
$UserForDelegatedPermissions = "user@domain.com"
$Password = "#########"
 
 
# Enter the required permissions below, separated by spaces eg: "Directory.Read.All Reports.Read.All Group.ReadWrite.All Directory.ReadWrite.All"
$ApplicationPermissions = "Reports.Read.All"
 
# Set DelegatePermissions to $null if you only require application permissions. 
$DelegatedPermissions = $null
# Otherwise, include the required delegated permissions below.
# $DelegatedPermissions = "Directory.Read.All Group.ReadWrite.All"
 
 
Function AddResourcePermission($requiredAccess, $exposedPermissions, $requiredAccesses, $permissionType) {
    foreach ($permission in $requiredAccesses.Trim().Split(" ")) {
        $reqPermission = $null
        $reqPermission = $exposedPermissions | Where-Object {$_.Value -contains $permission}
        Write-Host "Collected information for $($reqPermission.Value) of type $permissionType" -ForegroundColor Green
        $resourceAccess = New-Object Microsoft.Open.AzureAD.Model.ResourceAccess
        $resourceAccess.Type = $permissionType
        $resourceAccess.Id = $reqPermission.Id    
        $requiredAccess.ResourceAccess.Add($resourceAccess)
    }
}
 
Function GetRequiredPermissions($requiredDelegatedPermissions, $requiredApplicationPermissions, $reqsp) {
    $sp = $reqsp
    $appid = $sp.AppId
    $requiredAccess = New-Object Microsoft.Open.AzureAD.Model.RequiredResourceAccess
    $requiredAccess.ResourceAppId = $appid
    $requiredAccess.ResourceAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.ResourceAccess]
    if ($requiredDelegatedPermissions) {
        AddResourcePermission $requiredAccess -exposedPermissions $sp.Oauth2Permissions -requiredAccesses $requiredDelegatedPermissions -permissionType "Scope"
    } 
    if ($requiredApplicationPermissions) {
        AddResourcePermission $requiredAccess -exposedPermissions $sp.AppRoles -requiredAccesses $requiredApplicationPermissions -permissionType "Role"
    }
    return $requiredAccess
}
 
Function GenerateAppKey ($fromDate, $durationInYears, $pw) {
    $endDate = $fromDate.AddYears($durationInYears) 
    $keyId = (New-Guid).ToString();
    $key = New-Object Microsoft.Open.AzureAD.Model.PasswordCredential($null, $endDate, $keyId, $fromDate, $pw)
    return $key
}
 
Function CreateAppKey($fromDate, $durationInYears, $pw) {
 
    $testKey = GenerateAppKey -fromDate $fromDate -durationInYears $durationInYears -pw $pw
 
    while ($testKey.Value -match "\+" -or $testKey.Value -match "/") {
        Write-Host "Secret contains + or / and may not authenticate correctly. Regenerating..." -ForegroundColor Yellow
        $pw = ComputePassword
        $testKey = GenerateAppKey -fromDate $fromDate -durationInYears $durationInYears -pw $pw
    }
    Write-Host "Secret doesn't contain + or /. Continuing..." -ForegroundColor Green
    $key = $testKey
 
    return $key
}
 
Function ComputePassword {
    $aesManaged = New-Object "System.Security.Cryptography.AesManaged"
    $aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC
    $aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::Zeros
    $aesManaged.BlockSize = 128
    $aesManaged.KeySize = 256
    $aesManaged.GenerateKey()
    return [System.Convert]::ToBase64String($aesManaged.Key)
}
 
Function AddOAuth2PermissionGrants($DelegatedPermissions) {
    $resource = "https://graph.windows.net/"
    $client_id = $aadApplication.AppId
    $client_secret = $appkey.Value
    $authority = "https://login.microsoftonline.com/$tenant_id"
    $tokenEndpointUri = "$authority/oauth2/token"
    $content = "grant_type=client_credentials&client_id=$client_id&client_secret=$client_secret&resource=$resource"
 
    $Stoploop = $false
    [int]$Retrycount = "0"
 
    do {
        try {
            $response = Invoke-RestMethod -Uri $tokenEndpointUri -Body $content -Method Post -UseBasicParsing
            Write-Host "Retrieved Access Token for Azure AD Graph API" -ForegroundColor Green
            # Assign access token
            $access_token = $response.access_token
 
            $headers = @{
                Authorization = "Bearer $access_token"
            }
 
            if ($ConsentDelegatedPermissionsForAllUsers) {
                $principal = "AllPrincipals"
                $principalId = $null
            }
            else {
                $principal = "Principal"
                $principalId = (Get-AzureADUser -ObjectId $UserForDelegatedPermissions).ObjectId
            }
 
            $postbody = @{
                clientId    = $serviceprincipal.ObjectId
                consentType = $principal
                startTime   = ((get-date).AddDays(-1)).ToString("yyyy-MM-dd")
                principalId = $principalId
                resourceId  = $graphsp.ObjectId
                scope       = $DelegatedPermissions
                expiryTime  = ((get-date).AddYears(99)).ToString("yyyy-MM-dd")
            }
 
            $postbody = $postbody | ConvertTo-Json
 
            $body = Invoke-RestMethod -Uri "https://graph.windows.net/myorganization/oauth2PermissionGrants?api-version=1.6" -Body $postbody -Method POST -Headers $headers -ContentType "application/json"
            Write-Host "Created OAuth2PermissionGrants for $DelegatedPermissions" -ForegroundColor Green
 
            $Stoploop = $true
        }
        catch {
            if ($Retrycount -gt 5) {
                Write-Host "Could not get create OAuth2PermissionGrants after 6 retries." -ForegroundColor Red
                $Stoploop = $true
            }
            else {
                Write-Host "Could not create OAuth2PermissionGrants yet. Retrying in 5 seconds..." -ForegroundColor DarkYellow
                Start-Sleep -Seconds 5
                $Retrycount ++
            }
        }
    }
    While ($Stoploop -eq $false)
}
 
 
function GetOrCreateMicrosoftGraphServicePrincipal {
    $graphsp = Get-AzureADServicePrincipal -SearchString "Microsoft Graph"
    if (!$graphsp) {
        $graphsp = Get-AzureADServicePrincipal -SearchString "Microsoft.Azure.AgregatorService"
    }
    if (!$graphsp) {
        Login-AzureRmAccount
        New-AzureRmADServicePrincipal -ApplicationId "00000003-0000-0000-c000-000000000000"
        $graphsp = Get-AzureADServicePrincipal -SearchString "Microsoft Graph"
    }
 
    return $graphsp
}
 
Connect-AzureAd
Write-Host (Get-AzureADTenantDetail).displayName
 
# Check for a Microsoft Graph Service Principal. If it doesn't exist already, create it.
$graphsp = GetOrCreateMicrosoftGraphServicePrincipal
 
$existingapp = $null
$existingapp = get-azureadapplication -SearchString $applicationName
if ($existingapp) {
    Remove-Azureadapplication -ObjectId $existingApp.objectId
}

$rsps = @()
if ($graphsp) {
    $rsps += $graphsp
    $tenant_id = (Get-AzureADTenantDetail).ObjectId
    $tenantName = (Get-AzureADTenantDetail).DisplayName
    $azureadsp = Get-AzureADServicePrincipal -SearchString "Windows Azure Active Directory"
    $rsps += $azureadsp
 
    # Add Required Resources Access (Microsoft Graph)
    $requiredResourcesAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.RequiredResourceAccess]
    $microsoftGraphRequiredPermissions = GetRequiredPermissions -reqsp $graphsp -requiredApplicationPermissions $ApplicationPermissions -requiredDelegatedPermissions $DelegatedPermissions
    $requiredResourcesAccess.Add($microsoftGraphRequiredPermissions)
 
    if ($DelegatedPermissions) {
        Write-Host "Delegated Permissions specified, preparing permissions for Azure AD Graph API"
        # Add Required Resources Access (Azure AD Graph)
        $AzureADGraphRequiredPermissions = GetRequiredPermissions -reqsp $azureadsp -requiredApplicationPermissions "Directory.ReadWrite.All"
        $requiredResourcesAccess.Add($AzureADGraphRequiredPermissions)
    }
 
 
    # Get an application key
    $pw = ComputePassword
    $fromDate = [System.DateTime]::Now
    $appKey = CreateAppKey -fromDate $fromDate -durationInYears 2 -pw $pw
 
    Write-Host "Creating the AAD application $applicationName" -ForegroundColor Blue
    $aadApplication = New-AzureADApplication -DisplayName $applicationName `
        -HomePage $homePage `
        -ReplyUrls $homePage `
        -IdentifierUris $appIdURI `
        -LogoutUrl $logoutURI `
        -RequiredResourceAccess $requiredResourcesAccess `
        -PasswordCredentials $appKey
     
    # Creating the Service Principal for the application
    $servicePrincipal = New-AzureADServicePrincipal -AppId $aadApplication.AppId
 
    Write-Host "Assigning Permissions" -ForegroundColor Yellow
   
    # Assign application permissions to the application
    foreach ($app in $requiredResourcesAccess) {
 
        $reqAppSP = $rsps | Where-Object {$_.appid -contains $app.ResourceAppId}
        Write-Host "Assigning Application permissions for $($reqAppSP.displayName)" -ForegroundColor DarkYellow
 
        foreach ($resource in $app.ResourceAccess) {
            if ($resource.Type -match "Role") {
                New-AzureADServiceAppRoleAssignment -ObjectId $serviceprincipal.ObjectId `
                    -PrincipalId $serviceprincipal.ObjectId -ResourceId $reqAppSP.ObjectId -Id $resource.Id
            }
        }
    
    }
 
    # Assign delegated permissions to the application
    if ($requiredResourcesAccess.ResourceAccess -match "Scope") {
        Write-Host "Delegated Permissions found. Assigning permissions to required user"  -ForegroundColor DarkYellow
         
        foreach ($app in $requiredResourcesAccess) {
            $appDP = @()
            $reqAppSP = $rsps | Where-Object {$_.appid -contains $app.ResourceAppId}
 
            foreach ($resource in $app.ResourceAccess) {
                if ($resource.Type -match "Scope") {
                    $permission = $graphsp.oauth2permissions | Where-Object {$_.id -contains $resource.Id}
                    $appDP += $permission.Value
                }
            }
            if ($appDP) {
                Write-Host "Adding $appDP to user" -ForegroundColor DarkYellow
                $appDPString = $appDp -join " "
                AddOAuth2PermissionGrants -DelegatedPermissions $appDPString
            }
        }
    }
     
    Write-Host "App Created" -ForegroundColor Green
   
    # Define parameters for Microsoft Graph access token retrieval
    $client_id = $aadApplication.AppId;
    $client_secret = $appkey.Value
    $tenant_id = (Get-AzureADTenantDetail).ObjectId
    $resource = "https://graph.microsoft.com"
    $authority = "https://login.microsoftonline.com/$tenant_id"
    $tokenEndpointUri = "$authority/oauth2/token"
 
    # Get the access token using grant type password for Delegated Permissions or grant type client_credentials for Application Permissions
    if ($DelegatedPermissions -and $testCallRequiresDelegatePermissions) { 
        $content = "grant_type=password&client_id=$client_id&client_secret=$client_secret&username=$UserForDelegatedPermissions&password=$Password&resource=$resource";
        $testCallUri = $UriForDelegatedPermissionCall
    }
    else {
        $content = "grant_type=client_credentials&client_id=$client_id&client_secret=$client_secret&resource=$resource"
        $testCallUri = $UriForApplicationPermissionCall
    }
     
     
    # Try to execute the API call 6 times
 
    $Stoploop = $false
    [int]$Retrycount = "0"
    do {
        try {
            $response = Invoke-RestMethod -Uri $tokenEndpointUri -Body $content -Method Post -UseBasicParsing
            Write-Host "Retrieved Access Token" -ForegroundColor Green
            # Assign access token
            $access_token = $response.access_token
            $body = $null
 
            $body = Invoke-RestMethod `
                -Uri $testCallUri `
                -Headers @{"Authorization" = "Bearer $access_token"} `
                -ContentType "application/json" `
                -Method GET
                 
            Write-Host "Retrieved Graph content" -ForegroundColor Green
            $Stoploop = $true
        }
        catch {
            if ($Retrycount -gt 6) {
                Write-Host "Could not get Graph content after 7 retries." -ForegroundColor Red
                $Stoploop = $true
            }
            else {
                Write-Host "Could not get Graph content. Retrying in 5 seconds..." -ForegroundColor DarkYellow
                Start-Sleep -Seconds 5
                $Retrycount ++
            }
        }
    }
    While ($Stoploop -eq $false)
 
    if ($exportApplicationInfoToCSV) {
        $appProperties = @{
            ApplicationName        = $ApplicationName
            TenantName             = $tenantName
            TenantId               = $tenant_id
            clientId               = $client_id
            clientSecret           = $client_secret
            ApplicationPermissions = $ApplicationPermissions
            DelegatedPermissions   = $DelegatedPermissions
        }
     
        $AppInfo = New-Object PSObject -Property $appProperties
        $AppInfo | Select-Object ApplicationName, TenantName, TenantId, clientId, clientSecret, `
            ApplicationPermissions, DelegatedPermissions | Export-Csv C:\temp\AzureADApps.csv -Append -NoTypeInformation
    }
     
    if ($removeApplicationWhenComplete) {
        Remove-AzureADApplication -ObjectId $aadApplication.ObjectId
        $confirmRemoval = $null
        try {
            $confirmRemoval = Get-AzureADApplication -ObjectId $aadApplication.ObjectId
        }
        catch {
            Write-Host "Application Removed" -ForegroundColor Green
        }
    }
}
else {
    Write-Host "Microsoft Graph Service Principal could not be found or created" -ForegroundColor Red
}
 
# Export CSV of Secure Score
if ($body.secureScore) {
    Write-Host "Exporting Secure Score to CSV" -ForegroundColor Green
    $createdDateString = "$($body.createdDate.Year)-$($body.createdDate.Month)-$($body.createdDate.Day)"
    $body | Add-Member TenantName $tenantName
    $body | Add-Member dateCreated $createdDateString
    $createdDateString = $body | Select-Object @{n = "createdDate"; e = {"$($_.createdDate.Year)-$($_.createdDate.Month)-$($_.createdDate.Day)"}}
    $body | Select-Object TenantName, TenantId, DateCreated, secureScore, maxSecureScore, accountScore, dataScore, deviceScore, averageSecureScore `
        | Export-Csv C:\temp\SecureScore.csv -NoTypeInformation -Append
}

Some problems I ran into

If you’re looking at automating this process yourself, it might be helpful to learn from my mistakes.

Generated secrets with + or / characters would not authenticate correctly

This was a reported issue in a few forums. This script will keep generating application secrets until they don’t include a + or / character.

New-AzureAdApplicationPasswordCredential doesn’t seem to create a valid secret each time.

I had some issues using this command to generate secrets for the application. This script generates and uploads it’s own key when creating the application.

Microsoft Graph is missing from some tenants.

I wasn’t able to find the Microsoft Graph service principal in some of our customer tenants. There didn’t seem to be many commonalities between the tenants that were missing it. Some were quite old tenants, and some were relatively new. I’m not sure why the service principal was missing, however it might have been that users in the organisation had never manually consented an application to access the Microsoft Graph.

In any case, the Microsoft Graph can be found or created using the following methods.

The Microsoft Graph is sometimes listed as the Microsoft.Azure.AgregatorService Service Principal.

If you don’t get anything returned by running:

Get-AzureAdServicePrincipal -SearchString "Microsoft.Graph"

You can run:

Get-AzureAdServicePrincipal -SearchString "Microsoft.Azure.AgregatorService"

If you still don’t find anything, it’s possible that the Service Principal does not yet exist in the tenant.

You can create it by logging into the tenant using the AzureRM PowerShell Module

Login-AzureRMAccount

New-AzureRmADServicePrincipal -ApplicationId "00000003-0000-0000-c000-000000000000"

This will create the Microsoft Graph Service Principal in your tenant. Note that this script will automatically attempt to locate or generate the Microsoft Graph service principal for you.

Credit: a portion of this script uses a sample from Jean-Marc Prieur: See it here: https://github.com/Azure-Samples/active-directory-dotnet-webapp-roleclaims

Was this article helpful?

Related Articles