AADSTS54005: Authorization code already used (fix)
Short answer: AADSTS54005 means an app tried to redeem an OAuth 2.0 authorization code that had already been redeemed. Usually the app sent the same code twice: a double callback, a retry, or a page reload. It is mostly an app bug with low attack value. The app should get a new code, or use the refresh token it already has.
Fix: Developer: redeem each authorization code once, make the callback handler idempotent, don't retry a redemption after a timeout, and use the refresh token after the first redemption. User: start the sign-in again from the app's start page instead of reloading. Full steps
| Code | AADSTS54005 |
|---|---|
| Category | Tokens and sessions |
| Type | Failure |
| Attack signal | Low: usually configuration or user error |
| Where to look | SigninLogs, AADNonInteractiveUserSignInLogs |
| Microsoft lookup | login.microsoftonline.com/error?code=54005 |
What does AADSTS54005 mean?
Section titled “What does AADSTS54005 mean?”Microsoft’s description:
OAuth2 Authorization code was already redeemed, please retry with a new valid code or use an existing refresh token.
In the authorization code flow the app gets a code at its redirect URI and then sends it to the /token endpoint to get tokens. 54005 means a code reached the token endpoint a second time after it had already been exchanged.
Microsoft’s authorization code flow page says “Authorization codes are short lived” and lists the generic token-endpoint error for a bad code: invalid_grant, “The authorization code or PKCE code verifier is invalid or has expired.” Our reading: the app sees invalid_grant, and AADSTS54005 is in the error description and error_codes. Microsoft’s error reference tells developers to branch on the error field and not on the AADSTS number.
Is it an attack?
Section titled “Is it an attack?”Usually not. The common causes are in the app:
| Pattern | Likely meaning |
|---|---|
| A success and a 54005 for the same user and app, seconds apart, same IP | The app redeemed the code twice: two callback handlers, a retry after a slow response, or a framework that runs the callback twice. The first call worked. Benign. |
| 54005 after the user reloads or uses the back button on the callback page | The page sends the old code again. Benign. |
| 54005 for many users of one web app, from your own server’s IP | The app’s callback code has a bug. Fix the app. |
| 54005 from a different IP than the success, for the same user and app | Our interpretation, not documented: someone else got hold of the code and tried it after the real app. The refusal is the protection working, but find out how the code leaked (logs, referrer headers, a shared proxy). |
Going by Microsoft’s description, the second redemption gets nothing: a code that was already used can’t be exchanged for tokens again.
How do I find it in my logs (KQL)?
Section titled “How do I find it in my logs (KQL)?”Microsoft doesn’t document which sign-in table 54005 is logged in. Check where it lands in your workspace:
union withsource=SourceTable SigninLogs, AADNonInteractiveUserSignInLogs| where TimeGenerated > ago(7d)| where ResultType == "54005"| summarize Rows = count(), Users = dcount(UserPrincipalName) by SourceTableWhich apps produce it, and from how many IPs:
union SigninLogs, AADNonInteractiveUserSignInLogs| where TimeGenerated > ago(7d)| where ResultType == "54005"| summarize Rows = count(), Users = dcount(UserPrincipalName), IPs = dcount(IPAddress), SampleIPs = make_set(IPAddress, 5) by AppDisplayName, AppId| sort by Rows desc54005 from an IP that is different from the one that signed in successfully to the same app shortly before. This is the pattern from the last table row above, which Microsoft doesn’t document as an attack signal:
let window = 10m;let reuse = union SigninLogs, AADNonInteractiveUserSignInLogs | where TimeGenerated > ago(1d) | where ResultType == "54005" | project ReuseTime = TimeGenerated, UserPrincipalName, AppId, AppDisplayName, ReuseIP = IPAddress;let success = union SigninLogs, AADNonInteractiveUserSignInLogs | where TimeGenerated > ago(1d) | where ResultType == "0" | project SuccessTime = TimeGenerated, UserPrincipalName, AppId, SuccessIP = IPAddress;reuse| join kind=inner success on UserPrincipalName, AppId| where SuccessTime between ((ReuseTime - window) .. ReuseTime)| where ReuseIP != SuccessIP| summarize Reuses = count(), ReuseIPs = make_set(ReuseIP, 5), SuccessIPs = make_set(SuccessIP, 5) by UserPrincipalName, AppDisplayName| sort by Reuses descThe 10-minute window is a starting point. Expect false positives: a web app redeems the code from its server, so its IP can differ from the browser IP in the user’s interactive sign-in. Compare against the app’s known server IPs before you treat a hit as suspicious. Non-interactive rows are aggregated when application, user, IP address, status and resource ID match.
How do I fix AADSTS54005?
Section titled “How do I fix AADSTS54005?”For the user: start the sign-in again from the app’s start page, not by reloading the page you landed on. If it keeps happening, report it to whoever runs the app.
For developers:
- Redeem each code once. Make the callback handler idempotent: if a second request arrives with the same code, don’t send it to
/tokenagain. - Don’t retry a code redemption automatically after a timeout. Start a new sign-in instead.
- After the first redemption, use the refresh token, as Microsoft’s description says. The auth code flow page: “Provide the
refresh_tokeninstead of thecode.” Also: “Refresh tokens aren’t revoked when used to acquire new access tokens. You’re expected to discard the old refresh token.” - Use MSAL instead of hand-written token code; it handles the redemption and cache for you.
For admins:
- Use the second query to find the app and hand the time window to its developer.
- If the third query shows reuse from IPs that aren’t the app’s servers, treat it as a possible leak of authorization codes and review where codes could be logged or forwarded.
Related codes
- AADSTS50058: Session information isn't sufficient for single sign-on
- AADSTS700082: The refresh token has expired due to inactivity
- AADSTS700054: ID tokens not enabled for the app
- AADSTS50196: App is stuck in a sign-in loop