← Back to Blog

What Revoke Sessions Actually Stops in Microsoft Entra ID

3 November 2026 Identity Security 12 min read
After Revoke sessions: what stops, and roughly when (illustrative) Revoke 0 ~15 min 60 to 90 min later Entra refresh tokens, Entra cookies after a few minutes Access tokens, CAE clients latency of up to 15 minutes observed Access tokens, other clients until each token expires An app's own session cookie External users Red rows aren't ended by this revoke: the application, or the guest's home tenant, ends them. Times are documented defaults and observed latencies, not deadlines. Configuration and clients change them.

Revoke sessions invalidates the refresh and session material Entra ID issued. Tokens already handed out, and sessions Entra ID never issued, follow their own clocks.

Take a hypothetical account compromise. Someone on the team resets the password, selects Revoke sessions on the user in the Entra admin center, and closes the ticket. Forty minutes later the mailbox is still being read, by a scripted client that doesn't support Continuous Access Evaluation, replaying an access token it obtained before the revoke.

Nothing malfunctioned. Each action did what Microsoft documents, and that is narrower than most incident notes assume. This post covers what the revoke invalidates and what a password reset doesn't, what keeps working and why, a containment script that checks its own results, and three queries that each answer a different question about whether the account has stopped.

What the revoke actually does

Microsoft's revoke-access guidance pairs the portal's Revoke sessions with Revoke-MgUserSignInSession, the PowerShell form of the Graph revokeSignInSessions action. That action invalidates all the refresh tokens issued to applications for the user, and the session cookies in the user's browser, by resetting the user's signInSessionsValidFromDateTime to the current time. There can be a delay of a few minutes, and it doesn't revoke sessions for external users.

It invalidates what exists. It doesn't stop a fresh sign-in: if the account is still enabled and the attacker still holds a working password and second factor, they can simply authenticate again. That is why disabling the account comes first.

Refresh tokens are the reason the revoke matters. Microsoft gives them a default lifetime of 90 days (24 hours for single-page apps), not configurable, and using one returns a new one without revoking the old.

A password reset is not a revoke

The same refresh-token page has a revocation table, split by password-based and non-password-based cookies and tokens, plus tokens issued to confidential clients:

EventPassword-based cookie and tokenNon-password-based cookieNon-password-based tokenConfidential client token
User changes password, user runs SSPR, admin resets in the Azure portalRevokedStays aliveStays aliveStays alive
Admin resets in the Entra or Microsoft 365 admin centerRevokedStays aliveRevokedRevoked
Admin revokes all refresh tokens for the userRevokedRevokedRevokedRevoked

What a reset covers depends on where it was done, and no reset covers every column. Reset to take the stolen password out of play; revoke to take the sessions out of play.

What keeps working, and why

Access tokens already issued. An access token stays valid until it expires. Microsoft's access-token documentation assigns a random default lifetime of 60 to 90 minutes, 75 on average, and notes that tenants without Conditional Access get two hours for clients such as Teams and Microsoft 365. Configured token lifetime policies change it again. Plan on "until each token expires", not a fixed hour.

Unless Continuous Access Evaluation applies. CAE lets a resource reject an unexpired token after a critical event: account disabled or deleted, password changed or reset, MFA enabled for the user, all refresh tokens revoked by an administrator, or high user risk. Microsoft states the goal is near real time and that latency of up to 15 minutes might be observed. That is documented propagation latency, not a guaranteed deadline. It covers Exchange, Teams and SharePoint Online, needs a client that understands the CAE challenge, such as Outlook, Teams or OneDrive, and doesn't support guests. CAE sessions get long-lived tokens, 20 to 28 hours, because revocation becomes event-driven.

An application's own session. Many applications sign the user in with Entra ID and then issue their own cookie. Entra ID can't revoke a session token an application issued, and the application may not send the user back to Entra ID while it's valid. Deprovisioning, or ending the session inside the application, is the revocation there.

External users. Refresh tokens aren't revoked for B2B users in the resource tenant; the home tenant has to do it. In yours, block the guest object's sign-in and remove its access.

