Set a temporary Microsoft 365 password that the user must change at next sign-in

Build a workflow action that gives a Microsoft 365 user a random temporary password they must change at their next sign-in, so MSPs and IT teams can handle password resets by asking SuperIT.

Runs in Cloud sandbox (PowerShell)
Uses Microsoft Graph credential
Suggested approval Require approval from superit:admins for every run
Category Identity and access

Test it with SuperIT before you rely on it

This is an example, not a ready-made action, and its script hasn't been tested end to end in a SuperIT tenant. Before you use it, work through it with SuperIT: ask SuperIT to review the script against your environment and requirements, adjust it together, then test it on a test account or device. Test in the action editor runs the script for real.

Why use an action instead of asking SuperIT

SuperIT reads Microsoft 365 through your Microsoft 365 connection, but that connection is read-only, so SuperIT can't change anything in the tenant itself. Resetting a password is a write, which only an action with a delegated Microsoft Graph credential can make. This guide keeps the action for engineers and admins, with an admin approving every run. It doesn't cover end-user self-service resets: the script can't check that the requester owns the account.

To check whether an account is enabled or synced from on-premises Active Directory before you reset it, just ask SuperIT in chat.

When to use it

  • "Can you reset Jane's password? She's locked out."
  • "Tom is locked out after his holiday, he's verified on the phone."
  • "Reset the password for [email protected] and make him change it."

What the action does

  1. Checks the input and that the user exists.
  2. Refuses accounts synced from on-premises Active Directory, unless you've confirmed password writeback (see Variations).
  3. Refuses protected accounts: the admin account behind the credential, any account on the script's $protectedUpns list, and accounts that hold a Microsoft Entra admin role directly or through a role-assignable group. It doesn't see roles that are only eligible through Privileged Identity Management.
  4. Generates a random 16-character temporary password.
  5. Sets it, and by default makes the user change it at next sign-in.
  6. Prints the temporary password.

What it doesn't do: it doesn't unlock an on-premises AD account, reset MFA, send the password to the user, or reset an administrator's password. Reset admin passwords in the Microsoft Entra admin center.

Before you begin

  • A Microsoft Graph credential for user administration, named M365 Graph, with the scopes User-PasswordProfile.ReadWrite.All User.Read.All GroupMember.Read.All. The admin account that signed in needs Helpdesk Administrator, Password Administrator or User Administrator. Use this credential only for user-administration actions, and give security, reporting and mail actions their own credentials. See Automate Microsoft 365 with workflows.
  • These scopes come from Microsoft's documentation and haven't been tested with SuperIT. If the reset fails with Authorization_RequestDenied or 403, see If something goes wrong.
  • Your protected accounts. Edit $protectedUpns at the top of the script to list your emergency access (break-glass) accounts and any other account this action must never change.
  • An authorization policy with one rule: Specific groups superit:engineers, superit:admins → Require approval from superit:admins. Don't add an Anyone rule: the script doesn't receive the requester's identity, so an end user could reset someone else's password.
  • The Engineer or Admin role to build the action in Workflows → Actions. Only admins can create actions in chat.

Build it with SuperIT

Create a workflow action called "Reset Microsoft 365 password".
Runtime: PowerShell in the cloud sandbox. Credential: M365 Graph.
Inputs:
- user_upn (email, required)
- force_change (boolean, optional) - make the user change it at next sign-in; treat blank as true
It should refuse on-premises synced accounts, the credential's own account, a list
of protected accounts and admin role holders, then set a random 16-character
temporary password and print it.
Authorization policy: engineers and admins, every run approved by superit:admins.

SuperIT repeats the brief back, drafts the script, checks it, and creates the action as a draft. Review the script in Workflows → Actions, test it, then publish it.

Or build it yourself

Name Type Required Example
user_upn email Yes [email protected]
force_change boolean No on

The main function at the top lists every step in order; each function below it does one of them, and the last line runs it with the action's inputs.

# Goal: set a random temporary password for a cloud-only, non-admin Microsoft 365 user, by default
# one they must change at next sign-in, and output the password.
$token = $env:CREDENTIAL_M365_GRAPH_TOKEN
if (-not $token) { throw "Link the M365 Graph credential to this action." }
$headers = @{ Authorization = "Bearer $token" }
$baseUrl = 'https://graph.microsoft.com/v1.0'
# Emergency access (break-glass) and other accounts this action must never change. List yours.
$protectedUpns = @('[email protected]', '[email protected]')

function Invoke-PasswordReset {
    param([string]$UserUpn, [string]$ForceChange)
    $upn = $UserUpn.Trim()
    # A blank optional input is absent, so default to forcing a change.
    $mustChange = $ForceChange -ne 'false'
    Assert-Inputs -Upn $upn

    $user = Get-TargetUser -Upn $upn
    Assert-CloudOnlyUser -User $user -Upn $upn
    Assert-NotProtectedUser -User $user -Upn $upn
    $password = New-TemporaryPassword
    Set-UserPassword -UserId $user.id -Upn $upn -Password $password -MustChange $mustChange
    Write-ResetSummary -User $user -Upn $upn -Password $password -MustChange $mustChange

    # $script: because the runner returns the script-level $result, not a function's local copy.
    $script:result = @{ user = $upn; must_change = $mustChange }
}

