OAuth

Fix Google OAuth invalid_grant โ€” Service Accounts and Clock Skew

Updated April 2026

Reading this article? Verify your fix in real-time. Debug your OAuth error โ€” OAuthFixer โ†’

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