Fix Google OAuth invalid_grant โ Service Accounts and Clock Skew
Updated April 2026
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