Stop a user signing in to Microsoft 365, or let them sign in again

Build a workflow action that blocks a user from signing in to Microsoft 365, or unblocks them, so MSPs and IT teams can suspend and restore accounts 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. Blocking or unblocking sign-in is a write, which only an action with a delegated Microsoft Graph credential can make. A wrong block locks a person out of all their work, so an admin approves every run. If an account must be blocked before an approver is available, an admin can block it directly in the Microsoft 365 admin center.

To check whether an account is already blocked, just ask SuperIT in chat.

When to use it

  • "Block sign-in for Tom while he's suspended."
  • "Jane is back from leave, please re-enable her account."
  • "Disable [email protected] now and kick him out of everything."

What the action does

  1. Checks the inputs and that the user exists.
  2. Refuses accounts synced from on-premises Active Directory, which must be disabled or enabled there.
  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. Blocks or unblocks sign-in.
  5. When blocking, and if revoke_sessions is on, revokes all sign-in sessions after the block, so open apps ask the user to sign in again.
  6. Prints the account's new state.

What it doesn't do: it doesn't change the password, licences, groups or mailbox, and it doesn't sign the user out of Windows on their devices. Blocking sign-in doesn't end every session at once; Microsoft 365 apps can take up to an hour to notice unless sessions are revoked.

Before you begin

  • A Microsoft Graph credential for user administration, named M365 Graph, with the scopes User.EnableDisableAccount.All User.RevokeSessions.All User.Read.All GroupMember.Read.All. The admin account that signed in needs the User Administrator role. Use this credential only for user-administration actions, and give security, reporting and mail actions their own credentials. See Automate Microsoft 365 with workflows.
  • 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 block.
  • 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 block someone else.
  • 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 "Block or unblock Microsoft 365 sign-in".
Runtime: PowerShell in the cloud sandbox. Credential: M365 Graph.
Inputs:
- user_upn (email, required)
- operation (select, required, choices: block, unblock)
- revoke_sessions (boolean, optional) - only used when blocking
It should refuse on-premises synced accounts, the credential's own account, a list
of protected accounts and admin role holders, set accountEnabled, revoke sessions
after blocking when revoke_sessions is on, and print the new state.
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]
operation select (choices block, unblock) Yes block
revoke_sessions 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: block or unblock a cloud-only, non-admin Microsoft 365 user's sign-in, revoke their sessions
# when blocking if asked, and output the account's new state.
$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-SignInChange {
    param([string]$UserUpn, [string]$Operation, [string]$RevokeSessions)
    $upn = $UserUpn.Trim()
    $op = $Operation.Trim().ToLower()
    Assert-Inputs -Upn $upn -Operation $op

    $user = Get-TargetUser -Upn $upn
    Assert-CloudOnlyUser -User $user -Upn $upn
    Assert-NotProtectedUser -User $user -Upn $upn
    $enable = $op -eq 'unblock'
    $changed = Set-SignInState -User $user -Upn $upn -Enable $enable
    $revoked = (-not $enable) -and $RevokeSessions -eq 'true'
    if ($revoked) { Revoke-UserSessions -UserId $user.id -Upn $upn }
    Write-SignInSummary -User $user -Upn $upn -Enable $enable -Changed $changed -Revoked $revoked

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

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

function Get-TargetUser {
    param([string]$Upn)
    $select = 'id,displayName,userPrincipalName,accountEnabled,onPremisesSyncEnabled'
    try {
        Invoke-RestMethod -Uri "$baseUrl/users/$([uri]::EscapeDataString($Upn))?`$select=$select" -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. Change it 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. Change admin accounts in the Microsoft Entra admin center. Nothing was changed."
    }
}

function Set-SignInState {
    param($User, [string]$Upn, [bool]$Enable)
    if ($User.accountEnabled -eq $Enable) { return $false }
    try {
        Invoke-RestMethod -Method Patch -Uri "$baseUrl/users/$($User.id)" -Headers $headers `
            -ContentType 'application/json' -Body (@{ accountEnabled = $Enable } | ConvertTo-Json) | Out-Null
    } catch {
        Write-Error "Failed to change sign-in for '$Upn': $_"
        exit 1
    }
    $true
}

function Revoke-UserSessions {
    param([string]$UserId, [string]$Upn)
    try {
        Invoke-RestMethod -Method Post -Uri "$baseUrl/users/$UserId/revokeSignInSessions" -Headers $headers | Out-Null
    } catch {
        Write-Error "Failed to revoke sessions 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-SignInSummary {
    param($User, [string]$Upn, [bool]$Enable, [bool]$Changed, [bool]$Revoked)
    $state = if ($Enable) { 'allowed to sign in' } else { 'blocked from signing in' }
    Write-Output "=== SIGN-IN UPDATED ==="
    Write-Output "User             : $($User.displayName) ($Upn)"
    Write-Output "Sign-in          : $state$(if (-not $Changed) { ' (already, nothing changed)' })"
    Write-Output "Sessions revoked : $Revoked"
}

Invoke-SignInChange -UserUpn $ctx.inputs.user_upn -Operation $ctx.inputs.operation `
    -RevokeSessions $ctx.inputs.revoke_sessions

Test it

Test runs for real

Test skips authorization and approval, uses the real M365 Graph credential, and really blocks the account. 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. Enter a test user's address and block.
  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, and check that the test user can't sign in.
  4. Test again with unblock.
  5. Test with the credential's own admin account, and check the run is refused.

Use it

Publish the action. Engineers ask SuperIT, for example "Block sign-in for [email protected] and revoke his sessions". The run waits for an admin to approve it. To check first, ask "Is Tom's account enabled?"; SuperIT reads that without running the action.

For MSPs, publish to managed teams from the Publish dialog; an engineer or admin in each team authorizes its own M365 Graph credential.

Variations

  • Add a reason text input and print it, so the reason is kept in the run history.
  • Block a list of users at once, for example at the end of a contract.
  • For a suspected compromise, use Revoke sessions for a compromised account, which also resets the password.