Skip to content

Commit 458804e

Browse files
committed
fix(session-policy): resolve governing org by membership only — activeOrganizationId goes stale across transfer/leave
1 parent c17d5a1 commit 458804e

3 files changed

Lines changed: 25 additions & 23 deletions

File tree

apps/sim/lib/auth/auth.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -231,9 +231,7 @@ export const auth = betterAuth({
231231
* revocation latency becomes the policy cache TTL, not 24h.
232232
*/
233233
version: async (session) =>
234-
getSessionCookieCacheVersion(
235-
session as { userId?: string | null; activeOrganizationId?: string | null }
236-
),
234+
getSessionCookieCacheVersion(session as { userId?: string | null }),
237235
},
238236
expiresIn: 30 * 24 * 60 * 60, // 30 days (how long a session can last overall)
239237
updateAge: 24 * 60 * 60, // 24 hours (how often to refresh the expiry)
@@ -677,10 +675,7 @@ export const auth = betterAuth({
677675
organizationId: members[0].organizationId,
678676
})
679677

680-
const expiresAt = await clampExpiryForSession({
681-
...session,
682-
activeOrganizationId: members[0].organizationId,
683-
})
678+
const expiresAt = await clampExpiryForSession(session, members[0].organizationId)
684679

685680
return {
686681
data: {

apps/sim/lib/auth/security-policy.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -135,16 +135,16 @@ export async function bumpSecurityPolicyVersion(organizationId: string): Promise
135135
/**
136136
* Cookie-cache version for a session, consumed by Better Auth's
137137
* `session.cookieCache.version`. Embeds the member org's security-policy
138-
* version so bumps propagate to cached cookies. The org is the session's
139-
* `activeOrganizationId` when present, else the user's membership — so
140-
* sessions created before the user joined the org are still invalidated by a
141-
* version bump. Sessions of non-members use the static default.
138+
* version so bumps propagate to cached cookies. Resolved from the user's
139+
* MEMBERSHIP, never the session's `activeOrganizationId` — that field goes
140+
* stale on join/leave/transfer (it is only written at session creation), and
141+
* a stale org here would let cookies dodge the destination org's version
142+
* bumps for up to the 24h cookie lifetime. Sessions of non-members use the
143+
* static default.
142144
*/
143145
export async function getSessionCookieCacheVersion(session: {
144146
userId?: string | null
145-
activeOrganizationId?: string | null
146147
}): Promise<string> {
147-
const organizationId =
148-
session.activeOrganizationId ?? (await getMemberOrganizationId(session.userId))
148+
const organizationId = await getMemberOrganizationId(session.userId)
149149
return String(await getSecurityPolicyVersion(organizationId))
150150
}

apps/sim/lib/auth/session-policy.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -105,31 +105,38 @@ export function clampSessionExpiry(
105105
*/
106106
interface ClampableSession {
107107
userId?: string | null
108-
activeOrganizationId?: string | null
109108
impersonatedBy?: string | null
110109
createdAt?: Date | string | null
111110
expiresAt?: Date | string | null
112111
}
113112

114113
/**
115114
* Applies the org session policy to a session's proposed `expiresAt` from a
116-
* Better Auth database hook. The governing org is the session's
117-
* `activeOrganizationId` when present, else the user's membership — the same
118-
* resolution the cookie-cache version uses, so every member session
119-
* (including ones created before the user joined) is governed consistently.
120-
* Returns the original date when no clamp applies: impersonation sessions
121-
* are platform-admin tooling with their own short expiry, and non-member
115+
* Better Auth database hook. The governing org is the user's MEMBERSHIP —
116+
* never the session row's `activeOrganizationId`, which goes stale on
117+
* join/leave/transfer — matching the cookie-cache version resolution, so
118+
* every member session (including ones created before the user joined or
119+
* carried across a transfer) is governed consistently. Callers that have
120+
* JUST resolved the membership themselves (the session create hook) pass it
121+
* as `freshMembershipOrgId` to skip the duplicate lookup. Returns the
122+
* original date when no clamp applies: impersonation sessions are
123+
* platform-admin tooling with their own short expiry, and non-member
122124
* sessions have no policy.
123125
*/
124-
export async function clampExpiryForSession(session: ClampableSession): Promise<Date | undefined> {
126+
export async function clampExpiryForSession(
127+
session: ClampableSession,
128+
freshMembershipOrgId?: string | null
129+
): Promise<Date | undefined> {
125130
// Better Auth context values can cross a serialization boundary — normalize
126131
// date fields in case they arrive as ISO strings rather than Dates.
127132
const expiresAt = session.expiresAt ? new Date(session.expiresAt) : undefined
128133
if (!expiresAt || session.impersonatedBy) {
129134
return expiresAt
130135
}
131136
const organizationId =
132-
session.activeOrganizationId ?? (await getMemberOrganizationId(session.userId))
137+
freshMembershipOrgId !== undefined
138+
? freshMembershipOrgId
139+
: await getMemberOrganizationId(session.userId)
133140
if (!organizationId) return expiresAt
134141

135142
const policy = await getSessionPolicy(organizationId)

0 commit comments

Comments
 (0)