A deterministic, role-driven pipeline that onboards and offboards employees across on-prem Active Directory and Microsoft Entra ID — with human checkpoints for the judgment calls, and a complete, honest audit trail.
Onboarding and offboarding a hybrid employee is slow and error-prone because the steps span two identity systems plus mail, device, and delegation surfaces that are easy to miss.
The most common failure is incomplete access removal on offboarding — a disabled account that still holds group memberships, active sessions, MFA methods, shared-mailbox delegates, or third-party OAuth tokens. On the other side, onboarding is inconsistent: the same role gets a different set of groups depending on who typed the commands, and there is no record to prove it happened.
The fix is a deterministic, role-template-driven flow where AD is the source of truth, the deterministic bulk is automated, the judgment calls are explicit human checkpoints, and every step — including every failure — is written to a timestamped audit log.
On-prem Active Directory is the write path. Azure AD Connect syncs changes to Microsoft Entra ID. Microsoft Graph is used read-only to verify state and drive the audit — never as a second write source, so the two systems never disagree.
WRITE AD account + group membership ──(WinRM, DC)──► on-prem AD BRIDGE Azure AD Connect (delta sync) ───────────────► pushes AD → Entra ID VERIFY Microsoft Graph (read-only service principal) ► confirm + audit CONTROL explicit human checkpoints — logged, never auto-approved
Role templates drive assignment. Standard roles (e.g. IT-Engineer) run the full automated flow. Roles that declare elevated access (Domain Admin, jump hosts, financial write) stop at a checkpoint — nothing privileged is auto-assigned.
flowchart TD
A[INTAKE<br/>Parse HR PDF] --> B{CHECKPOINT<br/>HR-CONFIRM<br/>HR lead approves summary}
B --> C[P1: Account Creation<br/>Conflict-check →<br/>Create/reconcile AD account →<br/>Set attributes]
C --> D[Trigger sync →<br/>Wait for Entra →<br/>License-conflict check]
D --> E[P2: Access Assignment<br/>Assign role groups<br/>security / functional / mail]
E --> F[Nested-group resolution]
F --> G{Role declares<br/>elevated access?}
G -->|Yes| H{{CHECKPOINT<br/>ELEVATED<br/>Flow stops — nothing<br/>privileged auto-assigned}}
G -->|No| I[P3: Verify & Notify<br/>Verify AD + Entra →<br/>Notify hiring manager]
H -.->|manual approval required| I
classDef checkpoint fill:#ff9f0a,stroke:#ffc266,stroke-width:2px,color:#241804
classDef process fill:#13233b,stroke:#2997ff,stroke-width:1.4px,color:#ffffff
classDef decision fill:#2a2008,stroke:#ffb340,stroke-width:1.4px,color:#ffb340
class B,H checkpoint
class A,C,D,E,F,I process
class G decision
flowchart TD
A[P1: Immediate Containment<br/>Disable account →<br/>Revoke sessions + OAuth →<br/>Reset password →<br/>Revoke MFA] --> B[Trigger sync]
B --> C{Intune-managed<br/>device found?}
C -->|Yes| D{{CHECKPOINT: DEVICE<br/>Flag for manual decision<br/>wipe / unenroll / recover}}
C -->|No| E[P2: Access Cleanup<br/>Remove from ALL groups →<br/>Move to restricted OU]
D -.-> E
E --> F[Delegate check<br/>shared mailbox / SharePoint]
F --> G{{CHECKPOINT<br/>MANAGER-DATA<br/>Mailbox forwarding &<br/>data-archival decisions}}
G --> H{{CHECKPOINT<br/>DUAL-SIGNOFF<br/>IT + HR sign-off +<br/>legal-hold gate}}
H --> I[/Final Deletion<br/>NOT executed in demo —<br/>gated, logged as pending/]
classDef checkpoint fill:#ff9f0a,stroke:#ffc266,stroke-width:2px,color:#241804
classDef process fill:#13233b,stroke:#2997ff,stroke-width:1.4px,color:#ffffff
classDef decision fill:#2a2008,stroke:#ffb340,stroke-width:1.4px,color:#ffb340
classDef notrun fill:#17171a,stroke:#86868b,stroke-width:1.4px,stroke-dasharray:6 5,color:#86868b
class D,G,H checkpoint
class A,B,E,F process
class C decision
class I notrun
The design principle: automation does the deterministic bulk, humans own the judgment calls — elevated access, data handling, and destructive deletion. Nothing privileged or irreversible is auto-executed.
Both flows were run end-to-end against a real hybrid lab (on-prem AD, Azure AD Connect, Microsoft Entra ID) for a test employee, and every number below was read from the timestamped audit of that run.
A single known lab defect dominates the wall clock: the AD Connect sync-trigger cmdlet hangs (~90 s per run, finding F4). Stripping that one hung call out, the real automation work is small and fast:
| Operation | Measured | Note |
|---|---|---|
| Onboard — account + 7 groups + Entra verify | ~37 s | 95 s wall includes one 90 s sync-trigger hang |
| Offboard — disable, pw reset, 9 group removals | ~12 s | 92 s wall includes one 90 s sync-trigger hang |
| 9-group removal (the critical step) | < 1 s / group | via Set-ADGroup -Remove; the Remove-ADGroupMember cmdlet hung (F2) |
These are the non-obvious, verified-in-lab facts that any team building this automation will hit. Reported honestly — not hidden as errors.
| Finding | What happened |
|---|---|
| UPN suffix is not assumed | An AD account user@company.com synced to Entra as user@tenant.onmicrosoft.com — not the expected domain. All Entra lookups therefore key on onPremisesSamAccountName (reliable), never the guessed UPN. |
| Group-removal cmdlet hangs | Remove-ADGroupMember hung indefinitely in this multi-DC lab, while the delta-attribute path Set-ADGroup -Remove @{Member=…} completed all 9 removals in < 1 s/group. The pipeline uses the working path and a hard per-call timeout as the guard. |
| Session contamination | A hung WinRM op left the shared WSMan session locked, so every following call returned HTTP 400. Fixed by evicting the session on timeout so the next call opens fresh — after which the group assignments succeeded. |
| Disabling ≠ revoking OAuth | revokeSignInSessions returned 403 (scope not granted to the service principal). Disabling an account does not by itself invalidate third-party "Sign in with Microsoft" tokens — the scope must be granted and the call made. |
| Developer-tenant limits | No Exchange, no Intune, 0 subscribed SKUs, 0 dynamic groups. So mail delivery, shared-mailbox delegates, Intune wipe, and the license-conflict branch could not be exercised — each is logged as a valid negative result, not a silent skip. |