fix(auth): scope AuthKit sessions to an organization - #31
Merged
Conversation
Every grant except mfa-totp, organization-selection, and a refresh carrying an explicit organization_id could only mint an unscoped session, and the authorization_code path read its org off a code minted with organization_id: null. Tokens therefore never carried org_id, role, or permissions, so any API authorizing on the org claim rejected every token the emulator issued — it could not stand in for WorkOS when testing an org-scoped API. Nor was there a way around it: organization_selection_required was never emitted, so the organization-selection grant was unreachable except after an MFA challenge. Multi-organization users now receive a 403 selection error where they previously received a 200 with a null organization, matching production. The organization-selection grant also now verifies the user is an active member of the organization it is handed, which it never did while unreachable.
The spec puts invitation_token on the authorization_code, password and magic-auth grants, and it is the only way a caller can name an organization on a grant that takes no organization_id — accepting an invitation is what joins the user and scopes the session. The emulator ignored the field entirely, so an invited user authenticated into an unscoped session and the invitation stayed pending forever, which also left a multi-organization user stuck at a selection prompt they should never have seen. The token is validated before the grant runs so a bad invitation cannot burn the one-time code the same request carries, and it is carried across an MFA challenge rather than accepted early, so nothing is consumed until the second factor has proven who is signing in. Acceptance is now shared with the invitations REST route, which previously duplicated a membership when one already existed and could not readmit a deactivated member. While reading the same error enum: selecting an organization the user does not belong to reported invalid_organization_id, but the spec has organization_membership_not_found for exactly that case.
Both behaviors are ones a reader hits at runtime with nothing in the README to check against. Organization resolution now has three distinct outcomes, one of which is a 403 a client must handle. Refresh rotation is deliberately stricter than production, which documents rotation as optional — worth stating so it reads as a choice rather than a bug when a replayed token stops working.
Greptile SummaryThe PR scopes fresh AuthKit sessions to organizations and completes the invitation-continuation fixes.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains; current code revalidates deferred invitations before consuming continuation state and postpones invitation acceptance until organization selection has completed. Important Files Changed
|
Both continuation responses consumed state a client might still need. The MFA grant deleted the challenge and pending token before revalidating a deferred invitation, so an invitation revoked mid-challenge turned a retry into invalid_pending_authentication_token instead of reporting the actual cause. And an invitation naming no organization was accepted before the route worked out that organization selection was still required, spending a one-time invitation on a 403 the client may never return from — and never accepting it if the client did return, since the selection grant carried no invitation. Acceptance now happens once every continuation check has passed and the request is known to be issuing a session, still ahead of the role lookup that reads the membership it creates. Revalidation moved ahead of the state it invalidates in both grants, and the selection pending token carries any unspent invitation the way the MFA one already did. This is the rule the rest of the PR already followed for one-time codes, applied to the two paths that were missing it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
mfa-totp,organization-selection, and a refresh carrying an explicitorganization_idcould only mint an unscoped session. Tokens therefore never carriedorg_id,role, orpermissions, so any API authorizing on the org claim rejected every token the emulator issued. A single active membership is now selected implicitly; several returnorganization_selection_required(403) as production does; none leavesorganization_idnull rather than inventing one.organization_selection_requiredwas never emitted, so the grant was unreachable except after an MFA challenge; it also never checked that the user was an active member of the organization it was handed. It does now, reportingorganization_membership_not_found(the spec's code for that case, previously mis-reported asinvalid_organization_id), and it checks before consuming the pending token so a wrong pick can be retried.invitation_tokenis honored onauthorization_code,password, and Magic Auth. It is the only way a caller can name an organization on a grant that takes noorganization_id. The emulator ignored the field entirely, so an invited user landed in an unscoped session, the invitation stayed pending forever, and a multi-organization user hit a selection prompt they should never have seen. The token is validated before the grant runs so a bad invitation can't burn the one-time code in the same request, and it's carried across an MFA challenge rather than accepted early, so nothing is consumed until the second factor proves who is signing in.rolesalongsiderole. Production emits both and the WorkOS SDKs read the plural; a token with onlyrolepassed locally and lost the claim in production.Breaking change
Multi-organization users now receive a
403 organization_selection_requiredwhere they previously received a200with a null organization. This matches production, but any test fixture relying on the old unscoped response will need to complete the selection step.