Wes Ellis./ a personal notebook
Technology. Stories. Side projects.
A few things worth writing down.
← Back to Script Library

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.

AT A GLANCESet-EntraUserMobilePhoneMethod.ps1
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

Part 3 of the thread The Microsoft Graph toolbox

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.

Set-EntraUserMobilePhoneMethod.ps1Download
<#
.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

ParameterTypeDefaultWhat it's for
-UserPrincipalNamestring—The user to update. Also read from the pipeline, so a CSV with a UserPrincipalName column works.
-PhoneNumberstring—The number in the format Graph expects: a plus sign, the country code, one space, then the number. For example +1 4345550142.
-Overwriteswitch—Replace an existing, different mobile number. Only use it after you've confirmed who you're talking to.
-WhatIfswitch—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 -WhatIf

The real run, saved for the ticket.

Import-Csv .\new-hires.csv | .\Set-EntraUserMobilePhoneMethod.ps1 | Export-Csv .\mfa-phones.csv -NoTypeInformation

A verified user got a new phone number and can't sign in.

.\Set-EntraUserMobilePhoneMethod.ps1 -UserPrincipalName [email protected] -PhoneNumber '+1 4345550199' -Overwrite

What you'll see

Example outputvalues are illustrative
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

  1. Check what's already there. Get-MgUserAuthenticationPhoneMethod returns 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.
  2. 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 case Update-MgUserAuthenticationPhoneMethod replaces it.
  3. Keep going on errors. A missing user or a permissions problem becomes a Failed row, and the rest of the CSV still gets processed.
  4. 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 -PhoneNumber parameter also answers to MobilePhone, 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-MgUserAuthenticationTemporaryAccessPassMethod gives 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.