Azure Lesson 50 of 137

Migrating from AD FS to Entra ID Authentication: Staged Cutover with PHS, Staged Rollout, and Claims-Rule Mapping

In a nutshell

Picture your company moving out of an old office block into a shiny new one across the street. You do not hand everyone a new key on the same Monday morning and hope for the best. You move one department at a time. Accounting goes first; if the lifts or the Wi-Fi misbehave on their floor, you walk them straight back to the old building - which you deliberately kept open and running - while you fix the problem. Only once every department is settled and happy do you finally lock the old building and hand back the keys.

That is exactly what migrating from AD FS to Entra ID authentication looks like when you do it well. AD FS is the old building: a farm of Windows Servers, a DMZ tier, and certificates that expire at inconvenient hours, all doing one job - proving who a user is when they sign in. Entra ID is the new building, and it can do that job directly in the cloud. Staged Rollout is the department-by-department move: you shift small groups of users to cloud authentication while the old federation stays fully open, prove each group works, then expand. If a group has trouble, you remove it from the rollout group and those users are back on AD FS instantly - no downtime, no drama.

The single mental shift for a beginner: this is a migration with a reversible, per-group cutover, not a switch you flip on the domain. The scary-sounding final step - converting the domain from “federated” to “managed” - is the anticlimax at the end, not the event. All the real work is inventory, making sure each app still trusts the new identity provider, and proving one small group at a time.

AD FS → Entra ID: staged rollout of cloud auth (PHS/PTA) group-by-group

Left to right: pre-stage Password Hash Sync and Seamless SSO while everyone is still federated, move user cohorts to cloud authentication one security group at a time (the old AD FS building stays open the whole time), convert the domain to managed only once every cohort and app is proven, then soak AD FS at zero traffic and decommission it in dependency order.

Level: Advanced · Time: ~39 min

Prerequisites

After this lesson you can

AD FS was the right answer in 2014 and it is technical debt in 2026: a Windows Server farm, a WAP DMZ tier, certificates that expire at 2 a.m., and claims rules written in a language only one person on the team still reads. The destination is Entra ID handling authentication directly, with the AD FS estate gone. The trap is treating this as a flag flip on the domain - it is a migration with a reversible, per-cohort cutover so you can prove each step before the next. This runbook walks the full path: inventory, authentication method, Staged Rollout cohorts, translating claims issuance rules into claims mapping policies, moving relying parties to enterprise apps, reproducing access control in Conditional Access, the domain conversion, and the decommission.

I assume Hybrid Identity Administrator plus Application Administrator, Entra Connect already syncing the relevant forests, and Entra ID P1 (P2 if you want risk-based Conditional Access).

1. Assess the AD FS estate: relying parties, claims rules, access-control policies

The single most expensive mistake is discovering a relying party in production after you decommissioned the farm. Pull everything from AD FS as data first.

