From 44316731c06e2e222ea008cef70659dab098f51e Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Sun, 26 Oct 2025 16:52:15 -0700 Subject: [PATCH] enforce resource session length --- server/auth/sessions/resource.ts | 3 +- server/db/pg/schema/schema.ts | 3 +- server/db/queries/verifySessionQueries.ts | 10 ++- server/db/sqlite/schema/schema.ts | 3 +- server/lib/checkOrgAccessPolicy.ts | 9 ++- server/private/lib/checkOrgAccessPolicy.ts | 47 +++++++++--- server/private/routers/hybrid.ts | 89 ---------------------- server/routers/badger/verifySession.ts | 38 +++++++-- 8 files changed, 90 insertions(+), 112 deletions(-) diff --git a/server/auth/sessions/resource.ts b/server/auth/sessions/resource.ts index 31ab2b38..9a5b2b5f 100644 --- a/server/auth/sessions/resource.ts +++ b/server/auth/sessions/resource.ts @@ -50,7 +50,8 @@ export async function createResourceSession(opts: { doNotExtend: opts.doNotExtend || false, accessTokenId: opts.accessTokenId || null, isRequestToken: opts.isRequestToken || false, - userSessionId: opts.userSessionId || null + userSessionId: opts.userSessionId || null, + issuedAt: new Date().getTime() }; await db.insert(resourceSessions).values(session); diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index a1aaecf1..f6450d07 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -447,7 +447,8 @@ export const resourceSessions = pgTable("resourceSessions", { { onDelete: "cascade" } - ) + ), + issuedAt: bigint("issuedAt", { mode: "number" }) }); export const resourceWhitelist = pgTable("resourceWhitelist", { diff --git a/server/db/queries/verifySessionQueries.ts b/server/db/queries/verifySessionQueries.ts index 8944a491..85bd7cc7 100644 --- a/server/db/queries/verifySessionQueries.ts +++ b/server/db/queries/verifySessionQueries.ts @@ -1,4 +1,4 @@ -import { db, loginPage, LoginPage, loginPageOrg } from "@server/db"; +import { db, loginPage, LoginPage, loginPageOrg, Org, orgs } from "@server/db"; import { Resource, ResourcePassword, @@ -23,6 +23,7 @@ export type ResourceWithAuth = { pincode: ResourcePincode | null; password: ResourcePassword | null; headerAuth: ResourceHeaderAuth | null; + org: Org; }; export type UserSessionWithUser = { @@ -51,6 +52,10 @@ export async function getResourceByDomain( resourceHeaderAuth, eq(resourceHeaderAuth.resourceId, resources.resourceId) ) + .innerJoin( + orgs, + eq(orgs.orgId, resources.orgId) + ) .where(eq(resources.fullDomain, domain)) .limit(1); @@ -62,7 +67,8 @@ export async function getResourceByDomain( resource: result.resources, pincode: result.resourcePincode, password: result.resourcePassword, - headerAuth: result.resourceHeaderAuth + headerAuth: result.resourceHeaderAuth, + org: result.orgs }; } diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 1ab7d285..40d22bdf 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -587,7 +587,8 @@ export const resourceSessions = sqliteTable("resourceSessions", { { onDelete: "cascade" } - ) + ), + issuedAt: integer("issuedAt") }); export const resourceWhitelist = sqliteTable("resourceWhitelist", { diff --git a/server/lib/checkOrgAccessPolicy.ts b/server/lib/checkOrgAccessPolicy.ts index c74283cb..5ffe4702 100644 --- a/server/lib/checkOrgAccessPolicy.ts +++ b/server/lib/checkOrgAccessPolicy.ts @@ -1,4 +1,4 @@ -import { Org, Session, User } from "@server/db"; +import { Org, ResourceSession, Session, User } from "@server/db"; export type CheckOrgAccessPolicyProps = { orgId?: string; @@ -27,6 +27,13 @@ export type CheckOrgAccessPolicyResult = { }; }; +export async function enforceResourceSessionLength( + resourceSession: ResourceSession, + org: Org +): Promise<{ valid: boolean; error?: string }> { + return { valid: true }; +} + export async function checkOrgAccessPolicy( props: CheckOrgAccessPolicyProps ): Promise<{ diff --git a/server/private/lib/checkOrgAccessPolicy.ts b/server/private/lib/checkOrgAccessPolicy.ts index 5c212ac8..2137cd72 100644 --- a/server/private/lib/checkOrgAccessPolicy.ts +++ b/server/private/lib/checkOrgAccessPolicy.ts @@ -12,7 +12,14 @@ */ import { build } from "@server/build"; -import { db, Org, orgs, sessions, User, users } from "@server/db"; +import { + db, + Org, + orgs, + ResourceSession, + sessions, + users +} from "@server/db"; import { getOrgTierData } from "#private/lib/billing"; import { TierId } from "@server/lib/billing/tiers"; import license from "#private/license/license"; @@ -23,6 +30,35 @@ import { } from "@server/lib/checkOrgAccessPolicy"; import { UserType } from "@server/types/UserTypes"; +export async function enforceResourceSessionLength( + resourceSession: ResourceSession, + org: Org +): Promise<{ valid: boolean; error?: string }> { + if (org.maxSessionLengthHours) { + const sessionIssuedAt = resourceSession.issuedAt; // may be null + const maxSessionLengthHours = org.maxSessionLengthHours; + + if (sessionIssuedAt) { + const maxSessionLengthMs = maxSessionLengthHours * 60 * 60 * 1000; + const sessionAgeMs = Date.now() - sessionIssuedAt; + + if (sessionAgeMs > maxSessionLengthMs) { + return { + valid: false, + error: `Resource session has expired due to organization policy (max session length: ${maxSessionLengthHours} hours)` + }; + } + } else { + return { + valid: false, + error: `Resource session is invalid due to organization policy (max session length: ${maxSessionLengthHours} hours)` + }; + } + } + + return { valid: true }; +} + export async function checkOrgAccessPolicy( props: CheckOrgAccessPolicyProps ): Promise { @@ -43,15 +79,6 @@ export async function checkOrgAccessPolicy( return { allowed: false, error: "Session ID is required" }; } - if (build === "saas") { - const { tier } = await getOrgTierData(orgId); - const subscribed = tier === TierId.STANDARD; - // if not subscribed, don't check the policies - if (!subscribed) { - return { allowed: true }; - } - } - if (build === "enterprise") { const isUnlocked = await license.isUnlocked(); // if not licensed, don't check the policies diff --git a/server/private/routers/hybrid.ts b/server/private/routers/hybrid.ts index cf17cce3..9da80e87 100644 --- a/server/private/routers/hybrid.ts +++ b/server/private/routers/hybrid.ts @@ -73,8 +73,6 @@ import { validateResourceSessionToken } from "@server/auth/sessions/resource"; import { checkExitNodeOrg, resolveExitNodes } from "#private/lib/exitNodes"; import { maxmindLookup } from "@server/db/maxmind"; import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken"; -import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy"; -import { CheckOrgAccessPolicyResult } from "@server/lib/checkOrgAccessPolicy"; // Zod schemas for request validation const getResourceByDomainParamsSchema = z @@ -1585,90 +1583,3 @@ hybridRouter.post( } } ); - -const getOrgAccessPolicyParamsSchema = z - .object({ - orgId: z.string().min(1), - userId: z.string().min(1) - }) - .strict(); - -const getOrgAccessPolicyBodySchema = z - .object({ - sessionId: z.string().min(1) - }) - .strict(); - -hybridRouter.get( - "/org/:orgId/user/:userId/access", - async (req: Request, res: Response, next: NextFunction) => { - try { - const parsedParams = getOrgAccessPolicyParamsSchema.safeParse( - req.params - ); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const parsedBody = getOrgAccessPolicyBodySchema.safeParse(req.body); - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { orgId, userId } = parsedParams.data; - const { sessionId } = parsedBody.data; - const remoteExitNode = req.remoteExitNode; - - if (!remoteExitNode?.exitNodeId) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Remote exit node not found" - ) - ); - } - - if (await checkExitNodeOrg(remoteExitNode.exitNodeId, orgId)) { - // If the exit node is not allowed for the org, return an error - return next( - createHttpError( - HttpCode.FORBIDDEN, - "Exit node not allowed for this organization" - ) - ); - } - - const accessPolicy = await checkOrgAccessPolicy({ - orgId, - userId, - sessionId - }); - - return response(res, { - data: accessPolicy, - success: true, - error: false, - message: "Org access policy retrieved successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "Failed to get org login page" - ) - ); - } - } -); diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index e3a34eb1..758ffa1b 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -16,12 +16,14 @@ import { } from "@server/db/queries/verifySessionQueries"; import { LoginPage, + Org, Resource, ResourceAccessToken, ResourceHeaderAuth, ResourcePassword, ResourcePincode, - ResourceRule + ResourceRule, + resourceSessions } from "@server/db"; import config from "@server/lib/config"; import { isIpInCidr } from "@server/lib/ip"; @@ -37,7 +39,10 @@ import { getCountryCodeForIp } from "@server/lib/geoip"; import { getOrgTierData } from "#dynamic/lib/billing"; import { TierId } from "@server/lib/billing/tiers"; import { verifyPassword } from "@server/auth/password"; -import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy"; +import { + checkOrgAccessPolicy, + enforceResourceSessionLength +} from "#dynamic/lib/checkOrgAccessPolicy"; // We'll see if this speeds anything up const cache = new NodeCache({ @@ -142,6 +147,7 @@ export async function verifyResourceSession( pincode: ResourcePincode | null; password: ResourcePassword | null; headerAuth: ResourceHeaderAuth | null; + org: Org; } | undefined = cache.get(resourceCacheKey); @@ -380,6 +386,22 @@ export async function verifyResourceSession( } if (resourceSession) { + // only run this check if not SSO sesion; SSO session length is checked later + if (!(resourceSessions.userSessionId && sso)) { + const accessPolicy = await enforceResourceSessionLength( + resourceSession, + resourceData.org + ); + + if (!accessPolicy.valid) { + logger.debug( + "Resource session invalid due to org policy:", + accessPolicy.error + ); + return notAllowed(res, redirectPath, resource.orgId); + } + } + if (pincode && resourceSession.pincodeId) { logger.debug( "Resource allowed because pincode session is valid" @@ -422,7 +444,8 @@ export async function verifyResourceSession( if (allowedUserData === undefined) { allowedUserData = await isUserAllowedToAccessResource( resourceSession.userSessionId, - resource + resource, + resourceData.org ); cache.set(userAccessCacheKey, allowedUserData); @@ -564,7 +587,8 @@ function allowed(res: Response, userData?: BasicUserData) { async function isUserAllowedToAccessResource( userSessionId: string, - resource: Resource + resource: Resource, + org: Org ): Promise { const result = await getUserSessionWithUser(userSessionId); @@ -592,9 +616,9 @@ async function isUserAllowedToAccessResource( } const accessPolicy = await checkOrgAccessPolicy({ - orgId: resource.orgId, - userId: user.userId, - sessionId: session.sessionId + org, + user, + session }); if (!accessPolicy.allowed || accessPolicy.error) { logger.debug(`User not allowed by org access policy because`, {