Skip to content
  • There are no suggestions because the search field is empty.

Add Mobile MFA Solution to a Group of Sign In Accounts

PowerShell script to add MFA to a group if it doesn't already exist, for user entities.

You can run:

Get-MgGroupMember -GroupId $((Get-MgGroup -Filter "displayName eq 'Your Group Name Here'").Id) -All | % { New-MgUserAuthenticationPhoneMethod -UserId $_.Id -BodyParameter @{phoneType='mobile';phoneNumber='+44 your phone number here'} }

And then to check (be aware of latency if there are lots of changes:

(Get-MgGroupMember -GroupId $((Get-MgGroup -Filter "displayName eq 'Your Group Here'").Id) -All | select *,@{n='MFAMethodCount';e={(Get-MgUserAuthenticationMethod -UserId $_.Id | ?{ $_.AdditionalProperties['@odata.type'] -ne '#microsoft.graph.passwordAuthenticationMethod' }).Count}} | ? { $_.MFAMethodCount -ne 0 }).Count

However, the above require modules and you to be already signed in, so the below, should make life a bit easier, and will also not fail if it finds an account with a Telephone Number already, and will not add a number, if there is MFA already in place:

# Please make sure you test this script before using in production 

# **************************
# ** Configure the script **
# **************************
$GroupDisplayName = "Your Group Name Here"
$MobileNumber     = "+44 Your Mobile number here"
# Remember Microsoft like country-code, space, number-no-leading-0

# How many times to poll after changes, to reduce false negatives from latency
$VerificationRetries   = 6
$VerificationSleepSecs = 15

# Required Graph scopes
$RequiredScopes = @(
    "Group.Read.All",
    "User.Read.All",
    "UserAuthenticationMethod.Read.All",
    "UserAuthenticationMethod.ReadWrite.All"
)

# Required modules
$RequiredModules = @(
    "Microsoft.Graph.Authentication",
    "Microsoft.Graph.Groups",
    "Microsoft.Graph.Identity.SignIns",
    "Microsoft.Graph.Users"
)

# *****************************************
# ** Ensure package provider / PSGallery **
# *****************************************
function Initialize-PowerShellGallery {
    try {
        if (-not (Get-PackageProvider -Name NuGet -ErrorAction SilentlyContinue)) {
            Install-PackageProvider -Name NuGet -Scope CurrentUser -Force -ErrorAction Stop
        }

        $repo = Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue
        if ($repo -and $repo.InstallationPolicy -ne "Trusted") {
            Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
        }
    }
    catch {
        Write-Warning "Could not fully initialise PowerShell Gallery prerequisites. $($_.Exception.Message)"
    }
}

# **************************
# ** Ensure module exists **
# **************************
function Ensure-Module {
    param(
        [Parameter(Mandatory)]
        [string]$Name
    )

    $installed = Get-Module -ListAvailable -Name $Name |
        Sort-Object Version -Descending |
        Select-Object -First 1

    if (-not $installed) {
        Write-Host "Installing module: $Name" -ForegroundColor Yellow
        Install-Module -Name $Name -Scope CurrentUser -Repository PSGallery -Force -AllowClobber -ErrorAction Stop
    }

    Import-Module $Name -ErrorAction Stop
    Write-Host "Loaded module: $Name" -ForegroundColor Green
}

# ****************************************************
# ** Does user already have a mobile phone method? **
# ****************************************************
function Get-UserMobilePhoneMethod {
    param(
        [Parameter(Mandatory)]
        [string]$UserId
    )

    $methods = Get-MgUserAuthenticationPhoneMethod -UserId $UserId -ErrorAction Stop
    return $methods | Where-Object { $_.PhoneType -eq "mobile" } | Select-Object -First 1
}

# ********************************************************
# ** Does user have any non-password auth method? **
# ** We don’t need to add more if existing MFA in place **
# ********************************************************
function Get-UserNonPasswordAuthMethodCount {
    param(
        [Parameter(Mandatory)]
        [string]$UserId
    )

    $methods = Get-MgUserAuthenticationMethod -UserId $UserId -ErrorAction Stop
    return @(
        $methods | Where-Object {
            $_.AdditionalProperties.'@odata.type' -ne '#microsoft.graph.passwordAuthenticationMethod'
        }
    ).Count
}

# ***********************************
# ** Build verification snapshot **
# ** Enabled sign-in entities only **
# ***********************************
function Get-GroupMfaSnapshot {
    param(
        [Parameter(Mandatory)]
        [string]$GroupId
    )

    $members = Get-MgGroupMemberAsUser -GroupId $GroupId -All

    $snapshot = foreach ($member in $members) {
        try {
            $user = Get-MgUser -UserId $member.Id -Property "id,displayName,userPrincipalName,accountEnabled" -ErrorAction Stop

            if (-not $user.AccountEnabled) {
                continue
            }

            $nonPasswordCount = Get-UserNonPasswordAuthMethodCount -UserId $user.Id
            $mobileMethod     = Get-UserMobilePhoneMethod -UserId $user.Id

            [pscustomobject]@{
                Id                  = $user.Id
                DisplayName         = $user.DisplayName
                UserPrincipalName   = $user.UserPrincipalName
                AccountEnabled      = $user.AccountEnabled
                HasMobilePhone      = [bool]$mobileMethod
                MobilePhoneNumber   = if ($mobileMethod) { $mobileMethod.PhoneNumber } else { $null }
                MFAMethodCount      = $nonPasswordCount
                HasAnyMfaMethod     = ($nonPasswordCount -gt 0)
            }
        }
        catch {
            [pscustomobject]@{
                Id                  = $member.Id
                DisplayName         = $null
                UserPrincipalName   = $null
                AccountEnabled      = $null
                HasMobilePhone      = $false
                MobilePhoneNumber   = $null
                MFAMethodCount      = $null
                HasAnyMfaMethod     = $false
                Error               = $_.Exception.Message
            }
        }
    }

    return $snapshot
}

# **********************
# ** Start Processing **
# **********************
Initialize-PowerShellGallery

foreach ($module in $RequiredModules) {
    Ensure-Module -Name $module
}

Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan
Connect-MgGraph -Scopes $RequiredScopes -NoWelcome
$ctx = Get-MgContext
Write-Host "Connected as: $($ctx.Account)" -ForegroundColor Green

# ******************
# ** Locate group **
# ******************
$group = Get-MgGroup -Filter "displayName eq '$GroupDisplayName'" -ConsistencyLevel eventual

if (-not $group) {
    throw "Group '$GroupDisplayName' was not found."
}
if (@($group).Count -gt 1) {
    throw "More than one group matched '$GroupDisplayName'. Use a specific GroupId instead."
}

$group = @($group)[0]
Write-Host "Using group: $($group.DisplayName) [$($group.Id)]" -ForegroundColor Green

# ********************************
# ** Get enabled users in group **
# ********************************
$groupUsers = Get-MgGroupMemberAsUser -GroupId $group.Id -All

$enabledUsers = foreach ($member in $groupUsers) {
    try {
        $user = Get-MgUser -UserId $member.Id -Property "id,displayName,userPrincipalName,accountEnabled" -ErrorAction Stop
        if ($user.AccountEnabled) {
            $user
        }
    }
    catch {
        Write-Warning "Could not read user $($member.Id): $($_.Exception.Message)"
    }
}

Write-Host "Enabled user accounts found: $(@($enabledUsers).Count)" -ForegroundColor Cyan

# *****************************************
# ** Add mobile method only when missing **
# *****************************************
$results = foreach ($user in $enabledUsers) {
    try {
        $existingMobile = Get-UserMobilePhoneMethod -UserId $user.Id

        if ($existingMobile) {
            [pscustomobject]@{
                DisplayName       = $user.DisplayName
                UserPrincipalName = $user.UserPrincipalName
                Status            = "Skipped"
                Reason            = "Mobile method already exists"
                MobilePhone       = $existingMobile.PhoneNumber
            }
            continue
        }

        New-MgUserAuthenticationPhoneMethod -UserId $user.Id -BodyParameter @{
            phoneType   = "mobile"
            phoneNumber = $MobileNumber
        } -ErrorAction Stop | Out-Null

        [pscustomobject]@{
            DisplayName       = $user.DisplayName
            UserPrincipalName = $user.UserPrincipalName
            Status            = "Added"
            Reason            = ""
            MobilePhone       = $MobileNumber
        }
    }
    catch {
        [pscustomobject]@{
            DisplayName       = $user.DisplayName
            UserPrincipalName = $user.UserPrincipalName
            Status            = "Error"
            Reason            = $_.Exception.Message
            MobilePhone       = $MobileNumber
        }
    }
}

# **************************************************
# ** Immediate action summary of what we achieved **
# **************************************************
Write-Host ""
Write-Host "Write summary" -ForegroundColor Cyan
$results | Format-Table -AutoSize

$addedCount   = @($results | Where-Object Status -eq "Added").Count
$skippedCount = @($results | Where-Object Status -eq "Skipped").Count
$errorCount   = @($results | Where-Object Status -eq "Error").Count

Write-Host ""
Write-Host "Added   : $addedCount"
Write-Host "Skipped : $skippedCount"
Write-Host "Errors  : $errorCount"

# ***************************************************************
# ** Verification with retry due to latency **
# ** Reduces false negatives caused by Graph propagation delay **
# ***************************************************************
$finalSnapshot = $null

for ($attempt = 1; $attempt -le $VerificationRetries; $attempt++) {
    Write-Host ""
    Write-Host "Verification pass $attempt of $VerificationRetries..." -ForegroundColor Cyan

    $snapshot = Get-GroupMfaSnapshot -GroupId $group.Id
    $enabledSnapshot = @($snapshot | Where-Object { $_.AccountEnabled -eq $true })
    $withoutAnyMfa   = @($enabledSnapshot | Where-Object { $_.HasAnyMfaMethod -ne $true })
    $withoutMobile   = @($enabledSnapshot | Where-Object { $_.HasMobilePhone -ne $true })

    Write-Host "Enabled accounts checked        : $($enabledSnapshot.Count)"
    Write-Host "With any non-password auth      : $(@($enabledSnapshot | Where-Object HasAnyMfaMethod).Count)"
    Write-Host "Without any non-password auth   : $($withoutAnyMfa.Count)"
    Write-Host "With mobile phone auth method   : $(@($enabledSnapshot | Where-Object HasMobilePhone).Count)"
    Write-Host "Without mobile phone auth method: $($withoutMobile.Count)"

    $finalSnapshot = $snapshot

    if ($withoutMobile.Count -eq 0) {
        Write-Host "Verification passed: all enabled users in the group have a mobile phone authentication method." -ForegroundColor Green
        break
    }

    if ($attempt -lt $VerificationRetries) {
        Start-Sleep -Seconds $VerificationSleepSecs
    }
}

# *******************************
# ** Final detailed exceptions **
# *******************************
$finalEnabled = @($finalSnapshot | Where-Object { $_.AccountEnabled -eq $true })
$finalWithoutMobile = @($finalEnabled | Where-Object { $_.HasMobilePhone -ne $true })

Write-Host ""
Write-Host "Accounts still missing mobile phone auth method after verification:" -ForegroundColor Yellow
if ($finalWithoutMobile.Count -eq 0) {
    Write-Host "None"
}
else {
    $finalWithoutMobile |
        Select-Object DisplayName, UserPrincipalName, MFAMethodCount, HasAnyMfaMethod |
        Format-Table -AutoSize
}

# Optional exports
# $results | Export-Csv -Path ".\MFA-Mobile-WriteResults.csv" -NoTypeInformation -Encoding UTF8
# $finalSnapshot | Export-Csv -Path ".\MFA-Mobile-FinalSnapshot.csv" -NoTypeInformation -Encoding UTF8

Disconnect-MgGraph | Out-Null

# Please ensure you test this script before use in production!