Andrew Galbraith

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:

  1. Volume. Hundreds of applications, each with its own authentication scheme, authorization rules, and quirks, all described in one very large policy export.
  2. 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.
  3. 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:

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

.CLOSING

The code is the easy part. The vocabulary is the project. Budget accordingly.

#entra #migration #powershell #project