diff --git a/server/auth/resource.ts b/server/auth/resource.ts index f9697a70..b13b54d9 100644 --- a/server/auth/resource.ts +++ b/server/auth/resource.ts @@ -3,9 +3,13 @@ import { sha256 } from "@oslojs/crypto/sha2"; import { resourceSessions, ResourceSession } from "@server/db/schema"; import db from "@server/db"; import { eq, and } from "drizzle-orm"; +import config from "@server/config"; export const SESSION_COOKIE_NAME = "resource_session"; export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30; +export const SECURE_COOKIES = config.server.secure_cookies; +export const COOKIE_DOMAIN = + "." + new URL(config.app.base_url).hostname.split(".").slice(-2).join("."); export async function createResourceSession(opts: { token: string; @@ -115,25 +119,25 @@ export async function invalidateAllSessions( } export function serializeResourceSessionCookie( + cookieName: string, token: string, fqdn: string, - secure: boolean, ): string { - if (secure) { - return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=.localhost`; + if (SECURE_COOKIES) { + return `${cookieName}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`; } else { - return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=.localhost`; + return `${cookieName}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=${COOKIE_DOMAIN}`; } } export function createBlankResourceSessionTokenCookie( + cookieName: string, fqdn: string, - secure: boolean, ): string { - if (secure) { - return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Secure; Domain=${fqdn}`; + if (SECURE_COOKIES) { + return `${cookieName}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`; } else { - return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Domain=${fqdn}`; + return `${cookieName}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Domain=${COOKIE_DOMAIN}`; } } diff --git a/server/config.ts b/server/config.ts index 62bdfab4..1314208a 100644 --- a/server/config.ts +++ b/server/config.ts @@ -128,11 +128,15 @@ if (!parsedConfig.success) { process.env.SERVER_EXTERNAL_PORT = parsedConfig.data.server.external_port.toString(); +process.env.SERVER_INTERNAL_PORT = + parsedConfig.data.server.internal_port.toString(); 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_COOKIE_NAME = + parsedConfig.data.badger.resource_session_cookie_name; process.env.RESOURCE_SESSION_QUERY_PARAM_NAME = parsedConfig.data.badger.session_query_parameter; diff --git a/server/routers/auth/checkResourceSession.ts b/server/routers/auth/checkResourceSession.ts new file mode 100644 index 00000000..11f4ca90 --- /dev/null +++ b/server/routers/auth/checkResourceSession.ts @@ -0,0 +1,65 @@ +import { Request, Response, NextFunction } from "express"; +import createHttpError from "http-errors"; +import { z } from "zod"; +import { fromError } from "zod-validation-error"; +import HttpCode from "@server/types/HttpCode"; +import { response } from "@server/utils"; +import { validateSessionToken } from "@server/auth"; +import { validateResourceSessionToken } from "@server/auth/resource"; + +export const params = z.object({ + token: z.string(), + resourceId: z.string().transform(Number).pipe(z.number().int().positive()), +}); + +export type CheckResourceSessionParams = z.infer; + +export type CheckResourceSessionResponse = { + valid: boolean; +}; + +export async function checkResourceSession( + req: Request, + res: Response, + next: NextFunction, +): Promise { + const parsedParams = params.safeParse(req.params); + + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString(), + ), + ); + } + + const { token, resourceId } = parsedParams.data; + + try { + const { resourceSession } = await validateResourceSessionToken( + token, + resourceId, + ); + + let valid = false; + if (resourceSession) { + valid = true; + } + + return response(res, { + data: { valid }, + success: true, + error: false, + message: "Checked validity", + status: HttpCode.OK, + }); + } catch (e) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to reset password", + ), + ); + } +} diff --git a/server/routers/auth/index.ts b/server/routers/auth/index.ts index b6ce3a01..b2eaf8d2 100644 --- a/server/routers/auth/index.ts +++ b/server/routers/auth/index.ts @@ -9,3 +9,4 @@ export * from "./requestEmailVerificationCode"; export * from "./changePassword"; export * from "./requestPasswordReset"; export * from "./resetPassword"; +export * from "./checkResourceSession"; diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index cd8529a1..042713bf 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -19,10 +19,7 @@ import { Resource, roleResources, userResources } from "@server/db/schema"; import logger from "@server/logger"; const verifyResourceSessionSchema = z.object({ - sessions: z.object({ - session: z.string().nullable(), - resource_session: z.string().nullable(), - }), + sessions: z.record(z.string()).optional(), originalRequestURL: z.string().url(), scheme: z.string(), host: z.string(), @@ -98,10 +95,15 @@ export async function verifyResourceSession( const redirectUrl = `${config.app.base_url}/auth/resource/${encodeURIComponent(resource.resourceId)}?redirect=${encodeURIComponent(originalRequestURL)}`; - if (sso && sessions.session) { - const { session, user } = await validateSessionToken( - sessions.session, - ); + if (!sessions) { + return notAllowed(res); + } + + const sessionToken = sessions[config.server.session_cookie_name]; + + // check for unified login + if (sso && sessionToken) { + const { session, user } = await validateSessionToken(sessionToken); if (session && user) { const isAllowed = await isUserAllowedToAccessResource( user.userId, @@ -117,11 +119,17 @@ export async function verifyResourceSession( } } - if (password && sessions.resource_session) { + const resourceSessionToken = + sessions[ + `${config.badger.resource_session_cookie_name}_${resource.resourceId}` + ]; + + if ((pincode || password) && resourceSessionToken) { const { resourceSession } = await validateResourceSessionToken( - sessions.resource_session, + resourceSessionToken, resource.resourceId, ); + if (resourceSession) { if ( pincode && diff --git a/server/routers/internal.ts b/server/routers/internal.ts index 42aa7d47..fb0e1809 100644 --- a/server/routers/internal.ts +++ b/server/routers/internal.ts @@ -2,6 +2,7 @@ import { Router } from "express"; import * as gerbil from "@server/routers/gerbil"; import * as badger from "@server/routers/badger"; import * as traefik from "@server/routers/traefik"; +import * as auth from "@server/routers/auth"; import HttpCode from "@server/types/HttpCode"; // Root routes @@ -12,6 +13,10 @@ internalRouter.get("/", (_, res) => { }); internalRouter.get("/traefik-config", traefik.traefikConfigProvider); +internalRouter.get( + "/resource-session/:resourceId/:token", + auth.checkResourceSession, +); // Gerbil routes const gerbilRouter = Router(); diff --git a/server/routers/resource/authWithPassword.ts b/server/routers/resource/authWithPassword.ts index d803f279..b2a42572 100644 --- a/server/routers/resource/authWithPassword.ts +++ b/server/routers/resource/authWithPassword.ts @@ -14,6 +14,7 @@ import { serializeResourceSessionCookie, } from "@server/auth/resource"; import logger from "@server/logger"; +import config from "@server/config"; export const authWithPasswordBodySchema = z.object({ password: z.string(), @@ -131,15 +132,15 @@ 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 cookieName = `${config.badger.resource_session_cookie_name}_${resource.resourceId}`; + const cookie = serializeResourceSessionCookie( + cookieName, + token, + resource.fullDomain, + ); + res.appendHeader("Set-Cookie", cookie); - // logger.debug(cookie); // remove after testing + logger.debug(cookie); // remove after testing return response(res, { data: { diff --git a/server/routers/resource/authWithPincode.ts b/server/routers/resource/authWithPincode.ts index 8c618f8a..a89b1584 100644 --- a/server/routers/resource/authWithPincode.ts +++ b/server/routers/resource/authWithPincode.ts @@ -14,6 +14,7 @@ import { serializeResourceSessionCookie, } from "@server/auth/resource"; import logger from "@server/logger"; +import config from "@server/config"; export const authWithPincodeBodySchema = z.object({ pincode: z.string(), @@ -127,15 +128,15 @@ 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 cookieName = `${config.badger.resource_session_cookie_name}_${resource.resourceId}`; + const cookie = serializeResourceSessionCookie( + cookieName, + token, + resource.fullDomain, + ); + res.appendHeader("Set-Cookie", cookie); - // logger.debug(cookie); // remove after testing + logger.debug(cookie); // remove after testing return response(res, { data: { diff --git a/src/api/index.ts b/src/api/index.ts index 4da1a36e..d0c687f1 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -22,4 +22,12 @@ export const internal = axios.create({ }, }); +export const priv = axios.create({ + baseURL: `http://localhost:${process.env.SERVER_INTERNAL_PORT}/api/v1`, + timeout: 10000, + headers: { + "Content-Type": "application/json", + }, +}); + export default api; diff --git a/src/app/[orgId]/settings/resources/components/ResourcesTable.tsx b/src/app/[orgId]/settings/resources/components/ResourcesTable.tsx index 137b398a..e8dfc643 100644 --- a/src/app/[orgId]/settings/resources/components/ResourcesTable.tsx +++ b/src/app/[orgId]/settings/resources/components/ResourcesTable.tsx @@ -124,7 +124,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { return (
{ const session = res.data.data.session; if (session) { - window.location.href = constructRedirect( - props.redirect, - session, - ); + const url = constructRedirect(props.redirect); + console.log(url); + window.location.href = url; } }) .catch((e) => { @@ -156,10 +152,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { .then((res) => { const session = res.data.data.session; if (session) { - window.location.href = constructRedirect( - props.redirect, - session, - ); + window.location.href = constructRedirect(props.redirect); } }) .catch((e) => { diff --git a/src/app/auth/resource/[resourceId]/page.tsx b/src/app/auth/resource/[resourceId]/page.tsx index 32b7d067..6327970f 100644 --- a/src/app/auth/resource/[resourceId]/page.tsx +++ b/src/app/auth/resource/[resourceId]/page.tsx @@ -3,7 +3,7 @@ import { GetResourceResponse, } from "@server/routers/resource"; import ResourceAuthPortal from "./components/ResourceAuthPortal"; -import { internal } from "@app/api"; +import { internal, priv } from "@app/api"; import { AxiosResponse } from "axios"; import { authCookieHeader } from "@app/api/cookies"; import { cache } from "react"; @@ -11,10 +11,12 @@ import { verifySession } from "@app/lib/auth/verifySession"; import { redirect } from "next/navigation"; import ResourceNotFound from "./components/ResourceNotFound"; import ResourceAccessDenied from "./components/ResourceAccessDenied"; +import { cookies } from "next/headers"; +import { CheckResourceSessionResponse } from "@server/routers/auth"; export default async function ResourceAuthPage(props: { params: Promise<{ resourceId: number }>; - searchParams: Promise<{ redirect: string }>; + searchParams: Promise<{ redirect: string | undefined }>; }) { const params = await props.params; const searchParams = await props.searchParams; @@ -46,6 +48,32 @@ export default async function ResourceAuthPage(props: { const redirectUrl = searchParams.redirect || authInfo.url; + const allCookies = await cookies(); + const cookieName = + process.env.RESOURCE_SESSION_COOKIE_NAME + `_${params.resourceId}`; + const sessionId = allCookies.get(cookieName)?.value ?? null; + + if (sessionId) { + let doRedirect = false; + try { + const res = await priv.get< + AxiosResponse + >(`/resource-session/${params.resourceId}/${sessionId}`); + + console.log("resource session already exists and is valid"); + + if (res && res.data.data.valid) { + doRedirect = true; + } + } catch (e) { + console.error(e); + } + + if (doRedirect) { + redirect(redirectUrl); + } + } + if (!hasAuth) { // no authentication so always go straight to the resource redirect(redirectUrl); @@ -94,9 +122,6 @@ export default async function ResourceAuthPage(props: { id: authInfo.resourceId, }} redirect={redirectUrl} - queryParamName={ - process.env.RESOURCE_SESSION_QUERY_PARAM_NAME! - } />
diff --git a/src/app/setup/layout.tsx b/src/app/setup/layout.tsx index e0e4954f..c8c96ac7 100644 --- a/src/app/setup/layout.tsx +++ b/src/app/setup/layout.tsx @@ -8,6 +8,8 @@ export const metadata: Metadata = { description: "", }; +export const dynamic = "force-dynamic"; + export default async function SetupLayout({ children, }: {