Friday, June 26, 2026

Microsoft Entra ID — A Practical Introduction for M365 Admins


Microsoft Entra ID — A Practical Introduction for M365 Admins

Meta Description: New to Microsoft Entra ID? This practical guide covers the key concepts every M365 admin needs — Conditional Access, PIM, Dynamic Groups, SSO, Managed Identities, and Agent ID — plus a 5-step hands-on lab to get you started today.

Tags: Microsoft Entra ID · Azure Active Directory · Microsoft 365 · Identity and Access Management · Power Platform


If you've been working with Microsoft 365 for a while, you've probably heard the name Microsoft Entra ID pop up more and more. If you're still thinking of it as "just the new name for Azure Active Directory," you're in for a pleasant surprise — it's grown into something much more. This post gives you a friendly, no-fluff walkthrough of what Entra ID is, the concepts that matter most day-to-day, and a hands-on lab you can try right now.


What Is Microsoft Entra ID?

Microsoft Entra ID is Microsoft's cloud-based Identity and Access Management (IAM) platform. Think of it as the front door to everything in your Microsoft 365 and Azure world — and increasingly, to third-party apps like Salesforce, ServiceNow, and AWS too.

It was rebranded from Azure Active Directory (Azure AD) in 2023. The rename wasn't just cosmetic — it signals that identity now extends beyond Azure to cover every user, every app, and every device across your ecosystem, including AI agents.

If you manage M365 users, run Power Automate flows, or build Copilot Studio bots, Entra ID is already running quietly in the background. This post helps you move from "aware of it" to "actively using it."


Key Concepts You Need to Know

1. Conditional Access

This is the "if this, then that" engine for security. You define policies like: "If a user signs in from an untrusted location with medium risk, require MFA." It's your first line of defence against compromised credentials — and it's surprisingly intuitive to configure.

💡 Tip: Always set new Conditional Access policies to Report-only mode first. You'll see exactly which users would be impacted before you enforce anything.

2. Privileged Identity Management (PIM)

PIM gives you just-in-time admin elevation. Instead of leaving sensitive roles like Global Administrator permanently assigned, users activate them on demand, for a limited time, with optional approval and MFA. Far safer, and it keeps your audit logs clean.

3. Dynamic Groups

This is arguably the most underrated time-saver in Entra ID. Instead of manually adding users to groups, you write a rule — (user.department -eq "Engineering") — and Entra automatically manages group membership as user attributes change. Think of it as Power Automate for your directory.

💡 Tip: Dynamic Groups require an Entra ID P1 licence. They're available in the M365 Developer tenant sandbox for free.

4. Single Sign-On (SSO)

SSO lets your users sign in once with their work account and access thousands of apps without separate logins. Entra's App Gallery has pre-built integrations for popular platforms. For your users, it means fewer passwords. For you, it means fewer password reset tickets.

5. Managed Identities

When your Azure resources (like Logic Apps, Azure Functions, or Azure Automation) need to authenticate to other services, Managed Identities let them do so without storing credentials anywhere. It's the right answer to "how does my Power Automate flow securely talk to Key Vault?"

6. Agent ID (New in 2025)

This one's worth bookmarking. As AI agents proliferate — Copilot Studio bots, autonomous workflows, multi-agent scenarios — Entra ID now supports identities for AI agents, governed the same way as human users. Assign roles, audit actions, revoke access. Identity governance is no longer just for people.


5-Step Hands-On Lab

Before you start: Use an M365 Developer tenant (free via developer.microsoft.com). It comes with 25 × E5 licences — perfect for safe experimentation.

Step 1 — Explore the Admin Center Head to entra.microsoft.com. On the Overview page, note your Tenant ID, total user count, and Secure Score. Spend 5 minutes clicking through the left nav: Identity, Users, Groups, Applications, Devices. Get comfortable with the layout.

Step 2 — Create a User and Assign a Role Go to Users → New user → Create new user. Give them a test UPN, set a temporary password. Then go to Roles & Admins and assign the User Administrator role — not Global Admin. Practice least privilege from day one.

Step 3 — Build a Dynamic Group Groups → New group → Security. Switch membership type to Dynamic User. Add a rule: (user.department -eq "Engineering"). Save it. Now go back to your test user's profile and set their department. Watch the group populate automatically.

Step 4 — Configure a Conditional Access Policy Protection → Conditional Access → New policy. Target: All users. Condition: Sign-in risk = Medium or High. Grant: Require multifactor authentication. Set it to Report-only mode. Check the Insights workbook a day later to see what it would have blocked.

Step 5 — Read the Sign-in Logs Monitoring → Sign-in logs. Filter by your test user. For each entry, expand the detail and review the Conditional Access tab — you'll see exactly which policies evaluated and what they decided. This same log data feeds into Azure Monitor and Application Insights.


Tips & Gotchas

  • Don't assign Global Administrator to everyone. Use scoped roles like User Administrator, Exchange Administrator, or Teams Administrator.
  • Audit logs ≠ Sign-in logs. Sign-in logs track authentication events. Audit logs track configuration changes (who added who to what group, etc.). Both matter.
  • Security Defaults vs. Conditional Access. Security Defaults are great for small orgs starting out. Once you need granular control, switch to Conditional Access policies instead — they don't coexist well.
  • Licences matter. Many of the best features (PIM, Dynamic Groups, Identity Protection, access reviews) require Entra ID P1 or P2. Know what your tenant is licenced for before troubleshooting a missing feature.

Summary

Microsoft Entra ID is the identity backbone of the modern Microsoft cloud — and it's no longer just about managing user accounts. Conditional Access policies enforce Zero Trust security. PIM keeps admin access safe. Dynamic Groups automate directory management. SSO simplifies the user experience. Managed Identities remove credential risk from automation. And Agent ID is setting up identity governance for the AI era.

The best way to learn it? Open your developer tenant and start clicking. The five steps above will take less than an hour and give you a solid foundation to build on.



Wednesday, June 24, 2026

Automate Azure PIM Role Activation for Entra ID + Azure Resources with PowerShell

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:

