From d7c4bc43a4798d22078577324cd3824aa3d5cea0 Mon Sep 17 00:00:00 2001 From: Milo Schwartz Date: Sat, 23 Nov 2024 23:31:22 -0500 Subject: [PATCH] set resource session cookie in proxy via param --- config.example.yml | 9 +++- server/auth/index.ts | 4 +- server/auth/resource.ts | 34 ++++++------ server/config.ts | 16 ++++-- server/routers/auth/login.ts | 3 ++ server/routers/badger/verifySession.ts | 2 +- server/routers/resource/authWithPassword.ts | 54 ++++++++++--------- server/routers/resource/authWithPincode.ts | 23 ++++---- server/routers/traefik/getTraefikConfig.ts | 19 ++++--- src/api/cookies.ts | 9 ++-- .../components/ResourceAuthPortal.tsx | 43 ++++++++++++--- src/app/auth/resource/[resourceId]/page.tsx | 8 ++- 12 files changed, 143 insertions(+), 81 deletions(-) diff --git a/config.example.yml b/config.example.yml index 16fd5031..daf86165 100644 --- a/config.example.yml +++ b/config.example.yml @@ -2,6 +2,7 @@ app: base_url: http://localhost:3000 log_level: warning save_logs: false + sessionCookieName: session server: external_port: 3000 @@ -14,12 +15,16 @@ traefik: http_entrypoint: web https_entrypoint: websecure +badger: + session_query_parameter: __pang_sess + resource_session_cookie_name: resource_session + gerbil: start_port: 51820 base_endpoint: localhost block_size: 16 - subnet_group: 10.0.0.0/8 + subnet_group: 10.0.0.0/8 rate_limit: window_minutes: 1 - max_requests: 100 \ No newline at end of file + max_requests: 100 diff --git a/server/auth/index.ts b/server/auth/index.ts index 7692f9f5..54ba89a2 100644 --- a/server/auth/index.ts +++ b/server/auth/index.ts @@ -13,7 +13,7 @@ import config from "@server/config"; import type { RandomReader } from "@oslojs/crypto/random"; import { generateRandomString } from "@oslojs/crypto/random"; -export const SESSION_COOKIE_NAME = "session"; +export const SESSION_COOKIE_NAME = config.server.session_cookie_name; export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30; export const SECURE_COOKIES = config.server.secure_cookies; export const COOKIE_DOMAIN = @@ -63,7 +63,7 @@ export async function validateSessionToken( .where(eq(sessions.sessionId, session.sessionId)); return { session: null, user: null }; } - if (Date.now() >= session.expiresAt - (SESSION_COOKIE_EXPIRES / 2)) { + if (Date.now() >= session.expiresAt - SESSION_COOKIE_EXPIRES / 2) { session.expiresAt = new Date( Date.now() + SESSION_COOKIE_EXPIRES, ).getTime(); diff --git a/server/auth/resource.ts b/server/auth/resource.ts index 66bfb7b3..f9697a70 100644 --- a/server/auth/resource.ts +++ b/server/auth/resource.ts @@ -15,12 +15,12 @@ export async function createResourceSession(opts: { }): Promise { if (!opts.passwordId && !opts.pincodeId) { throw new Error( - "At least one of passwordId or pincodeId must be provided" + "At least one of passwordId or pincodeId must be provided", ); } const sessionId = encodeHexLowerCase( - sha256(new TextEncoder().encode(opts.token)) + sha256(new TextEncoder().encode(opts.token)), ); const session: ResourceSession = { @@ -38,10 +38,10 @@ export async function createResourceSession(opts: { export async function validateResourceSessionToken( token: string, - resourceId: number + resourceId: number, ): Promise { const sessionId = encodeHexLowerCase( - sha256(new TextEncoder().encode(token)) + sha256(new TextEncoder().encode(token)), ); const result = await db .select() @@ -49,8 +49,8 @@ export async function validateResourceSessionToken( .where( and( eq(resourceSessions.sessionId, sessionId), - eq(resourceSessions.resourceId, resourceId) - ) + eq(resourceSessions.resourceId, resourceId), + ), ); if (result.length < 1) { @@ -61,7 +61,7 @@ export async function validateResourceSessionToken( if (Date.now() >= resourceSession.expiresAt - SESSION_COOKIE_EXPIRES / 2) { resourceSession.expiresAt = new Date( - Date.now() + SESSION_COOKIE_EXPIRES + Date.now() + SESSION_COOKIE_EXPIRES, ).getTime(); await db .update(resourceSessions) @@ -75,7 +75,7 @@ export async function validateResourceSessionToken( } export async function invalidateResourceSession( - sessionId: string + sessionId: string, ): Promise { await db .delete(resourceSessions) @@ -87,7 +87,7 @@ export async function invalidateAllSessions( method?: { passwordId?: number; pincodeId?: number; - } + }, ): Promise { if (method?.passwordId) { await db @@ -95,8 +95,8 @@ export async function invalidateAllSessions( .where( and( eq(resourceSessions.resourceId, resourceId), - eq(resourceSessions.passwordId, method.passwordId) - ) + eq(resourceSessions.passwordId, method.passwordId), + ), ); } else if (method?.pincodeId) { await db @@ -104,8 +104,8 @@ export async function invalidateAllSessions( .where( and( eq(resourceSessions.resourceId, resourceId), - eq(resourceSessions.pincodeId, method.pincodeId) - ) + eq(resourceSessions.pincodeId, method.pincodeId), + ), ); } else { await db @@ -117,18 +117,18 @@ export async function invalidateAllSessions( export function serializeResourceSessionCookie( token: string, fqdn: string, - secure: boolean + secure: boolean, ): string { if (secure) { - return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${fqdn}`; + return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=.localhost`; } else { - return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=${fqdn}`; + return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=.localhost`; } } export function createBlankResourceSessionTokenCookie( fqdn: string, - secure: boolean + secure: boolean, ): string { if (secure) { return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Secure; Domain=${fqdn}`; diff --git a/server/config.ts b/server/config.ts index 665f3882..e0807988 100644 --- a/server/config.ts +++ b/server/config.ts @@ -24,6 +24,11 @@ const environmentSchema = z.object({ internal_hostname: z.string(), secure_cookies: z.boolean(), signup_secret: z.string().optional(), + session_cookie_name: z.string(), + }), + badger: z.object({ + session_query_parameter: z.string(), + resource_session_cookie_name: z.string(), }), traefik: z.object({ http_entrypoint: z.string(), @@ -68,7 +73,7 @@ const loadConfig = (configPath: string) => { } catch (error) { if (error instanceof Error) { throw new Error( - `Error loading configuration file: ${error.message}` + `Error loading configuration file: ${error.message}`, ); } throw error; @@ -90,21 +95,21 @@ if (!environment) { try { const exampleConfigContent = fs.readFileSync( exampleConfigPath, - "utf8" + "utf8", ); fs.writeFileSync(configFilePath1, exampleConfigContent, "utf8"); environment = loadConfig(configFilePath1); } catch (error) { if (error instanceof Error) { throw new Error( - `Error creating configuration file from example: ${error.message}` + `Error creating configuration file from example: ${error.message}`, ); } throw error; } } else { throw new Error( - "No configuration file found and no example configuration available" + "No configuration file found and no example configuration available", ); } } @@ -126,5 +131,8 @@ process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig.data.flags ?.require_email_verification ? "true" : "false"; +process.env.SESSION_COOKIE_NAME = parsedConfig.data.server.session_cookie_name; +process.env.RESOURCE_SESSION_QUERY_PARAM_NAME = + parsedConfig.data.badger.session_query_parameter; export default parsedConfig.data; diff --git a/server/routers/auth/login.ts b/server/routers/auth/login.ts index b1793b18..9ffe651c 100644 --- a/server/routers/auth/login.ts +++ b/server/routers/auth/login.ts @@ -16,6 +16,7 @@ import { z } from "zod"; import { fromError } from "zod-validation-error"; import { verifyTotpCode } from "@server/auth/2fa"; import config from "@server/config"; +import logger from "@server/logger"; export const loginBodySchema = z.object({ email: z.string().email(), @@ -125,6 +126,8 @@ export async function login( await createSession(token, existingUser.userId); const cookie = serializeSessionCookie(token); + logger.debug(cookie); + res.appendHeader("Set-Cookie", cookie); if ( diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index f3826607..e538023d 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -93,7 +93,7 @@ export async function verifyResourceSession( return allowed(res); } - const redirectUrl = `${config.app.base_url}/${resource.orgId}/auth/resource/${resource.resourceId}?r=${originalRequestURL}`; + const redirectUrl = `${config.app.base_url}/auth/resource/${resource.resourceId}?redirect=${originalRequestURL}`; if (sso && sessions.session) { const { session, user } = await validateSessionToken( diff --git a/server/routers/resource/authWithPassword.ts b/server/routers/resource/authWithPassword.ts index ed9a04a0..d803f279 100644 --- a/server/routers/resource/authWithPassword.ts +++ b/server/routers/resource/authWithPassword.ts @@ -27,12 +27,13 @@ export const authWithPasswordParamsSchema = z.object({ export type AuthWithPasswordResponse = { codeRequested?: boolean; + session?: string; }; export async function authWithPassword( req: Request, res: Response, - next: NextFunction + next: NextFunction, ): Promise { const parsedBody = authWithPasswordBodySchema.safeParse(req.body); @@ -40,8 +41,8 @@ export async function authWithPassword( return next( createHttpError( HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) + fromError(parsedBody.error).toString(), + ), ); } @@ -51,8 +52,8 @@ export async function authWithPassword( return next( createHttpError( HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) + fromError(parsedParams.error).toString(), + ), ); } @@ -65,7 +66,7 @@ export async function authWithPassword( .from(resources) .leftJoin( resourcePassword, - eq(resourcePassword.resourceId, resources.resourceId) + eq(resourcePassword.resourceId, resources.resourceId), ) .where(eq(resources.resourceId, resourceId)) .limit(1); @@ -75,7 +76,10 @@ export async function authWithPassword( if (!resource) { return next( - createHttpError(HttpCode.BAD_REQUEST, "Resource does not exist") + createHttpError( + HttpCode.BAD_REQUEST, + "Resource does not exist", + ), ); } @@ -85,9 +89,9 @@ export async function authWithPassword( HttpCode.UNAUTHORIZED, createHttpError( HttpCode.BAD_REQUEST, - "Resource has no password protection" - ) - ) + "Resource has no password protection", + ), + ), ); } @@ -99,11 +103,11 @@ export async function authWithPassword( timeCost: 2, outputLen: 32, parallelism: 1, - } + }, ); if (!validPassword) { return next( - createHttpError(HttpCode.UNAUTHORIZED, "Incorrect password") + createHttpError(HttpCode.UNAUTHORIZED, "Incorrect password"), ); } @@ -127,18 +131,20 @@ export async function authWithPassword( token, passwordId: definedPassword.passwordId, }); - const secureCookie = resource.ssl; - const cookie = serializeResourceSessionCookie( - token, - resource.fullDomain, - secureCookie - ); - res.appendHeader("Set-Cookie", cookie); + // const secureCookie = resource.ssl; + // const cookie = serializeResourceSessionCookie( + // token, + // resource.fullDomain, + // secureCookie, + // ); + // res.appendHeader("Set-Cookie", cookie); - logger.debug(cookie); // remove after testing + // logger.debug(cookie); // remove after testing - return response(res, { - data: null, + return response(res, { + data: { + session: token, + }, success: true, error: false, message: "Authenticated with resource successfully", @@ -148,8 +154,8 @@ export async function authWithPassword( return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, - "Failed to authenticate with resource" - ) + "Failed to authenticate with resource", + ), ); } } diff --git a/server/routers/resource/authWithPincode.ts b/server/routers/resource/authWithPincode.ts index 1fb0538f..8c618f8a 100644 --- a/server/routers/resource/authWithPincode.ts +++ b/server/routers/resource/authWithPincode.ts @@ -27,6 +27,7 @@ export const authWithPincodeParamsSchema = z.object({ export type AuthWithPincodeResponse = { codeRequested?: boolean; + session?: string; }; export async function authWithPincode( @@ -126,18 +127,20 @@ export async function authWithPincode( token, pincodeId: definedPincode.pincodeId, }); - const secureCookie = resource.ssl; - const cookie = serializeResourceSessionCookie( - token, - resource.fullDomain, - secureCookie, - ); - res.appendHeader("Set-Cookie", cookie); + // const secureCookie = resource.ssl; + // const cookie = serializeResourceSessionCookie( + // token, + // resource.fullDomain, + // secureCookie, + // ); + // res.appendHeader("Set-Cookie", cookie); - logger.debug(cookie); // remove after testing + // logger.debug(cookie); // remove after testing - return response(res, { - data: null, + return response(res, { + data: { + session: token, + }, success: true, error: false, message: "Authenticated with resource successfully", diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index e9747278..1051659a 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -8,7 +8,7 @@ import config from "@server/config"; export async function traefikConfigProvider( _: Request, - res: Response + res: Response, ): Promise { try { const all = await db @@ -16,18 +16,18 @@ export async function traefikConfigProvider( .from(schema.targets) .innerJoin( schema.resources, - eq(schema.targets.resourceId, schema.resources.resourceId) + eq(schema.targets.resourceId, schema.resources.resourceId), ) .innerJoin( schema.orgs, - eq(schema.resources.orgId, schema.orgs.orgId) + eq(schema.resources.orgId, schema.orgs.orgId), ) .where( and( eq(schema.targets.enabled, true), isNotNull(schema.resources.subdomain), - isNotNull(schema.orgs.domain) - ) + isNotNull(schema.orgs.domain), + ), ); if (!all.length) { @@ -48,9 +48,14 @@ export async function traefikConfigProvider( [badgerMiddlewareName]: { apiBaseUrl: new URL( "/api/v1", - `http://${config.server.internal_hostname}:${config.server.internal_port}` + `http://${config.server.internal_hostname}:${config.server.internal_port}`, ).href, - appBaseUrl: config.app.base_url, + resourceSessionCookieName: + config.badger.resource_session_cookie_name, + userSessionCookieName: + config.server.session_cookie_name, + sessionQueryParameter: + config.badger.session_query_parameter, }, }, }, diff --git a/src/api/cookies.ts b/src/api/cookies.ts index 9b1eb9aa..bfc03ded 100644 --- a/src/api/cookies.ts +++ b/src/api/cookies.ts @@ -2,10 +2,11 @@ import { cookies } from "next/headers"; export async function authCookieHeader() { const allCookies = await cookies(); - const sessionId = allCookies.get("session")?.value ?? null; + const cookieName = process.env.SESSION_COOKIE_NAME!; + const sessionId = allCookies.get(cookieName)?.value ?? null; return { headers: { - Cookie: `session=${sessionId}` - } - } + Cookie: `${cookieName}=${sessionId}`, + }, + }; } diff --git a/src/app/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx b/src/app/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx index ba146587..87226e24 100644 --- a/src/app/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx +++ b/src/app/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx @@ -37,6 +37,7 @@ import { AxiosResponse } from "axios"; import { LoginResponse } from "@server/routers/auth"; import ResourceAccessDenied from "./ResourceAccessDenied"; import LoginForm from "@app/components/LoginForm"; +import { AuthWithPasswordResponse } from "@server/routers/resource"; const pinSchema = z.object({ pin: z @@ -62,6 +63,7 @@ type ResourceAuthPortalProps = { id: number; }; redirect: string; + queryParamName: string; }; export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { @@ -112,13 +114,29 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { }, }); + function constructRedirect(redirect: string, token: string): string { + const redirectUrl = new URL(redirect); + redirectUrl.searchParams.delete(props.queryParamName); + redirectUrl.searchParams.append(props.queryParamName, token); + return redirectUrl.toString(); + } + const onPinSubmit = (values: z.infer) => { setLoadingLogin(true); - api.post(`/resource/${props.resource.id}/auth/pincode`, { - pincode: values.pin, - }) + api.post>( + `/resource/${props.resource.id}/auth/pincode`, + { + pincode: values.pin, + }, + ) .then((res) => { - window.location.href = props.redirect; + const session = res.data.data.session; + if (session) { + window.location.href = constructRedirect( + props.redirect, + session, + ); + } }) .catch((e) => { console.error(e); @@ -131,11 +149,20 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { const onPasswordSubmit = (values: z.infer) => { setLoadingLogin(true); - api.post(`/resource/${props.resource.id}/auth/password`, { - password: values.password, - }) + api.post>( + `/resource/${props.resource.id}/auth/password`, + { + password: values.password, + }, + ) .then((res) => { - window.location.href = props.redirect; + const session = res.data.data.session; + if (session) { + window.location.href = constructRedirect( + props.redirect, + session, + ); + } }) .catch((e) => { console.error(e); diff --git a/src/app/auth/resource/[resourceId]/page.tsx b/src/app/auth/resource/[resourceId]/page.tsx index ffb1ba39..32b7d067 100644 --- a/src/app/auth/resource/[resourceId]/page.tsx +++ b/src/app/auth/resource/[resourceId]/page.tsx @@ -14,7 +14,7 @@ import ResourceAccessDenied from "./components/ResourceAccessDenied"; export default async function ResourceAuthPage(props: { params: Promise<{ resourceId: number }>; - searchParams: Promise<{ r: string }>; + searchParams: Promise<{ redirect: string }>; }) { const params = await props.params; const searchParams = await props.searchParams; @@ -44,9 +44,10 @@ export default async function ResourceAuthPage(props: { const hasAuth = authInfo.password || authInfo.pincode || authInfo.sso; const isSSOOnly = authInfo.sso && !authInfo.password && !authInfo.pincode; - const redirectUrl = searchParams.r || authInfo.url; + const redirectUrl = searchParams.redirect || authInfo.url; if (!hasAuth) { + // no authentication so always go straight to the resource redirect(redirectUrl); } @@ -93,6 +94,9 @@ export default async function ResourceAuthPage(props: { id: authInfo.resourceId, }} redirect={redirectUrl} + queryParamName={ + process.env.RESOURCE_SESSION_QUERY_PARAM_NAME! + } />