How to Fix invalid_grant Error When Getting Google OAuth Token for Contacts API

If you’ve ever tried integrating Google Contacts API into your application, you’ve likely encountered the dreaded invalid_grant error when fetching an OAuth 2.0 token. This error is a common roadblock in Google’s OAuth flow, but it’s rarely straightforward to diagnose. Whether you’re building a contact sync tool, a CRM integration, or a productivity app, resolving invalid_grant is critical to ensuring seamless access to user contacts.

In this guide, we’ll demystify the invalid_grant error, explore its root causes, and provide step-by-step solutions to fix it—specifically tailored for the Google Contacts API. By the end, you’ll have the tools to troubleshoot and prevent this error in your application.

Table of Contents#

  1. What is the invalid_grant Error?
  2. Common Causes of invalid_grant for Google Contacts API
  3. Step-by-Step Troubleshooting Guide
  4. Preventive Measures to Avoid Future Errors
  5. Example Code: Handling OAuth for Google Contacts API
  6. Conclusion
  7. References

What is the invalid_grant Error?#

The invalid_grant error is returned by Google’s OAuth 2.0 authorization server when it rejects the “grant” (e.g., an authorization code or refresh token) used to request an access token. According to Google’s OAuth documentation, this error indicates:

“The authorization grant (e.g., authorization code, resource owner password credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.”

For the Google Contacts API, this error typically occurs during two phases:

  • Authorization Code Exchange: When exchanging an authorization code for an access/refresh token.
  • Refresh Token Usage: When using a refresh token to obtain a new access token.

Common Causes of invalid_grant for Google Contacts API#

Let’s break down the most frequent causes of invalid_grant and how to diagnose/fix them.

1. Expired or Revoked Refresh Tokens#

What happens: Refresh tokens are long-lived, but Google may invalidate them for security reasons:

  • The refresh token hasn’t been used for 6 months.
  • The user changed their Google Account password.
  • The user revoked the app’s access (see Section 4).
  • The app’s client secret was regenerated (invalidates all existing refresh tokens).

Diagnosis:

  • Check error details: Look for messages like Token has been expired or revoked.
  • Log refresh token usage: If the error occurs when using a refresh token, it’s likely expired/revoked.

Fix:

  • Force re-authentication: Prompt the user to re-authorize your app. This generates a new authorization code, which can be exchanged for fresh access/refresh tokens.
  • Handle token expiration gracefully: In code, catch invalid_grant errors when using refresh tokens and trigger a re-auth flow.

2. Incorrect Client Credentials#

What happens: The OAuth flow relies on your app’s client_id and client_secret (from the Google Cloud Console). If these are incorrect, expired, or belong to a different project, Google rejects the grant.

Diagnosis:

  • Verify credentials in the Google Cloud Console:
    1. Go to Google Cloud Console.
    2. Select your project > APIs & Services > Credentials.
    3. Check that the client_id and client_secret match those in your app.

Fix:

  • Regenerate credentials if unsure: If you suspect the secret is compromised or expired, click “Regenerate secret” in the Cloud Console (note: this invalidates existing refresh tokens).
  • Ensure credentials are not hardcoded: Store client_id/client_secret securely (e.g., environment variables) to avoid accidental exposure or corruption.

3. Stale or Reused Authorization Codes#

What happens: Authorization codes are short-lived (10 minutes) and single-use. If you:

  • Reuse a code (e.g., storing it and trying to exchange it multiple times).
  • Wait too long to exchange the code (e.g., >10 minutes after user authorization).

Diagnosis:

  • The error occurs immediately after exchanging the authorization code (not during refresh token usage).
  • Logs show repeated attempts to use the same code.

Fix:

  • Exchange codes immediately: Use the authorization code to fetch tokens as soon as it’s received from Google (don’t store or delay).
  • Avoid code reuse: Never cache or reuse authorization codes. Generate a new code via the OAuth flow if the first exchange fails.

4. User Revoked Access#

What happens: Users can revoke your app’s access to their Google Account via:

  • Google Account > Security > Third-party apps with account access > Select your app > Remove access.

Diagnosis:

  • Check the user’s Google Account settings (ask them to verify).
  • The error occurs even with valid credentials and fresh codes (since the user has blocked access).

Fix:

  • Prompt re-authorization: Ask the user to re-enable access by re-authenticating. The OAuth flow will now show a consent screen again, allowing them to grant access.

5. Clock Skew (Server Time Mismatch)#

What happens: Google’s servers validate token timestamps. If your app’s server clock is off by >5 minutes (e.g., due to manual time settings), tokens may be considered expired.

Diagnosis:

  • Check your server’s time: Run date (Linux/macOS) or w32tm /query /status (Windows) to compare with NTP time.

Fix:

  • Sync with NTP: Enable automatic time synchronization (e.g., via systemd-timesyncd on Linux, or “Set time automatically” on Windows/macOS).
  • Use a time service: For critical systems, use a service like ntpd to keep clocks accurate.

6. Scope Mismatch#

What happens: The scopes requested during user authorization must match those used when exchanging the code for tokens. For the Contacts API, you need the https://www.googleapis.com/auth/contacts (or contacts.readonly) scope.

Diagnosis:

  • Compare scopes in the authorization request and token exchange:
    • Authorization URL: Should include scope=https://www.googleapis.com/auth/contacts.
    • Token exchange request: Scopes are not explicitly sent here, but Google validates that they match the authorization step.

Fix:

  • Include the correct Contacts API scope: Always request the Contacts scope during authorization. Example authorization URL:
    https://accounts.google.com/o/oauth2/v2/auth?  
      client_id=YOUR_CLIENT_ID&  
      redirect_uri=YOUR_REDIRECT_URI&  
      response_type=code&  
      scope=https://www.googleapis.com/auth/contacts&  
      access_type=offline  # Required for refresh tokens  
    

7. Invalid Token Endpoint or Network Issues#

What happens: Using the wrong token endpoint (e.g., https://accounts.google.com/o/oauth2/token instead of the v2 endpoint) or network errors (e.g., timeouts, proxy issues) can corrupt the grant.

Diagnosis:

  • Verify the token endpoint: Use https://oauth2.googleapis.com/token (v2) for all token exchanges (Google’s legacy v1 endpoint is deprecated).
  • Check network logs: Look for 4xx/5xx errors or truncated requests (e.g., due to firewalls blocking the endpoint).

Fix:

  • Use the correct endpoint: Always exchange codes/refresh tokens at https://oauth2.googleapis.com/token.
  • Handle network errors: Retry failed requests with exponential backoff, and log detailed errors (e.g., HTTP status codes, response bodies).

Step-by-Step Troubleshooting Guide#

If you’re stuck, follow this workflow to isolate the issue:

  1. Check Client Credentials:

    • Confirm client_id/client_secret in Google Cloud Console match your app.
    • Regenerate the client secret if unsure.
  2. Verify Authorization Code Handling:

    • Ensure codes are used immediately (no storage/reuse).
    • Check for delays between user authorization and code exchange (<10 minutes).
  3. Test Refresh Token Validity:

    • If using a refresh token, try exchanging it for a new access token via cURL:
      curl -X POST https://oauth2.googleapis.com/token \  
        -d "client_id=YOUR_CLIENT_ID" \  
        -d "client_secret=YOUR_CLIENT_SECRET" \  
        -d "refresh_token=YOUR_REFRESH_TOKEN" \  
        -d "grant_type=refresh_token"  
      If this returns invalid_grant, the refresh token is expired/revoked.
  4. Check Scopes:

    • Ensure the authorization URL includes https://www.googleapis.com/auth/contacts (or .readonly).
  5. Sync Server Clock:

    • Run ntpq -p (Linux) to verify NTP sync. If off, restart ntpd or enable automatic time sync.
  6. Check User Revocation:

  7. Validate Token Endpoint:

    • Use curl to test the token endpoint with hardcoded credentials (temporarily) to rule out code issues.

Preventive Measures to Avoid Future Errors#

  • Use Google’s Client Libraries: Libraries like google-auth (Python) or google-oauth-java-client handle token expiration, clock skew, and endpoint validation automatically.
  • Store Refresh Tokens Securely: Encrypt refresh tokens at rest (e.g., in a database) and never expose them to clients.
  • Monitor Token Usage: Log invalid_grant errors to track patterns (e.g., mass refresh token expiration suggests client secret regeneration).
  • Request access_type=offline: Always include access_type=offline in the authorization URL to get a refresh token (critical for long-lived access).

Example Code: Handling OAuth for Google Contacts API#

Here’s a Python example using google-auth and google-auth-oauthlib to handle the OAuth flow and invalid_grant errors gracefully:

from google.oauth2.credentials import Credentials  
from google_auth_oauthlib.flow import InstalledAppFlow  
from google.auth.transport.requests import Request  
import os  
 
# Define Contacts API scope  
SCOPES = ["https://www.googleapis.com/auth/contacts"]  
 
def get_credentials():  
    creds = None  
    # Load existing credentials from file (if any)  
    if os.path.exists("token.json"):  
        creds = Credentials.from_authorized_user_file("token.json", SCOPES)  
 
    # If no valid credentials, trigger OAuth flow  
    if not creds or not creds.valid:  
        if creds and creds.expired and creds.refresh_token:  
            try:  
                # Attempt to refresh the token  
                creds.refresh(Request())  
            except Exception as e:  
                # Handle invalid_grant (e.g., expired refresh token)  
                print(f"Refresh failed: {e}. Re-authenticating...")  
                os.remove("token.json")  # Delete invalid token file  
                creds = None  # Force re-auth  
 
        if not creds:  
            # Run OAuth flow to get new credentials  
            flow = InstalledAppFlow.from_client_secrets_file(  
                "credentials.json",  # Path to your client secret JSON  
                SCOPES,  
                redirect_uri="urn:ietf:wg:oauth:2.0:oob"  # Use "http://localhost:8080" for web apps  
            )  
            creds = flow.run_local_server(port=0)  
 
        # Save credentials for future use  
        with open("token.json", "w") as token:  
            token.write(creds.to_json())  
 
    return creds  
 
# Usage  
creds = get_credentials()  
print("Successfully obtained credentials!")  

Key features:

  • Automatically refreshes tokens if possible.
  • Catches invalid_grant during refresh and triggers re-authentication.
  • Uses the correct Contacts API scope.

Conclusion#

The invalid_grant error is often a symptom of misconfiguration or misunderstood OAuth behavior. By systematically checking client credentials, authorization codes, refresh token validity, scopes, and server time, you can resolve most cases. Remember to use Google’s client libraries to reduce manual errors, and handle re-authentication gracefully when tokens expire.

With these steps, your app will reliably access the Google Contacts API without getting blocked by invalid_grant.

References#