Skip to content

fix(oauth): send the state binding back with the OAuth callback - #203

Merged
Bccorb merged 3 commits into
mainfrom
fix/oauth-state-binding
Oct 21, 2026
Merged

Bccorb merged 3 commits into
mainfrom
fix/oauth-state-binding

Conversation

@Bccorb

@Bccorb Bccorb commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Refs fells-code/seamless-auth-api#463

Client SDK half of binding the OAuth state to the client that started sign-in. Contract: fells-code/seamless-auth-api#465 (API) and fells-code/seamless-auth-types#104 (types), both unreleased.

What changes

  • POST /oauth/:providerId/start may now return binding. The session action startOAuthLogin stores { state, binding } in sessionStorage under seamless:oauth:binding (next to seamless:oauth:provider), with an in-memory copy for environments without sessionStorage (React Native) or with storage that refuses writes. A start response without a binding clears any stale one.
  • The session action finishOAuthLogin attaches the stored binding when the caller passed none and the callback's state matches the stored one, then clears it whether the callback succeeds or fails. An explicit binding wins.
  • createSeamlessAuthClient().finishOAuthLogin sends binding in the JSON body when present. It is never sent to the provider or put in a URL.
  • startOAuthSignIn's in-app browser path passes data.binding explicitly and refuses a callback whose state is not the one it started.
  • Because the handling sits in the session actions, apps that call startOAuthLogin / finishOAuthLogin through useAuth() (the seamless-templates react-oauth template, the admin dashboard) and the Angular, Vue and Svelte bindings get it with only an SDK upgrade.

Types

StartOAuthLoginResult aliases StartOAuthLoginResponse from @seamless-auth/types, which does not have binding yet and cannot be bumped until types#104 is published. It is extended locally (StartOAuthLoginResponse & { binding?: string }), and FinishOAuthLoginInput (already local) gains binding?: string. Once types ships, the local extension can be dropped in favour of the upstream field.

Compatibility and release order

Older auth servers return no binding, so nothing extra is sent and they are unaffected. API 0.19.x with #463 refuses a callback without the binding (400 Invalid OAuth state), so this SDK must reach users before or with that API release.

Release order: types (seamless-auth-types#104), then the API (seamless-auth-api#465), then this. Changesets are patch for @seamless-auth/client, @seamless-auth/react and @seamless-auth/react-native.

Docs

The React, React Native and client READMEs describe the binding for manual integrations, and AGENTS.md notes the local type extension.

Checks

  • npm run typecheck: pass
  • npm run lint: pass
  • npm run format:check: pass
  • npm test: Jest 47 suites, 618 tests pass; Vitest 3 files, 41 tests pass
  • npm run build: pass (including the Angular, Vue and Svelte dist checks)

The auth server can now bind the OAuth state to the client that started
sign-in and refuse a callback without that binding. The session actions
store the binding returned by startOAuthLogin with its state, attach it in
finishOAuthLogin when the state matches, and clear it afterwards.
startOAuthSignIn passes it explicitly on the in-app browser path and
refuses a callback whose state differs from the one it started.

Refs fells-code/seamless-auth-api#463
@Bccorb
Bccorb merged commit f21867d into main Oct 21, 2026
5 checks passed
@Bccorb
Bccorb deleted the fix/oauth-state-binding branch October 21, 2026 02:05
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.

1 participant