Values to calibrate against. Refresh tokens: 90 days by default, 24 hours for single-page apps. Access tokens: commonly 60 to 90 minutes under default settings, two hours for Teams and Microsoft 365 clients in tenants without Conditional Access, 20 to 28 hours in CAE sessions. CAE: latency of up to 15 minutes can be observed. The revoke: a delay of a few minutes. Provisioning cycles: typically 20 to 40 minutes. None of these is a containment deadline; they tell you how long to keep looking.

Contain, and check each step landed

Microsoft's sequence is disable the account, revoke the sessions, disable the user's devices, then deprovision from applications that keep their own sessions. For an account synchronized from Active Directory, disable it in AD too, because AD is the source of authority; Microsoft's hybrid steps also reset the password twice, tied to pass-the-hash risk during replication delays.

The script below stops on any failure, checks each result, and records outcomes. Permissions are per operation: User.EnableDisableAccount.All with User.Read.All is the least-privileged combination for disabling an account, User.RevokeSessions.All covers the revoke, and the device update needs Directory.AccessAsUser.All. Directory roles still apply on top: Privileged Authentication Administrator to disable an administrator, Cloud Device Administrator at least for devices.

Device disablement is a switch, not a default. Disabling an Entra device object stops it from authenticating to Entra ID; it doesn't isolate the endpoint, which is your EDR's job. Decide per incident whether the user's registered devices should be cut off.

# Contain one account and record what each step actually did.
param(
    [Parameter(Mandatory)] [string] $UserPrincipalName,
    [switch] $DisableDevices
)
$ErrorActionPreference = 'Stop'
Import-Module Microsoft.Graph.Authentication, Microsoft.Graph.Users, Microsoft.Graph.Users.Actions
$scopes = 'User.Read.All', 'User.EnableDisableAccount.All', 'User.RevokeSessions.All'
if ($DisableDevices) {
    Import-Module Microsoft.Graph.Identity.DirectoryManagement
    $scopes += 'Directory.AccessAsUser.All'
}
Connect-MgGraph -Scopes $scopes -NoWelcome
$ctx = Get-MgContext
Write-Host "Tenant $($ctx.TenantId) as $($ctx.Account). Continue? (y/n)"
if ((Read-Host) -ne 'y') { return }

$user = Get-MgUser -UserId $UserPrincipalName -Property Id, UserPrincipalName, AccountEnabled

# 1. Block fresh sign-ins, then confirm the property changed.
Update-MgUser -UserId $user.Id -AccountEnabled:$false
if ((Get-MgUser -UserId $user.Id -Property AccountEnabled).AccountEnabled) { throw 'Account is still enabled.' }

# 2. Revoke, keeping the submission time apart from the confirmed validity time.
$submittedAt = (Get-Date).ToUniversalTime()
$result = Revoke-MgUserSignInSession -UserId $user.Id
if ($result -eq $false) { throw 'Revoke returned false.' }
$validFrom = (Get-MgUser -UserId $user.Id -Property SignInSessionsValidFromDateTime).SignInSessionsValidFromDateTime
if (-not $validFrom -or $validFrom -lt $submittedAt.AddMinutes(-1)) { throw "SignInSessionsValidFromDateTime is $validFrom, older than the revoke." }

# 3. Optional: disable registered devices, recording each outcome.
$deviceResults = @()
if ($DisableDevices) {
    foreach ($d in Get-MgUserRegisteredDevice -UserId $user.Id -All) {
        try {
            Update-MgDevice -DeviceId $d.Id -AccountEnabled:$false
            $deviceResults += [pscustomobject]@{ DeviceObjectId = $d.Id; Outcome = 'disabled' }
        } catch {
            $deviceResults += [pscustomobject]@{ DeviceObjectId = $d.Id; Outcome = "failed: $($_.Exception.Message)" }
        }
    }
}

[pscustomobject]@{
    User                 = $user.UserPrincipalName
    UserObjectId         = $user.Id
    RevokeSubmittedUtc   = $submittedAt.ToString('o')
    SessionsValidFromUtc = ([datetime]$validFrom).ToUniversalTime().ToString('o')
}
$deviceResults

Use SessionsValidFromUtc, the value Entra ID returned, as the start time for the queries, not the time the script ran. The application step stays manual: list the applications that keep their own sessions before you need to, with who can end a session in each.

Verify: no new tokens issued

