Repository navigation
fix(account): restore server-started OAuth2 token sign-in, with guest fallback and optional state - #14157
Conversation
The token flow hands userId and secret to the app, whose server often starts the flow (the SSR pattern in the docs). The nonce cookie then lands on that server instead of the browser, so every server-started sign-in failed with user_oauth2_state_invalid since #14012. The session flow keeps the check: Appwrite sets that session itself, and the flow is browser-only.
🟢 Tier S · Ready to merge
Restores server-started OAuth2 token sign-in by treating callbacks without a matching nonce cookie as guests, avoiding identity linking to the browser’s signed-in account. Both OAuth2 initiation endpoints accept optional application state and return it on success and failure redirects. E2E tests cover token exchange, identity isolation, state round-tripping, and validation. Latest changes: The newest commits rename the application-state payload key from
📂 Walkthrough · 2
Reviewed the commits since |
There was a problem hiding this comment.
⛔ Tier F · 1 blocking finding to address. Summary
✨ Benchmark resultsComparing
Per-scenario breakdown & investigation detailsMetrics below reflect the current branch (after). Δ P95 compares against the base.
Top API waits (after)
|
Without the nonce cookie, the browser's signed-in user is no proof that it started the flow. Linking the provider identity to that user would let anyone attach their own identity to a signed-in account by sending the callback link, so an unbound token callback now resolves the user from the provider identity only.
|
@hansi-codes review |
There was a problem hiding this comment.
🔴 Tier D · 1 blocking finding to address. Summary
One flow covers both properties: a browser signed in to another account completes a server-started token flow without the state cookie, gets a token for the provider identity's account that exchanges for a session, and gains no identity.
|
@hansi-codes review |
There was a problem hiding this comment.
⛔ Tier F · 1 blocking finding to address. Summary
…rn the app's state Without the state cookie, the browser's signed-in user is no proof that it started the flow, so an unbound token callback resolves the user from the provider identity only and never links to that user. createOAuth2Token and createOAuth2Session take an optional state, returned unchanged on the success and failure URLs, so the app can bind the token it receives to the sign-in it started (RFC 6749 section 4.1.2).
…imit Text counts characters, but json_encode escapes non-ASCII as \uXXXX, so 256 emoji grew past the callback's Text(2048) and failed before any redirect. state now takes printable ASCII only (RFC 6749 Appendix A.5), which JSON escaping at most doubles, and the callbacks allow 512 more for it.
The public parameter is state, so the provider state carries it under the same name rather than appState.

Problem
Since #14012, OAuth2 sign-in started from an app's server (the SSR pattern in our docs) always fails with
user_oauth2_state_invalid. The server SDK sends the start request, so the state cookie lands on the server, not in the browser. Reproduced on Cloud production.Fix
stateoncreateOAuth2Token/createOAuth2Session(printable ASCII per RFC 6749 Appendix A.5, max 256 chars), returned unchanged on the success and failure URLs (RFC 6749 §4.1.2). The app compares it with its own cookie, so a forged callback is rejected by the app:Behaviour
Tests
testCreateOAuth2TokenStartedByServer: server-started flow, completed by a browser signed in to another account, without the cookie. Gets the samestateback, a token for the provider's own account that exchanges for a session, and links nothing. Fails onmainand on the previous branch head.testCreateOAuth2TokenReturnsStateOnFailure: provider error; the longeststate(256 chars that JSON escaping doubles) comes back on the failure URL, and a non-ASCIIstateis rejected at the start.OAuth2|Oidctests pass (11 tests, 260 assertions). Pint and PHPStan clean.Part of #14156.