Просмотр исходного кода

set resource session as base domain cookie

Milo Schwartz 8 месяцев назад
Родитель
Сommit
8178dd1525

+ 12 - 8
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}`;
     }
 }
 

+ 4 - 0
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;
 

+ 65 - 0
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<typeof params>;
+
+export type CheckResourceSessionResponse = {
+    valid: boolean;
+};
+
+export async function checkResourceSession(
+    req: Request,
+    res: Response,
+    next: NextFunction,
+): Promise<any> {
+    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<CheckResourceSessionResponse>(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",
+            ),
+        );
+    }
+}

+ 1 - 0
server/routers/auth/index.ts

@@ -9,3 +9,4 @@ export * from "./requestEmailVerificationCode";
 export * from "./changePassword";
 export * from "./requestPasswordReset";
 export * from "./resetPassword";
+export * from "./checkResourceSession";

+ 18 - 10
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 &&

+ 5 - 0
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();

+ 10 - 9
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);
-
-        // logger.debug(cookie); // remove after testing
+        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
 
         return response<AuthWithPasswordResponse>(res, {
             data: {

+ 10 - 9
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);
-
-        // logger.debug(cookie); // remove after testing
+        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
 
         return response<AuthWithPincodeResponse>(res, {
             data: {

+ 8 - 0
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;

+ 1 - 1
src/app/[orgId]/settings/resources/components/ResourcesTable.tsx

@@ -124,7 +124,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
                 return (
                     <div className="flex items-center">
                         <Link
-                            href={`https://${resourceRow.domain}`}
+                            href={resourceRow.domain}
                             target="_blank"
                             rel="noopener noreferrer"
                             className="hover:underline mr-2"

+ 5 - 12
src/app/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx

@@ -63,7 +63,6 @@ type ResourceAuthPortalProps = {
         id: number;
     };
     redirect: string;
-    queryParamName: string;
 };
 
 export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
@@ -114,10 +113,8 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
         },
     });
 
-    function constructRedirect(redirect: string, token: string): string {
+    function constructRedirect(redirect: string): string {
         const redirectUrl = new URL(redirect);
-        redirectUrl.searchParams.delete(props.queryParamName);
-        redirectUrl.searchParams.append(props.queryParamName, token);
         return redirectUrl.toString();
     }
 
@@ -130,10 +127,9 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
             .then((res) => {
                 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) => {

+ 30 - 5
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<CheckResourceSessionResponse>
+            >(`/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!
-                    }
                 />
             </div>
         </>

+ 2 - 0
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,
 }: {