ModulePurpose
Az.AccountsAzure authentication & context
Az.ResourcesAzure AD user lookup
Microsoft.Graph.AuthenticationGraph API authentication
Microsoft.Graph.Identity.GovernancePIM role management
Microsoft.Graph.Identity.SignInsSign-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.

Automate Azure PIM Role Activation with PowerShell

Automate Azure PIM Role Activation with PowerShell

Tired of manually clicking Activate in the Azure portal every time your PIM role expires?
This script automatically activates all eligible Azure resource roles — and even detects the maximum allowed duration from your PIM policy, so you never hit the "duration exceeds maximum" error again.


Prerequisites

  • Az PowerShell module installed (Install-Module Az -Scope CurrentUser -Force)
  • Eligible Azure RBAC roles assigned via PIM
  • Az.Accounts and Az.Resources modules

The Script

# ============================================================
#  Azure PIM Resource Role Activator - Full Script
# ============================================================

# ── Auto-install required 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
}

# Or if you want to install the full Az bundle (all Az modules) instead:
# if (-not (Get-Module -ListAvailable -Name Az.Accounts)) {
#     Write-Host "Installing Az module..." -ForegroundColor Yellow
#     Install-Module Az -Scope CurrentUser -Force -AllowClobber
# }
# Import-Module Az.Accounts, Az.Resources -ErrorAction Stop

Connect-AzAccount

# Get token (handle SecureString in newer Az versions)
$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" }
$subId   = (Get-AzContext).Subscription.Id
$userId  = (Get-AzADUser -SignedIn).Id

Write-Host "Subscription : $subId" -ForegroundColor Cyan
Write-Host "User ID      : $userId" -ForegroundColor Cyan

# ── Get eligible roles (tenant-level works, subscription-level doesn't) ──────
$roles = (Invoke-RestMethod `
    -Uri "https://management.azure.com/providers/Microsoft.Authorization/roleEligibilityScheduleInstances?api-version=2020-10-01&`$filter=asTarget()" `
    -Headers $headers).value
	
$roles | ForEach-Object {
    [PSCustomObject]@{
        Role         = $_.properties.expandedProperties.roleDefinition.displayName
        Resource     = $_.properties.expandedProperties.scope.displayName
        ResourceType = $_.properties.expandedProperties.scope.type
        Scope        = $_.properties.scope
        StartTime    = $_.properties.startDateTime
        EndTime      = $_.properties.endDateTime
        Status       = $_.properties.status
    }
} | Format-Table -AutoSize
	
Write-Host "`nFound $($roles.Count) eligible role(s)" -ForegroundColor Yellow
$roles | ForEach-Object {
    Write-Host "  • $($_.properties.expandedProperties.roleDefinition.displayName) → $($_.properties.expandedProperties.scope.displayName)"
}

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

1. Authentication — Connects to Azure and retrieves a bearer token, handling the SecureString conversion introduced in newer Az module versions.

2. Eligible Role Discovery — Queries the tenant-level PIM endpoint to fetch all eligible role assignments for the signed-in user.

3. Auto-Duration Detection — For each role, it reads the roleManagementPolicyAssignment to find the Expiration_EndUser_Assignment rule and extracts the maximumDuration (e.g. PT4H). This prevents the "duration exceeds maximum" API error.

4. Role Activation — Submits a SelfActivate request for each eligible role using the exact maximum duration allowed by policy.



Active Directory Using PowerShell

Active Directory Using PowerShell

Prerequisites

  • PowerShell 5.1 or PowerShell 7+
  • RSAT Active Directory module installed (Windows Server or Windows 10/11 with RSAT)
  • Read access to Active Directory
  • The exported agents CSV file

The Script

$CSVPath    = "C:\Temp\Agents.csv"
$OutputPath = "C:\Temp\agents_updated.csv"
$LogPath    = "C:\Temp\agents_ad_check.log"

Import-Module ActiveDirectory

function Write-Log {
    param([string]$Message, [string]$Level = "INFO")
    $line = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] [$Level] $Message"
    Write-Host $line
    Add-Content -Path $LogPath -Value $line
}

function Get-ADUserStatus {
    param([string]$UserEmail)

    # Check 1: Skip empty or whitespace values
    if ([string]::IsNullOrWhiteSpace($UserEmail)) { return "not-exist" }

    # Check 2: Domain normalization — replace legacy domain with current domain
    if ($UserEmail -like "*@olddomain.com") {
        $UserEmail = $UserEmail.Replace("@olddomain.com", "@newdomain.com")
    }

    try {
        # Primary lookup: by UserPrincipalName
        $user = Get-ADUser -Filter "UserPrincipalName -eq '$UserEmail'" `
                           -Properties Enabled -ErrorAction Stop

        # Fallback lookup: by mail attribute
        if ($null -eq $user) {
            $user = Get-ADUser -Filter "mail -eq '$UserEmail'" `
                               -Properties Enabled -ErrorAction Stop
        }

        if ($user.Enabled -eq $true) { return "exist" } else { return "not-exist" }
    }
    catch {
        Write-Log "ERROR: '$UserEmail' — $($_.Exception.Message)" -Level "ERROR"
        return "not-exist"
    }
}

$records = Import-Csv -Path $CSVPath

foreach ($row in $records) {
    $row.Is_Owner_Exist = Get-ADUserStatus -UserEmail $row.Owner.Trim()
    Write-Log "$($row.Owner) → $($row.Is_Owner_Exist)"
}

$records | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
Write-Log "=== Done. Output: $OutputPath ==="

PowerShell, ActiveDirectory, CopilotStudio, PowerPlatform, MicrosoftCopilot, EntraID, M365Governance, PowerShellAutomation, MicrosoftTeams, LowCode

Tuesday, June 23, 2026

SharePoint (Graph API) Integration with Adobe Workfront

SharePoint (Graph API) Integration with Adobe Workfront — Step-by-Step Guide

The modern, supported method. This guide uses the new SharePoint (Graph API) integration introduced in Workfront 22.3 (July 2022). It replaces the legacy ACS-based integration which was fully retired on April 2, 2026.


Why Graph API Instead of the Legacy Method?

