Back to the project overview

Identity and access / Lab walkthrough

Build my automated
identity lifecycle lab.

Practice joiner, mover, and leaver changes by hand, then build an event-driven pipeline that offboards a user automatically when an HR termination event arrives.

Microsoft Entra IDLogic AppsAzure AutomationMicrosoft GraphPowerShell

Why I built this lab

Orphaned accounts, where people keep access after they leave, are one of the most common identity risks. I wanted to go beyond creating users and show offboarding that is automatic, auditable, and has no manual steps, the way enterprise IAM teams handle terminations at scale.

Make it your own. This is how I did it, not the only way. Feel free to adjust the lab to your liking: change the names, tools, or scope, or take it further. What matters is that you understand what you built and why.

Before you start

  • An Azure subscription and a Microsoft Entra ID tenant you can test in. My lab used Entra ID P2.
  • Permission to create users, groups, an Automation account, and a Logic App.
  • Test accounts only. Never point this pipeline at real users.

What you’ll learn

  • Joiner/Mover/Leaver (JML) lifecycle management
  • Managed identity authentication with no stored credentials
  • Calling Microsoft Graph from a PowerShell runbook
  • Event-driven automation with Logic Apps
  • Verifying changes in Entra ID audit logs

How it fits together

  1. HTTP POSTA simulated HR termination event.
  2. Azure Logic AppReceives the event and starts the runbook.
  3. Automation runbookRuns PowerShell with a managed identity.
  4. Microsoft GraphDisables, revokes, removes groups and licenses.
  5. Audit logs + emailProof of every action and a completion notice.

