Scenario

Using Azure AD SSPR (Self-Service Password Reset) and a background process to assign user supplied contact data, newly onboarded users may both verify and set their first credential without needing to contact the IT Service Desk.

The below script demonstrates a means to faciliate this using native Microsoft Graph API calls.

PowerShell

Example Use

Add-PhoneAuthenticationMethod 'testuser@domain.com OR objectId' '+61 4xxxxxxxx' 'mobile'
Add-EmailAuthenticationMethod 'testuser@domain.com OR objectId' 'otherAddress@domain.com'

Code

using namespace System.Collections.Generic
$ErrorActionPreference = 'stop'

$graphVersion = 'https://graph.microsoft.com/beta'
# $bearerToken = 'REQUIRED'

# Obtain via MSAL and your chosen method (App Role or User Delegated Permission)
# Graph Scope: UserAuthenticationMethod.ReadWrite.All

# For delegated scenarios where an admin is acting on another user,
# the admin needs one of the following Azure AD roles:
#   Global administrator
#   Privileged authentication administrator
#   Authentication administrator

function Send-MicrosoftGraphRequest {
    [CmdletBinding()]
    [OutputType([List[PSCustomObject]])]
    param
    (
        [Parameter(
            Mandatory = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 0)]
        [String] $Method,
        [Parameter(
            Mandatory = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 1)]
        [String] $GraphUri,
        [Parameter(
            Mandatory = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 2)]
        [String] $BearerToken,
        [Parameter(
            Mandatory = $false,
            ValueFromPipelineByPropertyName = $true,
            Position = 3)]
        [String] $Body
    )
    process {
        $resultList = [List[PSCustomObject]]::new()
        $headers = @{
            Authorization = "$BearerToken"
        }
        try {
            do {
                Write-Debug ($Method + ' ' + $GraphUri)
                switch ($Method.ToLower()) {
                    { ($_ -eq 'post') -or $_ -eq 'put' } {
                        Write-Debug $Body
                        $result = Invoke-RestMethod -Method $Method -Uri $GraphUri `
                            -headers $headers -Body $Body -ContentType 'application/json'
                        break
                    }
                    Default {
                        $result = Invoke-RestMethod -Method $Method -Uri $GraphUri `
                            -headers $headers
                    }
                }
                
                if ($result.Value.Count -eq 0) {
                    $result.PSObject.Properties.Remove('@odata.context')
                    $resultList.Add($result)
                }
                else {
                    $result.Value.ForEach({
                            $resultList.Add($_)
                        })
                    $GraphUri = $result.'@Odata.NextLink'
                }
            } while ($result.'@Odata.NextLink')
        }
        catch {
            # Write-Warning $Error[0]
            throw $Error[0]
        }
        $resultList
    }
}

class PhoneMethod {
    [string]$phoneNumber # +xx xxxxxxxxx
    [string]$phoneType # 'mobile', 'alternateMobile' or 'office'

    PhoneMethod([string]$pNum, [string]$pType) {
        $this.phoneNumber = $pNum
        $this.phoneType = $pType
    }
}

function Add-PhoneAuthenticationMethod {
    [CmdletBinding()]
    param
    (
        [Parameter(
            Mandatory = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 0)]
        [String] $Id, # ObjectId or UserPrincipalName
        [Parameter(
            Mandatory = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 1)]
        [String] $PhoneNumber,
        [Parameter(
            Mandatory = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 2)]
        [String] $PhoneType # alternateMobile, office, mobile
    )
    process {

        $phoneTypeId = ''
        $phoneMethod = [PhoneMethod]::new($PhoneNumber, $PhoneType)
        $body = $phoneMethod | ConvertTo-Json

        switch ($PhoneType.ToLower()) {
            'alternateMobile' {
                $phoneTypeId = 'b6332ec1-7057-4abe-9331-3d72feddfe41'
                break 
            }
            'office' {
                $phoneTypeId = 'e37fc753-ff3b-4958-9484-eaa9425c82bc'
                break 
            }
            'mobile' {
                $phoneTypeId = '3179e48a-750b-4051-897c-87b9720928f7'
                break 
            }
            Default { throw "Unknown PhoneType" }
        }

        # Does this user have the phoneMethod already set?
        $request = "/users/$Id/authentication/methods"
        $existingPhoneType = Send-MicrosoftGraphRequest GET ($graphVersion + $request) $bearerToken  | Where-Object {$_.id -eq $phoneTypeId }

        # Add if its missing
        if ($null -eq $existingPhoneType) {
            $request = "/users/$Id/authentication/phoneMethods"
            Send-MicrosoftGraphRequest POST ($graphVersion + $request) $bearerToken $body
            Write-Debug "'$Id' had no phoneNumber and was updated to phoneNumber: '$PhoneNumber'."
        }
        else {
            # Otherwise compare and update (if desired)
            if ($existingPhoneType.phoneNumber -ne $PhoneNumber) {
                $request = "/users/$Id/authentication/phoneMethods/$phoneTypeId"
                Send-MicrosoftGraphRequest PUT ($graphVersion + $request) $bearerToken $body
                Write-Debug "'$Id' had phoneNumber: '$($existingPhoneType.phoneNumber)' and was updated to phoneNumber: '$PhoneNumber'."
            } else {
                Write-Debug "'$Id' already has the $PhoneType phoneNumber: '$PhoneNumber'."
            }
        }
    }
}