Legacy (SharePoint) ❌ New (SharePoint Graph API) ✅
Auth method Azure ACS via appregnew.aspx OAuth 2.0 via Microsoft Entra ID
Status Retired April 2, 2026 Fully supported — no retirement date
Admin setup required Yes — complex, multiple pages No — zero admin config needed
Per-user auth No Yes — each user signs in once
Permissions model App-only, tenant/site scope Delegated — user's own SharePoint access
Least privilege Difficult to enforce Built-in — users only see what they already have
New document linking ❌ Not possible ✅ Fully supported

How It Works — The Simple Version

Admin enables SharePoint (Graph API) in Workfront once
         ↓
Each user signs into their Microsoft account from Workfront
         ↓
Workfront uses OAuth2 to get an access token on their behalf
         ↓
User can now browse, link, and upload SharePoint documents
         ↓
Users only see files they already have access to in SharePoint

🔐 Least Privilege built-in: Permissions are delegated — the integration never gets more access than the individual user already has in SharePoint. No admin grants, no XML, no FullControl required.


What You Need Before You Start

Requirement Who
Workfront System Administrator access You (admin doing the setup)
Microsoft 365 account with SharePoint Online access Each user
SharePoint Online (not on-premises) Your org's Microsoft tenant
Workfront license: Standard or Plan Your users
Workfront access level: Edit access to Documents Your users

⚠️ This integration works with SharePoint Online only. On-premises SharePoint is not supported.


Part 1 — Admin Setup (One-Time, Done Once for All Users)

This part takes about 2 minutes. You do it once and all users benefit.


Step 1 — Log into Workfront as System Administrator

  1. Open your browser and go to:
    https://[your-tenant].my.workfront.com
    
  2. Log in with your Workfront System Administrator credentials.

Step 2 — Go to Setup

  1. Click the Main Menu icon (grid/waffle icon) in the top-right corner of the screen.
  2. Click Setup (the gear icon).

Step 3 — Enable SharePoint (Graph API) in Cloud Providers

  1. In the left panel, click Documents.

  2. Under Documents, click Cloud Providers.

  3. You will see a list of document integrations. Find SharePoint (Graph API) and make sure its checkbox is checked/enabled.

    ⚠️ If you also have the old SharePoint (legacy) integration listed, keep it enabled only if you have existing documents linked through it that users still need to access. For all new document linking, users must use SharePoint (Graph API).

  4. Click Save.

✅ Admin setup is complete. That is all the admin needs to do.


Part 2 — User Setup (Each User Does This Once)

Every user must connect their own Microsoft account to Workfront the first time they use the integration. This is a one-time step per user.


Step 4 — Go to the Documents Area

Users can connect SharePoint from any place in Workfront that has a Documents tab. For example:

  • Open any Project → click the Documents tab
  • Or go to the main Documents area from the Main Menu

Step 5 — Click Add New → From SharePoint (Graph API)

  1. Click the Add New button.

  2. In the dropdown, click From SharePoint (Graph API).

    If you do not see this option, ask your Workfront administrator to complete Part 1 (Step 3) above.


Step 6 — Sign into Your Microsoft Account

  1. A Microsoft login popup or redirect will appear.
  2. Enter your Microsoft 365 / work email and password.
  3. Complete any MFA (Multi-Factor Authentication) prompt if your organisation requires it.

Step 7 — Review and Accept Permissions

The first time you connect, Workfront will ask for permission to access your SharePoint. You will see a screen titled "Permissions requested" listing what Workfront needs:

Permission Workfront Requests Why It Needs It
Have full access to your files To link and upload documents on your behalf
Read items in all site collections To let you browse and navigate SharePoint sites
Edit or delete items in all site collections To create assets; delete is only used to clean up failed link attempts
Maintain access to data you have given it access to To generate a refresh token so you stay logged in
Sign in and read your profile To act on your behalf through the OAuth2 flow

🔐 These are delegated permissions — Workfront can only access what you personally can access in SharePoint. It cannot see anything beyond your own SharePoint permissions.

Click Accept (or Yes / Allow depending on what the screen shows).

⚠️ If you see "Approval Required" instead of "Permissions requested" — this means your organisation's Microsoft admin has restricted app consent. In that case, use the page to submit an approval request to your Microsoft 365 admin. Once the admin approves, come back and repeat from Step 5.


Step 8 — Browse and Select Files from SharePoint

After accepting, the SharePoint file picker will open inside Workfront. You can now:

  • Browse your SharePoint sites and document libraries
  • Use the search bar to find files by name
  • Select one or multiple files or folders
  • Click Link to attach them to Workfront

✅ The selected files are now linked to Workfront. A SharePoint icon will appear next to them in the Documents tab.

ℹ️ Files remain stored in SharePoint. Workfront only holds a link — not a copy of the file.


Part 3 — Day-to-Day Usage

Once set up, here is how users work with the integration every day.


Linking a Document from SharePoint to Workfront

  1. Go to the Documents tab of any Project, Task, or Issue.
  2. Click Add NewFrom SharePoint (Graph API).
  3. Browse or search for the file.
  4. Select the file(s) → click Link.

Sending a Document from Workfront to SharePoint

  1. Select a document already in Workfront.
  2. Click the More menu (three dots) → Send toSharePoint (Graph API).
  3. Browse to the SharePoint folder where you want to save it.
  4. Click Save.

The document is now stored in SharePoint and linked back to Workfront. Any updates in SharePoint will reflect in Workfront.


Linking a Folder from SharePoint

  1. Go to the Documents tab.
  2. Click Add NewFrom SharePoint (Graph API).
  3. Browse to the folder you want to link.
  4. Select the folder → click Link.

⚠️ Linked folders sync their contents. If someone adds, removes, or edits files in SharePoint, those changes appear in Workfront automatically. However, the sync uses the credentials of the user who originally linked the folder — so if that user leaves the organisation, the folder must be re-linked by another user.


Important Limitations to Know

