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_s

View on GitHub (pinned to 97fad417c5)

Solutions

  1. 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.
  2. 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.
  3. Confirm client and server belong to the same Google Cloud project and that the OAuth consent screen / client type matches the flow being used.
  4. 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

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.