Skip to content

After a compromise: revoke Entra ID sessions and prove it worked

To cut off a compromised Entra ID account, revoke its refresh tokens with Revoke-MgUserSignInSession or Revoke sessions in the admin center. That stops new tokens, not tokens already issued: access tokens last one hour by default unless Continuous Access Evaluation applies. Then prove it worked by reading the 50173 rows that follow.

Microsoft’s revoke-access guidance gives two ways to do it:

  • PowerShell: Revoke-MgUserSignInSession -UserId $User.Id, which revokes the user’s refresh tokens.
  • Entra admin center: open the user, and on the Overview page select Revoke sessions.

Both call the Graph revokeSignInSessions action. Its documentation says it “Invalidates all the refresh tokens issued to applications for a user (and session cookies in a user’s browser), by resetting the signInSessionsValidFromDateTime user property to the current date-time”, and that “there might be a small delay of a few minutes before tokens are revoked.”

Microsoft states the result of its full revoke-access procedure (disable the account, reset the password, revoke, disable devices): “After admins take the above steps, the user can’t gain new tokens for any application tied to Microsoft Entra ID.”

The key word is new. Revocation stops a client from turning its refresh token into a fresh access token. It doesn’t reach into a client and delete what that client already holds.

Why can the attacker still have access after you revoke?

Section titled “Why can the attacker still have access after you revoke?”

Because of access tokens that were issued before the revocation. Microsoft: “By default, access tokens issued by Microsoft Entra ID last for 1 hour.” Our reading: the resource (Exchange, SharePoint, Graph and so on) accepts the access token on its own, without asking Entra ID each time, so a token issued before the revocation can keep working until it expires.

The exception is Continuous Access Evaluation (CAE). Microsoft’s CAE page says the implementation “focuses on Exchange, Teams, and SharePoint Online”, and that it needs a CAE-capable client: current Outlook, Teams, OneDrive and Office desktop apps are listed as supported, Office web apps are not. The critical events it reacts to include “Password for a user is changed or reset”, “Administrator explicitly revokes all refresh tokens for a user” and “User Account is deleted or disabled”. On timing: “The goal for critical event evaluation is for response to be near real time, but latency of up to 15 minutes might be observed because of event propagation time”.

Two things cut the other way. CAE sessions use long-lived access tokens: “Token lifetime increases to long-lived, up to 28 hours, in CAE sessions.” So a CAE token that, for whatever reason, does not receive the revocation event lives far longer than an hour. And “If you aren’t using CAE-capable clients, your default access token lifetime remains 1 hour.” Microsoft also notes “CAE doesn’t support Guest user accounts.”

So: for Exchange, SharePoint and Teams reached through a CAE-capable client, expect the cut-off within about 15 minutes. For everything else, plan for up to an hour. Which case the attacker falls into depends on the token they hold, not on the tool they use (our reading of Microsoft’s flow, where the resource evaluates the token and the client only needs CAE support to act on the challenge): tokens an attacker obtained with their own non-CAE tooling are ordinary one-hour tokens, while a long-lived CAE token stolen from a real client is refused by the resource once the revocation event reaches it.

Two practical consequences (our reasoning):

  • During that window you will not see a revocation error for the attacker’s client. It doesn’t need Entra ID until its access token runs out.
  • Don’t close the incident minutes after revoking. Check the logs again once the access-token lifetime has passed.

No. Revocation ends the sessions that exist. It doesn’t stop someone from starting a new one.

  • If the attacker knows the password, they can sign in again. Reset it.
  • If the attacker registered their own MFA method, they pass MFA on that new sign-in. Review the user’s authentication methods and remove anything the user doesn’t recognise.
  • If the attacker has a device the user registered or joined, that device can keep getting tokens through device-based single sign-on (our reasoning). Microsoft’s revoke-access procedure disables the user’s devices as a separate step: Update-MgDevice -AccountEnabled:$false.
  • If the attacker got an app consented, the app has its own grant and survives all of the above. See the consent phishing guide.