Limitation Detail
One SharePoint instance per user A user can connect to only one SharePoint tenant. They cannot connect to a second SharePoint even if they have access to it.
No global/service account Each user must authenticate individually. There is no way to set up a single shared account for all users.
SharePoint Online only On-premises SharePoint is not supported.
File size limit Single file: 5 GB max. Multiple files at once: 1 GB total.
Folder ownership risk If the user who linked a folder loses SharePoint access or leaves the org, that folder becomes inaccessible in Workfront until re-linked.
New Documents area This integration is not available in the new Adobe cloud storage Documents area. It works in the classic Documents tab only.

Troubleshooting

Problem Fix
"From SharePoint (Graph API)" not visible in Add New Admin has not enabled it — complete Part 1, Step 3
"Approval Required" screen appears Microsoft admin needs to approve the Workfront app in their Entra ID tenant — submit the request from that screen
User can see some sites but not others Users only see what they have permission to in SharePoint — check their SharePoint site access
Linked folder content not loading The user who linked the folder may have lost access — re-link the folder with an active user who has access
MFA prompt keeps repeating Check if your org's Conditional Access policies require re-authentication — contact your Microsoft 365 admin
File linked but cannot open Verify the user has at least View access to that file in SharePoint

Comparison: What Each Role Does

Role Action When
Workfront Admin Enable SharePoint (Graph API) in Cloud Providers Once, before users start
Microsoft 365 Admin Approve the Workfront app (only if org restricts app consent) Once, if "Approval Required" screen appears
Each User Sign into Microsoft account from Workfront Once per user
Each User Link/browse/upload documents Every time they need a SharePoint file

Security Overview

  • All communication between Workfront and SharePoint is over HTTPS.
  • Workfront uses OAuth 2.0 — no passwords are stored by Workfront.
  • Permissions are delegated — Workfront acts on behalf of the signed-in user, never beyond their access.
  • Users can revoke access at any time from their Microsoft account's app permissions page at https://myapps.microsoft.com.
  • Workfront does not store or copy SharePoint files. Only thumbnails for preview are cached.

Official References

Resource URL
Adobe Docs — Configure SharePoint Integration https://experienceleague.adobe.com/en/docs/workfront/using/administration-and-setup/configure-integrations/configure-sharepoint-integration
Adobe Docs — Link Documents from External Apps https://experienceleague.adobe.com/en/docs/workfront/using/documents/add-new-documents-to-workfront/link-documents-from-external-apps
Adobe Docs — Configure Document Integrations https://experienceleague.adobe.com/en/docs/workfront/using/administration-and-setup/configure-integrations/configure-document-integrations
Microsoft — Azure ACS Retirement https://learn.microsoft.com/en-us/sharepoint/dev/sp-add-ins/retirement-announcement-for-azure-acs

What the Graph API Flow Actually Looks Like:

Workfront Setup
     ↓
Documents → Cloud Providers
     ↓
Tick the checkbox next to "SharePoint (Graph API)"
     ↓
Click Save  ← Admin is done. No form. No fields.
     ↓
User goes to any Project → Documents tab
     ↓
Click "Add New" → "From SharePoint (Graph API)"
     ↓
Microsoft login popup appears
     ↓
User signs in with their Microsoft 365 account
     ↓
"Permissions Requested" screen → click Accept
     ↓
SharePoint file browser opens → browse and link files




SharePoint Integration with Adobe Workfront

SharePoint Integration with Adobe Workfront — Step-by-Step Setup Guide

Who is this for? Anyone setting up the legacy SharePoint integration in Workfront from scratch. No prior experience needed — follow every step exactly as written.


What You Will Need Before You Start

Item Where to Get It
SharePoint Online admin or site admin access Your IT/Microsoft 365 admin
Adobe Workfront admin access Your Workfront administrator
Your SharePoint tenant URL Example: https://[your-tenant].sharepoint.com

Overview of the 3 Phases

Phase 1 → Go to SharePoint → Register the App → Get Client ID & Client Secret
Phase 2 → Go to Workfront → Fill in the Integration Form
Phase 3 → Verify & Test

Phase 1: Generate Client ID and Client Secret in SharePoint

This is the most important phase. You will visit a special SharePoint page to create an "App Registration" that gives Workfront permission to talk to SharePoint.


Step 1 — Open the App Registration Page in SharePoint

  1. Open your browser (Chrome or Edge recommended).

  2. In the address bar, type your SharePoint URL and add /_layouts/15/appregnew.aspx at the end.

    Example:

    https://[your-tenant].sharepoint.com/_layouts/15/appregnew.aspx
    

    Replace [your-tenant] with your actual SharePoint domain name.

  3. Press Enter. You will see a form titled "App Information" or "New App".

    ⚠️ If you see an error like "Sorry, you don't have access" — contact your Microsoft 365 Global Administrator. Only tenant admins can access this page.


Step 2 — Fill in the App Registration Form

You will see fields on the page. Fill them in exactly as described below:

Field What to Enter
Client Id Click the Generate button next to this field. A GUID (long code) will auto-fill.
Client Secret Click the Generate button next to this field. A secret code will auto-fill.
Title Type any name. Example: Workfront SharePoint App
App Domain Type exactly: my.workfront.com
Redirect URI Type exactly: https://oauth.my.workfront.com/oauth2/redirect

📋 CRITICAL: Before clicking Create, copy and save the Client ID and Client Secret to Notepad or a secure location. You will NOT be able to see the Client Secret again after leaving this page.


Step 3 — Click Create

  1. After filling all fields, scroll down and click Create.
  2. You will see a confirmation message: "The app identifier has been successfully created."
  3. The page will also display the Client ID and Client Secret one final time — copy both values now if you haven't already.

Step 4 — Grant Permissions to the App in SharePoint

Choose one of the three permission models below based on your organisation's requirement.


🔵 Option A — Tenant-Wide Access (All Site Collections)

Use this when Workfront needs access to all site collections across the entire SharePoint tenant.

URL to open:

https://[your-tenant]-admin.sharepoint.com/_layouts/15/appinv.aspx

⚠️ Note the -admin in the URL. Without it, tenant-scoped permissions will NOT be granted.

Permission XML to paste:

<AppPermissionRequests AllowAppOnlyPolicy="true">
  <AppPermissionRequest Scope="http://sharepoint/content/tenant" Right="FullControl" />
</AppPermissionRequests>

