Updated 6.6.2024
Managed Identity is definitely a better option for authentication in Azure Automation Runbooks than the RunAs account because it doesn't require certificate/secret renewal. Therefore it is maintenance-free. However, it took me a while to figure out how to use it to connect to various Azure services like Azure, Exchange, Graph API, Intune,...Moreover, some of the modules we are used to use don't work quite right with it. Therefore I decided to put all the information I was able to find on the internet plus my personal experience into this blog post.
For the sake of this post, I assume you have created an Azure Automation account with enabled Managed Identity.
Before we begin
You will need to define these variables in your PowerShell console before continuing. Of course, use IDs from your environment.
$automationAccountDisplayName = "myautomationaccountname"
$MSIObjectID = "bd5009b5-...-236e7f103696"
$MSIAppId = "9905d24f-...-17cc71ea7d9a"
How to add PS module to Azure Automation account
In this post, I mention several PS modules that are not available by default in the new Automation Account. To add a new module use 👇 or even better check this article to get automated solution.

AzureAD (using Connect-AzAccount)
- Connect to AzureAD using AZ module
Set permissions
- Add
Managed identity account to any Directory role you need (Security Reader or Directory Reader roles should be fine if you don't need to change anything)
Connect
Connect-AzAccount -Identity
Get some data
Get-AzADUser -UserPrincipalName "john@contoso.com"
AzureAD (using Connect-AzureAD)
- Connect to AzureAD using the AzureAD module
AzureAD module shouldn't be used, because AAD Graph will be deprecated soon. Use Connect-AzAccount instead.
Set permissions
- Add
Managed identity account to any Directory role you need (Security Reader or Directory Reader roles should be fine if you don't need to change anything)
Connect
$azureContext = Connect-AzAccount -Identity
$azureContext = Set-AzContext -SubscriptionName $azureContext.context.Subscription -DefaultProfile $azureContext.context
$graphToken = Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com/"
$aadToken = Get-AzAccessToken -ResourceUrl "https://graph.windows.net"
Connect-AzureAD -AccountId $azureContext.account.id -TenantId $azureContext.tenant.id -AadAccessToken $aadToken.token -MsAccessToken $graphToken.token
Get some data
Get-AzureADUser -SearchString "john"
Exchange Online
Set permissions
Connect-MgGraph
$EXOServicePrincipal = Get-MgServicePrincipal -Filter "displayName eq 'Office 365 Exchange Online'"
$Approle = $EXOServicePrincipal.AppRoles.Where({ $_.Value -eq 'Exchange.ManageAsApp' })
New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $MSIObjectID -PrincipalId $MSIObjectID -AppRoleId $Approle.Id -ResourceId $EXOServicePrincipal.Id
$AADRole = Get-MgDirectoryRole | where DisplayName -EQ 'Exchange Administrator'
$DirObject = @{
"@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$MSIObjectID"
}
New-MgDirectoryRoleMemberByRef -DirectoryRoleId $AADRole.Id -BodyParameter $DirObject
Connect-ExchangeOnline
New-ServicePrincipal -AppId $MSIAppID -ServiceId $MSIObjectID -DisplayName "IT_EnableO365AdvancedLogging"
New-ManagementRole -Name "Mailbox Auditing" -Parent "Audit Logs" -Verbose
Get-ManagementRoleEntry "Mailbox Auditing\*" | Where-Object { $_.Name -notin "Get-Mailbox" } | ForEach-Object { Remove-ManagementRoleEntry -Identity "Mailbox Auditing\$($_.Name)" -Verbose -Confirm:$false }
Add-ManagementRoleEntry -Identity "Mailbox Auditing\Set-Mailbox" -Parameters "Identity", "AuditAdmin", "AuditDelegate", "AuditOwner", "AuditLogAgeLimit", "AuditEnabled"
New-RoleGroup "Advanced Auditing Management" -Description "Limited scope for Azure Automation to set Advanced Auditing entries" -Roles "Mailbox Auditing" -Members $MSIObjectID -Confirm:$false -Verbose
Connect
More info at the official documentation
NEW ExchangeOnlineManagement V3 module way (REST API)
Connect to Exchange using the ExchangeOnlineManagement V3 module which is the preferred and easier way!
$tenantDomain = "contoso.onmicrosoft.com"
Connect-ExchangeOnline -ManagedIdentity -Organization $tenantDomain
OLD Exchange Online PowerShell V2 module way (OAuth)
Avoid this connection option if possible and use the previous V3 version instead! Connects to Exchange Online using AZ module and OAuth token.
$tenantDomain = "contoso.onmicrosoft.com"
function makeMSIOAuthCred () {
$accessToken = Get-AzAccessToken -ResourceUrl "https://outlook.office365.com/"
$authorization = "Bearer {0}" -f $accessToken.Token
$Password = ConvertTo-SecureString -AsPlainText $authorization -Force
$tenantID = (Get-AzTenant).Id
$MSIcred = New-Object System.Management.Automation.PSCredential -ArgumentList ("OAuthUser@$tenantID", $Password)
return $MSICred
}
function connectEXOAsMSI ($OAuthCredential) {
$psSessions = Get-PSSession | Select-Object -Property State, Name
If (((@($psSessions) -like '@{State=Opened; Name=RunSpace*').Count -gt 0) -ne $true) {
Write-Verbose "Creating new EXOPSSession..." -Verbose
try {
$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "https://outlook.office365.com/PowerShell-LiveId?BasicAuthToOAuthConversion=true&email=SystemMailbox%7bbb558c35-97f1-4cb9-8ff7-d53741dc928c%7d%40$tenantDomain" -Credential $OAuthCredential -Authentication Basic -AllowRedirection
$null = Import-PSSession $Session -DisableNameChecking -CommandName "*mailbox*", "*unified*" -AllowClobber
Write-Verbose "New EXOPSSession established!" -Verbose
} catch {
Write-Error $_
}
} else {
Write-Verbose "Found existing EXOPSSession! Skipping connection." -Verbose
}
}
$null = Connect-AzAccount -Identity
connectEXOAsMSI -OAuthCredential (makeMSIOAuthCred)
Get some data
Get-Mailbox "john"
Get-PSSession | Remove-PSSession
Disconnect-ExchangeOnline -Confirm:$false
Sharepoint Online
At present, only permissions can be granted to the Microsoft Graph and not to the SharePoint APIs, which effectively means that most of the PnP PowerShell cmdlets will not work. Only those solely and directly communicating with the Microsoft Graph, will be authorized to work, such as but not limited to: Get-PnPAzureAdUser, Get-PnPMicrosoft365Group, and Get-PnPTeamsTeam.
Set permissions
Detailed info
Required permissions depend on your needs, so for example, when you just want to read groups, you can grant permissions like this.
Connect-AzAccount
$graphServicePrincipal = Get-AzADServicePrincipal -SearchString "Microsoft Graph" | Select-Object -First 1
$appRole = $graphServicePrincipal.AppRole | Where-Object { $_.AllowedMemberType -eq "Application" -and $_.Value -eq "Group.Read.All" }
Add-AzADAppPermission -ObjectId $MSIObjectID -ApiId $graphServicePrincipal.AppId -PermissionId $appRole.Id -Type 'Role'
Connect
Connect-PnPOnline -ManagedIdentity
Get some data
Get-PnPMicrosoft365Group
Graph API (Azure)
- For whatever reason this method doesn't work for Intune Graph requests, therefore Intune is in a separate paragraph
Set permissions
- Required permissions depend on your needs, so for example, when you just want to read users (User.Read.All) and groups (Group.Read.All), you can grant permissions like this (run commands below in your PowerShell console).
$permissionList = 'Group.Read.All', 'User.Read.All'
$resourceSP = Get-MgServicePrincipal -Filter "startswith(DisplayName,'Microsoft Graph')" | Select-Object -First 1
foreach ($permission in $permissionList) {
$AppRole = $resourceSP.AppRoles | Where-Object { $_.Value -eq $permission -and $_.AllowedMemberTypes -contains "Application" }
if (!$AppRole) {
Write-Warning "Application permission '$permission' wasn't found in '$resourceAppId' application. Therefore it cannot be added."
continue
}
New-MgServicePrincipalAppRoleAssignment -AppRoleId $AppRole.Id -ResourceId $resourceSP.Id -ServicePrincipalId $MSIObjectID -PrincipalId $MSIObjectID
}
There are two main ways how to interact with the Graph API. Using the official PS Microsoft.Graph.Authentication module (Connect-MgGraph way) and using web request (Invoke-RestMethod way). I will show you both.
Connect-MgGraph way
Connect
Connect-MgGraph -Identity
Connect-AzAccount -Identity
$token = (Get-AzAccessToken -ResourceTypeName MSGraph).token
Connect-MgGraph -AccessToken $token
Get some data
Invoke-MgGraphRequest -method GET -Uri "https://graph.microsoft.com/v1.0/users/" -OutputType PSObject
Get-MgContext
Invoke-RestMethod way
Connect
Connect-AzAccount -Identity
$token = (Get-AzAccessToken -ResourceTypeName MSGraph).token
$header = @{
"Content-Type" = "application/json"
Authorization = "Bearer $token"
}
Get some data
Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/users/" -Method Get -Headers $header
Graph API (Intune)
Set permissions
- Required permissions depend on your needs, so for example, I will grant read permissions to most of the Intune parts like this (run commands below in your PowerShell console).
$permissionList = 'Device.Read.All', 'DeviceManagementApps.Read.All', 'DeviceManagementConfiguration.Read.All', 'DeviceManagementManagedDevices.Read.All', 'DeviceManagementRBAC.Read.All', 'DeviceManagementServiceConfig.Read.All'
$resourceSP = Get-MgServicePrincipal -Filter "startswith(DisplayName,'Microsoft Graph')" | Select-Object -First 1
foreach ($permission in $permissionList) {
$AppRole = $resourceSP.AppRoles | Where-Object { $_.Value -eq $permission -and $_.AllowedMemberTypes -contains "Application" }
if (!$AppRole) {
Write-Warning "Application permission '$permission' wasn't found in '$resourceAppId' application. Therefore it cannot be added."
continue
}
New-MgServicePrincipalAppRoleAssignment -AppRoleId $AppRole.Id -ResourceId $resourceSP.Id -ServicePrincipalId $MSIObjectID -PrincipalId $MSIObjectID
}
Invoke-RestMethod way
Connect
function Get-AuthToken {
try {
$ResourceURL = "https://graph.microsoft.com"
$Response = [System.Text.Encoding]::Default.GetString((Invoke-WebRequest -UseBasicParsing -Uri "$($env:IDENTITY_ENDPOINT)?resource=$resourceURL" -Method 'GET' -Headers @{'X-IDENTITY-HEADER' = "$env:IDENTITY_HEADER"; 'Metadata' = 'True' }).RawContentStream.ToArray()) | ConvertFrom-Json
$AuthHeader = @{
'Content-Type' = 'application/json'
'Authorization' = "Bearer " + $Response.access_token
}
} catch {
throw $_
}
return $authHeader
}
$header = Get-AuthToken
Get some data
Invoke-RestMethod -Uri 'https://graph.microsoft.com/v1.0/deviceManagement/managedDevices?$select=deviceName,userDisplayName' -Method GET -Headers $header
Connect-MgGraph way
Connect
Connect-MgGraph -Identity
$response = [System.Text.Encoding]::Default.GetString((Invoke-WebRequest -UseBasicParsing -Uri "$($env:IDENTITY_ENDPOINT)?resource=https://graph.microsoft.com/" -Method 'GET' -Headers @{'X-IDENTITY-HEADER' = "$env:IDENTITY_HEADER"; 'Metadata' = 'True' }).RawContentStream.ToArray()) | ConvertFrom-Json
$null = Connect-MgGraph -AccessToken $response.access_token
Get some data
Get-MgDeviceManagementManagedDevice
Invoke-MgGraphRequest -Uri 'https://graph.microsoft.com/v1.0/deviceManagement/managedDevices?$select=deviceName,userDisplayName' -Method GET -OutputType PSObject
Links
- https://github.com/mardahl/ExchangeOnlineScripts/blob/main/AzureAutomation/ConnectEXOwithMSIRunbookExample.ps1