# On a primary AD FS server. Export every relying party trust with its rules.
$rps = Get-AdfsRelyingPartyTrust
$rps | Select-Object Name, Identifier, Enabled, WSFedEndpoint, `
    @{n='SamlACS';e={$_.SamlEndpoints.Location}}, SignatureAlgorithm |
    Export-Csv C:\adfs-migration\relying-parties.csv -NoTypeInformation

# Dump the claims issuance rules and access-control policy per RP, as code to diff later
foreach ($rp in $rps) {
    $safe = ($rp.Name -replace '[^\w\-]','_')
    $rp.IssuanceTransformRules     | Out-File "C:\adfs-migration\rules\$safe.issuance.txt"
    $rp.IssuanceAuthorizationRules | Out-File "C:\adfs-migration\rules\$safe.authz.txt"
    $rp.AccessControlPolicyName    | Out-File "C:\adfs-migration\rules\$safe.acp.txt"
}

# Custom claim descriptions, endpoints, and the farm's signing/token-decrypt certs
Get-AdfsClaimDescription | Export-Csv C:\adfs-migration\claim-descriptions.csv -NoTypeInformation
Get-AdfsCertificate | Select-Object CertificateType, Thumbprint, NotAfter, IsPrimary |
    Export-Csv C:\adfs-migration\certs.csv -NoTypeInformation

Microsoft ships AD FS migration scripts (the ADFSToAADAppMigration / ADFSAADMigrationUtils module) that score each RP’s automatic-migration readiness. Use it for a triage list, but anything with custom claim rules or an unusual NameID needs a human.

Bucket every relying party into four lanes: (1) gallery app - migrate to the first-party gallery template; (2) OIDC-capable - re-platform onto OpenID Connect and retire SAML; (3) SAML/WS-Fed, keep the protocol - federate as a non-gallery enterprise app; (4) cannot move - smart-card-only, a third-party MFA adapter with no cloud equivalent, or a legacy WS-Trust client. Lane 4 decides whether you fully decommission or keep a minimal AD FS island.

Two findings matter most before you commit: RPs using non-standard NameID formats, and any RP whose IssuanceAuthorizationRules is more than “permit all” - those become Conditional Access work in step 6.

2. Choose the target: PHS or PTA, and the role of Seamless SSO

Authentication after AD FS means cloud authentication. Two supported methods:

Method Where the password is validated On-prem dependency at sign-in When to pick it
Password Hash Sync (PHS) In the cloud, against a synced hash-of-a-hash None The default. Survives an on-prem outage.
Pass-through Authentication (PTA) On-prem DC via lightweight agents Live agent + reachable DC “Passwords never leave the building” mandates

Default to PHS even if compliance pushes you toward PTA: Staged Rollout, leaked-credential detection (Entra ID Protection), and instant failover all depend on it. If you must run PTA, deploy at least three agents on separate hosts before cutover - PTA reintroduces the exact on-prem runtime dependency you are trying to delete.

Enable PHS now, while still federated. It changes nothing about how federated users sign in but pre-stages the hashes so a cohort can flip to cloud auth instantly.

# Confirm PHS is on for the connector even while the domain is still Federated
$c = Get-ADSyncConnector | Where-Object { $_.Type -eq 'Extensible2' }
Get-ADSyncAADPasswordSyncConfiguration -SourceConnector $c.Name   # Enabled should be True

Seamless SSO is orthogonal: it silently signs in domain-joined, corporate-network users via Kerberos - the thing AD FS did for you on the corp network. Enable it before cutover so the experience does not regress the moment a cohort leaves federation. It creates the AZUREADSSOACC computer object, whose Kerberos key must be rolled at least every 30 days.

3. Use Staged Rollout to migrate user cohorts off federation safely

Staged Rollout makes this reversible. It moves selected groups to cloud authentication (PHS or PTA) while the domain remains Federated: those users authenticate against Entra, everyone else still goes to AD FS. You validate one cohort, then expand - no big-bang flip, and rollback is removing a group.

Prerequisites the portal will not always shout at you: PHS (or PTA) configured, Seamless SSO on, and security groups of at most 50,000 users with direct membership - nested groups are not honored.

Enable it under Entra Connect > Connect Sync > Staged Rollout of cloud authentication in the portal, or drive it through Microsoft Graph as a featureRolloutPolicy:

# Create a Staged Rollout policy for Password Hash Sync
az rest --method POST \
  --url "https://graph.microsoft.com/v1.0/policies/featureRolloutPolicies" \
  --headers "Content-Type=application/json" \
  --body '{
    "displayName": "PHS Staged Rollout - Wave 1",
    "feature": "passwordHashSync",
    "isEnabled": true,
    "isAppliedToOrganization": false
  }'
# Add a pilot security group to the policy (appliesTo references the group object)
az rest --method POST \
  --url "https://graph.microsoft.com/v1.0/policies/featureRolloutPolicies/<policyId>/appliesTo/\$ref" \
  --headers "Content-Type=application/json" \
  --body '{ "@odata.id": "https://graph.microsoft.com/v1.0/directoryObjects/<groupId>" }'

The feature value is passwordHashSync, passThroughAuthentication, or seamlessSso. Run waves - a 20-user IT cohort, then a non-critical business unit, then broader - and confirm in the sign-in logs that each cohort authenticates in the cloud, not via AD FS, before expanding.

// Entra sign-in logs: are Staged Rollout users hitting the cloud, not AD FS?
SigninLogs
| where TimeGenerated > ago(1d)
| extend authDetail = tostring(parse_json(AuthenticationProcessingDetails))
| where UserPrincipalName in ("pilot1@contoso.com","pilot2@contoso.com")
| project TimeGenerated, UserPrincipalName, AppDisplayName,
          ResultType, AuthenticationRequirement, authDetail
| order by TimeGenerated desc

Staged Rollout migrates user authentication, not applications. A user in a rollout group still reaches a SAML relying party through AD FS until you move that RP (steps 4-5). Sequence it: prove cloud auth for the cohort, migrate the apps, then convert the domain - do not jump ahead because a pilot looked clean.

4. Translate AD FS claims issuance rules into Entra claims mapping and policies

This is the hard part. AD FS uses the claims rule language (acceptance and issuance transform rules over a claims pipeline). Entra has no equivalent language - it has claims mapping policies plus claim transformations and directory extensions per service principal. You are not porting syntax, you are re-expressing intent.

A typical AD FS issuance rule emitting an email NameID and a UPN claim:

@RuleName = "Email as NameID and UPN"
c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"]
 => issue(
      store = "Active Directory",
      types = ("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
               "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"),
      query = ";mail,userPrincipalName;{0}", param = c.Value);

In Entra the equivalent is the app’s Attributes & Claims blade, or for advanced cases a claimsMappingPolicy bound to the service principal. The default NameID is UPN; to emit mail as NameID and add UPN as a separate claim:

{
  "definition": [
    "{\"ClaimsMappingPolicy\":{\"Version\":1,\"IncludeBasicClaimSet\":\"true\",\"ClaimsSchema\":[{\"Source\":\"user\",\"ID\":\"mail\",\"SamlClaimType\":\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier\"},{\"Source\":\"user\",\"ID\":\"userprincipalname\",\"SamlClaimType\":\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn\"}]}}"
  ],
  "displayName": "billing-saml-claims",
  "isOrganizationDefault": false
}
# Create the policy, then assign it to the app's service principal
POLICY_ID=$(az rest --method POST \
  --url "https://graph.microsoft.com/v1.0/policies/claimsMappingPolicies" \
  --headers "Content-Type=application/json" \
  --body @billing-claims.json --query id -o tsv)

az rest --method POST \
  --url "https://graph.microsoft.com/v1.0/servicePrincipals/<spId>/claimsMappingPolicies/\$ref" \
  --headers "Content-Type=application/json" \
  --body "{ \"@odata.id\": \"https://graph.microsoft.com/v1.0/policies/claimsMappingPolicies/$POLICY_ID\" }"

Mapping table for the rules you will actually hit:

AD FS construct Entra equivalent
LDAP attribute -> claim (store = "Active Directory") Source user, an attribute ID, mapped to a SamlClaimType
Constant / literal issuance Transformation with a constant input, or a fixed value claim
RegexReplace / string manipulation Claim transformation (Join, ExtractMailPrefix, ToLowercase, RegexReplace, etc.)
Group membership -> role claim App roles assigned to groups, emitted as the roles claim
Custom claim type Custom claim with the original URI as SamlClaimType
Synced custom attribute (e.g. employeeId) Directory extension synced by Entra Connect, used as the claim source

Two rules cause most post-cutover breakage. NameID mismatch: if AD FS emitted mail and Entra defaults to UPN, the SP creates a new account or rejects the assertion - pin NameID explicitly per app. Group/role claims: AD FS often emitted group SIDs or names the app authorized against; reproduce that with app roles or a groups claim, deciding whether to emit objectId (default) or on-prem group names (which requires syncing the right attribute).

The SAML SSO flow attaches the policy to the service principal for you, so per-app claim customization takes effect without hand-managing signing keys.

5. Migrate SAML and WS-Fed relying parties to enterprise applications

Move the relying parties one lane at a time. Gallery apps are easiest: add from the gallery, configure SSO with the SP’s metadata, inherit a maintained connector. Non-gallery SAML/WS-Fed apps are instantiated from the generic SAML template and configured by metadata exchange.

# Instantiate a non-gallery SAML enterprise app from the generic template
az rest --method POST \
  --url "https://graph.microsoft.com/v1.0/applicationTemplates/8adf8e6e-67b2-4cf2-a259-e3dc5476c621/instantiate" \
  --headers "Content-Type=application/json" \
  --body '{"displayName": "acme-billing-saml"}'

The contract fields must match what AD FS published so the SP needs no change beyond trusting a new IdP: Identifier (Entity ID) = the RP Identifier (the assertion audience must equal this); Reply URL (ACS) = the SAML SamlEndpoints.Location (WS-Fed apps use WSFedEndpoint); plus Sign-on URL / RelayState for IdP-initiated or deep-link flows.

Then hand the SP Entra’s app-specific IdP metadata so it can validate the new signature, and import the new signing certificate:

https://login.microsoftonline.com/<tenant-id>/federationmetadata/2007-06/federationmetadata.xml?appid=<application-id>

Sequence the SP change to the moment of app cutover. While AD FS still serves an RP, do not point the SP at Entra; once the SP trusts Entra, that app no longer flows through AD FS - independent of any Staged Rollout group. App migration and user-auth migration are two switches; flip apps deliberately, one by one, with a rollback (re-point the SP at AD FS) ready for each.

WS-Fed RPs map to enterprise apps too (Entra supports WS-Fed as an SSO mode), but a WS-Trust client that cannot speak SAML 2.0 or OIDC is lane 4 - possibly your reason to keep a minimal AD FS island.

6. Reproduce access-control policies with Conditional Access equivalents

AD FS IssuanceAuthorizationRules and Access Control Policies (“permit intranet only”, “require MFA from extranet”, “permit specific groups”) do not migrate as data - they become Conditional Access. Map the intent:

AD FS access-control intent Conditional Access equivalent
Permit specific AD groups only CA assignment: include those groups; block others / assign-to-all-and-exclude
Require MFA from extranet CA grant: require MFA, scoped by network location (named locations)
Permit only from intranet IP ranges CA condition: locations, block outside trusted IPs
Device-bound access (registered/compliant) CA grant: require compliant or Hybrid Azure AD joined device
Per-RP authorization CA policy scoped to that cloud app (the enterprise app)

A representative policy: “this app requires MFA except from the trusted corporate network”:

{
  "displayName": "CA200 - Billing app requires MFA off-network",
  "state": "enabledForReportingButNotEnforced",
  "conditions": {
    "applications": { "includeApplications": ["<billing-app-appId>"] },
    "users": { "includeGroups": ["<billing-users-groupId>"] },
    "locations": {
      "includeLocations": ["All"],
      "excludeLocations": ["<trusted-corp-named-location-id>"]
    }
  },
  "grantControls": {
    "operator": "OR",
    "builtInControls": ["mfa"]
  }
}

Always create these in report-only mode first (enabledForReportingButNotEnforced). CA decisions surface in the sign-in logs’ Conditional Access tab - run the cohort through report-only, confirm the right policies would apply, then enforce. A missed authorization rule that silently lets everyone into a previously restricted app is a security regression, not just a bug.

The AD FS “intranet only” pattern is not a single Entra toggle - it is a CA policy referencing named locations, so enumerate your egress IPs as a named location first or you lock out on-network users.

7. Cutover: convert domains from federated to managed and monitor sign-ins

Once a domain’s users are validated under Staged Rollout and its relying parties are moved, convert the domain from federated to managed. It feels irreversible (it is reversible, but it re-touches every federated user), so do it per domain, off-hours, with a rollback rehearsed.

Use the Microsoft Graph PowerShell SDK - the legacy MSOnline Set-MsolDomainAuthentication path is deprecated and being retired.

Connect-MgGraph -Scopes "Domain.ReadWrite.All","Directory.AccessAsUser.All"

# Confirm current state before touching anything
Get-MgDomain -DomainId contoso.com | Select-Object Id, AuthenticationType, IsVerified

# Convert federated -> managed (cloud authentication via PHS/PTA)
Update-MgDomain -DomainId contoso.com -AuthenticationType "Managed"

# Verify it flipped
(Get-MgDomain -DomainId contoso.com).AuthenticationType   # expect: Managed

On conversion, users in a Staged Rollout group for that domain are removed from the rollout (the domain is now managed, a superset). Conversion is a control-plane operation; existing sessions and tokens stay valid until they expire, so the experience change is gradual.

Plan for the MFA-registration nuance before converting. Federated users may never have registered cloud MFA (AD FS or a third-party adapter handled it). After conversion, MFA is enforced by Conditional Access against Entra-registered methods - so drive registration before cutover (Staged Rollout is the window) or users hit an MFA wall at first managed sign-in. Pre-register via a CA “register security information” flow during the pilot.

Watch the sign-in logs closely for the first hours after conversion:

// Post-conversion health: surface failures by app and result code
SigninLogs
| where TimeGenerated > ago(2h)
| summarize Total=count(),
            Failures=countif(ResultType != 0),
            DistinctUsers=dcount(UserPrincipalName)
        by AppDisplayName, ResultType, ResultDescription
| where Failures > 0
| order by Failures desc

A spike in ResultType 50126 (invalid credentials) right after conversion usually means PHS was not actually flowing for some users - investigate before the next domain. 50076/50079 indicate MFA registration gaps, exactly what the pre-registration step prevents.

8. Decommission AD FS, WAP, and clean up DNS, certificates, and trusts

Do not power off the farm the day after conversion. Leave it idle for a soak period (a week or two) so any forgotten relying party announces itself in the AD FS logs, and watch the request rate drop to zero before removing anything.

# On AD FS: is anything still authenticating? Watch this trend to zero before decommission.
Get-WinEvent -LogName "AD FS/Admin" -MaxEvents 200 |
    Where-Object { $_.Id -in 299,324,412 } |
    Select-Object TimeCreated, Id, Message | Format-Table -Wrap

When traffic is genuinely zero, decommission in dependency order - DMZ first, trust last:

  1. WAP (DMZ) tier first. Remove the Web Application Proxy nodes from the load balancer, then the servers. They are the internet-facing attack surface; kill them first.
  2. Repoint or retire DNS. The sts.contoso.com / adfs.contoso.com A records (internal and external) and any enterpriseregistration records. Lower TTL ahead of time, then remove.
  3. Remove the AD FS role from the internal farm nodes, then delete the configuration database (WID or SQL).
  4. Certificates and trusts. Retire the token-signing and token-decrypting certificates, remove the SSL binding, and revoke if they were dedicated to AD FS. Remove device registration artifacts only if you have moved device registration to Entra - do not orphan Hybrid Azure AD Join.
  5. Service account and SPNs. De-provision the AD FS gMSA/service account and clean up the host/sts.contoso.com SPNs so they cannot be reused.
# Confirm Entra no longer thinks any domain is federated before you wipe the farm
Get-MgDomain | Where-Object { $_.AuthenticationType -eq 'Federated' } |
    Select-Object Id, AuthenticationType
# Empty result == safe to decommission AD FS for authentication purposes

The order matters: device registration and certain hybrid-join flows can ride on the AD FS device registration service. Confirm Hybrid Azure AD Join is healthy in the Entra device inventory before tearing down trusts, or you silently break device-based Conditional Access for the whole fleet.

Enterprise scenario

A financial-services platform team ran AD FS for 40-odd relying parties and was three months into a “flip to managed” plan that had stalled twice. The blocker: their flagship SAML payments app emitted the user’s on-prem sAMAccountName as the NameID (an AD FS rule querying ;sAMAccountName;{0}), and the SP keyed every payment record to that value. A naive cutover would make Entra emit UPN, the SP would create shadow accounts, and every payment mandate would orphan - a customer-visible failure. They were about to keep AD FS indefinitely.

The constraint was real: the NameID had to stay the legacy sAMAccountName, which is not a default Entra claim source. The fix was a directory-extension-backed claims mapping policy. They had Entra Connect sync sAMAccountName into a directory extension, then bound a policy emitting that extension as the NameID with the original claim URI - byte-for-byte what AD FS produced.

{
  "definition": [
    "{\"ClaimsMappingPolicy\":{\"Version\":1,\"IncludeBasicClaimSet\":\"true\",\"ClaimsSchema\":[{\"Source\":\"user\",\"ExtensionID\":\"extension_<appId>_onPremSamAccountName\",\"SamlClaimType\":\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier\",\"SamlNameIdentifierFormat\":\"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\"}]}}"
  ],
  "displayName": "payments-nameid-sam",
  "isOrganizationDefault": false
}

They migrated that one app under a Staged Rollout cohort of ten back-office users, ran a synthetic payment end to end, confirmed the SP matched the existing record by NameID, then expanded. The conversion stuck for three months completed in one off-hours window once NameID parity was proven. Lesson: the hard part is almost never the domain flip - it is one or two relying parties with a load-bearing claim that has no default Entra source. Solve those with directory extensions and a claims mapping policy on a tiny cohort, and the rest is mechanical.

Verify

Prove each layer before moving to the next domain.

# 1. Authentication method actually flowing in the cloud
$c = Get-ADSyncConnector | Where-Object { $_.Type -eq 'Extensible2' }
Get-ADSyncAADPasswordSyncConfiguration -SourceConnector $c.Name   # Enabled = True

# 2. Domain state is what you intend (Managed after cutover, Federated before)
Get-MgDomain | Select-Object Id, AuthenticationType, IsVerified | Sort-Object Id

# 3. Staged Rollout policies and their target groups
Get-MgPolicyFeatureRolloutPolicy | Select-Object DisplayName, Feature, IsEnabled

Then validate from the user’s chair: a cutover user signs in at https://myapps.microsoft.com, opens a migrated SAML app, and lands authenticated with the same identity (NameID) the SP recorded under AD FS. On a domain-joined corp machine they should not be prompted (Seamless SSO). Confirm in SigninLogs that sign-in is no longer federated and the expected Conditional Access policies applied, and that the AD FS request rate is zero before any teardown.

Checklist

Going deeper

The runbook above is the what and the when. This section is the why and the under-the-hood - the parts a beginner needs to trust the plan and an experienced engineer needs to defend it in a design review.

Why leave AD FS at all?

AD FS was the correct answer when the cloud could not authenticate your users for you. In 2026 it is mostly liability:

The honest counter-case (lane 4): a WS-Trust-only client, a third-party MFA adapter with no cloud equivalent, or a bespoke smartcard flow may pin you to a minimal AD FS island for one or two apps. That is a reason to shrink AD FS to a rump, not to keep the whole farm.

The target methods: PHS vs PTA vs staying federated

Cloud authentication means Entra validates the credential. There are two supported methods, and the choice is mostly about where the password is checked:

Default to PHS. Even under a “passwords must never leave the building” reading, note that PHS syncs a salted hash-of-a-hash, not the password and not a usable NTLM hash - many security teams accept it once they understand the transform. If you genuinely cannot, PTA is the fallback, but you pay for it in availability and lost features.

Seamless SSO - the corp-network convenience AD FS gave you

Seamless SSO silently authenticates users on domain-joined machines on the corporate network using Kerberos, with no password prompt - exactly what AD FS did for on-network users. Skip it and the day a cohort leaves federation they suddenly get prompted where they never were, and it reads as a regression. Enable it before cutover. Mechanically, it creates an AZUREADSSOACC$ computer object in on-prem AD that represents Entra; the Kerberos decryption key on that object must be rolled at least every 30 days (Entra Connect can automate this, but confirm it is scheduled) or SSO silently breaks. Note the scope: Seamless SSO serves browser and modern-auth clients on the corp network; it is not needed on Entra-joined or Hybrid-joined Windows devices, which get silent SSO from the Primary Refresh Token instead.

Staged Rollout internals

Staged Rollout is a featureRolloutPolicy object in Microsoft Graph (/policies/featureRolloutPolicies). Each policy targets one feature - passwordHashSync, passThroughAuthentication, seamlessSso, or certificateBasedAuthentication - and carries an appliesTo collection of directory objects (security groups). The constraints, in one place:

Portal path: Entra Connect → Connect Sync → Staged Rollout of cloud authentication. In Microsoft Graph PowerShell the cmdlet family is *-MgPolicyFeatureRolloutPolicy (New-, Get-, Update-, Remove-), plus New-MgPolicyFeatureRolloutPolicyAppliesToByRef to attach a group - the same objects the az rest calls in step 3 create. (There is no single Start-Migration cmdlet; “staged rollout” is this policy plus its groups.)

The cutover: what “convert to managed” actually does

Converting a domain from federated to managed is a single control-plane operation, but understand what it touches:

What breaks - and where each thing goes

Cloud authentication changes assumptions that on-prem apps and legacy clients quietly relied on. The four you will meet:

Conditional Access is not a claim-rule port - it is a re-expression

AD FS IssuanceAuthorizationRules and Access Control Policies encode authorization intent (“permit these groups”, “MFA from extranet”, “intranet IPs only”). None of it migrates as data. You re-express the intent as Conditional Access, scoped per enterprise app, and here is the subtle trap: an app with no CA policy is reachable by anyone who can authenticate. AD FS’s default-deny-per-RP posture becomes your responsibility to reproduce. So:

Rollback, at three levels

Keep reversibility explicit at every layer so nobody freezes mid-migration:

  1. A cohort misbehaves → remove the group from the Staged Rollout policy; those users are back on AD FS on their next sign-in. No domain change.
  2. A migrated app misbehaves → re-point that app’s service principal / relying party back at AD FS. App cutover is independent of user-auth cutover, so this touches one app only.
  3. A converted domain misbehavesUpdate-MgDomain -AuthenticationType "Federated" restores federation for the whole domain. It re-touches every user, so it is the last resort, not the first reflex.

Practice challenges

Work these in order; they escalate from “read the state” to “cut a domain over with a rollback ready”. Every command is real and current, but there is no live tenant here - treat outputs as representative and never point these at production without a change window.

1. (Beginner) Tell whether a domain is federated or managed.

<details> <summary>Solution</summary>

Connect-MgGraph -Scopes "Domain.Read.All"
Get-MgDomain | Select-Object Id, AuthenticationType, IsVerified | Sort-Object Id

Why: AuthenticationType is either Federated (still on AD FS) or Managed (cloud auth). This one column is the single source of truth for where a domain sits in the migration. </details>

2. (Beginner) Confirm Password Hash Sync is flowing while the domain is still federated.

<details> <summary>Solution</summary>

$c = Get-ADSyncConnector | Where-Object { $_.Type -eq 'Extensible2' }
Get-ADSyncAADPasswordSyncConfiguration -SourceConnector $c.Name   # Enabled = True

Why: a cohort can only flip to cloud auth if its password hashes are already in Entra. Enabling PHS while federated is invisible to users but pre-stages the hashes, so this must read True before you touch Staged Rollout. </details>

3. (Intermediate) Create a PHS Staged Rollout policy and attach a pilot group using Graph PowerShell (the SDK equivalent of the az rest calls in step 3).

<details> <summary>Solution</summary>

Connect-MgGraph -Scopes "Directory.ReadWrite.All"

# Create the policy for one feature
$policy = New-MgPolicyFeatureRolloutPolicy -DisplayName "PHS Staged Rollout - Wave 1" `
  -Feature passwordHashSync -IsEnabled:$true -IsAppliedToOrganization:$false