Steps:

  1. Open the URL above.
  2. Paste the Client ID in the App Id field → click Lookup.
  3. Paste the XML above into the Permission Request XML field.
  4. Click Create → click Trust It in the popup.

✅ The app now has access to all site collections in the tenant.


🟡 Option B — Single Site Collection Access

Use this when Workfront should access only one specific site collection.

URL to open (replace [your-tenant] and [SiteName] with actual values):

https://[your-tenant].sharepoint.com/sites/[SiteName]/_layouts/15/appinv.aspx

⚠️ Open this URL from within the target site collection — NOT from the admin URL.

Permission XML to paste:

<AppPermissionRequests AllowAppOnlyPolicy="true">
  <AppPermissionRequest Scope="http://sharepoint/content/sitecollection" Right="FullControl" />
</AppPermissionRequests>

Steps:

  1. Open the site-specific URL above.
  2. Paste the Client ID in the App Id field → click Lookup.
  3. Paste the XML above into the Permission Request XML field.
  4. Click Create → click Trust It in the popup.

✅ The app now has access to only that specific site collection.


🟢 Option C — Multiple Specific Site Collections

Use this when Workfront needs access to more than one site collection, but not all.

There is no single XML that can list multiple site collections. You must repeat Option B for each site collection individually.

For each site collection you want to add:

  1. Open:
    https://[your-tenant].sharepoint.com/sites/[SiteName]/_layouts/15/appinv.aspx
    
  2. Paste the Client ID → click Lookup.
  3. Paste this XML:
    <AppPermissionRequests AllowAppOnlyPolicy="true">  <AppPermissionRequest Scope="http://sharepoint/content/sitecollection" Right="FullControl" /></AppPermissionRequests>
    
  4. Click Create → click Trust It.
  5. Repeat from step 1 for each additional site collection.

✅ Each site collection is granted independently using the same Client ID.


Comparison Table — Which Option Should You Use?

Option A — Tenant Option B — Single Site Option C — Multiple Sites
Access Scope All site collections One site collection Selected site collections
URL Used -admin.sharepoint.com appinv.aspx sites/[Name] appinv.aspx sites/[Name] appinv.aspx (repeated)
XML Scope Value content/tenant content/sitecollection content/sitecollection
Steps Required One time One time Once per site
Best For Full enterprise rollout Single team or department Controlled multi-team rollout
Security Level Broad Tight Balanced

Phase 2: Configure the Integration in Adobe Workfront

Now you will go to Workfront and fill in the form.


Step 5 — Log into Workfront as Administrator

  1. Open your browser and go to your Workfront URL:
    https://[your-tenant].my.workfront.com
    
  2. Log in with your Workfront administrator credentials.

Step 6 — Navigate to SharePoint Integration Settings

  1. Click the Main Menu icon (grid/waffle icon) in the top-right corner of Workfront.

  2. Click Setup (gear icon).

  3. In the left panel, click Documents.

  4. Under Documents, click SharePoint Integration.

  5. Click the + Add SharePoint Integration button (or New Integration if shown).

    The SharePoint Integration form will open — this is the form shown in the screenshots.


Step 7 — Fill in the SharePoint Integration Form

The form has 4 sections in the left menu. Fill them in order:


Section 1: SharePoint Instance Name

Field What to Enter
Name A friendly label for this integration. Example: SharePoint Online Integration

Click Next or go to the next section.


Section 2: SharePoint Info

Field What to Enter
SharePoint Host Instance Your SharePoint URL without https://. Example: [your-tenant].sharepoint.com
Azure Access Domain Your SharePoint URL without https://. Example: [your-tenant].sharepoint.com
Site Collections Authentication Type [your-tenant].sharepoint.com (same as above)

Section 3: Connection Info

This is where you enter the credentials from Phase 1.

Field What to Enter
SharePoint Client ID Paste the Client ID you copied in Step 2
SharePoint Client Secret Paste the Client Secret you copied in Step 2
Workfront SharePoint AppDomain Pre-filled: my.workfront.comdo not change
Workfront SharePoint Redirect URI Pre-filled: https://oauth.my.workfront.com/oauth2/redirectdo not change

⚠️ Important: The Client Secret must be exactly 44 characters long. If it's shorter, go back to the SharePoint appregnew.aspx page and generate a new one. You may need to try 2–3 times until a 44-character secret is generated.


Section 4: Visible Site Collections

This section lists the SharePoint site collections that will be accessible inside Workfront.

  • If you chose Option A (tenant-wide) — leave this as default; all sites will appear automatically.
  • If you chose Option B or C (specific sites) — add only those site collection URLs here to match the permissions you granted.

Step 8 — Click Confirm

  1. Review all fields one more time.
  2. Click the Confirm button (blue button, bottom-right).
  3. Workfront will test the connection. If successful, the integration will be saved.

Step 9 — Enable the Integration in Cloud Providers Settings

  1. Go back to Setup → Documents → Cloud Providers.
  2. Make sure both SharePoint and SharePoint (Graph API) checkboxes are enabled (checked).
  3. Click Save.

Phase 3: Verify the Integration

Step 10 — Test from a Document Area

  1. Go to any Project or Task in Workfront.

  2. Click the Documents tab.

  3. Click Add New → From SharePoint.

  4. You should see your SharePoint sites listed and be able to browse folders.

    ✅ If you can browse SharePoint files — the integration is working correctly!


Troubleshooting

Problem Fix
"App not trusted" error Go back to appinv.aspx and click Trust It again
Client Secret not accepted / 44 character error Go to appregnew.aspx, generate a new Client Secret, and update it in Workfront
"Sorry, only tenant administrators can access" Ask your Microsoft 365 Global Admin to perform Steps 1–4
Cannot see SharePoint sites in Workfront Ensure your account has View access to those site collections in SharePoint
Previously linked documents not accessible A user with SharePoint access must re-link the folder from the Documents tab
Site not visible after Option B/C Confirm the appinv.aspx step was completed on that specific site URL, not the admin URL

Quick Reference — Values Summary

