Browse Source

refactor auth to work cross domain and with http resources closes #100

Milo Schwartz 5 months ago
parent
commit
9f1f291

+ 2 - 2
config/config.example.yml

@@ -10,9 +10,9 @@ server:
     next_port: 3002
     next_port: 3002
     internal_hostname: "pangolin"
     internal_hostname: "pangolin"
     secure_cookies: true
     secure_cookies: true
-    session_cookie_name: "p_session"
-    resource_session_cookie_name: "p_resource_session"
+    session_cookie_name: "p_session_token"
     resource_access_token_param: "p_token"
     resource_access_token_param: "p_token"
+    resource_session_request_param: "p_session_request"
 
 
 traefik:
 traefik:
     cert_resolver: "letsencrypt"
     cert_resolver: "letsencrypt"

+ 2 - 2
install/fs/config.yml

@@ -10,9 +10,9 @@ server:
     next_port: 3002
     next_port: 3002
     internal_hostname: "pangolin"
     internal_hostname: "pangolin"
     secure_cookies: true
     secure_cookies: true
-    session_cookie_name: "p_session"
-    resource_session_cookie_name: "p_resource_session"
+    session_cookie_name: "p_session_token"
     resource_access_token_param: "p_token"
     resource_access_token_param: "p_token"
+    resource_session_request_param: "p_session_request"
     cors:
     cors:
         origins: ["https://{{.DashboardDomain}}"]
         origins: ["https://{{.DashboardDomain}}"]
         methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
         methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]

+ 1 - 0
package.json

@@ -64,6 +64,7 @@
         "moment": "2.30.1",
         "moment": "2.30.1",
         "next": "15.1.3",
         "next": "15.1.3",
         "next-themes": "0.4.4",
         "next-themes": "0.4.4",
+        "node-cache": "5.1.2",
         "node-fetch": "3.3.2",
         "node-fetch": "3.3.2",
         "nodemailer": "6.9.16",
         "nodemailer": "6.9.16",
         "oslo": "1.2.1",
         "oslo": "1.2.1",

+ 31 - 11
server/auth/sessions/app.ts

@@ -3,7 +3,13 @@ import {
     encodeHexLowerCase
     encodeHexLowerCase
 } from "@oslojs/encoding";
 } from "@oslojs/encoding";
 import { sha256 } from "@oslojs/crypto/sha2";
 import { sha256 } from "@oslojs/crypto/sha2";
-import { Session, sessions, User, users } from "@server/db/schema";
+import {
+    resourceSessions,
+    Session,
+    sessions,
+    User,
+    users
+} from "@server/db/schema";
 import db from "@server/db";
 import db from "@server/db";
 import { eq } from "drizzle-orm";
 import { eq } from "drizzle-orm";
 import config from "@server/lib/config";
 import config from "@server/lib/config";
@@ -13,9 +19,14 @@ import logger from "@server/logger";
 
 
 export const SESSION_COOKIE_NAME =
 export const SESSION_COOKIE_NAME =
     config.getRawConfig().server.session_cookie_name;
     config.getRawConfig().server.session_cookie_name;
-export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30;
+export const SESSION_COOKIE_EXPIRES =
+    1000 *
+    60 *
+    60 *
+    config.getRawConfig().server.dashboard_session_length_hours;
 export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies;
 export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies;
-export const COOKIE_DOMAIN = "." + config.getBaseDomain();
+export const COOKIE_DOMAIN =
+    "." + new URL(config.getRawConfig().app.dashboard_url).hostname;
 
 
 export function generateSessionToken(): string {
 export function generateSessionToken(): string {
     const bytes = new Uint8Array(20);
     const bytes = new Uint8Array(20);
@@ -65,12 +76,21 @@ export async function validateSessionToken(
         session.expiresAt = new Date(
         session.expiresAt = new Date(
             Date.now() + SESSION_COOKIE_EXPIRES
             Date.now() + SESSION_COOKIE_EXPIRES
         ).getTime();
         ).getTime();
-        await db
-            .update(sessions)
-            .set({
-                expiresAt: session.expiresAt
-            })
-            .where(eq(sessions.sessionId, session.sessionId));
+        await db.transaction(async (trx) => {
+            await trx
+                .update(sessions)
+                .set({
+                    expiresAt: session.expiresAt
+                })
+                .where(eq(sessions.sessionId, session.sessionId));
+
+            await trx
+                .update(resourceSessions)
+                .set({
+                    expiresAt: session.expiresAt
+                })
+                .where(eq(resourceSessions.userSessionId, session.sessionId));
+        });
     }
     }
     return { session, user };
     return { session, user };
 }
 }
