Duo MFA Policy Analysis – Why is Entra still asking for Microsoft Authenticator?
So today I found myself looking at a fairly irritating MFA problem. Users were being asked to register Microsoft Authenticator, despite us having Duo configured and a Conditional Access policy with our old RequireDuoMfa control selected.
We also had the newer Duo External MFA integration registered in Entra. Surely that should satisfy Require multifactor authentication?
Yes, it can. But there were a couple of other things going on, including another policy I wasn't initially looking at. Of course there was!
The symptoms
The intention was straightforward: let users use Duo or Microsoft Authenticator. I wanted to keep Authenticator available as a choice, without making everyone enrol in it just to get past the sign-in screen.
What I actually saw was:
- Users being directed towards Microsoft Authenticator registration.
- An affected user with no Duo entry in their Entra authentication methods.
- My own account still not being offered Duo, even though it was registered.
- A further sign-in error after Duo finally appeared and I completed its challenge.
That last one turned out to be a separate problem. More on that in a moment.
First, check registration and targeting
The initial suspects were the Authenticator registration campaign and system-preferred MFA. In our case, the campaign targeted a different group, and system-preferred MFA was already disabled. Microsoft Authenticator was enabled deliberately.
Turning off a method we wanted people to use wasn't going to be a particularly satisfying fix.
The useful distinction was between configuring Duo as a provider, allowing a user to use it, and having the external method registered against that user. Those are separate things to check.
In Entra ID → Authentication methods → Policies, open the Duo external method and check that it is enabled, the user is included, they aren't excluded, and the provider has the required admin consent. Then open Users → All users → the affected user → Authentication methods and look for the Duo registration.
One of our affected users didn't have it. I added it using Add authentication method → External authentication method, selected Duo and saved. Microsoft supports administrators registering external MFA on a user's behalf; users can also register it through Security info. Microsoft's external MFA registration guidance covers both routes.
That gave us something concrete to fix, but it didn't explain everything. My own account already had Duo registered, yet choosing the alternative sign-in options still didn't offer it.
At that point, waiting longer for propagation wasn't much of a diagnosis. Why was this particular sign-in refusing to offer a method I already had?
The catch: MFA and authentication strength
The answer was another Conditional Access policy applying to my sign-in. It required an authentication strength, with the built-in MFA strength selected, rather than the ordinary Require multifactor authentication grant.
Those two settings look very similar when you're trying to get something working. For External MFA, the distinction matters.
| Requirement | What it means for Duo |
|---|---|
| Require multifactor authentication | Duo External MFA can satisfy this requirement. |
| Require authentication strength, including the built-in MFA strength | External MFA does not currently satisfy this control. |
| Legacy Duo custom control | This is a separate integration; completing it does not satisfy Entra's native MFA requirement. |
Microsoft explicitly documents the authentication-strength limitation, including the built-in MFA strength. Cisco explains the difference between the legacy custom control and Duo External MFA.
Changing the requirement in the other policy to ordinary MFA made Duo appear. That was the missing piece for my account.
This needs to match the protection you're actually trying to enforce. If a resource is meant to require phishing-resistant authentication, replacing that requirement with ordinary MFA changes its security requirements. Keep the appropriate strength and use a supported method there. In this case, the intended outcome was ordinary MFA with a choice of providers.
Also, checking one sensible-looking policy isn't enough. Another applicable policy can still impose a requirement that Duo cannot meet.
Follow the actual sign-in
For this sort of problem, the useful starting point is Entra ID → Monitoring & health → Sign-in logs. Find the specific attempt using the user, time and application; use the correlation ID if you have it.
Check the Conditional Access tab for the policies applying to that attempt, then inspect their grant requirements. The Authentication Details tab helps establish which authentication steps completed. Microsoft's Conditional Access troubleshooting guide walks through finding the right event.
I'd check for the following, in this order:
- A requirement for an authentication strength, including the built-in MFA strength.
- A legacy Duo custom-control policy still applying alongside the new External MFA policy.
- The user's eligibility for the external method and their registration.
- The result of the Duo step, followed by the result of the complete Entra sign-in.
For a migration from custom controls, Microsoft recommends separate policy assignments for the test group. Test users should satisfy the new MFA policy without also being required to complete the old custom control. That is a migration check, rather than something we proved was causing this particular failure.
And then… “flow expired”
Having finally got Duo to appear, I completed the challenge and was met with another message about needing additional sign-in methods. Wonderful.
The error detail was “flow expired”. I started the sign-in again from scratch and it worked.
So there were two different outcomes to keep straight: changing the policy made Duo available; starting a fresh authentication attempt cleared the later error. I didn't need another policy change to get past that expired attempt.
A fresh sign-in is a useful test after making changes. If it keeps failing, compare the Entra event with the corresponding Duo authentication log rather than assuming every failure has the same cause.
Cisco also documents a separate trap: a Duo policy that lets someone bypass MFA may not return the factor information Entra needs. That is worth checking if Duo appears to allow access but Entra rejects the result. It wasn't the confirmed cause of our expired flow. Duo's integration limitations explain the distinction, including support for remembered-device sessions.
The PowerShell detour
The next job was to prepare a script to register Duo External MFA for users in our chosen Entra group, including nested group members. The intended behaviour was to check existing registrations, add Duo only where missing, and leave the user's other methods alone. A preview mode and a CSV of the results make that much easier to review.
Then the first version hit this:
Get-MgUserAuthenticationExternalAuthenticationMethod is not recognized
as a name of a cmdlet, function, script file, or executable program.
The cmdlet does exist in Microsoft's documentation. It wasn't available in the PowerShell environment running the script. An older or unloaded module is something to investigate before deciding the API itself is missing:
Get-Module -ListAvailable Microsoft.Graph.Identity.SignIns |
Select-Object Name, Version, Path
Get-Command Get-MgUserAuthenticationExternalAuthenticationMethod `
-ErrorAction SilentlyContinue
There's a small gotcha here too: installing a module only if it is absent doesn't update an older copy that's already installed. That check alone doesn't guarantee a particular cmdlet will be available.
We revised the registration calls to use Invoke-MgGraphRequest. Both listing external registrations and creating one are documented in Graph v1.0, so those calls don't need the beta module or the generated SignIns cmdlets.
The full registration script
Here's the complete version, with placeholders for the organisation-specific values. Save it as Register-DuoMfa.ps1 and replace these three values at the top:
YOUR-TENANT-ID: the directory/tenant ID from Entra's overview page.YOUR-ENTRA-GROUP-NAME: the exact display name of the group you want to process. Start with a small pilot group.YOUR-DUO-PROVIDER-NAME: the exact name of your configured external MFA method in Entra, which may simply beDuo.
It stops if the placeholders haven't been replaced, if the group name isn't unique, or if it cannot find exactly one enabled provider with that name. The provider's configuration ID is discovered from Entra, so there's no client secret or password to put in the script.
Run this in a separate PowerShell session. It installs the two Graph modules for the current user if they are missing, signs in interactively, and writes a dated CSV in the current folder. Without -Apply, it makes no registration changes, although it still installs missing modules and creates the report. Keep that CSV private: it contains account identifiers and any error details.
You'll need consent for the requested Graph scopes and an active role that permits the operations. Reading the authentication methods policy requires a role such as Global Reader or Authentication Policy Administrator. Registering methods requires Authentication Administrator, or Privileged Authentication Administrator for privileged accounts. One role alone may not cover both jobs.
param(
[switch]$Apply
)
$ErrorActionPreference = 'Stop'
$TenantId = 'YOUR-TENANT-ID'
$GroupName = 'YOUR-ENTRA-GROUP-NAME'
$ProviderName = 'YOUR-DUO-PROVIDER-NAME'
foreach ($value in @($TenantId, $GroupName, $ProviderName)) {
if ([string]::IsNullOrWhiteSpace($value) -or $value.StartsWith('YOUR-')) {
throw 'Replace the tenant, group and provider placeholders before running.'
}
}
[void][guid]::Parse($TenantId)
$requiredModules = @(
'Microsoft.Graph.Authentication'
'Microsoft.Graph.Groups'
)
foreach ($module in $requiredModules) {
if (-not (Get-Module -ListAvailable -Name $module)) {
Install-Module -Name $module -Scope CurrentUser -Repository PSGallery `
-Force -ErrorAction Stop
}
Import-Module -Name $module -ErrorAction Stop
}
$scopes = @(
'Group.Read.All'
'User.ReadBasic.All'
'Policy.Read.AuthenticationMethod'
)
$scopes += if ($Apply) {
'UserAuthMethod-External.ReadWrite.All'
} else {
'UserAuthMethod-External.Read.All'
}
Connect-MgGraph -TenantId $TenantId -Scopes $scopes -ContextScope Process -NoWelcome
try {
$escapedGroupName = $GroupName.Replace("'", "''")
$groups = @(Get-MgGroup -Filter "displayName eq '$escapedGroupName'" `
-Property Id, DisplayName -All)
if ($groups.Count -ne 1) {
throw "Expected one group named '$GroupName'; found $($groups.Count)."
}
$group = $groups[0]
$policy = Invoke-MgGraphRequest -Method GET `
-Uri 'https://graph.microsoft.com/v1.0/policies/authenticationMethodsPolicy'
$providers = @($policy.authenticationMethodConfigurations | Where-Object {
$_.'@odata.type' -eq '#microsoft.graph.externalAuthenticationMethodConfiguration' -and
$_.displayName -eq $ProviderName -and
$_.state -eq 'enabled'
})
if ($providers.Count -ne 1) {
throw "Expected one enabled external MFA provider named '$ProviderName'."
}
$provider = $providers[0]
$users = @(Get-MgGroupTransitiveMemberAsUser -GroupId $group.Id `
-Property Id, DisplayName, UserPrincipalName `
-ConsistencyLevel eventual -CountVariable userCount -All)
Write-Host "Tenant: $TenantId"
Write-Host "Group: $($group.DisplayName) [$($group.Id)]"
Write-Host "Provider: $($provider.displayName) [$($provider.id)]"
Write-Host "Mode: $(if ($Apply) { 'APPLY' } else { 'PREVIEW' })"
Write-Host "Users found: $($users.Count)"
if ($users.Count -eq 0) {
Write-Warning 'No users found. No registrations or report were created.'
return
}
$results = @(foreach ($user in $users) {
$errorMessage = ''
try {
$uri = "https://graph.microsoft.com/v1.0/users/$($user.Id)/authentication/externalAuthenticationMethods"
$response = Invoke-MgGraphRequest -Method GET -Uri $uri
$registered = @($response.value | Where-Object {
$_.configurationId -eq $provider.id
}).Count -gt 0
if ($registered) {
$status = 'Already registered'
} elseif (-not $Apply) {
$status = 'Would register'
} else {
$body = @{
'@odata.type' = '#microsoft.graph.externalAuthenticationMethod'
configurationId = $provider.id
displayName = $provider.displayName
}
Invoke-MgGraphRequest -Method POST -Uri $uri `
-Body ($body | ConvertTo-Json) `
-ContentType 'application/json' | Out-Null
$status = 'Registered'
}
} catch {
$status = 'Failed'
$errorMessage = $_.Exception.Message
Write-Warning "$($user.UserPrincipalName): $errorMessage"
}
Write-Host "$($user.UserPrincipalName): $status"
[PSCustomObject]@{
UserId = $user.Id
UserPrincipalName = $user.UserPrincipalName
Status = $status
Error = $errorMessage
}
})
$reportName = 'DuoMfaRegistration-{0}.csv' -f (Get-Date -Format 'yyyyMMdd-HHmmss-fff')
$reportPath = Join-Path (Get-Location) $reportName
$results | Export-Csv -LiteralPath $reportPath -NoTypeInformation -Encoding UTF8 -NoClobber
$results | Group-Object Status | Select-Object Name, Count | Format-Table -AutoSize
Write-Host "Results saved to $reportPath"
if (@($results | Where-Object Status -eq 'Failed').Count -gt 0) {
throw "Some users failed. Review $reportPath before running again."
}
} finally {
Disconnect-MgGraph | Out-Null
}
Preview the group first:
.\Register-DuoMfa.ps1
Check the tenant, group, provider and CSV, then apply the missing registrations:
.\Register-DuoMfa.ps1 -Apply
The membership lookup includes nested users and follows all result pages. It uses the count and consistency options required by Graph's transitive-members query. Recently changed membership may take time to appear, so review the returned users before applying.
Existing Duo registrations are skipped. Other authentication methods are left alone, and a failure for one user is recorded while the remaining users are processed. After a partial failure, rerun the preview and inspect the report before applying again.
This registers the method in Entra. Users still need to be eligible under the external-method policy and able to authenticate with Duo. It doesn't enrol their phone in Duo, change Conditional Access, or remove registrations when someone leaves the group. The create API also doesn't support registering the signed-in administrator's own method; use Security info for that account.
The script has been checked with simulated Graph responses, including preview, repeat runs and failure cases. It hasn't been run against a tenant as part of preparing this article. The successful fresh sign-in was the result confirmed during the original investigation; it wasn't proof that a group-wide rollout had completed.
What we learned
- Check the policies applying to the actual sign-in. The policy you started investigating may not contain the requirement causing the problem.
- Provider setup, user targeting and registration each need checking. Adding a registration doesn't override a Conditional Access requirement.
- Read the exact grant control. Ordinary MFA and the built-in MFA authentication strength behave differently with External MFA.
- Keep separate errors separate. “Duo isn't offered” and “flow expired” needed different actions here.
- An installed Graph module isn't a guarantee that a cmdlet exists locally. Check the available commands, or use the documented API through
Invoke-MgGraphRequest.
The bit to remember: if Duo is registered but missing from the method picker, check for an authentication-strength requirement before spending another hour waiting for propagation!