Value Where It Comes From
Client ID Generated at appregnew.aspx (SharePoint)
Client Secret Generated at appregnew.aspx (SharePoint)
SharePoint Host Instance Your SharePoint domain (e.g. [your-tenant].sharepoint.com)
AppDomain Pre-filled in Workfront: my.workfront.com
Redirect URI Pre-filled in Workfront: https://oauth.my.workfront.com/oauth2/redirect

Useful Links

Purpose URL
Generate Client ID & Secret https://[your-tenant].sharepoint.com/_layouts/15/appregnew.aspx
Grant Tenant-Wide Permissions (Option A) https://[your-tenant]-admin.sharepoint.com/_layouts/15/appinv.aspx
Grant Site Collection Permissions (Option B/C) https://[your-tenant].sharepoint.com/sites/[SiteName]/_layouts/15/appinv.aspx
Verify App Principals https://[your-tenant].sharepoint.com/_layouts/15/appprincipals.aspx
Workfront Setup https://[your-tenant].my.workfront.com/setup
Official Adobe Docs https://experienceleague.adobe.com/en/docs/workfront/using/administration-and-setup/configure-integrations/configure-sharepoint-integration
Microsoft App Registration Docs https://learn.microsoft.com/en-us/sharepoint/dev/solution-guidance/security-apponly-azureacs

Note: The Client Secret generated via appregnew.aspx expires after 1 year. Set a calendar reminder to renew it before expiry to avoid losing access to linked documents.

Monday, June 22, 2026

Send Emails via Microsoft Graph API Using PowerShell and App Registration

Send Emails via Microsoft Graph API Using PowerShell and App Registration

Microsoft Graph API is the unified gateway to Microsoft 365 data and services. One of the most common automation scenarios is sending emails programmatically — without relying on a logged-in user, Outlook, or SMTP. In this post, we'll walk through how to register an Azure AD app, grant it the right permissions, and use a single end-to-end PowerShell script to send emails and query SharePoint via Graph API using client credentials (app-only auth).


Why Use Graph API for Sending Emails?

  • No user sign-in required — works great for background jobs, bots, and automation flows
  • Works from anywhere — PowerShell scripts, Power Automate Desktop, Azure Functions, etc.
  • Scalable and auditable — full control over sending identity and logging

Step 1: Register an App in Microsoft Entra ID (Azure AD)

Before writing any code, you need an app registration that represents your script or automation.

  1. Go to https://portal.azure.com
  2. Navigate to Microsoft Entra ID → App registrations → New registration
  3. Give it a meaningful name (e.g., GraphAPI-MailSender)
  4. Leave Redirect URI blank — not needed for client credentials flow
  5. Click Register

Once created, note down:

  • Application (client) ID → used as $clientId in the script
  • Directory (tenant) ID → used in the token endpoint URL

Step 2: Create a Client Secret

  1. Go to Certificates & secrets → New client secret
  2. Add a description and set an expiry (e.g., 12 months)
  3. Copy the Value immediately — it won't be shown again
  4. Store it securely (Azure Key Vault is recommended for production)

Step 3: Grant API Permissions

Your app needs the following Application permissions (not Delegated):

Permission Purpose
Mail.Send Send email as any mailbox in the tenant
Sites.Read.All Read SharePoint site data

Steps:

  1. Go to API permissions → Add a permission → Microsoft Graph → Application permissions
  2. Add Mail.Send and Sites.Read.All
  3. Click Grant admin consent — required for application permissions

⚠️ Mail.Send as an application permission allows sending mail as any user in the tenant. Use application access policies to restrict it to specific mailboxes in production.


Step 4: Full PowerShell Script

The script below handles everything in sequence:

  1. Acquires a token using client credentials
  2. Sends an email via Graph API
  3. Reuses the token to query the SharePoint root site
# ============================================================
# Microsoft Graph API — Send Email + Query SharePoint Root Site
# Using App-Only (Client Credentials) Authentication
# ============================================================

# ── Configuration ──────────────────────────────────────────
$clientId     = "<YOUR_CLIENT_ID>"       # Application (client) ID
$clientSecret = "<YOUR_CLIENT_SECRET>"   # Client secret value
$tenantId     = "<YOUR_TENANT_ID>"       # Directory (tenant) ID
$senderUPN    = "sender@yourdomain.com"  # Mailbox used to send email
$recipientUPN = "recipient@yourdomain.com" # Target recipient

# ── Part 1: Acquire Access Token ────────────────────────────
Write-Output "Acquiring access token..."

$tokenBody = @{
    client_id     = $clientId
    client_secret = $clientSecret
    scope         = "https://graph.microsoft.com/.default"
    grant_type    = "client_credentials"
}

$tokenResponse = Invoke-RestMethod `
    -Uri    "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" `
    -Method POST `
    -Body   $tokenBody

$token = $tokenResponse.access_token
Write-Output "Token acquired successfully."

# ── Part 2: Send Email via Graph API ────────────────────────
Write-Output "Sending email..."

$mailPayload = @{
    message = @{
        subject = "Test Email from Graph API"
        body    = @{
            contentType = "Text"
            content     = "This is a test email sent via Microsoft Graph API using PowerShell with app-only authentication."
        }
        toRecipients = @(
            @{
                emailAddress = @{
                    address = $recipientUPN
                }
            }
        )
    }
} | ConvertTo-Json -Depth 10

$headers = @{
    Authorization  = "Bearer $token"
    "Content-Type" = "application/json"
}

try {
    Invoke-RestMethod `
        -Uri         "https://graph.microsoft.com/v1.0/users/$senderUPN/sendMail" `
        -Method      POST `
        -Headers     $headers `
        -Body        $mailPayload `
        -ContentType "application/json"

    Write-Output "✅ Email sent successfully to $recipientUPN"
}
catch {
    Write-Error "❌ Failed to send email: $_"
}

# ── Part 3: Query SharePoint Root Site ──────────────────────
Write-Output "`nQuerying SharePoint root site..."

