Freika/dawarich · error · Auth::VerifyGoogleToken::InvalidToken

nonce mismatch

Error message

nonce mismatch

What it means

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.

Source

Thrown at app/services/auth/verify_google_token.rb:55

      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
      return if ActiveSupport::SecurityUtils.secure_compare(claim_nonce, @nonce.to_s)

      raise InvalidToken, 'nonce mismatch'
    end

    def log_missing_nonce_breadcrumb
      return unless defined?(Sentry)

      Sentry.capture_message(
        'google_id_token_missing_nonce',
        level: :warning,
        extra: { hint: 'Hard-require nonce after mobile client rollout' }
      )
    rescue StandardError
      nil
    end
  end
end

View on GitHub (pinned to 97fad417c5)

Solutions

  1. Make the client send back the exact nonce it used when initializing Google Identity Services (google.accounts.id.initialize / One Tap with nonce).
  2. 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.
  3. Check for double submission or page reload that regenerates the nonce after the token was already minted.
  4. Once mobile rollout completes, drop the log_missing_nonce_breadcrumb escape hatch and hard-require the nonce so silent skips cannot hide the bug.

Example fix

# before
verify = Auth::VerifyGoogleToken.new(params[:id_token], nonce: params[:nonce])

# after: server-generated nonce, same one handed to the GIS client
session[:gis_nonce] = SecureRandom.urlsafe_base64(24)
# ... browser JS: google.accounts.id.initialize({ nonce: "<%= session[:gis_nonce] %>" ... })
verify = Auth::VerifyGoogleToken.new(params[:id_token], nonce: session.delete(:gis_nonce))
Defensive patterns

Strategy: validation

Validate before calling

# Server-generated nonce bound to the browser session
session[:gis_nonce] ||= SecureRandom.urlsafe_base64(24)
@nonce = session[:gis_nonce]

Try / catch

begin
  Auth::VerifyGoogleToken.new(id_token, nonce: session.delete(:gis_nonce)).call
rescue Auth::VerifyGoogleToken::InvalidToken => e
  redirect_to login_path, alert: 'Sign-in could not be verified, try again'
end

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of Freika/dawarich@97fad417c5 (2026-08-21). Data as JSON: /api/errors/8503b80764d24aca. Report an issue: GitHub.