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!