# Attach a direct-membership security group by object reference
New-MgPolicyFeatureRolloutPolicyAppliesToByRef -FeatureRolloutPolicyId $policy.Id `
  -OdataId "https://graph.microsoft.com/v1.0/directoryObjects/<groupId>"

Why: the policy targets one feature; the group in appliesTo is exactly who moves to cloud auth while everyone else stays on AD FS. Keep the group direct-membership, ≤50,000 users, no nesting - and rollback is simply detaching it. </details>

4. (Intermediate) Write a KQL query that proves pilot users are authenticating in the cloud, not via AD FS.

<details> <summary>Solution</summary>

SigninLogs
| where TimeGenerated > ago(1d)
| where UserPrincipalName in ("pilot1@contoso.com","pilot2@contoso.com")
| summarize SignIns=count() by UserPrincipalName, TokenIssuerType, ResultType
| order by UserPrincipalName asc

Why: TokenIssuerType distinguishes AzureAD (cloud auth - what you want for a rolled-out cohort) from ADFederationServices (still via AD FS). If a pilot user still shows AD FS issuance, the rollout group did not take effect for them - fix that before you expand the wave. </details>

5. (Advanced) Pin a SAML app’s NameID to mail with a claims mapping policy so it does not default to UPN, and bind it to the app.

<details> <summary>Solution</summary>

nameid-mail.json (the policy definition - note the escaped inner JSON string):

{
  "definition": [
    "{\"ClaimsMappingPolicy\":{\"Version\":1,\"IncludeBasicClaimSet\":\"true\",\"ClaimsSchema\":[{\"Source\":\"user\",\"ID\":\"mail\",\"SamlClaimType\":\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier\"}]}}"
  ],
  "displayName": "billing-nameid-mail",
  "isOrganizationDefault": false
}
POLICY_ID=$(az rest --method POST \
  --url "https://graph.microsoft.com/v1.0/policies/claimsMappingPolicies" \
  --headers "Content-Type=application/json" \
  --body @nameid-mail.json --query id -o tsv)

az rest --method POST \
  --url "https://graph.microsoft.com/v1.0/servicePrincipals/<spId>/claimsMappingPolicies/\$ref" \
  --headers "Content-Type=application/json" \
  --body "{ \"@odata.id\": \"https://graph.microsoft.com/v1.0/policies/claimsMappingPolicies/$POLICY_ID\" }"

Why: if AD FS emitted mail as the NameID and Entra defaults to UPN, the app creates shadow accounts or rejects the assertion. Pinning NameID per app to exactly what AD FS produced is the number-one control against account orphaning at cutover. </details>

6. (Advanced) Convert a domain to managed, verify the flip, and write the one-line rollback.

<details> <summary>Solution</summary>

Connect-MgGraph -Scopes "Domain.ReadWrite.All","Directory.AccessAsUser.All"

# Cutover: federated -> managed
Update-MgDomain -DomainId contoso.com -AuthenticationType "Managed"
(Get-MgDomain -DomainId contoso.com).AuthenticationType    # expect: Managed

# Rollback (re-touches every user; last resort, after cohort/app rollback)
Update-MgDomain -DomainId contoso.com -AuthenticationType "Federated"

Why: the conversion is the anticlimactic last 5% once the groundwork is done. The rollback exists and works, but it re-federates every user in the domain, so it sits below cohort-removal and per-app re-pointing in your reversibility ladder. </details>

Common beginner mistakes

Glossary

Pitfalls and next steps

Next, retire lane-4 dependencies methodically (replace third-party MFA adapters with Entra MFA / authentication strengths, re-platform WS-Trust clients onto OIDC), wire Entra Connect Health and sign-in failure alerts into your incident channel, and fold the new enterprise apps into a persona-based Conditional Access framework so authentication and authorization are governed as one design.

Entra IDAD FSFederationMigrationStaged Rollout
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments