AuthTrace › Guides

Fix invalid_grant at the token endpoint

The user signs in, comes back to the app with a code, and then the app's request to the token endpoint fails with 400 Bad Request and {"error":"invalid_grant"}. The error is deliberately vague. The OAuth 2.0 specification lets the server return it when the grant is "invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client" - and for PKCE, when the verifier does not match.

The error_description often says more. Entra ID adds a code: AADSTS54005 (code already redeemed), AADSTS70008 (code or refresh token expired), AADSTS700082 (refresh token expired from inactivity).

Authorization codes: causes in order of likelihood

1. The code was redeemed twice

A code can be used once. The first token request succeeds; a second one with the same code gets invalid_grant, and some providers also revoke the tokens the first request got. Common reasons:

Look for two token requests with the same code in a few milliseconds. Guard the exchange so it runs once per code, and remove the code from the URL straight after.

2. The PKCE verifier doesn't match the challenge

With PKCE the app sends code_challenge on the authorize request and the matching code_verifier on the token request. For S256 the rule is code_challenge = BASE64URL(SHA-256(code_verifier)), with no padding. It breaks when:

Check a pair yourself:

printf %s "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '='
# must print exactly the code_challenge from the authorize request

3. redirect_uri differs between the two requests

If the authorize request carried a redirect_uri, the token request must send the identical string. A trailing slash, http vs https behind a proxy, or an added query string is enough. (If the authorize request itself was refused for its redirect URI, see AADSTS50011 and redirect_uri mismatch.)

4. The code expired

Codes are short-lived - often well under ten minutes. A breakpoint in the callback handler while debugging is a classic way to run out the clock. So is a server whose clock is far off.

5. Wrong client or wrong tenant

The token request uses a different client_id from the authorize request, or goes to a different tenant or authorization-server endpoint (for example an Okta custom authorization server vs the org server, or /common vs a tenant ID in Entra ID).

Refresh tokens

For grant_type=refresh_token, invalid_grant means the refresh token is no longer usable: expired, revoked by a password change or admin action, or - with refresh-token rotation - already used once. Rotation is the subtle one: if two tabs or two requests refresh at the same time, the second uses a token that was just replaced, and many providers then revoke the whole chain. Serialise refreshes, then send the user through sign-in again when this happens.

How AuthTrace shows it

Open the AuthTrace panel in DevTools before signing in. It links the authorize request, the callback and the token request in one timeline and checks them against each other: PKCE verifier does not match the challenge (with the expected value), redirect_uri differs between authorize and token, and the provider's error and vendor code explained in plain words. Duplicate token requests sit next to each other in the timeline.

Confidential clients swap the code on the server, so that request never passes through the browser and no browser tool can see it. AuthTrace still checks everything on the front channel - the code challenge, the redirect URI and the callback - which narrows it down to the server side.

More guides