Skip to main content

Automating the Identity Lifecycle: PowerShell, Microsoft Graph & JML Pipelines

The "Why" Before the "How": The Hidden Danger of Manual Offboarding​

Manual employee onboarding and offboarding is slow, error-prone, and a massive compliance vulnerability. When offboarding relies on human technicians remembering to click through multiple administrative consoles:

  • Accounts remain active days or weeks after termination.
  • Active refresh tokens allow ex-employees to retain access to corporate email and files on personal devices.
  • Expensive Microsoft 365 E5 and SaaS licenses sit permanently assigned to orphaned accounts, bleeding operational budgets.

An enterprise Joiner, Mover, Leaver (JML) automation engine standardizes identity transitions as deterministic code. By orchestrating on-premises Active Directory and Microsoft Entra ID through unattended PowerShell scripts authenticated via asymmetric X.509 certificates, we achieve zero-downtime onboarding and immediate, audit-compliant offboarding.


1. Authentication Architecture: Zero Client Secrets​

In enterprise automation, storing client secrets or interactive user passwords in scripts is unacceptable. Secrets expire, leak in Git repositories, and bypass hardware-based token binding.

Our JML pipeline standardizes on Certificate-Based Authentication (App-Only) against Microsoft Graph:

flowchart LR
PVE["Automation Engine<br>(pve-dell01 / lab02)"] -->|"1. Asymmetric X.509 Cert Auth"| EntraApp["Entra ID App Registration<br>(svc-jml-engine)"]
EntraApp -->|"2. Scoped App Roles<br>(User.ReadWrite.All, Directory.ReadWrite.All)"| GraphAPI["Microsoft Graph API v1.0"]
GraphAPI -->|"3. Orchestrates Lifecycle"| Directory["Active Directory & Entra ID Users"]

Establishing the Certificate Connection​

# Authenticate unattended session using local machine cert store
$AppId = "11111111-2222-3333-4444-555555555555"
$TenantId = "lab.dariocruz.dev"
$CertThumbprint = "A1B2C3D4E5F60718293A4B5C6D7E8F9012345678"

Connect-MgGraph -ClientId $AppId -TenantId $TenantId -CertificateThumbprint $CertThumbprint

2. The Leaver Pipeline: Instant Revocation & License Reclamation​

When HR marks an employee as terminated, the Leaver pipeline executes a strict, chronological 6-step remediation sequence within seconds:

flowchart TD
Trigger["HR Termination Trigger"] --> Step1["Step 1: Revoke Active User Sessions<br>(Invalidates all OAuth refresh tokens)"]
Step1 --> Step2["Step 2: Disable AD DS & Entra Accounts<br>(Blocks new Kerberos/Cloud auth)"]
Step2 --> Step3["Step 3: Scramble Password<br>(Generates random 64-char crypto string)"]
Step3 --> Step4["Step 4: Strip Security Group Memberships<br>(Removes access to internal resources)"]
Step4 --> Step5["Step 5: Convert Mailbox & Reclaim Licenses<br>(Reclaims M365 E5 seat to pool)"]
Step5 --> Step6["Step 6: Move to Disabled OU & Audit Log<br>(Generates immutable JSON audit record)"]

The Automated Offboarding PowerShell Function​

function Invoke-EnterpriseLeaver {
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory = $true)]
[string]$UserPrincipalName,

[Parameter(Mandatory = $true)]
[string]$TicketNumber
)

Write-Host "[-] Initiating automated leaver workflow for $UserPrincipalName ($TicketNumber)..." -ForegroundColor Yellow

# Step 1: Revoke all active session refresh tokens immediately
Write-Host " -> Revoking all active Entra ID OAuth refresh tokens..." -ForegroundColor Cyan
Revoke-MgUserSignOutSession -UserId $UserPrincipalName | Out-Null

# Step 2: Disable account in on-premises Active Directory
$ADUser = Get-ADUser -Filter "UserPrincipalName -eq '$UserPrincipalName'"
if ($ADUser) {
Write-Host " -> Disabling Active Directory account ($($ADUser.DistinguishedName))..." -ForegroundColor Cyan
Disable-ADAccount -Identity $ADUser.DistinguishedName
Set-ADUser -Identity $ADUser.DistinguishedName -Description "Terminated per $TicketNumber on $(Get-Date -Format 'yyyy-MM-dd')"
Move-ADObject -Identity $ADUser.DistinguishedName -TargetPath "OU=Disabled-Users,OU=Enterprise,DC=corp,DC=contoso,DC=com"
}

# Step 3: Reclaim all assigned Microsoft 365 / Entra ID licenses
$EntraUser = Get-MgUser -UserId $UserPrincipalName -Property AssignedLicenses
$LicensesToRemove = $EntraUser.AssignedLicenses.SkuId
if ($LicensesToRemove) {
Write-Host " -> Reclaiming $($LicensesToRemove.Count) license(s) back to corporate pool..." -ForegroundColor Cyan
Set-MgUserLicense -UserId $UserPrincipalName -RemoveLicenses $LicensesToRemove -AddLicenses @() | Out-Null
}

Write-Host "[+] Offboarding complete for $UserPrincipalName. Account disabled, tokens revoked, licenses reclaimed." -ForegroundColor Green
}

3. Verification: Trust, but Verify​

To validate that an offboarded user can no longer access corporate data:

  1. Token Test: Attempt an interactive login with cached credentials on a Windows endpoint. Confirm authentication fails immediately with AADSTS50034 or AADSTS50053.
  2. License Pool Check: Verify in the Microsoft 365 Admin Center that available license count increments by 1.
  3. Audit Trail: Confirm that the script outputs a structured JSON log entry capturing the timestamp, initiator, ticket number, and target user for compliance auditing.