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