From 1ba65d27744182879fe632de80b6dc929a7b75f2 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:56:46 -0300 Subject: [PATCH 1/2] Add test cases for expired session --- integration/tests/handshake.test.ts | 102 ++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/integration/tests/handshake.test.ts b/integration/tests/handshake.test.ts index ae9ee00e007..796e9bf6712 100644 --- a/integration/tests/handshake.test.ts +++ b/integration/tests/handshake.test.ts @@ -1263,6 +1263,108 @@ test.describe('Client handshake with organization activation @nextjs', () => { fapiOrganizationIdParamValue: null, }, }, + { + name: 'Expired session, org A active in session, but org B is requested by ID => attempts to activate org B', + when: { + initialAuthState: 'expired', + initialSessionClaims: new Map([['org_id', 'org_a']]), + orgSyncOptions: { + organizationPatterns: ['/organizations-by-id/:id', '/organizations-by-id/:id/(.*)'], + }, + appRequestPath: '/organizations-by-id/org_b', + tokenAppearsIn: 'cookie', + secFetchDestHeader: 'document', + }, + then: { + expectStatus: 307, + fapiOrganizationIdParamValue: 'org_b', + }, + }, + { + name: 'Expired session, no active org in session, but org B is requested by slug => attempts to activate org B', + when: { + initialAuthState: 'expired', + initialSessionClaims: new Map([]), + orgSyncOptions: { + organizationPatterns: [ + '/organizations-by-id/:id', + '/organizations-by-id/:id/(.*)', + '/organizations-by-slug/:slug', + '/organizations-by-slug/:id/(.*)', + ], + }, + appRequestPath: '/organizations-by-slug/bcorp', + tokenAppearsIn: 'cookie', + secFetchDestHeader: 'document', + }, + then: { + expectStatus: 307, + fapiOrganizationIdParamValue: 'bcorp', + }, + }, + { + name: 'Expired session, org A in session, but *an org B* is requested by slug => attempts to activate org B', + when: { + initialAuthState: 'expired', + initialSessionClaims: new Map([['org_id', 'org_a']]), + orgSyncOptions: { + organizationPatterns: [ + '/organizations-by-slug/:slug', + '/organizations-by-slug/:id/(.*)', + '/organizations-by-id/:id', + '/organizations-by-id/:id/(.*)', + ], + }, + appRequestPath: '/organizations-by-slug/bcorp/settings', + tokenAppearsIn: 'cookie', + secFetchDestHeader: 'document', + }, + then: { + expectStatus: 307, + fapiOrganizationIdParamValue: 'bcorp', + }, + }, + { + name: 'Expired session, org A in session, but *the personal account* is requested => attempts to activate personal account', + when: { + initialAuthState: 'expired', + initialSessionClaims: new Map([['org_id', 'org_a']]), + orgSyncOptions: { + organizationPatterns: [ + '/organizations-by-id/:id', + '/organizations-by-id/:id/(.*)', + '/organizations-by-slug/:slug', + '/organizations-by-slug/:id/(.*)', + ], + personalAccountPatterns: ['/personal-account', '/personal-account/(.*)'], + }, + appRequestPath: '/personal-account', + tokenAppearsIn: 'cookie', + secFetchDestHeader: 'document', + }, + then: { + expectStatus: 307, + fapiOrganizationIdParamValue: '', // <-- Empty string indicates personal account + }, + }, + { + name: 'Expired session, org A in session, and org A is requested => still handshakes to refresh session', + when: { + initialAuthState: 'expired', + initialSessionClaims: new Map([['org_id', 'org_a']]), + orgSyncOptions: { + organizationPatterns: ['/organizations-by-id/:id', '/organizations-by-id/:id/(.*)'], + personalAccountPatterns: ['/personal-account', '/personal-account/(.*)'], + }, + appRequestPath: '/organizations-by-id/org_a', + tokenAppearsIn: 'cookie', + secFetchDestHeader: 'document', + }, + then: { + expectStatus: 307, + fapiOrganizationIdParamValue: 'org_a', // Same org, but still handshakes to refresh the expired token + }, + }, { // NOTE(izaak): Would we prefer 500ing in this case? name: 'No config => nothing to activate, return 200', From a03570b4ea3f5f99456f13fce0056fbd10ad724e Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:58:18 -0300 Subject: [PATCH 2/2] Trigger org sync for pending sessions --- packages/backend/src/tokens/request.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/backend/src/tokens/request.ts b/packages/backend/src/tokens/request.ts index 1d2aaaa6d1e..e7f312bb69f 100644 --- a/packages/backend/src/tokens/request.ts +++ b/packages/backend/src/tokens/request.ts @@ -590,13 +590,14 @@ export const authenticateRequest: AuthenticateRequest = (async ( ); } - const authObject = signedInRequestState.toAuth(); - // Org sync if necessary - if (authObject.userId) { - const handshakeRequestState = handleMaybeOrganizationSyncHandshake(authenticateContext, authObject); - if (handshakeRequestState) { - return handshakeRequestState; - } + const authObject = signedInRequestState.toAuth({ + // The organization sync handshake will handle the pending session case, so we don't need to treat it as signed out here + treatPendingAsSignedOut: false, + }); + + const handshakeRequestState = handleMaybeOrganizationSyncHandshake(authenticateContext, authObject); + if (handshakeRequestState) { + return handshakeRequestState; } return signedInRequestState;