// Source: SigninLogs and AADNonInteractiveUserSignInLogs
//   (Microsoft Entra ID diagnostic settings to Log Analytics or Sentinel)
// Question: do the collected sign-in logs show any token issued to this
// account after the revoke? Investigation query; inputs come from the script.
let RevokedAt = datetime(2026-11-03T08:12:00Z);    // SessionsValidFromUtc
let UserObjectId = "<UserObjectId from the script>";
union SigninLogs, AADNonInteractiveUserSignInLogs
| where TimeGenerated between (RevokedAt .. (RevokedAt + 4h))
| where UserId == UserObjectId
| summarize Attempts = count(), FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated)
    by Type, ResultType, AppDisplayName, ResourceDisplayName, IncomingTokenType, IPAddress
| sort by FirstSeen asc
// Checkpoint, not a pass/fail line: successes ("0") in the first minutes are expected
// while the revoke propagates. A success well after that needs an explanation.

No successful rows means the tables you collect hold no matching successful sign-in in that window. It's evidence within that coverage, subject to which log categories you export, ingestion delay and the filters above, not proof that nothing anywhere issued a token.

Verify: which clients retried a revoked grant

A client presenting a revoked refresh token gets AADSTS50173, "The provided grant has expired due to it being revoked", recorded as ResultType 50173. Microsoft lists the causes as a password change or reset, an expired refresh token, or an administrator revoking it, so the code says a grant was rejected, not who presented it.

// Source: SigninLogs and AADNonInteractiveUserSignInLogs
// Question: which clients presented a refresh token the revoke had invalidated?
let RevokedAt = datetime(2026-11-03T08:12:00Z);
let UserObjectId = "<UserObjectId from the script>";
union SigninLogs, AADNonInteractiveUserSignInLogs
| where TimeGenerated between (RevokedAt .. (RevokedAt + 24h))
| where UserId == UserObjectId and ResultType == "50173"
| summarize Retries = count(), FirstRetry = min(TimeGenerated), LastRetry = max(TimeGenerated),
    UserAgents = make_set(UserAgent, 5), Devices = make_set(tostring(parse_json(tostring(DeviceDetail)).displayName), 5)
    by IPAddress, AutonomousSystemNumber, AppDisplayName
| sort by Retries desc

Expect the user's own devices here; they all hold tokens that just died. An address you don't recognize is a lead, not a verdict: VPNs, mobile networks, corporate proxies and service infrastructure all produce unfamiliar addresses. Check it against the device details, application, user agent, network owner and the incident timeline before you attribute it.

Verify: old tokens still being spent

The sign-in logs record token issuance; an access token issued before the revoke never appears there again. For Microsoft Graph, Graph activity logs show the requests themselves, with TokenIssuedAt and a SignInActivityId that matches the sign-in log's UniqueTokenIdentifier. They need Entra ID P1 or P2 and a diagnostic setting, and Microsoft notes that requests from its own applications may not all have a matching sign-in.

// Source: MicrosoftGraphActivityLogs (Entra ID P1/P2, diagnostic setting required)
// Question: did a token issued before the revoke make Graph requests after it,
// and did those requests succeed or get denied?
let RevokedAt = datetime(2026-11-03T08:12:00Z);
let UserObjectId = "<UserObjectId from the script>";
MicrosoftGraphActivityLogs
| where TimeGenerated between (RevokedAt .. (RevokedAt + 3h))
| where UserId == UserObjectId and TokenIssuedAt < RevokedAt
| extend Outcome = case(ResponseStatusCode between (200 .. 299), "succeeded",
                        ResponseStatusCode in (401, 403), "denied", "other")
| summarize Requests = count(), LastRequest = max(TimeGenerated),
    Examples = make_set(strcat(RequestMethod, " ", RequestUri), 5)
    by Outcome, ResponseStatusCode, AppId, IPAddress, SignInActivityId
| extend MinutesAfterRevoke = datetime_diff('minute', LastRequest, RevokedAt)
| sort by LastRequest desc
// "denied" rows are containment working. "succeeded" rows are what to read:
// the method and URI say whether mail or files were touched. A $batch request's
// outer status doesn't show the status of each request inside it.