Microsoft’s revoke-access procedure for removing a user’s access also starts by disabling the account. For a leaver, that is the end state. For a compromised user who will keep working, our approach is to disable the account while you clean up, then enable it again after the password reset and method review.

Which sign-in codes prove the revocation worked?

Section titled “Which sign-in codes prove the revocation worked?”

Each step you take produces its own error code when a client hits it. These rows are your evidence.

Code Microsoft’s description What it shows after your response
50173 “The provided grant has expired due to it being revoked, a fresh auth token is needed. The user might have changed or reset their password. The grant was issued on ‘{authTime}’ and the TokensValidFrom date (before which tokens are not valid) for this user is ‘{validDate}’.” A client tried to redeem a refresh token issued before the revocation. Microsoft’s troubleshooting article lists the causes as a password change or reset, the refresh token expiring, or “An administrator revokes the refresh token.”
50133 “SsoArtifactRevoked - The session isn’t valid due to password expiration or recent password change.” A session tied to the old password was refused. Microsoft names the password change as the cause.
135011 “Device used during the authentication is disabled.” A device you disabled tried to authenticate.
50057 “UserDisabled - The user account is disabled. …” Something tried to sign in while the account was disabled.

How to read them (our reasoning):

  • 50173 from the user’s own devices and usual IPs: the legitimate user’s apps being signed out. Expected.
  • 50173 from an IP, country or device you can’t tie to the user: a client you didn’t know about was holding a token. Treat it as the attacker and pivot on that IP across other accounts.
  • 50173 followed by interactive attempts from the same unknown IP: the attacker is trying to get back in. Check the password reset and the MFA methods.
  • No 50173 at all: nothing refreshed a token for that user in the window you checked. That is not proof that nothing was stolen. The client may not have needed a new token yet, or the token may not have been used.

TokensValidFrom in the 50173 message is the cut-off date. Tokens issued before it are refused. The Graph documentation says the revocation works “by resetting the signInSessionsValidFromDateTime user property to the current date-time”, so you can read that property back (Get-MgUser -UserId <id> -Property SignInSessionsValidFromDateTime) and it doubles as a timestamp check. lab check pending: that the value in the 50173 message matches that property.

The Graph documentation says revocation also invalidates “session cookies in a user’s browser”. Whether an admin revocation alone (with no password change) shows up as 50133, whose description names only password expiry or change, is not stated by Microsoft. lab check pending.

Refresh-token redemption is a non-interactive sign-in. Microsoft lists “A client app uses an OAuth 2.0 refresh token to get an access token” as an example. So 50173 rows land mostly in AADNonInteractiveUserSignInLogs. In that table DeviceDetail, LocationDetails and Status are strings, not dynamic, so read their fields through parse_json.

Who was still holding a token after you revoked?

Section titled “Who was still holding a token after you revoked?”
let revokedUser = "user@contoso.com";
let revokedAt = datetime(2026-10-11T09:00:00Z);
AADNonInteractiveUserSignInLogs
| where TimeGenerated > revokedAt
| where UserPrincipalName =~ revokedUser
| where ResultType == "50173"
| extend Device = parse_json(DeviceDetail),
Location = parse_json(LocationDetails),
FailureReason = tostring(parse_json(Status).failureReason)
| summarize FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated),
Rows = count(),
Apps = make_set(AppDisplayName, 10)
by IPAddress,
Country = tostring(Location.countryOrRegion),
OS = tostring(Device.operatingSystem),
DeviceId = tostring(Device.deviceId),
IsManaged = tostring(Device.isManaged),
UserAgent,
FailureReason
| sort by FirstSeen asc

The JSON field names (countryOrRegion, operatingSystem, deviceId, isManaged, failureReason) follow the Microsoft Graph signIn resource. Check them against one row in your own workspace.

Non-interactive rows are aggregated when application, user, IP address, status and resource match. Rows counts log rows, not refresh attempts.

This shows every sign-in result for the user after the revocation, from both user sign-in tables, labelled by what it means:

