Freika/dawarich · error · Auth::VerifyGoogleToken::InvalidToken
validator returned nil
Error message
validator returned nil
What it means
Raised by Auth::VerifyGoogleToken#call after the loop over configured client IDs fails to produce claims. GoogleIDToken::Validator#check raised AudienceMismatchError for every client ID (kept in audience_error, whose message is preferred), or it returned nil without an audience error. In practice this means the token is valid JWT-wise but was issued for an audience (client_id / 'aud' claim) that matches none of your configured Google client IDs.
Source
Thrown at app/services/auth/verify_google_token.rb:34
ENV['GOOGLE_IOS_CLIENT_ID'],
ENV['GOOGLE_ANDROID_CLIENT_ID'],
ENV['GOOGLE_OAUTH_CLIENT_ID']
].compact
raise InvalidToken, 'Google client IDs not configured' if client_ids.empty?
validator = GoogleIDToken::Validator.new
claims = nil
audience_error = nil
client_ids.each do |client_id|
claims = validator.check(@id_token, client_id)
break if claims
rescue GoogleIDToken::AudienceMismatchError => e
audience_error = e
next
end
raise InvalidToken, audience_error&.message || 'validator returned nil' if claims.nil?
claims = claims.symbolize_keys
verify_nonce!(claims)
claims
rescue GoogleIDToken::ValidationError => e
raise InvalidToken, e.message
end
private
def verify_nonce!(claims)
if @nonce.blank?
log_missing_nonce_breadcrumb
return
end
claim_nonce = claims[:nonce].to_sView on GitHub (pinned to 97fad417c5)
Solutions
- Decode the id_token without verification (e.g. JWT.decode(token, nil, false)) and inspect the 'aud' claim to see which client ID it was actually issued for.
- Add that exact audience as GOOGLE_IOS_CLIENT_ID / GOOGLE_ANDROID_CLIENT_ID / GOOGLE_OAUTH_CLIENT_ID (all three are tried in order) and redeploy.
- Confirm client and server belong to the same Google Cloud project and that the OAuth consent screen / client type matches the flow being used.
- If the token is old, have the client re-run the sign-in flow to mint a fresh id_token before retrying.
Example fix
# before: token aud = 999...apps.googleusercontent.com, env only has 111... # -> AudienceMismatchError for every client_id -> 'validator returned nil' # after: inspect aud, then configure it payload, = JWT.decode(id_token, nil, false) puts payload['aud'] # => '999888777-xyz.apps.googleusercontent.com' # ENV['GOOGLE_OAUTH_CLIENT_ID'] = that exact value
Defensive patterns
Strategy: try-catch
Validate before calling
# Compare the token's aud against configured client IDs before validating payload, = JWT.decode(id_token, nil, false) configured = [ENV['GOOGLE_IOS_CLIENT_ID'], ENV['GOOGLE_ANDROID_CLIENT_ID'], ENV['GOOGLE_OAUTH_CLIENT_ID']].compact raise 'audience not configured' unless configured.include?(payload['aud'])
Try / catch
begin
claims = Auth::VerifyGoogleToken.new(id_token, nonce:).call
rescue Auth::VerifyGoogleToken::InvalidToken => e
Sentry.capture_message('google aud mismatch', extra: { message: e.message })
render json: { error: 'Could not verify Google sign-in' }, status: :unauthorized
end Prevention
- Keep the set of configured client IDs in sync with every platform (web/iOS/Android) actually shipping sign-in.
- Log the token's aud claim (never the token) when validation fails to spot misconfigurations quickly.
- Use one Google Cloud project per environment so dev tokens cannot be sent to prod audiences.
When it happens
Trigger: An id_token minted for a different Google Cloud project or a different OAuth client than any of the three configured env vars; a web token sent when only the iOS client ID is configured (or vice versa); a token from Google One Tap whose audience is the web client ID you never set; a stale/expired token can also fail validation here.
Common situations: Copied the wrong numeric client ID from Google Cloud Console (e.g. the iOS 'reversed client ID' instead of the OAuth client ID), dev token validated against prod credentials, added a new mobile platform but forgot its client ID env var, or the token was refreshed by the client after server config changed.
Related errors
AI-assisted analysis of Freika/dawarich@97fad417c5 (2026-08-21).
Data as JSON: /api/errors/e40e32bfc6565ac6.
Report an issue: GitHub.