OAuth

Fix Google OAuth invalid_grant — Service Accounts and Clock Skew

Google OAuth for service accounts uses JWTs with expiry timestamps. If your server clock is off by more than 5 minutes from Google's servers, every token request fails with invalid_grant. Fix your system time first.

Google API Error Response
{"error": "invalid_grant", "error_description": "Invalid JWT: Token must be a short-lived token (60 minutes) and in a reasonable timeframe. Check your iat and exp values and use a clock with skew to account for differences in time."}

Fix 1 — Sync your server time

# Check current time offset
date
curl -s --head https://google.com | grep -i date

# Sync immediately (Ubuntu/Debian)
sudo ntpdate -u pool.ntp.org

# Enable automatic sync
sudo timedatectl set-ntp true
timedatectl status  # confirm NTPService: active

Fix 2 — Add clock skew tolerance in your JWT

import time
import jwt

now = int(time.time())
CLOCK_SKEW = 60  # 60 second buffer

payload = {
    'iss': service_account_email,
    'sub': service_account_email,
    'aud': 'https://oauth2.googleapis.com/token',
    'iat': now - CLOCK_SKEW,  # issued slightly in the past
    'exp': now + 3600 - CLOCK_SKEW,  # expire slightly earlier
    'scope': 'https://www.googleapis.com/auth/...'
}

Fix 3 — Rotate service account keys

Old service account keys do not expire but can be revoked. If you are getting invalid_grant on a key that worked before, check if it was revoked:

# Check key status via gcloud
gcloud iam service-accounts keys list \
  --iam-account=your-sa@your-project.iam.gserviceaccount.com

# Create a new key if needed
gcloud iam service-accounts keys create new-key.json \
  --iam-account=your-sa@your-project.iam.gserviceaccount.com

Fix 4 — User consent required again

For user-facing OAuth flows, invalid_grant can mean the user revoked access or Google invalidated the refresh token due to a security event (password change, suspicious activity). Handle it:

def refresh_google_token(refresh_token):
    response = requests.post('https://oauth2.googleapis.com/token', data={
        'grant_type': 'refresh_token',
        'refresh_token': refresh_token,
        'client_id': CLIENT_ID,
        'client_secret': CLIENT_SECRET,
    })

    if response.status_code == 400:
        error = response.json()
        if error.get('error') == 'invalid_grant':
            # Redirect user to re-authorize
            raise TokenExpiredError('Google token revoked — re-authorization required')

    return response.json()
Debug your OAuth error live → OAuthFixer