30-Second Fix: Force a Token Refresh
Most of the time, the culprit here is a stale access token that the client hasn't bothered to refresh. Don't overthink it. Open your browser's dev tools (F12), find the Application tab, and under Storage -> Local Storage or Session Storage, locate the token entry (usually named access_token or token). Delete it. Then reload the page. The OAuth flow should kick in and redirect you to the identity provider for a fresh token.
If you're testing with cURL or Postman, just call the token endpoint again:
curl -X POST https://your-identity-provider.com/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "refresh_token=YOUR_REFRESH_TOKEN"If that returns a new access_token, you're done. The original issue was just a timing problem — your previous token expired and the client didn't handle the 401 gracefully.
5-Minute Fix: Check Token Lifetime & Clock Skew
If the refresh works but the new token expires too fast, you've got a lifetime configuration problem. On Azure AD, the default access token lifetime is 60-90 minutes. But your app might be set to a much shorter value. Go to your identity provider's admin console — for Azure, it's under Azure Active Directory -> App registrations -> your app -> Token configuration. Look for accessTokenAcceptedVersion and tokenLifetime settings. You can also use the optionalClaims to add the exp claim if it's missing.
For Azure AD with the v2 endpoint, run this PowerShell to check the current policy:
Get-AzureADPolicy | Where-Object {$_.Type -eq 'TokenLifetimePolicy'}If you find one, note the AccessTokenLifetime value. The default is 86400 seconds (24 hours) for refresh tokens, but access tokens might be lower. Set it to something sensible — 3600 seconds (1 hour) is standard for most production apps.
Also check your server's clock. If it's more than 5 minutes off from NTP, tokens will appear expired immediately because the nbf (not before) and exp (expiration) claims are timestamp-driven. Fix it with w32tm /resync on Windows or ntpdate on Linux.
Another common issue: the iat (issued at) claim is set to the server's time, but your app validates it against its own clock. If they don't match within the allowed skew (usually 5 minutes), you'll get the error. Most libraries let you set a clockTolerance parameter. In the msal library, it's systemOptions: { tokenRenewalOffsetSeconds: 300 }.
15-Minute Fix: Deep Clean Token Store and Rotate Secrets
If the above didn't work, you've got a persistent issue — either a corrupted token store or a refresh token that's been revoked or expired. Here's the nuclear option:
- Clear all cached tokens. On the client side, this means removing everything from
localStorageorsessionStorage. For mobile apps, clear the keychain or shared preferences. For server-side apps, delete the session store or database rows that hold tokens. - Revoke the old refresh token. In Azure AD, you can do this via the
/oauth2/v2.0/revokeendpoint:
curl -X POST https://login.microsoftonline.com/YOUR_TENANT_ID/oauth2/v2.0/revoke \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "token=THE_OLD_REFRESH_TOKEN"- Rotate your client secret. If someone has leaked it, tokens can be minted until the secret changes. Generate a new secret in the Azure portal and update your app config. Do this at 2 AM when no one is watching — and test immediately.
Why This Happens (The Nitty-Gritty)
OAuth token expiration failures fall into three buckets:
- Clock skew: The server and client disagree on what time it is. Like I said, NTP fixes this.
- Token lifecycle mismatch: The access token expires but the client doesn't request a refresh in time. Most libraries have a
tokenRenewalOffsetSeconds— set it to 300 seconds (5 minutes) before expiration. - Refresh token revoked: This happens when a user's password changes, they're disabled in the directory, or the token was issued with a limited lifetime (e.g., 24 hours). If you're using Azure AD, check the
signInActivityfor the user — they might be inactive.
In my experience, 70% of these cases are just clock skew. Another 20% are the client not renewing early enough. Only 10% are actual token store corruption or secret rotation issues. So start with the easy stuff and work your way up.
One more thing: if you're using a JWT library to validate tokens locally, make sure you're checking the exp claim correctly. Some libraries default to UTC, others to local time. Always use UTC. A simple test: decode the token on jwt.io and see if the exp timestamp makes sense. If it's already in the past, the issuer's clock is messed up.