class EmailMethod {
    [string]$emailAddress

    EmailMethod([string]$eAddress) {
        $this.emailAddress = $eAddress
    }
}

function Add-EmailAuthenticationMethod {
    [CmdletBinding()]
    param
    (
        [Parameter(
            Mandatory = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 0)]
        [String] $Id, # ObjectId or UserPrincipalName
        [Parameter(
            Mandatory = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 1)]
        [String] $EmailAddress
    )
    process {

        $emailTypeId = '3ddfcfc8-9383-446f-83cc-3ab9be4be18f'
        $emailMethod = [EmailMethod]::new($EmailAddress)
        $body = $emailMethod | ConvertTo-Json

        # Does this user have the emailMethod already set?
        $request = "/users/$Id/authentication/methods"
        $existingEmailType = Send-MicrosoftGraphRequest GET ($graphVersion + $request) $bearerToken  | Where-Object {$_.id -eq $emailTypeId }

        # Add if its missing
        if ($null -eq $existingEmailType) {
            $request = "/users/$Id/authentication/emailMethods"
            Send-MicrosoftGraphRequest POST ($graphVersion + $request) $bearerToken $body
            Write-Debug "'$Id' had no email address and was updated to emailAddress: '$EmailAddress'."
        }
        else {
            # Otherwise compare and update (if desired)
            if ($existingEmailType.emailAddress -ne $EmailAddress) {
                $request = "/users/$Id/authentication/emailMethods/$emailTypeId"
                Send-MicrosoftGraphRequest PUT ($graphVersion + $request) $bearerToken $body
                Write-Debug "'$Id' had emailAddress: '$($existingEmailType.emailAddress)' and was updated to emailAddress: '$EmailAddress'."
            } else {
                Write-Debug "'$Id' already has emailAddress: '$EmailAddress'."
            }
        }
    }
}


# $DebugPreference = 'Continue'

# Add-PhoneAuthenticationMethod $identifier '+61 4xxxxxxxx' 'mobile'
# Add-EmailAuthenticationMethod $identifier 'chris.dymond@otherDomain.com'

SSPR

https://aka.ms/sspr

Parameters

Realm

https://aka.ms/sspr?whr=domain.com

Username

https://aka.ms/sspr?username=firstname.lastname@domain.com

Final Redirection

Final step redirection.

https://aka.ms/sspr?ru={HTTP Encoded Parameter}

https://login.microsoftonline.com/tenantId/oauth2/v2.0/authorize?client_id=appId&scope=https://yourApp.com/scope&redirect_uri=https://yourApp.com/

The above plaintext URL must be HTTP encoded when supplied as the parameter in the SSPR URL.

Mobile Method Screenshots

The following steps outline the user experience for the single (mobile) authentication method.

Landing Page

SSPR - Get Back Into Your Account

Phone Number Verification

SSPR - Mobile Number Verification

Six Digit One Time Code

SSPR - Mobile Number Code

Password Reset

SSPR - New Password

Final Step

SSPR - Final

Final Step with optional URL

SSPR - Final With Option

Categories:

Updated: