feat: always pass nonce & always seal state parameter#390
feat: always pass nonce & always seal state parameter#390nicknisi merged 2 commits intoworkos:nicknisi/pkce-support-fixesfrom
Conversation
|
cc @nicknisi tyvm |
|
@cn-stephen Thanks for this. Taking a look now. Two changes I'm considering:
I'm also going to add graceful PKCE degradation: try/catch around What do you think of these proposed changes? |
|
Love it! My somewhat devil's advocate replies:
Thanks for giving this PR a fair go :) |
|
Fair on both points! |
Remove the '/' default for returnPathname in getAuthorizationUrl so
handleAuth({ returnPathname }) isn't silently ignored.
Fix "delete PKCE cookie after failed authentication" test to set the
state param so it actually exercises the auth failure path.
|
I'm going to go ahead and merge this and get #388 ready for a review to get merged tomorrow. Thank you for this! Also, we're getting reports that the PKCE-on-by-default change is breaking some customers with custom middleware proxies that don't propagate The plan is to make PKCE opt-in for now ( |
Co-authored-by: Nick Nisi <nick.nisi@workos.com>
This PR enhances the security and reliability of the authentication flow by ensuring the OAuth state parameter is always encrypted (sealed) and verified upon callback. It also introduces structured runtime validation for authentication state data.
refs:
Key Changes
Sealed State Management: Replaced the previous Base64-encoded state parameter with a sealed (encrypted) object using iron-session. This ensures that sensitive data like returnPathname and customState cannot be tampered with, read by the client or guessed by an attacker.
Mandatory Nonce & CSRF Protection: A unique nonce is now generated and included in the state for every request, regardless of whether PKCE is enabled. This provides a unified "Defense in Depth" approach to CSRF protection, and simplifies code paths.
Runtime Type Safety: Integrated valibot to define a strict StateSchema. This ensures that the data unsealed during the callback is structured correctly before it is used. This is because TypeScript cannot guarantee that
unsealwill actually return an object in the same shape as wassealedat compile time, so we have to do so at runtime to trust our type system.Simplified Callback Logic: Refactored handleAuth to strictly enforce the presence of the code, state, and PKCE cookie. The callback now automatically destroys the PKCE cookie regardless of success or failure to prevent replay attacks and stale sessions.
BREAKING CHANGE
This PR is breaking the legacy state format. Previously, the library supported a mix of raw strings and Base64-encoded JSON objects. Now, it strictly requires an encrypted, sealed string that matches a single Schema.
Why this is OK