{"record":{"id":"8503b80764d24aca","repo":"Freika/dawarich","slug":"nonce-mismatch","errorCode":null,"errorMessage":"nonce mismatch","messagePattern":"nonce mismatch","errorType":"exception","errorClass":"Auth::VerifyGoogleToken::InvalidToken","httpStatus":401,"severity":"error","filePath":"app/services/auth/verify_google_token.rb","lineNumber":55,"sourceCode":"      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\n      return if ActiveSupport::SecurityUtils.secure_compare(claim_nonce, @nonce.to_s)\n\n      raise InvalidToken, 'nonce mismatch'\n    end\n\n    def log_missing_nonce_breadcrumb\n      return unless defined?(Sentry)\n\n      Sentry.capture_message(\n        'google_id_token_missing_nonce',\n        level: :warning,\n        extra: { hint: 'Hard-require nonce after mobile client rollout' }\n      )\n    rescue StandardError\n      nil\n    end\n  end\nend\n","sourceCodeStart":37,"sourceCodeEnd":71,"githubUrl":"https://github.com/Freika/dawarich/blob/97fad417c5a11b0eb11157890635e015723a2e97/app/services/auth/verify_google_token.rb#L37-L71","documentation":"Raised by Auth::VerifyGoogleToken#verify_nonce! when a nonce was passed to the service but the token's 'nonce' claim does not equal it (compared with ActiveSupport::SecurityUtils.secure_compare after to_s). The nonce binds a sign-in session to a single token; a mismatch usually means a replayed, cross-session, or manually constructed token. Note the service logs a Sentry breadcrumb and skips the check entirely when no nonce is supplied — this error only fires when the caller did pass one.","triggerScenarios":"Frontend generates a nonce, includes it in the Google request, but sends a different (or missing) nonce alongside the returned id_token; replaying a captured id_token against a session that expects a fresh nonce; multiple tabs/sign-in attempts reusing tokens across nonces.","commonSituations":"Nonce stored in sessionStorage but read back from localStorage after a redirect, race where the nonce is regenerated between issuing the Google prompt and submitting the token, a mobile webview that drops custom state params, or the caller forwarding params[:nonce] from a stale form submission.","solutions":["Make the client send back the exact nonce it used when initializing Google Identity Services (google.accounts.id.initialize / One Tap with nonce).","On the server, generate the nonce, persist it server-side (session/cache) keyed to the browser, and compare against that stored value rather than a client-supplied one.","Check for double submission or page reload that regenerates the nonce after the token was already minted.","Once mobile rollout completes, drop the log_missing_nonce_breadcrumb escape hatch and hard-require the nonce so silent skips cannot hide the bug."],"exampleFix":"# before\nverify = Auth::VerifyGoogleToken.new(params[:id_token], nonce: params[:nonce])\n\n# after: server-generated nonce, same one handed to the GIS client\nsession[:gis_nonce] = SecureRandom.urlsafe_base64(24)\n# ... browser JS: google.accounts.id.initialize({ nonce: \"<%= session[:gis_nonce] %>\" ... })\nverify = Auth::VerifyGoogleToken.new(params[:id_token], nonce: session.delete(:gis_nonce))","handlingStrategy":"validation","validationCode":"# Server-generated nonce bound to the browser session\nsession[:gis_nonce] ||= SecureRandom.urlsafe_base64(24)\n@nonce = session[:gis_nonce]","typeGuard":null,"tryCatchPattern":"begin\n  Auth::VerifyGoogleToken.new(id_token, nonce: session.delete(:gis_nonce)).call\nrescue Auth::VerifyGoogleToken::InvalidToken => e\n  redirect_to login_path, alert: 'Sign-in could not be verified, try again'\nend","preventionTips":["Generate the nonce server-side, pass it into the GIS client, and compare against the stored value — never trust a client-echoed nonce.","Consume (delete) the nonce on use so a second sign-in cannot reuse it.","After mobile rollout, make nonce mandatory and alert on the missing-nonce Sentry breadcrumb instead of silently passing."],"tags":["authentication","openid-connect","nonce","replay-protection","security","ruby"],"backgroundTag":"openid-connect-nonce-mismatch","analyzedSha":"97fad417c5a11b0eb11157890635e015723a2e97","analyzedAt":"2026-08-21T17:04:17.778Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}