function Assert-Inputs {
    param([string]$Upn)
    if ($Upn -notmatch '^[^@\s]+@[^@\s]+$') { throw "user_upn must be a sign-in address." }
}

function Get-TargetUser {
    param([string]$Upn)
    try {
        Invoke-RestMethod -Uri "$baseUrl/users/$([uri]::EscapeDataString($Upn))?`$select=id,displayName,userPrincipalName,onPremisesSyncEnabled" `
            -Headers $headers
    } catch {
        Write-Error "Failed to find user '$Upn': $_"
        exit 1
    }
}

function Assert-CloudOnlyUser {
    param($User, [string]$Upn)
    if ($User.onPremisesSyncEnabled) {
        throw "$Upn is synced from on-premises Active Directory. Reset the password there."
    }
}

function Assert-NotProtectedUser {
    param($User, [string]$Upn)
    if ($protectedUpns -contains $Upn -or $protectedUpns -contains $User.userPrincipalName) {
        throw "$Upn is on this action's protected list. Nothing was changed."
    }
    try {
        $me = Invoke-RestMethod -Uri "$baseUrl/me?`$select=id" -Headers $headers
        $memberOf = @(Get-GraphCollection -Uri "$baseUrl/users/$($User.id)/transitiveMemberOf?`$select=id,isAssignableToRole")
    } catch {
        Write-Error "Failed to check whether '$Upn' is a protected account: $_"
        exit 1
    }
    if ($me.id -eq $User.id) { throw "$Upn is the account behind this action's credential. Nothing was changed." }
    # Without a group-read scope Graph returns groups with only id and type, which would hide a role held through a role-assignable group.
    $unreadable = @($memberOf | Where-Object { $_.'@odata.type' -eq '#microsoft.graph.group' -and -not $_.PSObject.Properties['isAssignableToRole'] })
    if ($unreadable.Count -gt 0) {
        throw "Can't read $Upn's groups to check for admin roles. Add GroupMember.Read.All to the credential and re-authorize it. Nothing was changed."
    }
    $adminRoles = @($memberOf | Where-Object { $_.'@odata.type' -eq '#microsoft.graph.directoryRole' -or $_.isAssignableToRole })
    if ($adminRoles.Count -gt 0) {
        throw "$Upn holds a Microsoft Entra admin role. Reset admin passwords in the Microsoft Entra admin center. Nothing was changed."
    }
}

function New-TemporaryPassword {
    $chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!#%*?'
    do {
        $bytes = [Security.Cryptography.RandomNumberGenerator]::GetBytes(16)
        $password = -join ($bytes | ForEach-Object { $chars[$_ % $chars.Length] })
    } until ($password -cmatch '[A-Z]' -and $password -cmatch '[a-z]' -and $password -match '\d')
    $password
}

function Set-UserPassword {
    param([string]$UserId, [string]$Upn, [string]$Password, [bool]$MustChange)
    $body = @{ passwordProfile = @{ password = $Password; forceChangePasswordNextSignIn = $MustChange } } | ConvertTo-Json
    try {
        Invoke-RestMethod -Method Patch -Uri "$baseUrl/users/$UserId" -Headers $headers `
            -ContentType 'application/json' -Body $body | Out-Null
    } catch {
        Write-Error "Failed to reset the password for '$Upn': $_"
        exit 1
    }
}

function Get-GraphCollection {
    param([string]$Uri)
    $items = @()
    while ($Uri) {
        $page = Invoke-RestMethod -Uri $Uri -Headers $headers
        $items += $page.value
        $Uri = $page.'@odata.nextLink'
    }
    $items
}

function Write-ResetSummary {
    param($User, [string]$Upn, [string]$Password, [bool]$MustChange)
    Write-Output "=== PASSWORD RESET ==="
    Write-Output "User                         : $($User.displayName) ($Upn)"
    Write-Output "Temporary password           : $Password"
    Write-Output "Must change at next sign-in  : $MustChange"
}

Invoke-PasswordReset -UserUpn $ctx.inputs.user_upn -ForceChange $ctx.inputs.force_change

The password isn't put in $result, so it appears only in Stdout.

The temporary password stays in run history

Stdout is stored with the run. Every engineer and admin in the team, and the approver, can see the temporary password in the run's details, and engineers see it in their chat with SuperIT. Leave force_change on, so the user must replace the password at their next sign-in. If the run history might be shared, or the user doesn't sign in soon, reset the password again.

Test it

Test runs for real

Test skips authorization and approval, uses the real M365 Graph credential, and really changes the password. Only test on a test user.

The script is untested, so treat your first test runs as debugging, and don't publish the action until a test run does exactly what you expect.

  1. On the action, select Test and enter a test user's address.
  2. Read the This is not a dry run. warning, tick "I understand this is a real execution that may change real systems." and select Start test run.
  3. Check Stdout for the temporary password, then sign in as the test user with it. Microsoft should ask for a new password.
  4. Test again with the credential's own admin account, and check the run is refused.

Use it

Publish the action. Engineers ask SuperIT, for example "Reset Jane Doe's password". The run waits for an admin to approve, then SuperIT shows the temporary password to the engineer, who passes it to the user, for example by phone after checking their identity.

Variations

  • For accounts synced from on-premises AD with password writeback turned on, drop the synced-account check. Test carefully first.
  • Also revoke sessions after the reset. See Revoke sessions for a compromised account.