Skip to content

fix: validate OAuth state before token exchange - #381

Open
ryanduguid wants to merge 4 commits into
XeroAPI:masterfrom
ryanduguid:codex/validate-oauth-state-before-exchange
Open

fix: validate OAuth state before token exchange#381
ryanduguid wants to merge 4 commits into
XeroAPI:masterfrom
ryanduguid:codex/validate-oauth-state-before-exchange

Conversation

@ryanduguid

@ryanduguid ryanduguid commented Aug 6, 2026

Copy link
Copy Markdown

Summary

  • validate a supplied OAuth callback state before the authorisation-code exchange
  • prevent a mismatched callback from making a token request or overwriting the current token set
  • return a static mismatch error that exposes neither the expected nonce nor attacker-supplied state
  • add regression coverage for mismatched, matching and legacy no-state callback paths

This preserves the SDK's existing optional-state compatibility: callers that do not configure state continue to work. It makes the existing configured-state protection effective before the mutating token exchange occurs.

Validation

  • Ruby 3.2 syntax checks for the implementation and focused spec
  • git diff --check

The full RSpec suite could not run locally because this Windows environment lacks MSYS2/native gem build support. The focused spec retains explicit no-token-request and no-token-mutation assertions, and now also proves that neither synthetic state value appears in the error.

@ryanduguid
ryanduguid marked this pull request as draft August 12, 2026 17:49
@ryanduguid
ryanduguid marked this pull request as ready for review August 18, 2026 15:08
…allback

validate_state only rejected a callback when params['state'] differed from
@State. @State is nil unless the consumer passes state: in credentials, so the
pre-exchange check was inert in the default configuration and a callback with no
state parameter passed straight through to token_request and set_token_set.

- Reject a missing or blank callback state whenever a state is configured, and
  raise before the authorization code is exchanged.
- Compare the nonce in constant time over SHA-256 digests instead of String#==,
  which short circuits on length and then memcmps.
- Keep the no-state-configured path permissive to avoid a breaking API change,
  and document the CSRF risk of omitting state in the README.

Specs: the mismatched-state test used the symbol key 'state': so params['state']
was nil and it actually exercised the missing-state branch. Use the string key
and cover matching, mismatched, absent and blank states separately, plus an
end-to-end case asserting no token request or mutation on an absent state.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants