How to use Managed Identity to connect to Azure, Exchange, Graph, Intune,... in Azure Automation Runbook

Ondrej Sebela's photo
Ondrej Sebela
·May 12, 2022·

6 min read

Subscribe to my newsletter and never miss my upcoming articles

Table of contents

Managed Identity is definitely a better option for authentication in Azure Automation Runbooks than 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 information I was able to find on the internet plus my personal experience into this blog post.

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

# display name of the automation account
$automationAccountDisplayName = "myautomationaccountname"
# ObjectID of the System assigned (Managed identity)
$MSIObjectID = "bd5009b5-...-236e7f103696" 
# AppID of the Enterprise application that represents System assigned (Managed identity) 
$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 👇 image.png


AzureAD (using Connect-AzAccount)

  • Connect to AzureAD using AZ module

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

Required 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

  • Connect to Exchange Online using AZ module

Required permissions

  • Needs Exchange.ManageAsApp Exchange application permission andExchange Administrator role. Both can be set using the code below.
    Connect-AzureAD
    $EXOServicePrincipal = Get-AzureADServicePrincipal -Filter "displayName eq 'Office 365 Exchange Online'"
    $Approle = $EXOServicePrincipal.AppRoles.Where({ $_.Value -eq 'Exchange.ManageAsApp' })
    New-AzureADServiceAppRoleAssignment -ObjectId $MSIObjectID -Id $Approle[0].Id -PrincipalId $MSIObjectID -ResourceId $EXOServicePrincipal.ObjectId
    $AADRole = Get-AzureADDirectoryRole | where DisplayName -EQ 'Exchange Administrator'
    Add-AzureADDirectoryRoleMember -ObjectId $AADRole.ObjectId -RefObjectId $MSIObjectID
    
    You have to manually grant Admin consent to added permissions.

Connect

Don't forget to set variable $tenantDomain!

$tenantDomain = "contoso.onmicrosoft.com" # Domain of the tenant the managed identity belongs to
#region functions
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) {
    #Function to connect to Exchange Online using OAuth credentials from the MSI
    $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
    }
}
#endregion functions

$null = Connect-AzAccount -Identity
# connect using Managed Identity (but using basic auth!)
connectEXOAsMSI -OAuthCredential (makeMSIOAuthCred)

Get some data

Get-Mailbox "john"

# don't forget to disconnect the Exchange session to avoid throttling (there is a limit to open session).
Get-PSSession | Remove-PSSession

Graph API (Azure)

  • For whatevere reason this method doesn't work for Intune Graph requests, therefore Intune is in separate paragraph

    Required permissions

  • Required permissions depends on your needs, so for example when you just want to read users and groups, you can grant permissions like this.
# '00000003-0000-0000-c000-000000000000' is Graph application
$resourceAppId = '00000003-0000-0000-c000-000000000000'
# list of all Graph permissions + description https://graphpermissions.merill.net/index.html
$permissionList = 'Group.Read.All', 'User.Read.All'

$MSI = (Get-AzureADServicePrincipal -Filter "displayName eq '$automationAccountDisplayName'")
if (!$MSI) { throw "Automation account '$automationAccountDisplayName' doesn't exist" }
$resourceSP = Get-AzureADServicePrincipal -Filter "appId eq '$resourceAppId'"
if (!$MSI) { throw "Resource '$resourceAppId' doesn't exist" }
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-AzureADServiceAppRoleAssignment -ObjectId $MSI.ObjectId -PrincipalId $MSI.ObjectId -ResourceId $resourceSP.ObjectId -Id $AppRole.Id
}

There are two main ways how to interact with the Graph API. Using the official PS Microsoft.Graph module (Connect-MgGraph way) and using web request (Invoke-RestMethod way). I will show you both.

Connect-MgGraph way

Connect

Connect-AzAccount -Identity
$token = (Get-AzAccessToken -ResourceTypeName MSGraph).token # Get-PnPAccessToken if you are already connected to Sharepoint
Connect-MgGraph -AccessToken $token

Get some data

Invoke-MgGraphRequest -method GET -Uri "https://graph.microsoft.com/v1.0/users/" -OutputType PSObject

Invoke-RestMethod way

Connect

Connect-AzAccount -Identity
$token = (Get-AzAccessToken -ResourceTypeName MSGraph).token # Get-PnPAccessToken pokud jsem uz prihlaseny do sharepointu
$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)

I don't know why but all my attempts when Managed identity was used, failed with 403 (forbidden) error. Even though that Managed identity account service principal had exactly the same application permissions as automation runas account service principal.

Required permissions

  • Required permissions depends on your needs, so for example I will grant read permissions to most of the Intune parts like this.
# '00000003-0000-0000-c000-000000000000' graph api
$resourceAppId = '00000003-0000-0000-c000-000000000000'
# list of all Graph permissions + description https://graphpermissions.merill.net/index.html
$permissionList = 'Device.Read.All', 'DeviceManagementApps.Read.All', 'DeviceManagementConfiguration.Read.All', 'DeviceManagementManagedDevices.Read.All', 'DeviceManagementRBAC.Read.All', 'DeviceManagementServiceConfig.Read.All'

$MSI = (Get-AzureADServicePrincipal -Filter "displayName eq '$automationAccountDisplayName'")
if (!$MSI) { throw "Automation account '$automationAccountDisplayName' doesn't exist" }
$resourceSP = Get-AzureADServicePrincipal -Filter "appId eq '$resourceAppId'"
if (!$MSI) { throw "Resource '$resourceAppId' doesn't exist" }
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-AzureADServiceAppRoleAssignment -ObjectId $MSI.ObjectId -PrincipalId $MSI.ObjectId -ResourceId $resourceSP.ObjectId -Id $AppRole.Id
}

Invoke-RestMethod way

Connect

function Get-AuthToken {
    try {
        # obtain AccessToken for Microsoft Graph via the managed identity
        $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

        # construct AuthHeader
        $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

Invoke-MgGraphRequest -Uri 'https://graph.microsoft.com/v1.0/deviceManagement/managedDevices?$select=deviceName,userDisplayName' -Method GET -Headers $header -OutputType PSObject

Connect-MgGraph way

  • THIS DIDN'T WORK VERY WELL FOR ME SO IN CASE OF ANY PROBLEMS, USE PREVIOUS (Invoke-RestMethod) METHOD
    • Invoke-MgGraphRequest always threw Forbidden error, some cmdlets worked without any problem, some others returned an error that I am missing some permissions which were already assigned 🤷‍♀️

Connect

$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-MgDeviceManagement

Links


Did you find this article valuable?

Support Ondrej Sebela by becoming a sponsor. Any amount is appreciated!

See recent sponsors Learn more about Hashnode Sponsors
 
Share this