Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions integration/tests/handshake.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>([['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<string, string>([]),
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<string, string>([['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<string, string>([['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<string, string>([['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',
Expand Down
15 changes: 8 additions & 7 deletions packages/backend/src/tokens/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading