Translating legacy SSO policy into Entra ID, one object at a time
.SYNOPSIS
Large organizations often have hundreds of internal web apps that sign people in through an older single sign-on product. Moving them to Microsoft Entra ID means rebuilding each app's access rules in a system that uses different words for the same ideas. I built a proof-of-concept tool that reads the old system's policy export and creates the matching Entra objects automatically. I designed its translation table to double as a teaching aid, so the people who know the old system and the people who know the new one can have the same conversation.
If you stop reading here, that's the whole story. Everything below is how.
.CONTEXT
An on-premises web access manager, the kind that sits in front of internal apps, checks who you are, and decides whether to let you through, was reaching end of life. Everything behind it had to move to Entra ID.
Three things made it hard:
- Volume. Hundreds of applications, each with its own authentication scheme, authorization rules, and quirks, all described in one very large policy export.
- Vocabulary. The engineers who understood the legacy system and the engineers who understood Entra ID were describing the same concepts with different words, and sometimes the same word for different concepts. Most of the early friction was translation, not technology.
- Constraints. No third-party bridging products; Microsoft-native only. Windows PowerShell 5.1, fixed. A pinned Microsoft Graph SDK version with known regressions in some typed cmdlets. And one operation, publishing an app through Application Proxy, that requires a delegated, interactive session rather than an app-only one.
.PROBLEM
Doing this by hand meant an engineer reading a policy definition, deciding what it "meant" in Entra terms, clicking through the portal, and hoping the next engineer made the same decisions. Slow, inconsistent, and impossible to audit.
.SOLUTION
Four stages. The middle two are the interesting ones.
1. Parse. Read the export once into a normalized object model: applications, protected resources, authentication schemes, authorization constraints. The parser is boring on purpose. It makes no decisions, it just makes the data legible.
2. Map. A translation table from legacy concepts to Entra concepts. Simplified examples:
| Legacy concept | Entra equivalent |
|---|---|
| Protected resource | App registration + enterprise application |
| Authorization policy (who may enter) | Group assignment, plus Conditional Access where the rule is contextual |
| Header-based auth to the backend | Application Proxy with header pass-through |
| Session / idle timeout | Sign-in frequency and token lifetime policy |
This table is the actual deliverable. The code is just what makes it executable.
3. Create. Idempotent bulk creation against Microsoft Graph. Every run checks for existing objects before creating anything, so it can be re-run safely after a partial failure. -WhatIf prints the plan without touching the tenant. Output is logged as JSON with -Depth 10 so nothing truncates silently. Where the typed SDK cmdlets misbehave on the pinned version, raw Invoke-MgGraphRequest calls replace them.
A representative fragment. The shape, not the production code:
function Confirm-AppRegistration {
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[Parameter(Mandatory)] [string] $DisplayName,
[Parameter(Mandatory)] [string[]] $RedirectUris
)
# Look before you leap: idempotency starts with a read.
$filter = "displayName eq '$($DisplayName.Replace("'", "''"))'"
$existing = Invoke-MgGraphRequest -Method GET `
-Uri "https://graph.microsoft.com/v1.0/applications?`$filter=$filter" |
Select-Object -ExpandProperty value
if ($existing) {
Write-Host " exists $DisplayName" -ForegroundColor DarkGray
return $existing[0]
}
$body = @{
displayName = $DisplayName
web = @{ redirectUris = $RedirectUris }
} | ConvertTo-Json -Depth 10
if ($PSCmdlet.ShouldProcess($DisplayName, 'Create app registration')) {
Write-Host " creating $DisplayName" -ForegroundColor Yellow
$app = Invoke-MgGraphRequest -Method POST `
-Uri 'https://graph.microsoft.com/v1.0/applications' `
-Body $body -ContentType 'application/json'
Write-Host " created $($app.appId)" -ForegroundColor Green
return $app
}
}
4. Teach. The translation table, laid out as a two-column glossary with one anonymized example per row, is built to be the agenda for a workshop between the two teams. The tool's -WhatIf output ("here is what I would create, and why") doubles as an explanation of Entra's object model in terms the legacy side already knows.
.RESULT
A working proof of concept, not a finished migration. It does two jobs:
- A pipeline. One script reads the full policy export and bulk-creates the matching Entra objects, the same way every time.
- A Rosetta Stone. The translation table gives the legacy experts and the Entra experts one shared reference, and it doubles as workshop material.
The biggest finding wasn't technical. The real blocker was the vocabulary gap between two groups of experts, and the tool is most useful as a way to close it.
.NOTES
- The delegated-session requirement for Application Proxy publishing means the pipeline runs in two phases with two auth contexts. Plan for it up front. Retrofitting it is miserable.
- Pinning the Graph SDK is the right call for reproducibility and the wrong call for ergonomics. Raw REST calls are less pretty and more predictable.
- If I did it again, I'd build the translation table first, in a spreadsheet, with both teams in the room, and write the parser to target it, rather than discovering the table by writing the parser.
.CLOSING
The code is the easy part. The vocabulary is the project. Budget accordingly.