Olympus Docs
TroubleshootingOAuth2 issues

OAuth2 invalid_grant

Hydra rejects an authorization code or refresh token exchange

invalid_grant is the OAuth2 error code when Hydra rejects a token exchange. The cause is one of several misconfigurations or expired-token situations.

Specific sub-errors

The error_description distinguishes:

error_descriptionCause
Authorization code has already been usedThe code was already exchanged. Either you're retrying after a successful exchange (don't), or two parallel requests raced and one lost.
Authorization code is expired or invalidThe code's lifetime (~10 minutes) elapsed before exchange. Or the code was never valid (typo, tampered query string).
Refresh token has been revokedThe refresh token was explicitly revoked (via /oauth2/revoke), or the user's identity was deleted, or rotation was broken.
Refresh token has expiredDefault 30-day lifetime exceeded. User must log in again.
Mismatching client_idThe token was issued for client X but you're exchanging it as client Y. Use the same client.
Mismatching redirect_uriThe /oauth2/token request includes a redirect_uri that doesn't match the /oauth2/auth request's redirect_uri. They must match exactly (case-sensitive, trailing slashes included).
Code verifier does not match challengePKCE verification failed, the code_verifier sent at token exchange doesn't hash to the code_challenge sent at authorization.

Fix per case

"Authorization code has already been used"

Check whether your token exchange logic runs twice. Common cause: React StrictMode in development re-renders, double-firing your callback handler.

Fix: dedupe by storing the code in component state and ignoring duplicate fires; or wrap the exchange in a server component that runs once.

"Authorization code is expired or invalid"

If you exchange immediately after callback, this shouldn't happen unless the user took >10 min between auth and callback. Or the code is being base64-decoded incorrectly somewhere.

Verify the code arrives intact:

const code = new URL(window.location.href).searchParams.get('code');
console.log('code length:', code?.length);  // ~64 chars typically

"Refresh token has been revoked"

Either:

  1. Someone revoked the token explicitly.
  2. Refresh-token rotation was broken, you exchanged a refresh token but didn't store the new one returned in the response, so on next refresh you sent the dead old one.

Fix the rotation logic: on every refresh, store the new refresh_token from the response immediately, and use that for the next refresh.

"Mismatching redirect_uri"

Exact-match the redirect URI between the auth request and the token request. Differences that trip people up:

  • https://app.example.com/callback vs https://app.example.com/callback/ (trailing slash)
  • https://app.example.com/callback vs http://app.example.com/callback
  • Different ports.

"Code verifier does not match challenge"

You're sending a different code_verifier than the one you generated.

Common cause: cookie or sessionStorage state was lost between authorization and callback. The verifier needs to persist across the redirect.

Recommended pattern: store the verifier in an HttpOnly cookie set by your backend before redirecting to Hydra, read it back in your callback.

On this page