Automate Azure PIM Role Activation for Entra ID + Azure Resources with PowerShell
If you're working in a Zero Trust security environment, your Azure roles are likely managed through Privileged Identity Management (PIM) — meaning they expire and need to be manually re-activated every few hours. Clicking through the portal every time is tedious.
This PowerShell script automates activation for both Entra ID directory roles (via Microsoft Graph) and Azure resource roles (via ARM REST API) in a single run — with automatic detection of the maximum allowed duration from your PIM policy.
What This Script Does
- Auto-installs all required PowerShell modules if not present
- Activates all eligible Entra ID (Microsoft Graph) PIM roles — e.g. Global Reader, Security Administrator
- Activates all eligible Azure Resource PIM roles — e.g. Contributor, Owner on subscriptions
- Auto-detects the maximum allowed activation duration from your PIM policy per role
- Displays a clean summary table of all eligible roles before activation
Prerequisites
- PowerShell 5.1+ or PowerShell 7+
- Internet access (modules are auto-installed if missing)
- An account with eligible PIM role assignments in Entra ID or Azure subscriptions
The script will auto-install these modules if not found:
| Module | Purpose |
|---|---|
Az.Accounts | Azure authentication & context |
Az.Resources | Azure AD user lookup |
Microsoft.Graph.Authentication | Graph API authentication |
Microsoft.Graph.Identity.Governance | PIM role management |
Microsoft.Graph.Identity.SignIns | Sign-in & identity operations |
The Script
# ============================================================
# Azure PIM Role Activator - Entra ID + Azure Resources
# ============================================================
# ── Install & Import Modules ─────────────────────────────────────────────────
foreach ($module in @("Az.Accounts", "Az.Resources")) {
if (-not (Get-Module -ListAvailable -Name $module)) {
Write-Host "Installing $module..." -ForegroundColor Yellow
Install-Module $module -Scope CurrentUser -Force -AllowClobber
}
Import-Module $module -ErrorAction Stop
}
$targetVersion = (Get-Module -ListAvailable -Name Microsoft.Graph.Authentication | Sort-Object Version -Descending | Select-Object -First 1).Version
if (-not $targetVersion) {
Install-Module Microsoft.Graph.Authentication -Scope CurrentUser -Force -AllowClobber
$targetVersion = (Get-Module -ListAvailable -Name Microsoft.Graph.Authentication | Sort-Object Version -Descending | Select-Object -First 1).Version
}
foreach ($module in @("Microsoft.Graph.Authentication", "Microsoft.Graph.Identity.Governance", "Microsoft.Graph.Identity.SignIns")) {
if (-not (Get-Module -ListAvailable -Name $module | Where-Object { $_.Version -eq $targetVersion })) {
Write-Host "Installing $module $targetVersion..." -ForegroundColor Yellow
Install-Module $module -RequiredVersion $targetVersion -Scope CurrentUser -Force -AllowClobber
}
Import-Module $module -RequiredVersion $targetVersion -ErrorAction Stop
}
# ============================================================
# PART 1 — Entra ID (Microsoft Graph) PIM Roles
# ============================================================
Write-Host "`n===== ENTRA ID ROLES =====" -ForegroundColor Cyan
Disconnect-MgGraph -ErrorAction SilentlyContinue
Connect-MgGraph -Scopes "User.Read", "RoleManagement.ReadWrite.Directory", "RoleAssignmentSchedule.ReadWrite.Directory" -NoWelcome
$user = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/me" -Method GET
if (-not $user) { Write-Error "Failed to retrieve user."; exit }
Write-Host "Logged in as: $($user.userPrincipalName)" -ForegroundColor Green
$eligibleRoles = Get-MgRoleManagementDirectoryRoleEligibilitySchedule -Filter "principalId eq '$($user.id)'" -ExpandProperty RoleDefinition
if (-not $eligibleRoles) { Write-Warning "No eligible Entra ID PIM roles found." }
foreach ($role in $eligibleRoles) {
try {
$rawDuration = ((Get-MgPolicyRoleManagementPolicyAssignment `
-Filter "scopeId eq '/' and scopeType eq 'DirectoryRole' and roleDefinitionId eq '$($role.RoleDefinitionId)'" `
-ExpandProperty "Policy(`$expand=Rules)").Policy.Rules | Where-Object { $_.Id -eq "Expiration_EndUser_Assignment" }).AdditionalProperties["maximumDuration"]
$hours = if ($rawDuration -match "PT(\d+)H") { [int]$Matches[1] } elseif ($rawDuration -match "P(\d+)D") { [int]$Matches[1]*24 } else { 8 }
New-MgRoleManagementDirectoryRoleAssignmentScheduleRequest -BodyParameter @{
Action = "selfActivate"
PrincipalId = $user.id
RoleDefinitionId = $role.RoleDefinitionId
DirectoryScopeId = $role.DirectoryScopeId
Justification = "M365 Team"
ScheduleInfo = @{ StartDateTime = (Get-Date).ToUniversalTime(); Expiration = @{ Type = "AfterDuration"; Duration = "PT${hours}H" } }
} | Out-Null
Write-Host " ✔ Activated : $($role.RoleDefinition.DisplayName) for $hours hr(s)" -ForegroundColor Green
}
catch { Write-Host " ✘ Failed : $($role.RoleDefinition.DisplayName) — $($_.Exception.Message)" -ForegroundColor Red }
}
# ============================================================
# PART 2 — Azure Resources (ARM) PIM Roles
# ============================================================
Write-Host "`n===== AZURE RESOURCE ROLES =====" -ForegroundColor Cyan
Connect-AzAccount
$tokenObj = Get-AzAccessToken -ResourceUrl "https://management.azure.com"
$token = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(
[System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($tokenObj.Token))
$headers = @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" }
$userId = (Get-AzADUser -SignedIn).Id
Write-Host "Subscription : $((Get-AzContext).Subscription.Name)" -ForegroundColor Green
Write-Host "User ID : $userId" -ForegroundColor Green
$roles = (Invoke-RestMethod `
-Uri "https://management.azure.com/providers/Microsoft.Authorization/roleEligibilityScheduleInstances?api-version=2020-10-01&`$filter=asTarget()" `
-Headers $headers).value
if (-not $roles) { Write-Warning "No eligible Azure resource roles found." } else {
$roles | ForEach-Object {
[PSCustomObject]@{
Role = $_.properties.expandedProperties.roleDefinition.displayName
Resource = $_.properties.expandedProperties.scope.displayName
Type = $_.properties.expandedProperties.scope.type
Status = $_.properties.status
}
} | Format-Table -AutoSize
}
Write-Host "`n===== ACTIVATING ALL ROLES =====" -ForegroundColor Yellow
foreach ($role in $roles) {
$roleName = $role.properties.expandedProperties.roleDefinition.displayName
$scopePath = $role.properties.scope
$roleDefId = $role.properties.roleDefinitionId
# ── Auto-detect max allowed hours from PIM policy assignment ─────────────
try {
$policyAssignments = (Invoke-RestMethod `
-Uri "https://management.azure.com$scopePath/providers/Microsoft.Authorization/roleManagementPolicyAssignments?api-version=2020-10-01&`$filter=roleDefinitionId eq '$roleDefId'" `
-Headers $headers).value
$policyId = $policyAssignments[0].properties.policyId
$rules = (Invoke-RestMethod `
-Uri "https://management.azure.com$policyId`?api-version=2020-10-01" `
-Headers $headers).properties.effectiveRules
$expiryRule = $rules | Where-Object { $_.id -eq "Expiration_EndUser_Assignment" }
$rawDuration = $expiryRule.maximumDuration
$hours = if ($rawDuration -match "PT(\d+)H") { [int]$Matches[1] }
elseif ($rawDuration -match "P(\d+)D") { [int]$Matches[1] * 24 }
else { 1 }
Write-Host " Policy max duration for '$roleName': $rawDuration ($hours hr)" -ForegroundColor DarkGray
}
catch {
$hours = 1
Write-Host " Could not read policy for '$roleName', defaulting to 1hr" -ForegroundColor DarkYellow
}
# ── Activate the role ─────────────────────────────────────────────────────
try {
$body = @{ properties = @{
principalId = $userId
roleDefinitionId = $roleDefId
requestType = "SelfActivate"
linkedRoleEligibilityScheduleId = $role.properties.roleEligibilityScheduleId
justification = "Self activation"
scheduleInfo = @{ expiration = @{ type = "AfterDuration"; duration = "PT${hours}H" } }
}} | ConvertTo-Json -Depth 10
Invoke-RestMethod `
-Uri "https://management.azure.com$scopePath/providers/Microsoft.Authorization/roleAssignmentScheduleRequests/$([guid]::NewGuid())?api-version=2020-10-01" `
-Headers $headers -Method Put -Body $body | Out-Null
Write-Host " ✔ Activated : $roleName → $($role.properties.expandedProperties.scope.displayName) for $hours hour(s)" -ForegroundColor Green
}
catch {
$err = ($_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue).error.message
Write-Host " ✘ Failed : $roleName — $err" -ForegroundColor Red
}
}
Write-Host "`nDone!" -ForegroundColor Cyan
How It Works
Module Auto-Install — The script checks for all required Az and Microsoft Graph modules and installs any missing ones before proceeding. It also pins Graph modules to the same version to avoid compatibility conflicts.
Part 1 — Entra ID Roles (Graph API) — Connects via Connect-MgGraph with the required scopes, fetches all eligible directory role assignments for the signed-in user, reads the max allowed duration from each role's PIM policy, and submits a selfActivate request via New-MgRoleManagementDirectoryRoleAssignmentScheduleRequest.
Part 2 — Azure Resource Roles (ARM REST API) — Connects via Connect-AzAccount, retrieves a bearer token (handling the SecureString conversion in newer Az module versions), queries the tenant-level PIM endpoint for eligible resource role assignments, auto-detects max duration from roleManagementPolicyAssignments, and submits activation requests via the ARM REST API.