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