@@ -90,9 +110,9 @@ export function serializeSessionCookie(
     if (isSecure) {
     if (isSecure) {
         logger.debug("Setting cookie for secure origin");
         logger.debug("Setting cookie for secure origin");
         if (SECURE_COOKIES) {
         if (SECURE_COOKIES) {
-            return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
+            return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
         } else {
         } else {
-            return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=${COOKIE_DOMAIN}`;
+            return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Domain=${COOKIE_DOMAIN}`;
         }
         }
     } else {
     } else {
         return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/;`;
         return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/;`;

+ 24 - 17
server/auth/sessions/resource.ts

@@ -6,19 +6,20 @@ import { eq, and } from "drizzle-orm";
 import config from "@server/lib/config";
 import config from "@server/lib/config";
 
 
 export const SESSION_COOKIE_NAME =
 export const SESSION_COOKIE_NAME =
-    config.getRawConfig().server.resource_session_cookie_name;
-export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30;
+    config.getRawConfig().server.session_cookie_name;
+export const SESSION_COOKIE_EXPIRES =
+    1000 * 60 * 60 * config.getRawConfig().server.resource_session_length_hours;
 export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies;
 export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies;
-export const COOKIE_DOMAIN = "." + config.getBaseDomain();
 
 
 export async function createResourceSession(opts: {
 export async function createResourceSession(opts: {
     token: string;
     token: string;
     resourceId: number;
     resourceId: number;
-    passwordId?: number;
-    pincodeId?: number;
-    whitelistId?: number;
-    accessTokenId?: string;
-    usedOtp?: boolean;
+    isRequestToken?: boolean;
+    passwordId?: number | null;
+    pincodeId?: number | null;
+    userSessionId?: string | null;
+    whitelistId?: number | null;
+    accessTokenId?: string | null;
     doNotExtend?: boolean;
     doNotExtend?: boolean;
     expiresAt?: number | null;
     expiresAt?: number | null;
     sessionLength?: number | null;
     sessionLength?: number | null;
@@ -27,7 +28,8 @@ export async function createResourceSession(opts: {
         !opts.passwordId &&
         !opts.passwordId &&
         !opts.pincodeId &&
         !opts.pincodeId &&
         !opts.whitelistId &&
         !opts.whitelistId &&
-        !opts.accessTokenId
+        !opts.accessTokenId &&
+        !opts.userSessionId
     ) {
     ) {
         throw new Error("Auth method must be provided");
         throw new Error("Auth method must be provided");
     }
     }
@@ -47,7 +49,9 @@ export async function createResourceSession(opts: {
         pincodeId: opts.pincodeId || null,
         pincodeId: opts.pincodeId || null,
         whitelistId: opts.whitelistId || null,
         whitelistId: opts.whitelistId || null,
         doNotExtend: opts.doNotExtend || false,
         doNotExtend: opts.doNotExtend || false,
-        accessTokenId: opts.accessTokenId || null
+        accessTokenId: opts.accessTokenId || null,
+        isRequestToken: opts.isRequestToken || false,
+        userSessionId: opts.userSessionId || null
     };
     };
 
 
     await db.insert(resourceSessions).values(session);
     await db.insert(resourceSessions).values(session);
@@ -162,22 +166,25 @@ export async function invalidateAllSessions(
 
 
 export function serializeResourceSessionCookie(
 export function serializeResourceSessionCookie(
     cookieName: string,
     cookieName: string,
-    token: string
+    domain: string,
+    token: string,
+    isHttp: boolean = false
 ): string {
 ): string {
-    if (SECURE_COOKIES) {
-        return `${cookieName}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
+    if (SECURE_COOKIES && !isHttp) {
+        return `${cookieName}_s=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Secure; Domain=${"." + domain}`;
     } else {
     } else {
-        return `${cookieName}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=${COOKIE_DOMAIN}`;
+        return `${cookieName}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Domain=${"." + domain}`;
     }
     }
 }
 }
 
 
 export function createBlankResourceSessionTokenCookie(
 export function createBlankResourceSessionTokenCookie(
-    cookieName: string
+    cookieName: string,
+    domain: string
 ): string {
 ): string {
     if (SECURE_COOKIES) {
     if (SECURE_COOKIES) {
-        return `${cookieName}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
+        return `${cookieName}_s=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${"." + domain}`;
     } else {
     } else {
-        return `${cookieName}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Domain=${COOKIE_DOMAIN}`;
+        return `${cookieName}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Domain=${"." + domain}`;
     }
     }
 }
 }
 
 

+ 4 - 0
server/db/schema.ts

@@ -313,6 +313,10 @@ export const resourceSessions = sqliteTable("resourceSessions", {
     doNotExtend: integer("doNotExtend", { mode: "boolean" })
     doNotExtend: integer("doNotExtend", { mode: "boolean" })
         .notNull()
         .notNull()
         .default(false),
         .default(false),
+    isRequestToken: integer("isRequestToken", { mode: "boolean" }),
+    userSessionId: text("userSessionId").references(() => sessions.sessionId, {
+        onDelete: "cascade"
+    }),
     passwordId: integer("passwordId").references(
     passwordId: integer("passwordId").references(
         () => resourcePassword.passwordId,
         () => resourcePassword.passwordId,
         {
         {

+ 2 - 1
server/index.ts

@@ -2,7 +2,7 @@ import { runSetupFunctions } from "./setup";
 import { createApiServer } from "./apiServer";
 import { createApiServer } from "./apiServer";
 import { createNextServer } from "./nextServer";
 import { createNextServer } from "./nextServer";
 import { createInternalServer } from "./internalServer";
 import { createInternalServer } from "./internalServer";
-import { User, UserOrg } from "./db/schema";
+import { Session, User, UserOrg } from "./db/schema";
 
 
 async function startServers() {
 async function startServers() {
     await runSetupFunctions();
     await runSetupFunctions();
@@ -24,6 +24,7 @@ declare global {
     namespace Express {
     namespace Express {
         interface Request {
         interface Request {
             user?: User;
             user?: User;
+            session?: Session;
             userOrg?: UserOrg;
             userOrg?: UserOrg;
             userOrgRoleId?: number;
             userOrgRoleId?: number;
             userOrgId?: string;
             userOrgId?: string;

+ 15 - 3
server/lib/config.ts

@@ -61,8 +61,20 @@ const configSchema = z.object({
         internal_hostname: z.string().transform((url) => url.toLowerCase()),
         internal_hostname: z.string().transform((url) => url.toLowerCase()),
         secure_cookies: z.boolean(),
         secure_cookies: z.boolean(),
         session_cookie_name: z.string(),
         session_cookie_name: z.string(),
-        resource_session_cookie_name: z.string(),
         resource_access_token_param: z.string(),
         resource_access_token_param: z.string(),
+        resource_session_request_param: z.string(),
+        dashboard_session_length_hours: z
+            .number()
+            .positive()
+            .gt(0)
+            .optional()
+            .default(720),
+        resource_session_length_hours: z
+            .number()
+            .positive()
+            .gt(0)
+            .optional()
+            .default(720),
         cors: z
         cors: z
             .object({
             .object({
                 origins: z.array(z.string()).optional(),
                 origins: z.array(z.string()).optional(),
@@ -241,8 +253,6 @@ export class Config {
             : "false";
             : "false";
         process.env.SESSION_COOKIE_NAME =
         process.env.SESSION_COOKIE_NAME =
             parsedConfig.data.server.session_cookie_name;
             parsedConfig.data.server.session_cookie_name;
-        process.env.RESOURCE_SESSION_COOKIE_NAME =
-            parsedConfig.data.server.resource_session_cookie_name;
         process.env.EMAIL_ENABLED = parsedConfig.data.email ? "true" : "false";
         process.env.EMAIL_ENABLED = parsedConfig.data.email ? "true" : "false";
         process.env.DISABLE_SIGNUP_WITHOUT_INVITE = parsedConfig.data.flags
         process.env.DISABLE_SIGNUP_WITHOUT_INVITE = parsedConfig.data.flags
             ?.disable_signup_without_invite
             ?.disable_signup_without_invite
@@ -254,6 +264,8 @@ export class Config {
             : "false";
             : "false";
         process.env.RESOURCE_ACCESS_TOKEN_PARAM =
         process.env.RESOURCE_ACCESS_TOKEN_PARAM =
             parsedConfig.data.server.resource_access_token_param;
             parsedConfig.data.server.resource_access_token_param;
+        process.env.RESOURCE_SESSION_REQUEST_PARAM =
+            parsedConfig.data.server.resource_session_request_param;
 
 
         this.rawConfig = parsedConfig.data;
         this.rawConfig = parsedConfig.data;
     }
     }

+ 5 - 6
server/routers/auth/logout.ts

@@ -5,18 +5,17 @@ import response from "@server/lib/response";
 import logger from "@server/logger";
 import logger from "@server/logger";
 import {
 import {
     createBlankSessionTokenCookie,
     createBlankSessionTokenCookie,
-    invalidateSession,
-    SESSION_COOKIE_NAME
+    invalidateSession
 } from "@server/auth/sessions/app";
 } from "@server/auth/sessions/app";
+import { verifySession } from "@server/auth/sessions/verifySession";
 
 
 export async function logout(
 export async function logout(
     req: Request,
     req: Request,
     res: Response,
     res: Response,
     next: NextFunction
     next: NextFunction
 ): Promise<any> {
 ): Promise<any> {
-    const sessionId = req.cookies[SESSION_COOKIE_NAME];
-
-    if (!sessionId) {
+    const { user, session } = await verifySession(req);
+    if (!user || !session) {
         return next(
         return next(
             createHttpError(
             createHttpError(
                 HttpCode.BAD_REQUEST,
                 HttpCode.BAD_REQUEST,
@@ -26,7 +25,7 @@ export async function logout(
     }
     }
 
 
     try {
     try {
-        await invalidateSession(sessionId);
+        await invalidateSession(session.sessionId);
         const isSecure = req.protocol === "https";
         const isSecure = req.protocol === "https";
         res.setHeader("Set-Cookie", createBlankSessionTokenCookie(isSecure));
         res.setHeader("Set-Cookie", createBlankSessionTokenCookie(isSecure));
 
 

+ 175 - 0
server/routers/badger/exchangeSession.ts

@@ -0,0 +1,175 @@
+import HttpCode from "@server/types/HttpCode";
+import { NextFunction, Request, Response } from "express";
+import createHttpError from "http-errors";
+import { z } from "zod";
+import { fromError } from "zod-validation-error";
+import logger from "@server/logger";
+import { resourceAccessToken, resources, sessions } from "@server/db/schema";
+import db from "@server/db";
+import { eq } from "drizzle-orm";
+import {
+    createResourceSession,
+    serializeResourceSessionCookie,
+    validateResourceSessionToken
+} from "@server/auth/sessions/resource";
+import { generateSessionToken } from "@server/auth";
+import { SESSION_COOKIE_EXPIRES } from "@server/auth/sessions/app";
+import { SESSION_COOKIE_EXPIRES as RESOURCE_SESSION_COOKIE_EXPIRES } from "@server/auth/sessions/resource";
+import config from "@server/lib/config";
+import { response } from "@server/lib";
+
+const exchangeSessionBodySchema = z.object({
+    requestToken: z.string(),
+    host: z.string()
+});
+
+export type ExchangeSessionBodySchema = z.infer<
+    typeof exchangeSessionBodySchema
+>;
+
+export type ExchangeSessionResponse = {
+    valid: boolean;
+    cookie?: string;
+};
+
+export async function exchangeSession(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
+    logger.debug("Exchange session: Badger sent", req.body);
+
+    const parsedBody = exchangeSessionBodySchema.safeParse(req.body);
+
+    if (!parsedBody.success) {
+        return next(
+            createHttpError(
+                HttpCode.BAD_REQUEST,
+                fromError(parsedBody.error).toString()
+            )
+        );
+    }
+
+    try {
+        const { requestToken, host } = parsedBody.data;
+
+        const [resource] = await db
+            .select()
+            .from(resources)
+            .where(eq(resources.fullDomain, host))
+            .limit(1);
+
+        if (!resource) {
+            return next(
+                createHttpError(
+                    HttpCode.NOT_FOUND,
+                    `Resource with host ${host} not found`
+                )
+            );
+        }
+
+        const { resourceSession: requestSession } =
+            await validateResourceSessionToken(
+                requestToken,
+                resource.resourceId
+            );
+
+        if (!requestSession) {
+            return next(
+                createHttpError(HttpCode.UNAUTHORIZED, "Invalid request token")
+            );
+        }
+
+        if (!requestSession.isRequestToken) {
+            return next(
+                createHttpError(HttpCode.UNAUTHORIZED, "Invalid request token")
+            );
+        }
+
+        await db.delete(sessions).where(eq(sessions.sessionId, requestToken));
+
+        const token = generateSessionToken();
+
+        if (requestSession.userSessionId) {
+            const [res] = await db
+                .select()
+                .from(sessions)
+                .where(eq(sessions.sessionId, requestSession.userSessionId))
+                .limit(1);
+            if (res) {
+                await createResourceSession({
+                    token,
+                    resourceId: resource.resourceId,
+                    isRequestToken: false,
+                    userSessionId: requestSession.userSessionId,
+                    doNotExtend: false,
+                    expiresAt: res.expiresAt,
+                    sessionLength: SESSION_COOKIE_EXPIRES
+                });
+            }
+        } else if (requestSession.accessTokenId) {
+            const [res] = await db
+                .select()
+                .from(resourceAccessToken)
+                .where(
+                    eq(
+                        resourceAccessToken.accessTokenId,
+                        requestSession.accessTokenId
+                    )
+                )
+                .limit(1);
+            if (res) {
+                await createResourceSession({
+                    token,
+                    resourceId: resource.resourceId,
+                    isRequestToken: false,
+                    accessTokenId: requestSession.accessTokenId,
+                    doNotExtend: true,
+                    expiresAt: res.expiresAt,
+                    sessionLength: res.sessionLength
+                });
+            }
+        } else {
+            await createResourceSession({
+                token,
+                resourceId: resource.resourceId,
+                isRequestToken: false,
+                passwordId: requestSession.passwordId,
+                pincodeId: requestSession.pincodeId,
+                userSessionId: requestSession.userSessionId,
+                whitelistId: requestSession.whitelistId,
+                accessTokenId: requestSession.accessTokenId,
+                doNotExtend: false,
+                expiresAt: new Date(
+                    Date.now() + SESSION_COOKIE_EXPIRES
+                ).getTime(),
+                sessionLength: RESOURCE_SESSION_COOKIE_EXPIRES
+            });
+        }
+
+        const cookieName = `${config.getRawConfig().server.session_cookie_name}`;
+        const cookie = serializeResourceSessionCookie(
+            cookieName,
+            resource.fullDomain,
+            token,
+            !resource.ssl
+        );
+
+        logger.debug(JSON.stringify("Exchange cookie: " + cookie));
+        return response<ExchangeSessionResponse>(res, {
+            data: { valid: true, cookie },
+            success: true,
+            error: false,
+            message: "Session exchanged successfully",
+            status: HttpCode.OK
+        });
+    } catch (e) {
+        console.error(e);
+        return next(
+            createHttpError(
+                HttpCode.INTERNAL_SERVER_ERROR,
+                "Failed to exchange session"
+            )
+        );
+    }
+}

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

@@ -1 +1,2 @@
 export * from "./verifySession";
 export * from "./verifySession";
+export * from "./exchangeSession";

+ 122 - 55
server/routers/badger/verifySession.ts

@@ -4,17 +4,17 @@ import createHttpError from "http-errors";
 import { z } from "zod";
 import { z } from "zod";
 import { fromError } from "zod-validation-error";
 import { fromError } from "zod-validation-error";
 import { response } from "@server/lib/response";
 import { response } from "@server/lib/response";
-import { validateSessionToken } from "@server/auth/sessions/app";
 import db from "@server/db";
 import db from "@server/db";
 import {
 import {
     ResourceAccessToken,
     ResourceAccessToken,
-    resourceAccessToken,
+    ResourcePassword,
     resourcePassword,
     resourcePassword,
+    ResourcePincode,
     resourcePincode,
     resourcePincode,
     resources,
     resources,
-    resourceWhitelist,
-    User,
-    userOrgs
+    sessions,
+    userOrgs,
+    users
 } from "@server/db/schema";
 } from "@server/db/schema";
 import { and, eq } from "drizzle-orm";
 import { and, eq } from "drizzle-orm";
 import config from "@server/lib/config";
 import config from "@server/lib/config";
@@ -27,6 +27,12 @@ import { Resource, roleResources, userResources } from "@server/db/schema";
 import logger from "@server/logger";
 import logger from "@server/logger";
 import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
 import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
 import { generateSessionToken } from "@server/auth";
 import { generateSessionToken } from "@server/auth";
+import NodeCache from "node-cache";
+
+// We'll see if this speeds anything up
+const cache = new NodeCache({
+    stdTTL: 5 // seconds
+});
 
 
 const verifyResourceSessionSchema = z.object({
 const verifyResourceSessionSchema = z.object({
     sessions: z.record(z.string()).optional(),
     sessions: z.record(z.string()).optional(),
@@ -53,7 +59,7 @@ export async function verifyResourceSession(
     res: Response,
     res: Response,
     next: NextFunction
     next: NextFunction
 ): Promise<any> {
 ): Promise<any> {
-    logger.debug("Badger sent", req.body); // remove when done testing
+    logger.debug("Verify session: Badger sent", req.body); // remove when done testing
 
 
     const parsedBody = verifyResourceSessionSchema.safeParse(req.body);
     const parsedBody = verifyResourceSessionSchema.safeParse(req.body);
 
 
@@ -67,26 +73,52 @@ export async function verifyResourceSession(
     }
     }
 
 
     try {
     try {
-        const { sessions, host, originalRequestURL, accessToken: token } =
-            parsedBody.data;
-
-        const [result] = await db
-            .select()
-            .from(resources)
-            .leftJoin(
-                resourcePincode,
-                eq(resourcePincode.resourceId, resources.resourceId)
-            )
-            .leftJoin(
-                resourcePassword,
-                eq(resourcePassword.resourceId, resources.resourceId)
-            )
-            .where(eq(resources.fullDomain, host))
-            .limit(1);
+        const {
+            sessions,
+            host,
+            originalRequestURL,
+            accessToken: token
+        } = parsedBody.data;
+
+        const resourceCacheKey = `resource:${host}`;
+        let resourceData:
+            | {
+                  resource: Resource | null;
+                  pincode: ResourcePincode | null;
+                  password: ResourcePassword | null;
+              }
+            | undefined = cache.get(resourceCacheKey);
+
+        if (!resourceData) {
+            const [result] = await db
+                .select()
+                .from(resources)
+                .leftJoin(
+                    resourcePincode,
+                    eq(resourcePincode.resourceId, resources.resourceId)
+                )
+                .leftJoin(
+                    resourcePassword,
+                    eq(resourcePassword.resourceId, resources.resourceId)
+                )
+                .where(eq(resources.fullDomain, host))
+                .limit(1);
+
+            if (!result) {
+                logger.debug("Resource not found", host);
+                return notAllowed(res);
+            }
 
 
-        const resource = result?.resources;
-        const pincode = result?.resourcePincode;
-        const password = result?.resourcePassword;
+            resourceData = {
+                resource: result.resources,
+                pincode: result.resourcePincode,
+                password: result.resourcePassword
+            };
+
+            cache.set(resourceCacheKey, resourceData);
+        }
+
+        const { resource, pincode, password } = resourceData;
 
 
         if (!resource) {
         if (!resource) {
             logger.debug("Resource not found", host);
             logger.debug("Resource not found", host);
@@ -145,37 +177,31 @@ export async function verifyResourceSession(
             return notAllowed(res);
             return notAllowed(res);
         }
         }
 
 
-        const sessionToken =
-            sessions[config.getRawConfig().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,
-                    resource
-                );
-
-                if (isAllowed) {
-                    logger.debug(
-                        "Resource allowed because user session is valid"
-                    );
-                    return allowed(res);
-                }
-            }
-        }
-
         const resourceSessionToken =
         const resourceSessionToken =
             sessions[
             sessions[
-                `${config.getRawConfig().server.resource_session_cookie_name}_${resource.resourceId}`
+                `${config.getRawConfig().server.session_cookie_name}${resource.ssl ? "_s" : ""}`
             ];
             ];
 
 
         if (resourceSessionToken) {
         if (resourceSessionToken) {
-            const { resourceSession } = await validateResourceSessionToken(
-                resourceSessionToken,
-                resource.resourceId
-            );
+            const sessionCacheKey = `session:${resourceSessionToken}`;
+            let resourceSession: any = cache.get(sessionCacheKey);
+
+            if (!resourceSession) {
+                const result = await validateResourceSessionToken(
+                    resourceSessionToken,
+                    resource.resourceId
+                );
+
+                resourceSession = result?.resourceSession;
+                cache.set(sessionCacheKey, resourceSession);
+            }
+
+            if (resourceSession?.isRequestToken) {
+                logger.debug(
+                    "Resource not allowed because session is a temporary request token"
+                );
+                return notAllowed(res);
+            }
 
 
             if (resourceSession) {
             if (resourceSession) {
                 if (pincode && resourceSession.pincodeId) {
                 if (pincode && resourceSession.pincodeId) {
@@ -208,6 +234,29 @@ export async function verifyResourceSession(
                     );
                     );
                     return allowed(res);
                     return allowed(res);
                 }
                 }
+
+                if (resourceSession.userSessionId && sso) {
+                    const userAccessCacheKey = `userAccess:${resourceSession.userSessionId}:${resource.resourceId}`;
+
+                    let isAllowed: boolean | undefined =
+                        cache.get(userAccessCacheKey);
+
+                    if (isAllowed === undefined) {
+                        isAllowed = await isUserAllowedToAccessResource(
+                            resourceSession.userSessionId,
+                            resource
+                        );
+
+                        cache.set(userAccessCacheKey, isAllowed);
+                    }
+
+                    if (isAllowed) {
+                        logger.debug(
+                            "Resource allowed because user session is valid"
+                        );
+                        return allowed(res);
+                    }
+                }
             }
             }
         }
         }
 
 
@@ -272,10 +321,15 @@ async function createAccessTokenSession(
         expiresAt: tokenItem.expiresAt,
         expiresAt: tokenItem.expiresAt,
         doNotExtend: tokenItem.expiresAt ? true : false
         doNotExtend: tokenItem.expiresAt ? true : false
     });
     });
-    const cookieName = `${config.getRawConfig().server.resource_session_cookie_name}_${resource.resourceId}`;
-    const cookie = serializeResourceSessionCookie(cookieName, token);
+    const cookieName = `${config.getRawConfig().server.session_cookie_name}`;
+    const cookie = serializeResourceSessionCookie(
+        cookieName,
+        resource.fullDomain,
+        token,
+        !resource.ssl
+    );
     res.appendHeader("Set-Cookie", cookie);
     res.appendHeader("Set-Cookie", cookie);
-    logger.debug("Access token is valid, creating new session")
+    logger.debug("Access token is valid, creating new session");
     return response<VerifyUserResponse>(res, {
     return response<VerifyUserResponse>(res, {
         data: { valid: true },
         data: { valid: true },
         success: true,
         success: true,
@@ -286,9 +340,22 @@ async function createAccessTokenSession(
 }
 }
 
 
 async function isUserAllowedToAccessResource(
 async function isUserAllowedToAccessResource(
-    user: User,
+    userSessionId: string,
     resource: Resource
     resource: Resource
 ): Promise<boolean> {
 ): Promise<boolean> {
+    const [res] = await db
+        .select()
+        .from(sessions)
+        .leftJoin(users, eq(users.userId, sessions.userId))
+        .where(eq(sessions.sessionId, userSessionId));
+
+    const user = res.user;
+    const session = res.session;
+
+    if (!user || !session) {
+        return false;
+    }
+
     if (
     if (
         config.getRawConfig().flags?.require_email_verification &&
         config.getRawConfig().flags?.require_email_verification &&
         !user.emailVerified
         !user.emailVerified

+ 11 - 1
server/routers/internal.ts

@@ -3,7 +3,9 @@ import * as gerbil from "@server/routers/gerbil";
 import * as badger from "@server/routers/badger";
 import * as badger from "@server/routers/badger";
 import * as traefik from "@server/routers/traefik";
 import * as traefik from "@server/routers/traefik";
 import * as auth from "@server/routers/auth";
 import * as auth from "@server/routers/auth";
+import * as resource from "@server/routers/resource";
 import HttpCode from "@server/types/HttpCode";
 import HttpCode from "@server/types/HttpCode";
+import { verifyResourceAccess, verifySessionUserMiddleware } from "@server/middlewares";
 
 
 // Root routes
 // Root routes
 const internalRouter = Router();
 const internalRouter = Router();
@@ -15,7 +17,14 @@ internalRouter.get("/", (_, res) => {
 internalRouter.get("/traefik-config", traefik.traefikConfigProvider);
 internalRouter.get("/traefik-config", traefik.traefikConfigProvider);
 internalRouter.get(
 internalRouter.get(
     "/resource-session/:resourceId/:token",
     "/resource-session/:resourceId/:token",
-    auth.checkResourceSession,
+    auth.checkResourceSession
+);
+
+internalRouter.post(
+    `/resource/:resourceId/get-exchange-token`,
+    verifySessionUserMiddleware,
+    verifyResourceAccess,
+    resource.getExchangeToken
 );
 );
 
 
 // Gerbil routes
 // Gerbil routes
@@ -30,5 +39,6 @@ const badgerRouter = Router();
 internalRouter.use("/badger", badgerRouter);
 internalRouter.use("/badger", badgerRouter);
 
 
 badgerRouter.post("/verify-session", badger.verifyResourceSession);
 badgerRouter.post("/verify-session", badger.verifyResourceSession);
+badgerRouter.post("/exchange-session", badger.exchangeSession);
 
 
 export default internalRouter;
 export default internalRouter;

+ 6 - 10
server/routers/resource/authWithAccessToken.ts

@@ -1,18 +1,16 @@
 import { generateSessionToken } from "@server/auth/sessions/app";
 import { generateSessionToken } from "@server/auth/sessions/app";
 import db from "@server/db";
 import db from "@server/db";
-import { resourceAccessToken, resources } from "@server/db/schema";
+import { resources } from "@server/db/schema";
 import HttpCode from "@server/types/HttpCode";
 import HttpCode from "@server/types/HttpCode";
 import response from "@server/lib/response";
 import response from "@server/lib/response";
-import { eq, and } from "drizzle-orm";
+import { eq } from "drizzle-orm";
 import { NextFunction, Request, Response } from "express";
 import { NextFunction, Request, Response } from "express";
 import createHttpError from "http-errors";
 import createHttpError from "http-errors";
 import { z } from "zod";
 import { z } from "zod";
 import { fromError } from "zod-validation-error";
 import { fromError } from "zod-validation-error";
 import {
 import {
     createResourceSession,
     createResourceSession,
-    serializeResourceSessionCookie
 } from "@server/auth/sessions/resource";
 } from "@server/auth/sessions/resource";
-import config from "@server/lib/config";
 import logger from "@server/logger";
 import logger from "@server/logger";
 import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
 import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
 
 
@@ -108,13 +106,11 @@ export async function authWithAccessToken(
             resourceId,
             resourceId,
             token,
             token,
             accessTokenId: tokenItem.accessTokenId,
             accessTokenId: tokenItem.accessTokenId,
-            sessionLength: tokenItem.sessionLength,
-            expiresAt: tokenItem.expiresAt,
-            doNotExtend: tokenItem.expiresAt ? true : false
+            isRequestToken: true,
+            expiresAt: Date.now() + 1000 * 30, // 30 seconds
+            sessionLength: 1000 * 30,
+            doNotExtend: true
         });
         });
-        const cookieName = `${config.getRawConfig().server.resource_session_cookie_name}_${resource.resourceId}`;
-        const cookie = serializeResourceSessionCookie(cookieName, token);
-        res.appendHeader("Set-Cookie", cookie);
 
 
         return response<AuthWithAccessTokenResponse>(res, {
         return response<AuthWithAccessTokenResponse>(res, {
             data: {
             data: {

+ 5 - 6
server/routers/resource/authWithPassword.ts

@@ -11,9 +11,7 @@ import { z } from "zod";
 import { fromError } from "zod-validation-error";
 import { fromError } from "zod-validation-error";
 import {
 import {
     createResourceSession,
     createResourceSession,
-    serializeResourceSessionCookie
 } from "@server/auth/sessions/resource";
 } from "@server/auth/sessions/resource";
-import config from "@server/lib/config";
 import logger from "@server/logger";
 import logger from "@server/logger";
 import { verifyPassword } from "@server/auth/password";
 import { verifyPassword } from "@server/auth/password";
 
 
@@ -120,11 +118,12 @@ export async function authWithPassword(
         await createResourceSession({
         await createResourceSession({
             resourceId,
             resourceId,
             token,
             token,
-            passwordId: definedPassword.passwordId
+            passwordId: definedPassword.passwordId,
+            isRequestToken: true,
+            expiresAt: Date.now() + 1000 * 30, // 30 seconds
+            sessionLength: 1000 * 30,
+            doNotExtend: true
         });
         });
-        const cookieName = `${config.getRawConfig().server.resource_session_cookie_name}_${resource.resourceId}`;
-        const cookie = serializeResourceSessionCookie(cookieName, token);
-        res.appendHeader("Set-Cookie", cookie);
 
 
         return response<AuthWithPasswordResponse>(res, {
         return response<AuthWithPasswordResponse>(res, {
             data: {
             data: {

+ 6 - 12
server/routers/resource/authWithPincode.ts

@@ -1,28 +1,21 @@
-import { verify } from "@node-rs/argon2";
 import { generateSessionToken } from "@server/auth/sessions/app";
 import { generateSessionToken } from "@server/auth/sessions/app";
 import db from "@server/db";
 import db from "@server/db";
 import {
 import {
     orgs,
     orgs,
-    resourceOtp,
     resourcePincode,
     resourcePincode,
     resources,
     resources,
-    resourceWhitelist
 } from "@server/db/schema";
 } from "@server/db/schema";
 import HttpCode from "@server/types/HttpCode";
 import HttpCode from "@server/types/HttpCode";
 import response from "@server/lib/response";
 import response from "@server/lib/response";
-import { and, eq } from "drizzle-orm";
+import { eq } from "drizzle-orm";
 import { NextFunction, Request, Response } from "express";
 import { NextFunction, Request, Response } from "express";
 import createHttpError from "http-errors";
 import createHttpError from "http-errors";
 import { z } from "zod";
 import { z } from "zod";
 import { fromError } from "zod-validation-error";
 import { fromError } from "zod-validation-error";
 import {
 import {
     createResourceSession,
     createResourceSession,
-    serializeResourceSessionCookie
 } from "@server/auth/sessions/resource";
 } from "@server/auth/sessions/resource";
 import logger from "@server/logger";
 import logger from "@server/logger";
-import config from "@server/lib/config";
-import { AuthWithPasswordResponse } from "./authWithPassword";
-import { isValidOtp, sendResourceOtpEmail } from "@server/auth/resourceOtp";
 import { verifyPassword } from "@server/auth/password";
 import { verifyPassword } from "@server/auth/password";
 
 
 export const authWithPincodeBodySchema = z
 export const authWithPincodeBodySchema = z
@@ -128,11 +121,12 @@ export async function authWithPincode(
         await createResourceSession({
         await createResourceSession({
             resourceId,
             resourceId,
             token,
             token,
-            pincodeId: definedPincode.pincodeId
+            pincodeId: definedPincode.pincodeId,
+            isRequestToken: true,
+            expiresAt: Date.now() + 1000 * 30, // 30 seconds
+            sessionLength: 1000 * 30,
+            doNotExtend: true
         });
         });
-        const cookieName = `${config.getRawConfig().server.resource_session_cookie_name}_${resource.resourceId}`;
-        const cookie = serializeResourceSessionCookie(cookieName, token);
-        res.appendHeader("Set-Cookie", cookie);
 
 
         return response<AuthWithPincodeResponse>(res, {
         return response<AuthWithPincodeResponse>(res, {
             data: {
             data: {

+ 5 - 7
server/routers/resource/authWithWhitelist.ts

@@ -3,7 +3,6 @@ import db from "@server/db";
 import {
 import {
     orgs,
     orgs,
     resourceOtp,
     resourceOtp,
-    resourcePassword,
     resources,
     resources,
     resourceWhitelist
     resourceWhitelist
 } from "@server/db/schema";
 } from "@server/db/schema";
@@ -16,9 +15,7 @@ import { z } from "zod";
 import { fromError } from "zod-validation-error";
 import { fromError } from "zod-validation-error";
 import {
 import {
     createResourceSession,
     createResourceSession,
-    serializeResourceSessionCookie
 } from "@server/auth/sessions/resource";
 } from "@server/auth/sessions/resource";
-import config from "@server/lib/config";
 import { isValidOtp, sendResourceOtpEmail } from "@server/auth/resourceOtp";
 import { isValidOtp, sendResourceOtpEmail } from "@server/auth/resourceOtp";
 import logger from "@server/logger";
 import logger from "@server/logger";
 
 
@@ -178,11 +175,12 @@ export async function authWithWhitelist(
         await createResourceSession({
         await createResourceSession({
             resourceId,
             resourceId,
             token,
             token,
-            whitelistId: whitelistedEmail.whitelistId
+            whitelistId: whitelistedEmail.whitelistId,
+            isRequestToken: true,
+            expiresAt: Date.now() + 1000 * 30, // 30 seconds
+            sessionLength: 1000 * 30,
+            doNotExtend: true
         });
         });
-        const cookieName = `${config.getRawConfig().server.resource_session_cookie_name}_${resource.resourceId}`;
-        const cookie = serializeResourceSessionCookie(cookieName, token);
-        res.appendHeader("Set-Cookie", cookie);
 
 
         return response<AuthWithWhitelistResponse>(res, {
         return response<AuthWithWhitelistResponse>(res, {
             data: {
             data: {

+ 109 - 0
server/routers/resource/getExchangeToken.ts

@@ -0,0 +1,109 @@
+import { Request, Response, NextFunction } from "express";
+import { z } from "zod";
+import { db } from "@server/db";
+import { resources } from "@server/db/schema";
+import { eq } from "drizzle-orm";
+import { createResourceSession } from "@server/auth/sessions/resource";
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import { fromError } from "zod-validation-error";
+import logger from "@server/logger";
+import { generateSessionToken } from "@server/auth/sessions/app";
+import config from "@server/lib/config";
+import {
+    encodeHexLowerCase
+} from "@oslojs/encoding";
+import { sha256 } from "@oslojs/crypto/sha2";
+import { response } from "@server/lib";
+
+const getExchangeTokenParams = z
+    .object({
+        resourceId: z
+            .string()
+            .transform(Number)
+            .pipe(z.number().int().positive())
+    })
+    .strict();
+
+export type GetExchangeTokenResponse = {
+    requestToken: string;
+};
+
+export async function getExchangeToken(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
+    try {
+        const parsedParams = getExchangeTokenParams.safeParse(req.params);
+        if (!parsedParams.success) {
+            return next(
+                createHttpError(
+                    HttpCode.BAD_REQUEST,
+                    fromError(parsedParams.error).toString()
+                )
+            );
+        }
+
+        const { resourceId } = parsedParams.data;
+
+        const resource = await db
+            .select()
+            .from(resources)
+            .where(eq(resources.resourceId, resourceId))
+            .limit(1);
+
+        if (resource.length === 0) {
+            return next(
+                createHttpError(
+                    HttpCode.NOT_FOUND,
+                    `Resource with ID ${resourceId} not found`
+                )
+            );
+        }
+
+        const ssoSession =
+            req.cookies[config.getRawConfig().server.session_cookie_name];
+        if (!ssoSession) {
+            logger.debug(ssoSession);
+            return next(
+                createHttpError(
+                    HttpCode.UNAUTHORIZED,
+                    "Missing SSO session cookie"
+                )
+            );
+        }
+
+        const sessionId = encodeHexLowerCase(
+            sha256(new TextEncoder().encode(ssoSession))
+        );
+
+        const token = generateSessionToken();
+        await createResourceSession({
+            resourceId,
+            token,
+            userSessionId: sessionId,
+            isRequestToken: true,
+            expiresAt: Date.now() + 1000 * 30, // 30 seconds
+            sessionLength: 1000 * 30,
+            doNotExtend: true
+        });
+
+        logger.debug("Request token created successfully");
+
+        return response<GetExchangeTokenResponse>(res, {
+            data: {
+                requestToken: token
+            },
+            success: true,
+            error: false,
+            message: "Request token created successfully",
+            status: HttpCode.OK
+        });
+    } catch (error) {
+        logger.error(error);
+        return next(
+            createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
+        );
+    }
+}

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

@@ -16,3 +16,4 @@ export * from "./setResourceWhitelist";
 export * from "./getResourceWhitelist";
 export * from "./getResourceWhitelist";
 export * from "./authWithWhitelist";
 export * from "./authWithWhitelist";
 export * from "./authWithAccessToken";
 export * from "./authWithAccessToken";
+export * from "./getExchangeToken";

+ 4 - 0
server/routers/resource/updateResource.ts

@@ -111,6 +111,10 @@ export async function updateResource(
             );
             );
         }
         }
 
 
+        if (resource[0].resources.ssl !== updatedResource[0].ssl) {
+            // invalidate all sessions?
+        }
+
         return response(res, {
         return response(res, {
             data: updatedResource[0],
             data: updatedResource[0],
             success: true,
             success: true,

+ 5 - 7
server/routers/traefik/getTraefikConfig.ts

@@ -39,7 +39,7 @@ export async function traefikConfigProvider(
         }
         }
 
 
         const badgerMiddlewareName = "badger";
         const badgerMiddlewareName = "badger";
-        const redirectMiddlewareName = "redirect-to-https";
+        const redirectHttpsMiddlewareName = "redirect-to-https";
 
 
         const http: any = {
         const http: any = {
             routers: {},
             routers: {},
@@ -52,19 +52,18 @@ export async function traefikConfigProvider(
                                 "/api/v1",
                                 "/api/v1",
                                 `http://${config.getRawConfig().server.internal_hostname}:${config.getRawConfig().server.internal_port}`,
                                 `http://${config.getRawConfig().server.internal_hostname}:${config.getRawConfig().server.internal_port}`,
                             ).href,
                             ).href,
-                            resourceSessionCookieName:
-                                config.getRawConfig().server.resource_session_cookie_name,
                             userSessionCookieName:
                             userSessionCookieName:
                                 config.getRawConfig().server.session_cookie_name,
                                 config.getRawConfig().server.session_cookie_name,
                             accessTokenQueryParam: config.getRawConfig().server.resource_access_token_param,
                             accessTokenQueryParam: config.getRawConfig().server.resource_access_token_param,
+                            resourceSessionRequestParam: config.getRawConfig().server.resource_session_request_param
                         },
                         },
                     },
                     },
                 },
                 },
-                [redirectMiddlewareName]: {
+                [redirectHttpsMiddlewareName]: {
                     redirectScheme: {
                     redirectScheme: {
                         scheme: "https"
                         scheme: "https"
                     },
                     },
-                },
+                }
             },
             },
         };
         };
         for (const item of all) {
         for (const item of all) {
@@ -120,10 +119,9 @@ export async function traefikConfigProvider(
             };
             };
 
 
             if (resource.ssl) {
             if (resource.ssl) {
-                // this is a redirect router; all it does is redirect to the https version if tls is enabled
                 http.routers![routerName + "-redirect"] = {
                 http.routers![routerName + "-redirect"] = {
                     entryPoints: [config.getRawConfig().traefik.http_entrypoint],
                     entryPoints: [config.getRawConfig().traefik.http_entrypoint],
-                    middlewares: [redirectMiddlewareName],
+                    middlewares: [redirectHttpsMiddlewareName],
                     service: serviceName,
                     service: serviceName,
                     rule: `Host(\`${fullDomain}\`)`,
                     rule: `Host(\`${fullDomain}\`)`,
                 };
                 };

+ 97 - 0
server/setup/scripts/1.0.0-beta9.ts

@@ -7,7 +7,13 @@ import {
     userInvites,
     userInvites,
     users
     users
 } from "@server/db/schema";
 } from "@server/db/schema";
+import { APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts";
 import { sql } from "drizzle-orm";
 import { sql } from "drizzle-orm";
+import fs from "fs";
+import yaml from "js-yaml";
+import path from "path";
+import { z } from "zod";
+import { fromZodError } from "zod-validation-error";
 
 
 export default async function migration() {
 export default async function migration() {
     console.log("Running setup script 1.0.0-beta.9...");
     console.log("Running setup script 1.0.0-beta.9...");
@@ -32,5 +38,96 @@ export default async function migration() {
         console.error(error);
         console.error(error);
     }
     }
 
 
+    try {
+        // Determine which config file exists
+        const filePaths = [configFilePath1, configFilePath2];
+        let filePath = "";
+        for (const path of filePaths) {
+            if (fs.existsSync(path)) {
+                filePath = path;
+                break;
+            }
+        }
+
+        if (!filePath) {
+            throw new Error(
+                `No config file found (expected config.yml or config.yaml).`
+            );
+        }
+
+        // Read and parse the YAML file
+        let rawConfig: any;
+        const fileContents = fs.readFileSync(filePath, "utf8");
+        rawConfig = yaml.load(fileContents);
+
+        rawConfig.server.resource_session_request_param = "p_session_request";
+        rawConfig.server.session_cookie_name = "p_session_token"; // rename to prevent conflicts
+        delete rawConfig.server.resource_session_cookie_name;
+
+        // Write the updated YAML back to the file
+        const updatedYaml = yaml.dump(rawConfig);
+        fs.writeFileSync(filePath, updatedYaml, "utf8");
+    } catch (e) {
+        console.log(
+            `Failed to add resource_session_request_param to config. Please add it manually. https://docs.fossorial.io/Pangolin/Configuration/config`
+        );
+        throw e;
+    }
+
+    try {
+        const traefikPath = path.join(
+            APP_PATH,
+            "traefik",
+            "traefik_config.yml"
+        );
+
+        const schema = z.object({
+            experimental: z.object({
+                plugins: z.object({
+                    badger: z.object({
+                        moduleName: z.string(),
+                        version: z.string()
+                    })
+                })
+            })
+        });
+
+        const traefikFileContents = fs.readFileSync(traefikPath, "utf8");
+        const traefikConfig = yaml.load(traefikFileContents) as any;
+
+        const parsedConfig = schema.safeParse(traefikConfig);
+
+        if (!parsedConfig.success) {
+            throw new Error(fromZodError(parsedConfig.error).toString());
+        }
+
+        traefikConfig.experimental.plugins.badger.version = "v1.0.0-beta.3";
+
+        const updatedTraefikYaml = yaml.dump(traefikConfig);
+
+        fs.writeFileSync(traefikPath, updatedTraefikYaml, "utf8");
+
+        console.log(
+            "Updated the version of Badger in your Traefik configuration to v1.0.0-beta.3."
+        );
+    } catch (e) {
+        console.log(
+            "We were unable to update the version of Badger in your Traefik configuration. Please update it manually."
+        );
+        console.error(e);
+    }
+
+    try {
+        await db.transaction(async (trx) => {
+            trx.run(sql`ALTER TABLE 'resourceSessions' ADD 'isRequestToken' integer;`);
+            trx.run(sql`ALTER TABLE 'resourceSessions' ADD 'userSessionId' text REFERENCES session(id);`);
+        });
+    } catch (e) {
+        console.log(
+            "We were unable to add columns to the resourceSessions table."
+        );
+        throw e;
+    }
+
     console.log("Done.");
     console.log("Done.");
 }
 }

+ 15 - 4
src/app/auth/resource/[resourceId]/AccessToken.tsx

@@ -5,14 +5,12 @@ import { Button } from "@app/components/ui/button";
 import {
 import {
     Card,
     Card,
     CardContent,
     CardContent,
-    CardFooter,
     CardHeader,
     CardHeader,
     CardTitle
     CardTitle
 } from "@app/components/ui/card";
 } from "@app/components/ui/card";
 import { useEnvContext } from "@app/hooks/useEnvContext";
 import { useEnvContext } from "@app/hooks/useEnvContext";
 import { AuthWithAccessTokenResponse } from "@server/routers/resource";
 import { AuthWithAccessTokenResponse } from "@server/routers/resource";
 import { AxiosResponse } from "axios";
 import { AxiosResponse } from "axios";
-import { Loader2 } from "lucide-react";
 import Link from "next/link";
 import Link from "next/link";
 import { useEffect, useState } from "react";
 import { useEffect, useState } from "react";
 
 
@@ -32,7 +30,17 @@ export default function AccessToken({
     const [loading, setLoading] = useState(true);
     const [loading, setLoading] = useState(true);
     const [isValid, setIsValid] = useState(false);
     const [isValid, setIsValid] = useState(false);
 
 
-    const api = createApiClient(useEnvContext());
+    const { env } = useEnvContext();
+    const api = createApiClient({ env });
+
+    function appendRequestToken(url: string, token: string) {
+        const fullUrl = new URL(url);
+        fullUrl.searchParams.append(
+            env.server.resourceSessionRequestParam,
+            token
+        );
+        return fullUrl.toString();
+    }
 
 
     useEffect(() => {
     useEffect(() => {
         if (!accessTokenId || !accessToken) {
         if (!accessTokenId || !accessToken) {
@@ -51,7 +59,10 @@ export default function AccessToken({
 
 
                 if (res.data.data.session) {
                 if (res.data.data.session) {
                     setIsValid(true);
                     setIsValid(true);
-                    window.location.href = redirectUrl;
+                    window.location.href = appendRequestToken(
+                        redirectUrl,
+                        res.data.data.session
+                    );
                 }
                 }
             } catch (e) {
             } catch (e) {
                 console.error("Error checking access token", e);
                 console.error("Error checking access token", e);

+ 16 - 21
src/app/auth/resource/[resourceId]/ResourceAuthPortal.tsx

@@ -1,6 +1,6 @@
 "use client";
 "use client";
 
 
-import { useEffect, useState, useSyncExternalStore } from "react";
+import { useState } from "react";
 import { zodResolver } from "@hookform/resolvers/zod";
 import { zodResolver } from "@hookform/resolvers/zod";
 import { useForm } from "react-hook-form";
 import { useForm } from "react-hook-form";
 import * as z from "zod";
 import * as z from "zod";
@@ -8,7 +8,6 @@ import {
     Card,
     Card,
     CardContent,
     CardContent,
     CardDescription,
     CardDescription,
-    CardFooter,
     CardHeader,
     CardHeader,
     CardTitle
     CardTitle
 } from "@/components/ui/card";
 } from "@/components/ui/card";
@@ -30,9 +29,6 @@ import {
     Key,
     Key,
     User,
     User,
     Send,
     Send,
-    ArrowLeft,
-    ArrowRight,
-    Lock,
     AtSign
     AtSign
 } from "lucide-react";
 } from "lucide-react";
 import {
 import {
@@ -47,10 +43,8 @@ import { AxiosResponse } from "axios";
 import LoginForm from "@app/components/LoginForm";
 import LoginForm from "@app/components/LoginForm";
 import {
 import {
     AuthWithPasswordResponse,
     AuthWithPasswordResponse,
-    AuthWithAccessTokenResponse,
     AuthWithWhitelistResponse
     AuthWithWhitelistResponse
 } from "@server/routers/resource";
 } from "@server/routers/resource";
-import { redirect } from "next/dist/server/api-utils";
 import ResourceAccessDenied from "./ResourceAccessDenied";
 import ResourceAccessDenied from "./ResourceAccessDenied";
 import { createApiClient } from "@app/lib/api";
 import { createApiClient } from "@app/lib/api";
 import { useEnvContext } from "@app/hooks/useEnvContext";
 import { useEnvContext } from "@app/hooks/useEnvContext";
@@ -118,7 +112,9 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
 
 
     const [otpState, setOtpState] = useState<"idle" | "otp_sent">("idle");
     const [otpState, setOtpState] = useState<"idle" | "otp_sent">("idle");
 
 
-    const api = createApiClient(useEnvContext());
+    const { env } = useEnvContext();
+
+    const api = createApiClient({ env });
 
 
     function getDefaultSelectedMethod() {
     function getDefaultSelectedMethod() {
         if (props.methods.sso) {
         if (props.methods.sso) {
@@ -169,6 +165,15 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
         }
         }
     });
     });
 
 
+    function appendRequestToken(url: string, token: string) {
+        const fullUrl = new URL(url);
+        fullUrl.searchParams.append(
+            env.server.resourceSessionRequestParam,
+            token
+        );
+        return fullUrl.toString();
+    }
+
     const onWhitelistSubmit = (values: any) => {
     const onWhitelistSubmit = (values: any) => {
         setLoadingLogin(true);
         setLoadingLogin(true);
         api.post<AxiosResponse<AuthWithWhitelistResponse>>(
         api.post<AxiosResponse<AuthWithWhitelistResponse>>(
@@ -190,7 +195,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
 
 
                 const session = res.data.data.session;
                 const session = res.data.data.session;
                 if (session) {
                 if (session) {
-                    window.location.href = props.redirect;
+                    window.location.href = appendRequestToken(props.redirect, session);
                 }
                 }
             })
             })
             .catch((e) => {
             .catch((e) => {
@@ -212,7 +217,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
                 setPincodeError(null);
                 setPincodeError(null);
                 const session = res.data.data.session;
                 const session = res.data.data.session;
                 if (session) {
                 if (session) {
-                    window.location.href = props.redirect;
+                    window.location.href = appendRequestToken(props.redirect, session);
                 }
                 }
             })
             })
             .catch((e) => {
             .catch((e) => {
@@ -237,7 +242,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
                 setPasswordError(null);
                 setPasswordError(null);
                 const session = res.data.data.session;
                 const session = res.data.data.session;
                 if (session) {
                 if (session) {
-                    window.location.href = props.redirect;
+                    window.location.href = appendRequestToken(props.redirect, session);
                 }
                 }
             })
             })
             .catch((e) => {
             .catch((e) => {
@@ -619,16 +624,6 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
                             </Tabs>
                             </Tabs>
                         </CardContent>
                         </CardContent>
                     </Card>
                     </Card>
-                    {/* {activeTab === "sso" && (
-                        <div className="flex justify-center mt-4">
-                            <p className="text-sm text-muted-foreground">
-                                Don't have an account?{" "}
-                                <a href="#" className="underline">
-                                    Sign up
-                                </a>
-                            </p>
-                        </div>
-                    )} */}
                 </div>
                 </div>
             ) : (
             ) : (
                 <ResourceAccessDenied />
                 <ResourceAccessDenied />

+ 22 - 34
src/app/auth/resource/[resourceId]/page.tsx

@@ -1,7 +1,6 @@
 import {
 import {
-    AuthWithAccessTokenResponse,
     GetResourceAuthInfoResponse,
     GetResourceAuthInfoResponse,
-    GetResourceResponse
+    GetExchangeTokenResponse
 } from "@server/routers/resource";
 } from "@server/routers/resource";
 import ResourceAuthPortal from "./ResourceAuthPortal";
 import ResourceAuthPortal from "./ResourceAuthPortal";
 import { internal, priv } from "@app/lib/api";
 import { internal, priv } from "@app/lib/api";
@@ -12,9 +11,6 @@ import { verifySession } from "@app/lib/auth/verifySession";
 import { redirect } from "next/navigation";
 import { redirect } from "next/navigation";
 import ResourceNotFound from "./ResourceNotFound";
 import ResourceNotFound from "./ResourceNotFound";
 import ResourceAccessDenied from "./ResourceAccessDenied";
 import ResourceAccessDenied from "./ResourceAccessDenied";
-import { cookies } from "next/headers";
-import { CheckResourceSessionResponse } from "@server/routers/auth";
-import AccessTokenInvalid from "./AccessToken";
 import AccessToken from "./AccessToken";
 import AccessToken from "./AccessToken";
 import { pullEnv } from "@app/lib/pullEnv";
 import { pullEnv } from "@app/lib/pullEnv";
 
 
@@ -48,7 +44,7 @@ export default async function ResourceAuthPage(props: {
         // TODO: fix this
         // TODO: fix this
         return (
         return (
             <div className="w-full max-w-md">
             <div className="w-full max-w-md">
-            {/* @ts-ignore */}
+                {/* @ts-ignore */}
                 <ResourceNotFound />
                 <ResourceNotFound />
             </div>
             </div>
         );
         );
@@ -83,49 +79,41 @@ export default async function ResourceAuthPage(props: {
         );
         );
     }
     }
 
 
-    const allCookies = await cookies();
-    const cookieName =
-        env.server.resourceSessionCookieName + `_${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}`);
-
-            if (res && res.data.data.valid) {
-                doRedirect = true;
-            }
-        } catch (e) {}
-
-        if (doRedirect) {
-            redirect(redirectUrl);
-        }
-    }
-
     if (!hasAuth) {
     if (!hasAuth) {
         // no authentication so always go straight to the resource
         // no authentication so always go straight to the resource
         redirect(redirectUrl);
         redirect(redirectUrl);
     }
     }
 
 
+
+    // convert the dashboard token into a resource session token
     let userIsUnauthorized = false;
     let userIsUnauthorized = false;
     if (user && authInfo.sso) {
     if (user && authInfo.sso) {
-        let doRedirect = false;
+        let redirectToUrl: string | undefined;
         try {
         try {
-            const res = await internal.get<AxiosResponse<GetResourceResponse>>(
-                `/resource/${params.resourceId}`,
+            const res = await priv.post<
+                AxiosResponse<GetExchangeTokenResponse>
+            >(
+                `/resource/${params.resourceId}/get-exchange-token`,
+                {},
                 await authCookieHeader()
                 await authCookieHeader()
             );
             );
 
 
-            doRedirect = true;
+            if (res.data.data.requestToken) {
+                const paramName = env.server.resourceSessionRequestParam;
+                // append the param with the token to the redirect url
+                const fullUrl = new URL(redirectUrl);
+                fullUrl.searchParams.append(
+                    paramName,
+                    res.data.data.requestToken
+                );
+                redirectToUrl = fullUrl.toString();
+            }
         } catch (e) {
         } catch (e) {
             userIsUnauthorized = true;
             userIsUnauthorized = true;
         }
         }
 
 
-        if (doRedirect) {
-            redirect(redirectUrl);
+        if (redirectToUrl) {
+            redirect(redirectToUrl);
         }
         }
     }
     }
 
 

+ 2 - 2
src/lib/pullEnv.ts

@@ -6,8 +6,8 @@ export function pullEnv(): Env {
             nextPort: process.env.NEXT_PORT as string,
             nextPort: process.env.NEXT_PORT as string,
             externalPort: process.env.SERVER_EXTERNAL_PORT as string,
             externalPort: process.env.SERVER_EXTERNAL_PORT as string,
             sessionCookieName: process.env.SESSION_COOKIE_NAME as string,
             sessionCookieName: process.env.SESSION_COOKIE_NAME as string,
-            resourceSessionCookieName: process.env.RESOURCE_SESSION_COOKIE_NAME as string,
-            resourceAccessTokenParam: process.env.RESOURCE_ACCESS_TOKEN_PARAM as string
+            resourceAccessTokenParam: process.env.RESOURCE_ACCESS_TOKEN_PARAM as string,
+            resourceSessionRequestParam: process.env.RESOURCE_SESSION_REQUEST_PARAM as string
         },
         },
         app: {
         app: {
             environment: process.env.ENVIRONMENT as string,
             environment: process.env.ENVIRONMENT as string,

+ 1 - 1
src/lib/types/env.ts

@@ -7,8 +7,8 @@ export type Env = {
         externalPort: string;
         externalPort: string;
         nextPort: string;
         nextPort: string;
         sessionCookieName: string;
         sessionCookieName: string;
-        resourceSessionCookieName: string;
         resourceAccessTokenParam: string;
         resourceAccessTokenParam: string;
+        resourceSessionRequestParam: string;
     },
     },
     email: {
     email: {
         emailEnabled: boolean;
         emailEnabled: boolean;