@@ -197,6 +197,20 @@ if (validStripeKey) {
197197 } )
198198}
199199
200+ /**
201+ * Error code tagged on the FORBIDDEN thrown by the get-session network-policy
202+ * check, so the `getSessionImpl` wrapper can translate exactly that denial to
203+ * a null session (signed-out) without swallowing unrelated errors.
204+ */
205+ const ORG_IP_RESTRICTED_CODE = 'ORG_IP_RESTRICTED'
206+
207+ function isOrgIpRestrictedError ( error : unknown ) : boolean {
208+ return (
209+ error instanceof APIError &&
210+ ( error . body as { code ?: string } | undefined ) ?. code === ORG_IP_RESTRICTED_CODE
211+ )
212+ }
213+
200214/**
201215 * Reverse-proxy hops trusted for forwarded-IP resolution. When configured,
202216 * Better Auth walks the x-forwarded-for chain right to left, skips these
@@ -1046,10 +1060,29 @@ export const auth = betterAuth({
10461060 oneTimeToken ( {
10471061 expiresIn : 24 * 60 , // 24 hours in minutes (better-auth's expiresIn unit)
10481062 } ) ,
1049- customSession ( async ( { user, session } ) => ( {
1050- user,
1051- session,
1052- } ) ) ,
1063+ customSession ( async ( { user, session } , ctx ) => {
1064+ // Single get-session chokepoint for the org IP allowlist: this handler
1065+ // runs for the server `auth.api.getSession` path AND the browser
1066+ // `client.getSession` HTTP call, so a blocked member is signed out on
1067+ // both. Throwing FORBIDDEN (consistent with the sign-in and
1068+ // access-control denials) yields a 403 for the HTTP client and is
1069+ // translated to a null session by the `getSessionImpl` wrapper for
1070+ // server callers. Impersonation sessions are platform tooling and exempt.
1071+ const impersonatedBy = ( session as { impersonatedBy ?: string | null } ) . impersonatedBy
1072+ if ( user ?. id && ! impersonatedBy ) {
1073+ const network = await enforceOrgNetworkPolicy ( user . id , ( ) =>
1074+ getTrustedClientIp ( new Request ( 'http://localhost/' , { headers : ctx . headers } ) )
1075+ )
1076+ if ( ! network . allowed ) {
1077+ logger . warn ( 'Session denied by org network policy' , { userId : user . id } )
1078+ throw new APIError ( 'FORBIDDEN' , {
1079+ code : ORG_IP_RESTRICTED_CODE ,
1080+ message : network . reason ?? 'Access restricted. Please contact your administrator.' ,
1081+ } )
1082+ }
1083+ }
1084+ return { user, session }
1085+ } ) ,
10531086 emailOTP ( {
10541087 sendVerificationOTP : async ( data ) => {
10551088 if ( ! isEmailVerificationEnabled ) {
@@ -3642,29 +3675,20 @@ async function getSessionImpl() {
36423675 return createAnonymousSession ( )
36433676 }
36443677
3678+ // Org IP-allowlist enforcement lives in the `customSession` get-session
3679+ // handler, which runs for BOTH this server path and the browser
3680+ // `client.getSession` HTTP call. It throws FORBIDDEN with
3681+ // ORG_IP_RESTRICTED_CODE on a blocked member; translate that to a null
3682+ // session here so server callers see a clean signed-out state rather than
3683+ // an exception (the HTTP client receives the 403 and treats it as no
3684+ // session). Any other error propagates unchanged.
36453685 const hdrs = await headers ( )
3646- const session = await auth . api . getSession ( {
3647- headers : hdrs ,
3648- } )
3649-
3650- // Org IP allowlists are enforced at this chokepoint so every route and
3651- // layout that calls getSession directly re-checks the policy, not just
3652- // hybrid-auth routes. A denied member is treated as signed out.
3653- // Impersonation sessions are platform tooling and exempt.
3654- const impersonatedBy = session
3655- ? ( session . session as { impersonatedBy ?: string | null } ) . impersonatedBy
3656- : null
3657- if ( session ?. user ?. id && ! impersonatedBy ) {
3658- const network = await enforceOrgNetworkPolicy ( session . user . id , ( ) =>
3659- getTrustedClientIp ( new Request ( 'http://localhost/' , { headers : hdrs } ) )
3660- )
3661- if ( ! network . allowed ) {
3662- logger . warn ( 'Session denied by org network policy' , { userId : session . user . id } )
3663- return null
3664- }
3686+ try {
3687+ return await auth . api . getSession ( { headers : hdrs } )
3688+ } catch ( error ) {
3689+ if ( isOrgIpRestrictedError ( error ) ) return null
3690+ throw error
36653691 }
3666-
3667- return session
36683692}
36693693
36703694export const getSession = cache ( getSessionImpl )
0 commit comments