Overview

In a Managed Service Provider (MSP) environment, manual offboarding is a massive liability. Missing a step when revoking access can lead to data breaches, compliance violations, and wasted licensing costs.

This guide outlines an architectural approach to “Zero-Touch” offboarding, leveraging a self-hosted n8n instance running in Docker to trigger a robust PowerShell workflow that interacts directly with the Microsoft Graph API.

The Architecture

Relying on technicians to manually run scripts on their local machines creates bottlenecks. By containerizing the automation engine, we achieve predictable, auditable execution.

  1. Dockerized n8n: Acts as the orchestration layer. It listens for webhook payloads (e.g., from a Zendesk or ConnectWise ticket closure) or runs on a scheduled Cron trigger.
  2. Azure App Registration: Provides secure, certificate-based authentication to the Microsoft Graph API, bypassing the need for vulnerable service account passwords.
  3. PowerShell Core Workflow: The execution layer that processes the API calls to strip licenses, convert mailboxes, and revoke sessions.

The Execution Script

This PowerShell script is designed to be triggered by the n8n node. It utilizes the Microsoft.Graph module to ensure modern authentication compliance.

<#
.SYNOPSIS
    Automated M365 User Offboarding via Graph API.
.DESCRIPTION
    Revokes sessions, blocks sign-in, strips licenses, and converts to a shared mailbox.
#>

param (
    [Parameter(Mandatory=$true)]
    [string]$UserPrincipalName,
    
    [Parameter(Mandatory=$true)]
    [string]$TenantId,
    
    [Parameter(Mandatory=$true)]
    [string]$ClientId,
    
    [Parameter(Mandatory=$true)]
    [string]$ClientSecret
)

# Authenticate to Graph API via App Registration
$TokenBody = @{
    Grant_Type    = "client_credentials"
    Scope         = "[https://graph.microsoft.com/.default](https://graph.microsoft.com/.default)"
    Client_Id     = $ClientId
    Client_Secret = $ClientSecret
}
$TokenResponse = Invoke-RestMethod -Uri "[https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token](https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token)" -Method Post -Body $TokenBody
$Headers = @{ Authorization = "Bearer $($TokenResponse.access_token)" }

Write-Host "Initiating offboarding for $UserPrincipalName..."

# 1. Block Sign-in
$BlockSignInUrl = "[https://graph.microsoft.com/v1.0/users/$UserPrincipalName](https://graph.microsoft.com/v1.0/users/$UserPrincipalName)"
$BlockSignInBody = @{ accountEnabled = $false } | ConvertTo-Json
Invoke-RestMethod -Uri $BlockSignInUrl -Method Patch -Headers $Headers -Body $BlockSignInBody -ContentType "application/json"
Write-Host "[-] Sign-in blocked."

# 2. Revoke Refresh Tokens (Force Sign-out)
$RevokeUrl = "[https://graph.microsoft.com/v1.0/users/$UserPrincipalName/revokeSignInSessions](https://graph.microsoft.com/v1.0/users/$UserPrincipalName/revokeSignInSessions)"
Invoke-RestMethod -Uri $RevokeUrl -Method Post -Headers $Headers
Write-Host "[-] Active sessions revoked."

# 3. Strip All Licenses
$GetLicensesUrl = "[https://graph.microsoft.com/v1.0/users/$UserPrincipalName/licenseDetails](https://graph.microsoft.com/v1.0/users/$UserPrincipalName/licenseDetails)"
$Licenses = Invoke-RestMethod -Uri $GetLicensesUrl -Method Get -Headers $Headers
$SkuIds = $Licenses.value.skuId

if ($SkuIds) {
    $RemoveLicenseUrl = "[https://graph.microsoft.com/v1.0/users/$UserPrincipalName/assignLicense](https://graph.microsoft.com/v1.0/users/$UserPrincipalName/assignLicense)"
    $RemoveLicenseBody = @{
        addLicenses = @()
        removeLicenses = $SkuIds
    } | ConvertTo-Json
    Invoke-RestMethod -Uri $RemoveLicenseUrl -Method Post -Headers $Headers -Body $RemoveLicenseBody -ContentType "application/json"
    Write-Host "[-] Licenses stripped."
}

# 4. Convert to Shared Mailbox (Requires Exchange Online Module logic or direct Graph beta endpoints)
# Note: For full automation, ensure the Exchange Administrator role is assigned to the App Reg.
Write-Host "[✓] Core offboarding complete."