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

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

```powershell
# 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 👇 or even better [check this article to get automated solution.](https://doitpshway.com/import-new-or-update-existing-powershell-module-including-its-dependencies-into-azure-automation-account-using-powershell)

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1652358660229/EP-Py4PtR.png align="left")

---

# AzureAD (using Connect-AzAccount)

* Connect to AzureAD using [AZ](https://github.com/Azure/azure-powershell) 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

```powershell
Connect-AzAccount -Identity
```

## Get some data

```powershell
Get-AzADUser -UserPrincipalName "john@contoso.com"
```

---

# AzureAD (using Connect-AzureAD)

* Connect to AzureAD using the [AzureAD](https://docs.microsoft.com/en-us/powershell/module/azuread/?view=azureadps-2.0) module
    

AzureAD module shouldn't be used, because AAD Graph will be [deprecated soon](https://docs.microsoft.com/en-us/answers/questions/813086/connect-azuread-using-managed-identity-without-azu.html). **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

```powershell
$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

```powershell
Get-AzureADUser -SearchString "john"
```

---

# Exchange Online

## Set permissions

* To work with Exchange, the account needs Exchange application permission `Exchange.ManageAsApp` and Azure Directory role `Exchange Administrator` or EXO management role (built-in or a custom one). Both can be set using the code below.
    
    * [Official documentation](https://learn.microsoft.com/en-us/powershell/exchange/connect-exo-powershell-managed-identity?view=exchange-ps#step-4-grant-the-exchangemanageasapp-api-permission-for-the-managed-identity-to-call-exchange-online)
        
    * How to analyze [what EXO role is needed for specific command to run](https://learn.microsoft.com/en-us/powershell/exchange/find-exchange-cmdlet-permissions?view=exchange-ps)
        

```powershell
Connect-MgGraph
$EXOServicePrincipal = Get-MgServicePrincipal -Filter "displayName eq 'Office 365 Exchange Online'"
$Approle = $EXOServicePrincipal.AppRoles.Where({ $_.Value -eq 'Exchange.ManageAsApp' })
# assign Exchange.ManageAsApp permission
New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $MSIObjectID -PrincipalId $MSIObjectID -AppRoleId $Approle.Id -ResourceId $EXOServicePrincipal.Id


# 1. add to 'Exchange Administrator' role
$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


# 2. create custom EXO management role and assign it to our managed identity
# https://blog.nathanmcnulty.com/azure-automation-advanced-auditing/

# Connect to Exchange Online 
Connect-ExchangeOnline

# get list of all EXO management roles
#Get-ManagementRole

# Create linked Service Principal
New-ServicePrincipal -AppId $MSIAppID -ServiceId $MSIObjectID -DisplayName "IT_EnableO365AdvancedLogging"

# Create new Management role
New-ManagementRole -Name "Mailbox Auditing" -Parent "Audit Logs" -Verbose

# Remove unnecessary permissions
Get-ManagementRoleEntry "Mailbox Auditing\*" | Where-Object { $_.Name -notin "Get-Mailbox" } | ForEach-Object { Remove-ManagementRoleEntry -Identity "Mailbox Auditing\$($_.Name)" -Verbose -Confirm:$false }

# Add limited Set-Mailbox permissions
Add-ManagementRoleEntry -Identity "Mailbox Auditing\Set-Mailbox" -Parameters "Identity", "AuditAdmin", "AuditDelegate", "AuditOwner", "AuditLogAgeLimit", "AuditEnabled"

# Create a Role Group, add our custom Mailbox Auditing role, and add our Service Principal as a member
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](https://learn.microsoft.com/en-us/powershell/exchange/connect-to-exchange-online-powershell?view=exchange-ps)

### NEW ExchangeOnlineManagement V3 module way (REST API)

Connect to Exchange using the [ExchangeOnlineManagement V3](https://www.powershellgallery.com/packages/ExchangeOnlineManagement/3.0.0) module which is the **preferred and easier** way!

```powershell

$tenantDomain = "contoso.onmicrosoft.com" # Domain of the tenant the managed identity belongs to 
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](https://github.com/Azure/azure-powershell) module and OAuth token.

```powershell
$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

```powershell
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 # for old V2 connections
Disconnect-ExchangeOnline -Confirm:$false # for new V3 connections
```

---

# Sharepoint Online

* Connect to Sharepoint Online using [PnP.PowerShell](https://www.powershellgallery.com/packages/PnP.PowerShell) module
    
* [This original and very good post where I found the information below 👇](https://pnp.github.io/powershell/articles/azurefunctions.html#by-using-a-managed-identity)
    

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](https://pnp.github.io/powershell/articles/azurefunctions.html#assigning-microsoft-graph-permissions-to-the-managed-identity)
    
* Required permissions depend on your needs, so for example, when you just want to read groups, you can grant permissions like this.
    

```powershell
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

```powershell
Connect-PnPOnline -ManagedIdentity
```

## Get some data

```powershell
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).
    

```powershell
# list of all Graph permissions + description https://graphpermissions.merill.net/index.html
$permissionList = 'Group.Read.All', 'User.Read.All'
# get Graph API app
$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

* Requires [Microsoft.Graph.Authentication](https://www.powershellgallery.com/packages/Microsoft.Graph.Authentication) module
    
* More info at [devblogs.microsoft.com](https://devblogs.microsoft.com/microsoft365dev/microsoft-graph-powershell-v2-is-now-in-public-preview-half-the-size-and-will-speed-up-your-automations/#a-system-assigned-managed-identity) 👈
    

### Connect

```powershell
# using 2.x.x version of Microsoft.Graph.Authentication module
Connect-MgGraph -Identity

# using 1.x.x versions of Microsoft.Graph.Authentication module
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

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

Get-MgContext
```

## Invoke-RestMethod way

* Requires [Microsoft.Graph.Authentication](https://www.powershellgallery.com/packages/Microsoft.Graph.Authentication) module
    

### Connect

```powershell
Connect-AzAccount -Identity
$token = (Get-AzAccessToken -ResourceTypeName MSGraph).token # Get-PnPAccessToken if connected to Sharepoint already
$header = @{
    "Content-Type" = "application/json"
    Authorization  = "Bearer $token"
}
```

### Get some data

```powershell
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).
    

```powershell
# 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'
# get Graph API app
$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

```powershell
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

```powershell
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

* **With the <mark>v1 version</mark> of the Microsoft.Graph.Authentication module this didn't work very well. In case of any problems, use the previous (Invoke-RestMethod) method**
    
    * `Invoke-MgGraphRequest` always threw `Forbidden` error, some cmdlets worked without any problem, and some others returned an error that I am missing some permissions that were already assigned 🤷‍♀️
        

### Connect

```powershell
# 2.0.0-preview2 version of Microsoft.Graph.Authentication module
Connect-MgGraph -Identity

# previous versions of Microsoft.Graph.Authentication module
$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

```powershell
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
    

---