try {
    $rootSite = Invoke-RestMethod `
        -Uri     "https://graph.microsoft.com/v1.0/sites/root" `
        -Method  GET `
        -Headers $headers

    Write-Output "✅ SharePoint root site retrieved:"
    Write-Output ($rootSite | ConvertTo-Json -Depth 3)
}
catch {
    Write-Error "❌ Failed to query SharePoint root site: $_"
}

How to Run the Script

  1. Open PowerShell (or Windows PowerShell ISE / VS Code)
  2. Replace the four placeholder values at the top of the script:
    • <YOUR_CLIENT_ID>
    • <YOUR_CLIENT_SECRET>
    • <YOUR_TENANT_ID>
    • sender@yourdomain.com and recipient@yourdomain.com
  3. Save as Send-GraphEmail.ps1
  4. Run:
.\Send-GraphEmail.ps1

Expected Output

Acquiring access token...
Token acquired successfully.
Sending email...
✅ Email sent successfully to recipient@yourdomain.com

Querying SharePoint root site...
✅ SharePoint root site retrieved:
{
  "id": "yourtenant.sharepoint.com,xxxxxxxx-...",
  "displayName": "Communication site",
  "name": "root",
  "webUrl": "https://yourtenant.sharepoint.com"
}

Script Breakdown

Section What it does
Configuration block Centralises all credentials and addresses at the top — easy to maintain
Token acquisition Posts to the OAuth 2.0 token endpoint using client credentials
Email sending Constructs the mail JSON payload and calls /users/{sender}/sendMail
SharePoint query Reuses the same token to call /sites/root — no second token needed
try/catch blocks Catches errors per operation so one failure doesn't stop the rest

Security Best Practices

Practice Recommendation
Never hardcode secrets Move $clientSecret to Azure Key Vault or environment variables
Rotate secrets regularly Set reminders before expiry; automate via Key Vault rotation
Restrict Mail.Send scope Use application access policies to limit to specific mailboxes
Prefer certificates Certificate-based auth (New-SelfSignedCertificate) is more secure than secrets
Audit permissions Review app registrations and permissions in Entra ID quarterly

Summary

In this post, we covered:

  • Registering an app in Microsoft Entra ID and creating a client secret
  • Granting Mail.Send and Sites.Read.All application permissions with admin consent
  • A complete PowerShell script that acquires a token, sends an email, and queries SharePoint — all using app-only auth
  • Error handling with try/catch per operation
  • Security recommendations for production use

This pattern is perfect for Power Automate Desktop flows, scheduled PowerShell jobs, or any scenario where no interactive user is present. The same token works across multiple Graph API endpoints — making it efficient to chain operations in a single script.


Tags: MicrosoftGraph, PowerShell, AzureAD, AppRegistration, Microsoft365, EmailAutomation, GraphAPI, EntraID, PowerAutomate, SharePoint


Part 2: End-to-End in Power Automate Desktop (PAD)

The same logic — get a token, send an email, query SharePoint — can run entirely inside a Power Automate Desktop flow without any external PowerShell script. PAD has built-in HTTP actions and a Run PowerShell script action, giving you two clean approaches.


Approach A: Using the "Run PowerShell Script" Action (Quickest Way)

This embeds the entire PowerShell script directly inside a PAD flow. Best when you already have the script and just want to automate it.

PAD Flow Structure

Flow: Send Email via Graph API
│
├── [1] Set Variable — clientId
├── [2] Set Variable — clientSecret
├── [3] Set Variable — tenantId
├── [4] Set Variable — senderUPN
├── [5] Set Variable — recipientUPN
├── [6] Run PowerShell Script
│       └── Script: (full script using %clientId%, %clientSecret%, etc.)
│       └── Output: PowershellOutput
├── [7] IF PowershellOutput contains "successfully"
│       └── Display Message — "Flow completed successfully"
│   ELSE
│       └── Display Message — "Flow encountered an error"
└── [8] (Optional) Write output to text file or log

Step-by-Step in PAD Designer

Step 1 — Set your variables

Add one Set variable action for each credential. Using variables (rather than hardcoding) keeps the script clean and makes future updates easy.

Variable Name Value
clientId Your Application (client) ID
clientSecret Your client secret value
tenantId Your Directory (tenant) ID
senderUPN sender@yourdomain.com
recipientUPN recipient@yourdomain.com

In PAD, go to Actions panel → Variables → Set variable, set Name and Value for each.

Step 2 — Add "Run PowerShell Script" action

Search for Run PowerShell script in the Actions panel (under Scripting). Paste the script below into the script body. PAD variables are referenced using %variableName% syntax inside the script block.

# ============================================================
# Graph API — Send Email + Query SharePoint Root Site
# Running inside Power Automate Desktop
# ============================================================

$clientId     = "%clientId%"
$clientSecret = "%clientSecret%"
$tenantId     = "%tenantId%"
$senderUPN    = "%senderUPN%"
$recipientUPN = "%recipientUPN%"

# ── Part 1: Acquire Token ────────────────────────────────────
$tokenBody = @{
    client_id     = $clientId
    client_secret = $clientSecret
    scope         = "https://graph.microsoft.com/.default"
    grant_type    = "client_credentials"
}

$tokenResponse = Invoke-RestMethod `
    -Uri    "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" `
    -Method POST `
    -Body   $tokenBody

$token = $tokenResponse.access_token

# ── Part 2: Send Email ───────────────────────────────────────
$mailPayload = @{
    message = @{
        subject = "Test Email from Graph API via PAD"
        body    = @{
            contentType = "Text"
            content     = "This email was sent via Microsoft Graph API running inside a Power Automate Desktop flow."
        }
        toRecipients = @(
            @{ emailAddress = @{ address = $recipientUPN } }
        )
    }
} | ConvertTo-Json -Depth 10

$headers = @{
    Authorization  = "Bearer $token"
    "Content-Type" = "application/json"
}

try {
    Invoke-RestMethod `
        -Uri         "https://graph.microsoft.com/v1.0/users/$senderUPN/sendMail" `
        -Method      POST `
        -Headers     $headers `
        -Body        $mailPayload `
        -ContentType "application/json"

    Write-Output "Email sent successfully to $recipientUPN"
}
catch {
    Write-Output "ERROR sending email: $_"
}

# ── Part 3: Query SharePoint Root Site ──────────────────────
try {
    $rootSite = Invoke-RestMethod `
        -Uri     "https://graph.microsoft.com/v1.0/sites/root" `
        -Method  GET `
        -Headers $headers

    Write-Output "SharePoint root site: $($rootSite.webUrl)"
}
catch {
    Write-Output "ERROR querying SharePoint: $_"
}

In the action settings:

  • Script to run → paste the script above
  • PowerShell script output → save to variable PowershellOutput
  • Script error output → save to variable ScriptError

Step 3 — Handle output with an IF condition

Add an If action:

  • First operand: %PowershellOutput%
  • Operator: Contains
  • Second operand: successfully

Inside the If block → add Display message: "✅ Email sent and SharePoint queried successfully"
Inside the Else block → add Display message: "❌ Flow error — check ScriptError variable"

Step 4 — (Optional) Log output to a file

Add a Write text to file action after the If block:

  • File path: C:\PAD-Logs\GraphAPI-Run-%CurrentDateTime%.txt
  • Text to write: %PowershellOutput%%NewLine%%ScriptError%
  • If file exists: Append

Approach B: Using PAD HTTP Actions (No PowerShell Required)

PAD has native HTTP actions under Web → Invoke web service that can call REST APIs directly — no PowerShell needed. This is the cleanest approach for PAD-native flows.

PAD Flow Structure

Flow: Send Email via Graph API (HTTP Native)
│
├── [1]  Set Variable — clientId
├── [2]  Set Variable — clientSecret  
├── [3]  Set Variable — tenantId
├── [4]  Set Variable — senderUPN
├── [5]  Set Variable — recipientUPN
│
├── [6]  Invoke web service — POST token endpoint
│        └── Saves response → TokenResponse (JSON)
│
├── [7]  Get JSON key — extract access_token
│        └── Saves → AccessToken
│
├── [8]  Invoke web service — POST sendMail
│        └── Uses Bearer %AccessToken% in header
│        └── Saves response → MailResult
│
├── [9]  Invoke web service — GET sites/root
│        └── Uses Bearer %AccessToken% in header
│        └── Saves response → SharePointResult
│
└── [10] Display message — show results

Step-by-Step: HTTP Actions in PAD

Step 1 — Set variables (same as Approach A)

Step 2 — Get Access Token (Invoke web service)

Action: Web → Invoke web service

Field Value
URL https://login.microsoftonline.com/%tenantId%/oauth2/v2.0/token
Method POST
Accept application/json
Content type application/x-www-form-urlencoded
Custom headers (leave blank)
Request body client_id=%clientId%&client_secret=%clientSecret%&scope=https://graph.microsoft.com/.default&grant_type=client_credentials
Save response into TokenResponse

Step 3 — Extract the access token

Action: Variables → Get JSON key value
(or use Convert JSON to custom object then dot-notation)

  • JSON: %TokenResponse%
  • Key: access_token
  • Save into: AccessToken

Alternatively, use a Run PowerShell script just for this one-liner:

$json = '%TokenResponse%' | ConvertFrom-Json
Write-Output $json.access_token

Save output into AccessToken.

Step 4 — Send Email (Invoke web service)

Action: Web → Invoke web service

Field Value
URL https://graph.microsoft.com/v1.0/users/%senderUPN%/sendMail
Method POST
Accept application/json
Content type application/json
Custom headers Authorization: Bearer %AccessToken%
Request body (see JSON below)
Save response into MailResult

Request body (paste as-is, update addresses via variables):

{
  "message": {
    "subject": "Test Email from PAD via Graph API",
    "body": {
      "contentType": "Text",
      "content": "This email was sent using Power Automate Desktop with Microsoft Graph API HTTP actions."
    },
    "toRecipients": [
      {
        "emailAddress": {
          "address": "%recipientUPN%"
        }
      }
    ]
  }
}

Step 5 — Query SharePoint Root Site (Invoke web service)

Action: Web → Invoke web service

Field Value
URL https://graph.microsoft.com/v1.0/sites/root
Method GET
Accept application/json
Custom headers Authorization: Bearer %AccessToken%
Save response into SharePointResult

Step 6 — Display results

Action: Message boxes → Display message
Message: SharePoint site: %SharePointResult%


Approach Comparison

Approach A (Run PowerShell) Approach B (HTTP Actions)
Complexity Low — paste and run Medium — multiple actions
PAD native No (calls PowerShell) Yes (pure PAD)
Debugging Via script output variable Per-action response variables
Best for Existing PowerShell scripts New PAD-native flows
Token parsing Built into the script Needs extra step in PAD
Error handling try/catch in script On block error / IF conditions

Recommendation: Use Approach A if you already have the PowerShell script. Use Approach B if you want a fully PAD-native flow with no external dependencies.


Tips for PAD Flows with Graph API

  • Store secrets in PAD Input variables marked as Sensitive — they are masked in logs and not stored in plain text in the flow definition
  • Add On block error handlers around each Invoke web service action to gracefully catch HTTP failures (401 Unauthorized, 403 Forbidden, etc.)
  • Token expiry: The client credentials token is valid for 1 hour. For long-running flows, re-acquire the token mid-flow if needed
  • Test in PAD debugger using the step-through (F10) to inspect each variable value before running end-to-end

Full Flow Summary

App Registration (Entra ID)
        │
        ▼
Client Credentials → OAuth 2.0 Token Endpoint
        │
        ▼
Access Token (Bearer)
        │
        ├──▶ POST /users/{sender}/sendMail   → Email delivered ✅
        │
        └──▶ GET  /sites/root               → SharePoint data ✅

All three entry points — standalone PowerShell, PAD via PowerShell action, and PAD via HTTP actions — use the exact same app registration, permissions, and token. The only difference is where and how the script runs.


Tags: MicrosoftGraph, PowerShell, PowerAutomateDesktop, PAD, AzureAD, AppRegistration, Microsoft365, EmailAutomation, GraphAPI, EntraID, SharePoint, RPA

Featured Post

Microsoft Entra ID — A Practical Introduction for M365 Admins

Microsoft Entra ID — A Practical Introduction for M365 Admins Meta Description: New to Microsoft Entra ID? This practical guide covers the...

Popular posts