Skip to content

AADSTS70008: The refresh token has expired or was revoked (fix)

Short answer: AADSTS70008 means an app tried to use a refresh token or grant that is no longer valid. Microsoft's text names inactivity; the code name also says revoked. It is usually part of the normal token lifecycle and has low attack value. The app should send the user to an interactive sign-in. Hunt it in the non-interactive sign-in logs.

Fix: User: sign in again when the app asks; if it keeps failing without asking, sign out of the app and back in. Developer: treat 70008 as interactive sign-in needed and fall back from the silent call to an interactive one. Don't retry the same refresh token. Full steps

At a glance
CodeAADSTS70008
Microsoft nameExpiredOrRevokedGrant
CategoryTokens and sessions
TypeFailure
Attack signalLow: usually configuration or user error
Where to lookAADNonInteractiveUserSignInLogs, SigninLogs
Microsoft lookuplogin.microsoftonline.com/error?code=70008

Microsoft’s description:

ExpiredOrRevokedGrant: The refresh token has expired due to inactivity. The token was issued on XXX and was inactive for a certain amount of time.

An app presented a refresh token (a “grant”) to get a new access token, and Microsoft Entra ID refused it. The description talks about inactivity. The code name, ExpiredOrRevokedGrant, also covers revocation. Microsoft doesn’t say which cases return 70008 rather than a more specific code.

Two sibling codes in the same reference table are more specific:

  • 700082 ExpiredOrRevokedGrantInactiveToken, with almost the same text, which Microsoft calls an “Expected part of the token lifecycle”.
  • 50173, a grant revoked after a credential change: “The user might have changed or reset their password.”

Our reading: 70008 is the more generic form of the same event. Treat it like 700082 unless the context points to revocation.

How long refresh tokens live. Microsoft’s refresh token page gives the defaults: “24 hours for single-page applications”, 24 hours for the email one-time passcode flow, and “90 days for all other scenarios.” The configurable token lifetimes page lists a non-configurable Refresh Token Max Inactive Time of 90 days. Tokens “can be revoked by the sign-in service at any time before their expiration”, for example when the user changes their password or an admin revokes all refresh tokens for the user.

Usually not. A refresh token that expired or was revoked gives nobody access. The common sources are a device or app that wasn’t used for a long time, and an app that keeps an old token after a password change or a session revocation.

Pattern Likely meaning
70008 from a user’s known device, then an interactive success The user came back after a long break, or after a password change, and signed in again. Benign.
70008 for many users of one app, all at once Our interpretation: a mass revocation (for example after an incident response) or an app holding tokens far past their lifetime. Check what happened that day.
70008 shortly after you revoked a user’s sessions The revocation is working. The old token was refused.
70008 from an IP or device the user doesn’t normally use Someone presented an old refresh token from an unfamiliar place. Reasoning, not a documented detection: if the token was stolen, expiry or revocation is what stopped it. Worth a short look.

IP caveat. Microsoft’s non-interactive sign-ins page says the IP of non-interactive sign-ins by confidential clients “doesn’t match the actual source IP of where the refresh token request is coming from. Instead, it shows the original IP used for the original token issuance.” An unfamiliar IP on a 70008 row is a meaningful signal mainly for public clients (desktop, mobile and browser apps).

Refresh token redemption is a non-interactive sign-in (“A client app uses an OAuth 2.0 refresh token to get an access token” is one of Microsoft’s examples), so expect most rows in AADNonInteractiveUserSignInLogs. Microsoft doesn’t document the table for this code specifically, so check:

union withsource=SourceTable SigninLogs, AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(7d)
| where ResultType == "70008"
| summarize Rows = count(), Users = dcount(UserPrincipalName) by SourceTable

Baseline by app and client:

AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(7d)
| where ResultType == "70008"
| summarize Rows = count(), Users = dcount(UserPrincipalName) by AppDisplayName, ClientAppUsed
| sort by Rows desc

70008 from an IP the user had no successful sign-in from in the previous 13 days (14 days back up to yesterday). Use it for triage, not as an alert on its own, and keep the confidential-client caveat above in mind:

let lookback = 14d;
let knownIPs =
union SigninLogs, AADNonInteractiveUserSignInLogs
| where TimeGenerated between (ago(lookback) .. ago(1d))
| where ResultType == "0"
| distinct UserPrincipalName, IPAddress;
AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(1d)
| where ResultType == "70008"
| join kind=leftanti knownIPs on UserPrincipalName, IPAddress
| summarize Rows = count(), Apps = make_set(AppDisplayName, 5), UserAgents = make_set(UserAgent, 3)
by UserPrincipalName, IPAddress
| sort by Rows desc

Non-interactive rows are aggregated when application, user, IP address, status and resource ID match, so Rows is not a request count. ResultType is a string column, so compare it with "70008", not 70008.

  • Refresh token lifetimes can’t be changed with policy. Microsoft says that “As of January 30, 2021, refresh and session token lifetimes are no longer configurable through token lifetime policies.”
  • To control how often users sign in, Microsoft points to Conditional Access sign-in frequency. A refresh refused because of sign-in frequency is a different code, 70043.

For the user: sign in again when the app asks. If the app keeps failing without asking, sign out of it and back in.

For developers: treat 70008 as “interactive sign-in needed”. Microsoft’s guidance for refresh tokens is that the app “should handle such revocations gracefully by redirecting the user to an interactive sign-in prompt”. With MSAL, catch the error from the silent call and fall back to an interactive call. Don’t retry the same refresh token. For single-page apps, plan for the 24-hour limit: Microsoft says they “must be prepared to rerun the authorization code flow using an interactive authentication”.

For admins:

  1. Nothing is broken in the normal case. Exclude "70008" from failure-rate dashboards.
  2. If one app produces most of the rows and users complain, the app probably doesn’t fall back to an interactive sign-in. Report it to the developer.
  3. If the third query shows rows from unfamiliar public-client IPs, check that user’s other sign-ins from the same IP. If anything succeeded there, treat it as a possible token theft and revoke the user’s sessions.
  • AADSTS700082: The refresh token has expired due to inactivity
  • AADSTS50173: Grant expired because it was revoked
  • AADSTS70043: Refresh token expired due to sign-in frequency checks
  • AADSTS50133: Session invalid after password change or expiry
  • AADSTS700081: Refresh token reached its maximum lifetime
  • AADSTS7000110: Code or refresh token used by a different app

Sources

  1. Microsoft Entra authentication and authorization error codes
  2. Refresh tokens in the Microsoft identity platform
  3. Configurable token lifetimes in the Microsoft identity platform
  4. Non-interactive user sign-ins
  5. AADNonInteractiveUserSignInLogs table reference (Azure Monitor)
  6. SigninLogs table reference (Azure Monitor)