{"record":{"id":"e40e32bfc6565ac6","repo":"Freika/dawarich","slug":"validator-returned-nil","errorCode":null,"errorMessage":"validator returned nil","messagePattern":"validator returned nil","errorType":"exception","errorClass":"Auth::VerifyGoogleToken::InvalidToken","httpStatus":401,"severity":"error","filePath":"app/services/auth/verify_google_token.rb","lineNumber":34,"sourceCode":"        ENV['GOOGLE_IOS_CLIENT_ID'],\n        ENV['GOOGLE_ANDROID_CLIENT_ID'],\n        ENV['GOOGLE_OAUTH_CLIENT_ID']\n      ].compact\n      raise InvalidToken, 'Google client IDs not configured' if client_ids.empty?\n\n      validator = GoogleIDToken::Validator.new\n      claims = nil\n      audience_error = nil\n\n      client_ids.each do |client_id|\n        claims = validator.check(@id_token, client_id)\n        break if claims\n      rescue GoogleIDToken::AudienceMismatchError => e\n        audience_error = e\n        next\n      end\n\n      raise InvalidToken, audience_error&.message || 'validator returned nil' if claims.nil?\n\n      claims = claims.symbolize_keys\n      verify_nonce!(claims)\n\n      claims\n    rescue GoogleIDToken::ValidationError => e\n      raise InvalidToken, e.message\n    end\n\n    private\n\n    def verify_nonce!(claims)\n      if @nonce.blank?\n        log_missing_nonce_breadcrumb\n        return\n      end\n\n      claim_nonce = claims[:nonce].to_s","sourceCodeStart":16,"sourceCodeEnd":52,"githubUrl":"https://github.com/Freika/dawarich/blob/97fad417c5a11b0eb11157890635e015723a2e97/app/services/auth/verify_google_token.rb#L16-L52","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before: token aud = 999...apps.googleusercontent.com, env only has 111...\n# -> AudienceMismatchError for every client_id -> 'validator returned nil'\n\n# after: inspect aud, then configure it\npayload, = JWT.decode(id_token, nil, false)\nputs payload['aud'] # => '999888777-xyz.apps.googleusercontent.com'\n# ENV['GOOGLE_OAUTH_CLIENT_ID'] = that exact value","handlingStrategy":"try-catch","validationCode":"# Compare the token's aud against configured client IDs before validating\npayload, = JWT.decode(id_token, nil, false)\nconfigured = [ENV['GOOGLE_IOS_CLIENT_ID'], ENV['GOOGLE_ANDROID_CLIENT_ID'], ENV['GOOGLE_OAUTH_CLIENT_ID']].compact\nraise 'audience not configured' unless configured.include?(payload['aud'])","typeGuard":null,"tryCatchPattern":"begin\n  claims = Auth::VerifyGoogleToken.new(id_token, nonce:).call\nrescue Auth::VerifyGoogleToken::InvalidToken => e\n  Sentry.capture_message('google aud mismatch', extra: { message: e.message })\n  render json: { error: 'Could not verify Google sign-in' }, status: :unauthorized\nend","preventionTips":["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."],"tags":["authentication","google-sign-in","audience-mismatch","oauth","ruby"],"backgroundTag":"oauth-audience-mismatch","analyzedSha":"97fad417c5a11b0eb11157890635e015723a2e97","analyzedAt":"2026-08-21T17:04:17.778Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}