SCRIPT LIBRARY · POWERSHELL
Pre-Register MFA Phone Numbers for Entra ID Users with PowerShell
Load mobile numbers into Entra ID as an authentication method before users ever sign in, without stomping on numbers they've already registered.
- What it does
- Adds a mobile phone authentication method for each user you give it, leaves existing numbers alone unless you say otherwise, and returns a row per user showing what changed.
- Requires
- PowerShell 7.2+ (Windows PowerShell 5.1 works too)
- Microsoft.Graph.Identity.SignIns and Microsoft.Graph.Authentication modules
- Permissions
- Graph scope: UserAuthenticationMethod.ReadWrite.All, plus the Authentication Administrator role (Privileged Authentication Administrator for users who hold admin roles).
- Runs on
- Windows, macOS, Linux
- Tested
- Parse-checked and dry-run with mocked Graph cmdlets in PowerShell 7.4
New hires have a rough first morning. Before they can do anything, they're asked to set up MFA, usually on a laptop they've owned for eleven minutes. If you already have their mobile number from HR, you can register it for them ahead of time, so their first sign-in just texts them a code.
That's what this does. It uses the authentication methods API in Microsoft Graph to add a mobile number as a sign-in method. And it's careful about it: if someone already has a different number registered, the script leaves it alone and tells you, because quietly replacing a person's MFA phone is the same as resetting their MFA. You have to ask for that on purpose with -Overwrite.
The old version of this post tried to copy a number from an extension attribute into a property called AuthenticationContactInfo using the AzureAD module. That property never existed, and the module has since been retired. This is a proper rebuild on the Microsoft Graph PowerShell SDK.
<#
.SYNOPSIS
Pre-registers a mobile phone number as an authentication method for Microsoft Entra ID users.
.DESCRIPTION
Uses the Microsoft Graph authentication methods cmdlets to add a mobile number that users
can use for SMS or voice verification. Users who already have a mobile number registered
are left alone unless you pass -Overwrite, because replacing someone's MFA number is
effectively resetting their MFA. Takes pipeline input, so a CSV with UserPrincipalName and
PhoneNumber columns works as-is. Supports -WhatIf.
.PARAMETER UserPrincipalName
The user to update. Accepts pipeline input by property name.
.PARAMETER PhoneNumber
The number in Graph's format: +<country code> <number>, for example '+1 4345550142'.
.PARAMETER Overwrite
Replace an existing, different mobile number. Only do this after you've verified the user.
.EXAMPLE
.\Set-EntraUserMobilePhoneMethod.ps1 -UserPrincipalName [email protected] -PhoneNumber '+1 4345550142'
.EXAMPLE
Import-Csv .\new-hires.csv | .\Set-EntraUserMobilePhoneMethod.ps1 -WhatIf
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[Alias('UPN', 'UserId')]
[string]$UserPrincipalName,
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[Alias('Phone', 'MobilePhone')]
[ValidatePattern('^\+\d{1,3} \d{4,15}$')]
[string]$PhoneNumber,
[switch]$Overwrite
)
begin {
if (-not (Get-MgContext)) { Connect-MgGraph -Scopes 'UserAuthenticationMethod.ReadWrite.All' -NoWelcome }
# The mobile phone method always has this fixed ID. Alternate mobile and office use others.
$mobileMethodId = '3179e48a-750b-4051-897c-87b9720928f7'
}
process {
$result = [pscustomobject]@{
UserPrincipalName = $UserPrincipalName
OldNumber = $null
NewNumber = $PhoneNumber
Result = $null
}
try {
$existing = Get-MgUserAuthenticationPhoneMethod -UserId $UserPrincipalName -ErrorAction Stop |
Where-Object { $_.Id -eq $mobileMethodId }
}
catch {
$result.Result = "Failed: $($_.Exception.Message)"
return $result
}
# Graph returns numbers in the same '+1 4345550142' shape, so compare without spaces to be safe.
$normalize = { param($n) ($n -replace '\s', '') }
if ($existing) {
$result.OldNumber = $existing.PhoneNumber
if ((& $normalize $existing.PhoneNumber) -eq (& $normalize $PhoneNumber)) {
$result.Result = 'Unchanged'
}
elseif (-not $Overwrite) {
$result.Result = 'Skipped: a different mobile number is already registered (use -Overwrite)'
}
elseif ($PSCmdlet.ShouldProcess($UserPrincipalName, "Replace mobile MFA number $($existing.PhoneNumber) with $PhoneNumber")) {
try {
Update-MgUserAuthenticationPhoneMethod -UserId $UserPrincipalName -PhoneAuthenticationMethodId $mobileMethodId -PhoneNumber $PhoneNumber -PhoneType 'mobile' -ErrorAction Stop | Out-Null
$result.Result = 'Updated'
}
catch { $result.Result = "Failed: $($_.Exception.Message)" }
}
else { $result.Result = 'WhatIf' }
}
elseif ($PSCmdlet.ShouldProcess($UserPrincipalName, "Register $PhoneNumber as mobile MFA number")) {
try {
New-MgUserAuthenticationPhoneMethod -UserId $UserPrincipalName -PhoneNumber $PhoneNumber -PhoneType 'mobile' -ErrorAction Stop | Out-Null
$result.Result = 'Added'
}
catch { $result.Result = "Failed: $($_.Exception.Message)" }
}
else { $result.Result = 'WhatIf' }
Write-Verbose "$UserPrincipalName`: $($result.Result)"
$result
}
Parameters
| Parameter | Type | Default | What it's for |
|---|---|---|---|
-UserPrincipalName | string | — | The user to update. Also read from the pipeline, so a CSV with a UserPrincipalName column works. |
-PhoneNumber | string | — | The number in the format Graph expects: a plus sign, the country code, one space, then the number. For example +1 4345550142. |
-Overwrite | switch | — | Replace an existing, different mobile number. Only use it after you've confirmed who you're talking to. |
-WhatIf | switch | — | Show what would change without writing anything. |
Run it
One user.
.\Set-EntraUserMobilePhoneMethod.ps1 -UserPrincipalName [email protected] -PhoneNumber '+1 4345550142'This week's new hires from a CSV (columns UserPrincipalName and PhoneNumber), as a dry run.
Import-Csv .\new-hires.csv | .\Set-EntraUserMobilePhoneMethod.ps1 -WhatIfThe real run, saved for the ticket.
Import-Csv .\new-hires.csv | .\Set-EntraUserMobilePhoneMethod.ps1 | Export-Csv .\mfa-phones.csv -NoTypeInformationA verified user got a new phone number and can't sign in.
.\Set-EntraUserMobilePhoneMethod.ps1 -UserPrincipalName [email protected] -PhoneNumber '+1 4345550199' -OverwriteWhat you'll see
UserPrincipalName OldNumber NewNumber Result
----------------- --------- --------- ------
[email protected] +1 4345550142 Added
[email protected] +1 4345550100 +1 4345550188 Skipped: a different mobile number is already registered (use -Overwrite)
[email protected] +1 4345550173 +1 4345550173 Unchanged
[email protected] +1 4345550160 Failed: Resource '[email protected]' does not exist
How it works
- Check what's already there.
Get-MgUserAuthenticationPhoneMethodreturns the user's phone methods. The mobile one always has the same fixed ID (3179e48a-750b-4051-897c-87b9720928f7), so the script picks it out by that rather than by guessing from the number. - Decide, don't assume. No mobile number yet? It adds one with
New-MgUserAuthenticationPhoneMethod. Same number already there? Unchanged. A different number? Skipped, unless you passed-Overwrite, in which caseUpdate-MgUserAuthenticationPhoneMethodreplaces it. - Keep going on errors. A missing user or a permissions problem becomes a Failed row, and the rest of the CSV still gets processed.
- Return objects. One row per user with the old number, the new one and the result, ready for
Export-Csv.
Take it further
- Pull numbers from the directory. If the mobile number already lives on the user object, skip the CSV. The
-PhoneNumberparameter also answers toMobilePhone, so this works as-is:Get-MgUser -All -Property UserPrincipalName,MobilePhone | Where-Object MobilePhone | .\Set-EntraUserMobilePhoneMethod.ps1 -WhatIf. Expect a few validation errors the first time (those users are skipped and the rest carry on); directory numbers are often full of dashes and parentheses. - Hand out Temporary Access Passes instead. For passwordless rollouts,
New-MgUserAuthenticationTemporaryAccessPassMethodgives new hires a one-time pass they can use to register a passkey or Authenticator, which beats SMS on every front. - Audit who has what. The authentication methods registration report in the Entra admin center shows who's still on SMS only, which is a good list to work through.
Things that'll trip you up
- The number format is picky. Graph wants +<country code><space><number>, like +1 4345550142. No dashes, no parentheses. The script rejects anything else up front, so fix your CSV rather than the script.
- SMS has to be allowed. A registered number only helps if SMS or voice is enabled in the Authentication methods policy (Entra admin center, Protection, Authentication methods) for those users. If it isn't, the number sits there unused.
- Whoever controls the source data controls MFA. If you feed this from an HR export, anyone who can edit a phone number in HR can effectively choose where a user's codes go. Lock that data down, and never pre-register numbers for admin accounts this way.
- Treat it as a bootstrap, not the destination. SMS is the weakest method Entra supports. Use it to get people signed in on day one, then nudge them to the Microsoft Authenticator app, passkeys or Windows Hello with a registration campaign.
- Admins need a bigger role. Authentication Administrator can manage methods for regular users. For anyone holding an admin role, you'll need Privileged Authentication Administrator, and those rows will show Failed until you have it.