Without Graph activity logs, OfficeActivity gives what the account did in Exchange, SharePoint and Teams after the revoke, by workload and client address, but not which token carried it.

The issuance check in Splunk, on the azure:monitor:aad sourcetype with the CIM fields user, action, app, src_ip and signature_id (the ResultType); map those first if your add-on names them differently. Collect the non-interactive sign-in category too, and set earliest and latest in your Splunk user's time zone.

index=azure sourcetype="azure:monitor:aad" user="j.doe@contoso.com"
    earliest="11/03/2026:08:12:00" latest="11/03/2026:12:12:00"
| eval outcome=case(action="success", "token issued", signature_id="50173", "revoked grant retried", true(), "other failure")
| stats count AS attempts, min(_time) AS first_seen, max(_time) AS last_seen BY outcome, app, src_ip
| sort first_seen
| convert ctime(first_seen) ctime(last_seen)

What survives a revoke, mechanism by mechanism

Revocation removes sessions. Configuration an attacker left behind survives it, and each kind needs its own removal:

  • A registered MFA method survives. It doesn't grant access by itself, but it lets whoever controls it pass MFA at the next sign-in once the account is enabled. Remove methods the user doesn't recognize.
  • A delegated OAuth consent grant stays authorized. Using it still needs a token for the user, so it's subject to sign-in and revocation controls, but the grant is ready the moment a token is obtained. Remove the grant.
  • Application permissions on an application the attacker controls don't involve the user's tokens at all. The application signs in with its own credential. Remove the permissions and the credential, or the application.
  • A mailbox forwarding rule runs server-side. It keeps copying mail with no session behind it. Remove the rule.
Don't close the ticket on "Revoke sessions succeeded". The portal confirms a timestamp was reset, not that access stopped. Close it against explicit criteria instead.

When to call it contained

Close the incident when you can record all four:

  1. Identity actions confirmed: the account disabled, SignInSessionsValidFromDateTime at or after the revoke, devices handled as decided, the on-premises account disabled if synchronized.
  2. No successful resource activity on old tokens after the token lifetimes you calibrated for, from Graph activity logs or OfficeActivity, with any successes explained.
  3. Application sessions ended in every application that keeps its own, or the user deprovisioned from them.
  4. Persistence checked and removed: MFA methods, consent grants, application credentials, mailbox rules.

The logs supply the evidence for 1 and 2 within what you collect. Items 3 and 4 need someone to look.

Where to read more

  • Microsoft Learn, "Revoke user access in Microsoft Entra ID", and the Graph revokeSignInSessions reference.
  • Microsoft Learn, "Refresh tokens in the Microsoft identity platform" and "Access tokens in the Microsoft identity platform", for lifetimes and the revocation table.
  • Microsoft Learn, "Continuous access evaluation".
  • Microsoft Learn, AADSTS50173, and "Access Microsoft Graph activity logs".
  • MITRE ATT&CK, T1550.001 Application Access Token, and T1528 Steal Application Access Token.

What to do this week

  1. Run the 50173 query against your last three compromised-account tickets. List the addresses you can't explain, and check each against the device, application, user agent, network owner and timeline before recording a conclusion.
  2. Check your runbook for the reset-only path. If containment can end at a password reset, add the disable and the revoke, and note which portal your team resets from.
  3. Turn on Microsoft Graph activity logs if you have P1 or P2 and don't collect them. The third query can't be run after the fact.
  4. List the applications that keep their own sessions, with who can end a user's session in each.
  5. Write the four closure criteria into your ticket template, so "contained" means the same thing on every incident.
Ridgeline Cyber Defence Written by security professionals. Published weekly on Tuesdays.

Related Articles

20 October 2026

Your Break-Glass Account Is Excluded From Every Policy. It Still May Not Get You In.

Excluding emergency access accounts from Conditional Access doesn't exempt them from Microsoft's mandatory MFA. Check th

13 October 2026

The Accounts That Can Take Your Domain Aren't on Your Admin List

adminCount stays set after an account leaves a protected group, and accounts with DCSync rights never get it. Find both,

3 May 2026

How to Investigate an M365 Identity Compromise from Sign-in Logs to Containment

The sign-in log tells you how they got in. The audit log tells you what they did. Here's the sequence that turns both in