Step by step

  1. Practice the manual lifecycle first

    Doing it by hand first teaches you what the automation needs to do.

    • Joiner: create a test user, add the right attributes, assign groups and apps for their role, and require MFA registration at first sign-in.
    • Mover: change the user’s department and manager, add the new role’s groups and apps, and remove access from the old role.
    • Leaver: disable the account, revoke sessions, remove app assignments, and confirm no access is left behind.
  2. Create a test user for offboarding

    Create one more test user and add them to a few groups and apps (and a license, if you have one) so the pipeline has something to remove.

  3. Create the Azure Automation account

    In the Azure portal, create an Automation account. Under Account settings → Identity, turn the System assigned managed identity On.

    Tip The managed identity is what lets the runbook call Microsoft Graph without storing a password or secret anywhere.

  4. Grant the managed identity Graph permissions

    My lab granted User.ReadWrite.All, GroupMember.ReadWrite.All, and Directory.ReadWrite.All, configured with PowerShell in Cloud Shell. Here is one way to do it with the Microsoft Graph PowerShell module:

    powershell
    Connect-MgGraph -Scopes "Application.Read.All","AppRoleAssignment.ReadWrite.All"
    
    # Your Automation account's managed identity and the Microsoft Graph service principal
    $mi    = Get-MgServicePrincipal -Filter "displayName eq '<your-automation-account-name>'"
    $graph = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"
    
    foreach ($perm in "User.ReadWrite.All","GroupMember.ReadWrite.All","Directory.ReadWrite.All") {
      $role = $graph.AppRoles | Where-Object { $_.Value -eq $perm -and $_.AllowedMemberTypes -contains "Application" }
      New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $mi.Id `
        -PrincipalId $mi.Id -ResourceId $graph.Id -AppRoleId $role.Id
    }

    Check the result in Entra ID → Enterprise applications (filter by managed identities) → Permissions.

  5. Create the PowerShell runbook

    In the Automation account, create a PowerShell runbook named Invoke-JMLOffboard, paste the script, save, and Publish. It accepts a user’s UPN and logs each step.

    powershell
    param(
      [Parameter(Mandatory=$true)]
      [string]$UserPrincipalName
    )
    
    # Authenticate using managed identity
    Connect-AzAccount -Identity | Out-Null
    $token = (Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com").Token
    $headers = @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" }
    $graphBase = "https://graph.microsoft.com/v1.0"
    
    # Get user ID from UPN
    Write-Output "Looking up user: $UserPrincipalName"
    $user = Invoke-RestMethod -Uri "$graphBase/users/$UserPrincipalName" -Headers $headers
    $userId = $user.id
    Write-Output "Found user ID: $userId"
    
    # Step 1: Disable account
    Write-Output "Disabling account..."
    Invoke-RestMethod -Method PATCH -Uri "$graphBase/users/$userId" `
      -Headers $headers -Body '{"accountEnabled": false}'
    
    # Step 2: Revoke all active sessions
    Write-Output "Revoking sessions..."
    Invoke-RestMethod -Method POST `
      -Uri "$graphBase/users/$userId/revokeSignInSessions" -Headers $headers
    
    # Step 3: Remove all group memberships
    Write-Output "Removing group memberships..."
    $groups = Invoke-RestMethod -Uri "$graphBase/users/$userId/memberOf" -Headers $headers
    foreach ($group in $groups.value) {
      try {
        Invoke-RestMethod -Method DELETE `
          -Uri "$graphBase/groups/$($group.id)/members/$userId/`$ref" -Headers $headers
        Write-Output "Removed from group: $($group.displayName)"
      } catch {
        Write-Output "Could not remove from $($group.displayName): $_"
      }
    }
    
    # Step 4: Remove license assignments
    Write-Output "Removing licenses..."
    $licenses = Invoke-RestMethod -Uri "$graphBase/users/$userId/licenseDetails" -Headers $headers
    $skuIds = $licenses.value | ForEach-Object { $_.skuId }
    if ($skuIds.Count -gt 0) {
      $body = @{ addLicenses = @(); removeLicenses = $skuIds } | ConvertTo-Json
      Invoke-RestMethod -Method POST `
        -Uri "$graphBase/users/$userId/assignLicense" -Headers $headers -Body $body
      Write-Output "Licenses removed"
    } else {
      Write-Output "No licenses to remove"
    }
    
    Write-Output "Offboarding complete for $UserPrincipalName"

    Tip Newer versions of the Az module can return the access token as a secure string. If Graph calls fail with an authorization error, convert it first: $token = [System.Net.NetworkCredential]::new('', $token).Password

    Use the runbook’s Test pane with your test user’s UPN before wiring up the trigger.

  6. Build the Logic App trigger

    • Create a Consumption Logic App. Mine is named jml-trigger.
    • Trigger: When an HTTP request is received, with a JSON body like the one below.
    • Action: the Azure Automation Create job action. Pick your Automation account and Invoke-JMLOffboard, and pass userPrincipalName from the request body.
    • Action: send an email notification when the job finishes.
    • Save, then copy the trigger’s HTTP POST URL.
    json
    { "userPrincipalName": "testuser@yourtenant.onmicrosoft.com", "action": "terminate" }

    Tip Treat the HTTP POST URL like a password. It contains a key that lets anyone start the workflow.

  7. Fire a test termination event

    Send the event from PowerShell (or any HTTP client):

    powershell
    $body = '{ "userPrincipalName": "testuser@yourtenant.onmicrosoft.com", "action": "terminate" }'
    Invoke-RestMethod -Method Post -Uri "<your-logic-app-http-post-url>" `
      -Body $body -ContentType "application/json"

Check your work

Clean up

Delete the test users, then delete the resource group that holds the Automation account and Logic App. Remove the managed identity’s Graph permissions if you keep the account.

Interview ready

Be ready to explain

  • Why orphaned accounts are a risk, and how automated offboarding removes that gap.
  • Why a managed identity is safer than storing credentials in a script.
  • What each Graph call does: disable, revoke sessions, remove groups, remove licenses.
  • How the HTTP trigger could be swapped for an HR system webhook in production.
  • How you proved it worked: run history, runbook output, and audit logs.

Know your work front to back and you’ll be ready when someone says “walk me through it.”

This walkthrough follows my lab. Portals and menus change over time, so names may look slightly different when you try it. See my original write-up on GitHub

Back to the project overview