OAuth

Fix OAuth invalid_grant Error — Auth0, Okta, Google, Cognito

invalid_grant is the most common OAuth error and has four different causes. The error message rarely tells you which one. Here is how to diagnose and fix each.

OAuth Error Response
{"error": "invalid_grant", "error_description": "Invalid authorization code"}

Cause 1 — Authorization code expired

Authorization codes expire fast — usually 10 minutes or less. If your exchange request arrives after expiry, you get invalid_grant.

Fix: Ensure your code exchange happens immediately after the redirect. Do not store the code or delay the exchange. If your server is slow, check the time between the redirect and the POST to /token.

Cause 2 — Authorization code reused

Each authorization code can only be used once. If your code exchange runs twice (e.g., due to a React re-render, a duplicated API call, or a retry), the second call gets invalid_grant.

// Prevent double exchange — check if already processing
if (sessionStorage.getItem('exchanging')) return;
sessionStorage.setItem('exchanging', 'true');

const tokens = await exchangeCode(code);
sessionStorage.removeItem('exchanging');

Cause 3 — PKCE verifier mismatch

The code_verifier in your token request must match the code_challenge you sent in the authorization request. Any mismatch causes invalid_grant.

// Generate and STORE the verifier before redirecting
const verifier = generateRandomString(64);
sessionStorage.setItem('pkce_verifier', verifier);

const challenge = await generateChallenge(verifier);
// redirect to auth with code_challenge=challenge

// On callback — retrieve the SAME verifier
const verifier = sessionStorage.getItem('pkce_verifier');
const tokens = await fetch('/token', {
  body: new URLSearchParams({
    code_verifier: verifier, // must match original
    code: authCode,
    ...
  })
});

Cause 4 — Refresh token revoked or rotated

Refresh tokens can be revoked by the user, expired due to inactivity, or invalidated by token rotation (using a rotated refresh token triggers invalid_grant).

Fix: Catch invalid_grant on refresh token calls and redirect to login:

async function refreshTokens(refreshToken) {
  const response = await fetch('/token', {
    method: 'POST',
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
    })
  });

  if (!response.ok) {
    const error = await response.json();
    if (error.error === 'invalid_grant') {
      // Token revoked or expired — force re-login
      clearTokens();
      window.location.href = '/login';
      return;
    }
    throw new Error('Token refresh failed');
  }

  return response.json();
}

Clock skew (service accounts)

For server-to-server OAuth (Google service accounts, JWT assertions), clock skew of more than 5 minutes between your server and the auth server causes invalid_grant. Sync your server time:

sudo ntpdate -u pool.ntp.org
# or
sudo timedatectl set-ntp true
Debug your OAuth error live → OAuthFixer