let revokedUser = "user@contoso.com";
let revokedAt = datetime(2026-10-11T09:00:00Z);
union
(SigninLogs
| where TimeGenerated > revokedAt
| where UserPrincipalName =~ revokedUser
| extend Country = tostring(LocationDetails.countryOrRegion),
DeviceId = tostring(DeviceDetail.deviceId),
Log = "Interactive"),
(AADNonInteractiveUserSignInLogs
| where TimeGenerated > revokedAt
| where UserPrincipalName =~ revokedUser
| extend Country = tostring(parse_json(LocationDetails).countryOrRegion),
DeviceId = tostring(parse_json(DeviceDetail).deviceId),
Log = "NonInteractive")
| extend Meaning = case(
ResultType == "0", "Success: a token was issued",
ResultType == "50173", "Refresh token refused (revoked)",
ResultType == "50133", "Session refused (password change)",
ResultType == "135011", "Disabled device refused",
ResultType == "50057", "Disabled account refused",
"Other: look it up")
| summarize Rows = count(),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated),
Apps = make_set(AppDisplayName, 10)
by Log, ResultType, Meaning, IPAddress, Country, DeviceId
| sort by FirstSeen asc

DeviceDetail and LocationDetails are dynamic in SigninLogs and strings in AADNonInteractiveUserSignInLogs, so each leg extracts its fields before the tables are combined. ResultType is a string in both, so compare with "50173", not 50173.

The row to worry about is ResultType == "0" from an IP or device you can’t tie to the user. A success after the revocation means a new token was issued. If that wasn’t the user signing in again, revocation didn’t end the compromise: the attacker has the password, an MFA method, a device or a grant you haven’t dealt with yet.

Is the attacker’s IP touching other accounts?

Section titled “Is the attacker’s IP touching other accounts?”
let suspectIPs = dynamic(["203.0.113.10"]);
union
(SigninLogs
| project TimeGenerated, UserPrincipalName, IPAddress, ResultType, AppDisplayName, Log = "Interactive"),
(AADNonInteractiveUserSignInLogs
| project TimeGenerated, UserPrincipalName, IPAddress, ResultType, AppDisplayName, Log = "NonInteractive")
| where TimeGenerated > ago(14d)
| where IPAddress in (suspectIPs)
| summarize Users = make_set(UserPrincipalName, 20),
Results = make_set(ResultType, 20),
Apps = make_set(AppDisplayName, 20),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated)
by IPAddress, Log
  1. Note the time you start. Every check after this uses it.
  2. Disable the account if the user doesn’t need to work during the cleanup. Afterwards, sign-in attempts show as 50057.
  3. Reset the password if it may be known to the attacker. Microsoft’s procedure resets before it revokes; doing it in that order means the attacker can’t slip a new sign-in in between if the account is still enabled.
  4. Revoke sessions: Revoke-MgUserSignInSession -UserId $User.Id, or Revoke sessions on the user’s Overview page.
  5. Review authentication methods and remove any the user doesn’t recognise.
  6. Disable suspect devices: Update-MgDevice -DeviceId <device-object-id> -AccountEnabled:$false. Microsoft’s procedure disables all of the user’s devices; for a user who keeps working, disable at least the ones you can’t account for. Afterwards they show as 135011.
  7. Check app consents for the user and remove anything malicious. Revocation doesn’t remove a grant.
  8. Wait out the access-token window. Up to one hour by default for clients and resources without CAE; about 15 minutes for Exchange, SharePoint and Teams through CAE-capable clients.
  9. Run the queries above for the time after step 1. Account for every 50173, 50133 and 135011 row, and for every success.
  10. Pivot on any IP you can’t tie to the user, across all accounts.
  11. Re-enable the account once the password, methods, devices and consents are clean.
  12. Check again the next day. A client that was idle during your first check may only try to refresh later.

Sources

  1. Revoke user access in Microsoft Entra ID
  2. Continuous access evaluation
  3. Microsoft Graph: user: revokeSignInSessions
  4. Microsoft Entra authentication and authorization error codes
  5. Error AADSTS50173 - The provided grant has expired due to it being revoked
  6. Non-interactive user sign-ins
  7. AADNonInteractiveUserSignInLogs table reference (Azure Monitor)
  8. SigninLogs table reference (Azure Monitor)