Cybersecurity / Networking

How to Resolve "OAuth2 invalid_grant: Bad Request" Token Error

4 min read by DebuggedIt

Quick answer

During OAuth 2.0 authorization code exchange or refresh token flows, identity providers like Google, Azure AD, Okta, and GitHub return the error response...

During OAuth 2.0 authorization code exchange or refresh token flows, identity providers like Google, Azure AD, Okta, and GitHub return the error response {"error": "invalid_grant", "error_description": "Bad Request"}. This RFC 6749 compliance error indicates that the authorization grant (code, user credentials, or refresh token) is invalid, expired, revoked, or mismatched with client configuration. Identifying specific causes like redirect URI discrepancies or single-use authorization code exhaustion allows developers to fix authentication integrations instantly.

The Problem

When sending requests or configuring components in your development environment, the application or HTTP client fails with the following exact error trace:

# Example 1: JSON Error Response from Token Endpoint
HTTP/1.1 400 Bad Request
Content-Type: application/json;charset=UTF-8

{
  "error": "invalid_grant",
  "error_description": "Malformed auth code or code expired."
}

# Example 2: Google OAuth2 SDK Exception
google.auth.exceptions.RefreshError: ('invalid_grant: Bad Request', {'error': 'invalid_grant', 'error_description': 'Bad Request'})

# Example 3: Axios/Node.js API Request Failure
AxiosError: Request failed with status code 400
data: { error: 'invalid_grant', error_description: 'redirect_uri_mismatch' }

This error halts script execution, prevents API communication, or results in immediate client-side connection drops.

Client Application Reuse Auth Code / Expired Token POST /oauth/v2/token 400 Bad Request: invalid_grant Authorization Server (Google / Azure / Okta)

Why It Happens

This failure occurs due to misconfigurations at the networking, cryptographic, or application protocol layer. The primary causes include:

  • Expired or Single-Use Authorization Code: OAuth 2.0 authorization codes expire quickly (typically within 1 to 10 minutes) and can strictly be exchanged exactly once. Double-invoking the token exchange endpoint invalidates the grant immediately.
  • Redirect URI Mismatch: The redirect_uri parameter sent in the POST body to /oauth/token does not match character-for-character with the redirect_uri used in the initial authorization request URL.
  • Revoked Refresh Token: The refresh token was explicitly revoked by the user, expired due to max lifetime policies, or invalidated because the client app reached max refresh token caps (e.g., 50 active tokens per user account in Google OAuth).
  • System Clock Skew: The server or container clock issuing the token request is out of sync with Network Time Protocol (NTP), causing premature expiration validation failures.

The Fix

Follow these step-by-step solutions to resolve the error in your environment.

Step 1: Ensure Exact Match for redirect_uri Parameter

The redirect_uri string sent during the authorization code exchange must be byte-for-byte identical to the URI passed during the initial login step. Verify query strings, trailing slashes, and protocol schemes:

# Example cURL token exchange payload
curl -X POST https://oauth2.googleapis.com/token   -H "Content-Type: application/x-www-form-urlencoded"   -d "grant_type=authorization_code"   -d "client_id=YOUR_CLIENT_ID.apps.googleusercontent.com"   -d "client_secret=YOUR_CLIENT_SECRET"   -d "code=4/0AVG7fi..."   -d "redirect_uri=https://yourdomain.com/oauth/callback"

Step 2: Prevent Double Authorization Code Consumption

If your frontend application invokes React useEffect hooks twice in Strict Mode, or if parallel server requests handle the callback route, the authorization code will be consumed on attempt #1 and trigger invalid_grant on attempt #2. Guard code exchange logic using state flags or Redis lock keys:

import axios from 'axios';

async function exchangeAuthCode(code) {
  try {
    const response = await axios.post('https://oauth2.googleapis.com/token', new URLSearchParams({
      grant_type: 'authorization_code',
      client_id: process.env.OAUTH_CLIENT_ID,
      client_secret: process.env.OAUTH_CLIENT_SECRET,
      code: code,
      redirect_uri: process.env.OAUTH_REDIRECT_URI
    }), {
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
    });
    return response.data;
  } catch (error) {
    if (error.response && error.response.data.error === 'invalid_grant') {
      console.error('OAuth2 invalid_grant: Authorization code already used or expired.');
    }
    throw error;
  }
}

Step 3: Sync System Time via NTP

Ensure your application server system clock is synchronized. Clock drift greater than a few seconds causes OAuth provider token timestamps to be flagged as invalid:

# Check and sync time on Ubuntu/Debian
sudo systemctl status systemd-timesyncd
sudo timedatectl set-ntp true

Still Not Working?

Refresh Token Rotation Invalidation

When OAuth 2.0 Refresh Token Rotation is enabled (common in Auth0 and Okta), using an outdated refresh token invalidates the entire token chain for security reasons. If multiple worker processes or client instances attempt to refresh tokens concurrently using the same stored refresh token, only the first request succeeds while subsequent requests return invalid_grant: Bad Request. Ensure your database or token storage locks during token refresh operations so only a single process executes the rotation.