Prechádzať zdrojové kódy

allow controlling cors from config and add cors middleware to traefik

Milo Schwartz 6 mesiacov pred
rodič
commit
ab18e15a71

+ 7 - 5
config/config.example.yml

@@ -1,15 +1,15 @@
 app:
-    dashboard_url: http://localhost
+    dashboard_url: http://localhost:3002
     base_domain: localhost
-    log_level: debug
+    log_level: info
     save_logs: false
 
 server:
     external_port: 3000
     internal_port: 3001
     next_port: 3002
-    internal_hostname: localhost
-    secure_cookies: false
+    internal_hostname: pangolin
+    secure_cookies: true
     session_cookie_name: p_session
     resource_session_cookie_name: p_resource_session
     resource_access_token_param: p_token
@@ -38,4 +38,6 @@ users:
         password: Password123!
 
 flags:
-    require_email_verification: false
+    require_email_verification: true
+    disable_signup_without_invite: true
+    disable_user_create_org: true

+ 22 - 0
install/fs/traefik/dynamic_config.yml

@@ -4,6 +4,21 @@ http:
       redirectScheme:
         scheme: https
         permanent: true
+    cors:
+      headers:
+        accessControlAllowMethods:
+          - GET
+          - PUT
+          - POST
+          - DELETE
+          - PATCH
+        accessControlAllowHeaders:
+          - Content-Type
+          - X-CSRF-Token
+        accessControlAllowOriginList:
+          - https://{{.DashboardDomain}}
+        accessControlAllowCredentials: false
+
 
   routers:
     # HTTP to HTTPS redirect router
@@ -14,6 +29,7 @@ http:
         - web
       middlewares:
         - redirect-to-https
+        - cors
 
     # Next.js router (handles everything except API and WebSocket paths)
     next-router:
@@ -21,6 +37,8 @@ http:
       service: next-service
       entryPoints:
         - websecure
+      middlewares:
+        - cors
       tls:
         certResolver: letsencrypt
 
@@ -30,6 +48,8 @@ http:
       service: api-service
       entryPoints:
         - websecure
+      middlewares:
+        - cors
       tls:
         certResolver: letsencrypt
 
@@ -39,6 +59,8 @@ http:
       service: api-service
       entryPoints:
         - websecure
+      middlewares:
+        - cors
       tls:
         certResolver: letsencrypt
 

+ 25 - 17
server/apiServer.ts

@@ -20,23 +20,30 @@ const externalPort = config.getRawConfig().server.external_port;
 export function createApiServer() {
     const apiServer = express();
 
-    // Middleware setup
     apiServer.set("trust proxy", 1);
-    if (dev) {
-        apiServer.use(
-            cors({
-                origin: `http://localhost:${config.getRawConfig().server.next_port}`,
-                credentials: true
-            })
-        );
-    } else {
-        const corsOptions = {
-            origin: config.getRawConfig().app.dashboard_url,
-            methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
-            allowedHeaders: ["Content-Type", "X-CSRF-Token"]
-        };
-
-        apiServer.use(cors(corsOptions));
+
+    const corsConfig = config.getRawConfig().server.cors;
+
+    const options = {
+        ...(corsConfig?.origins
+            ? { origin: corsConfig.origins }
+            : {
+                  origin: (origin: any, callback: any) => {
+                      callback(null, true);
+                  }
+              }),
+        ...(corsConfig?.methods && { methods: corsConfig.methods }),
+        ...(corsConfig?.allowed_headers && {
+            allowedHeaders: corsConfig.allowed_headers
+        }),
+        credentials: !(corsConfig?.credentials === false)
+    };
+
+    logger.debug("Using CORS options", options);
+
+    apiServer.use(cors(options));
+
+    if (!dev) {
         apiServer.use(helmet());
         apiServer.use(csrfProtectionMiddleware);
     }
@@ -47,7 +54,8 @@ export function createApiServer() {
     if (!dev) {
         apiServer.use(
             rateLimitMiddleware({
-                windowMin: config.getRawConfig().rate_limits.global.window_minutes,
+                windowMin:
+                    config.getRawConfig().rate_limits.global.window_minutes,
                 max: config.getRawConfig().rate_limits.global.max_requests,
                 type: "IP_AND_PATH"
             })

+ 32 - 18
server/auth/sessions/app.ts

@@ -1,6 +1,6 @@
 import {
     encodeBase32LowerCaseNoPadding,
-    encodeHexLowerCase,
+    encodeHexLowerCase
 } from "@oslojs/encoding";
 import { sha256 } from "@oslojs/crypto/sha2";
 import { Session, sessions, User, users } from "@server/db/schema";
@@ -9,8 +9,10 @@ import { eq } from "drizzle-orm";
 import config from "@server/lib/config";
 import type { RandomReader } from "@oslojs/crypto/random";
 import { generateRandomString } from "@oslojs/crypto/random";
+import logger from "@server/logger";
 
-export const SESSION_COOKIE_NAME = config.getRawConfig().server.session_cookie_name;
+export const SESSION_COOKIE_NAME =
+    config.getRawConfig().server.session_cookie_name;
 export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30;
 export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies;
 export const COOKIE_DOMAIN = "." + config.getBaseDomain();
@@ -24,25 +26,25 @@ export function generateSessionToken(): string {
 
 export async function createSession(
     token: string,
-    userId: string,
+    userId: string
 ): Promise<Session> {
     const sessionId = encodeHexLowerCase(
-        sha256(new TextEncoder().encode(token)),
+        sha256(new TextEncoder().encode(token))
     );
     const session: Session = {
         sessionId: sessionId,
         userId,
-        expiresAt: new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime(),
+        expiresAt: new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime()
     };
     await db.insert(sessions).values(session);
     return session;
 }
 
 export async function validateSessionToken(
-    token: string,
+    token: string
 ): Promise<SessionValidationResult> {
     const sessionId = encodeHexLowerCase(
-        sha256(new TextEncoder().encode(token)),
+        sha256(new TextEncoder().encode(token))
     );
     const result = await db
         .select({ user: users, session: sessions })
@@ -61,12 +63,12 @@ export async function validateSessionToken(
     }
     if (Date.now() >= session.expiresAt - SESSION_COOKIE_EXPIRES / 2) {
         session.expiresAt = new Date(
-            Date.now() + SESSION_COOKIE_EXPIRES,
+            Date.now() + SESSION_COOKIE_EXPIRES
         ).getTime();
         await db
             .update(sessions)
             .set({
-                expiresAt: session.expiresAt,
+                expiresAt: session.expiresAt
             })
             .where(eq(sessions.sessionId, session.sessionId));
     }
@@ -81,26 +83,38 @@ export async function invalidateAllSessions(userId: string): Promise<void> {
     await db.delete(sessions).where(eq(sessions.userId, userId));
 }
 
-export function serializeSessionCookie(token: string): string {
-    if (SECURE_COOKIES) {
-        return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
+export function serializeSessionCookie(
+    token: string,
+    isSecure: boolean
+): string {
+    if (isSecure) {
+        logger.debug("Setting cookie for secure origin");
+        if (SECURE_COOKIES) {
+            return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
+        } else {
+            return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=${COOKIE_DOMAIN}`;
+        }
     } 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=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/;`;
     }
 }
 
-export function createBlankSessionTokenCookie(): string {
-    if (SECURE_COOKIES) {
-        return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
+export function createBlankSessionTokenCookie(isSecure: boolean): string {
+    if (isSecure) {
+        if (SECURE_COOKIES) {
+            return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
+        } else {
+            return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Domain=${COOKIE_DOMAIN}`;
+        }
     } else {
-        return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Domain=${COOKIE_DOMAIN}`;
+        return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/;`;
     }
 }
 
 const random: RandomReader = {
     read(bytes: Uint8Array): void {
         crypto.getRandomValues(bytes);
-    },
+    }
 };
 
 export function generateId(length: number): string {

+ 7 - 1
server/lib/config.ts

@@ -38,7 +38,13 @@ const environmentSchema = z.object({
         secure_cookies: z.boolean(),
         session_cookie_name: z.string(),
         resource_session_cookie_name: z.string(),
-        resource_access_token_param: z.string()
+        resource_access_token_param: z.string(),
+        cors: z.object({
+            origins: z.array(z.string()).optional(),
+            methods: z.array(z.string()).optional(),
+            allowed_headers: z.array(z.string()).optional(),
+            credentials: z.boolean().optional(),
+        }).optional()
     }),
     traefik: z.object({
         http_entrypoint: z.string(),

+ 2 - 1
server/routers/auth/login.ts

@@ -120,7 +120,8 @@ export async function login(
 
         const token = generateSessionToken();
         await createSession(token, existingUser.userId);
-        const cookie = serializeSessionCookie(token);
+        const isSecure = req.protocol === "https";
+        const cookie = serializeSessionCookie(token, isSecure);
 
         res.appendHeader("Set-Cookie", cookie);
 

+ 2 - 1
server/routers/auth/logout.ts

@@ -27,7 +27,8 @@ export async function logout(
 
     try {
         await invalidateSession(sessionId);
-        res.setHeader("Set-Cookie", createBlankSessionTokenCookie());
+        const isSecure = req.protocol === "https";
+        res.setHeader("Set-Cookie", createBlankSessionTokenCookie(isSecure));
 
         return response<null>(res, {
             data: null,

+ 2 - 1
server/routers/auth/signup.ts

@@ -158,7 +158,8 @@ export async function signup(
 
         const token = generateSessionToken();
         await createSession(token, userId);
-        const cookie = serializeSessionCookie(token);
+        const isSecure = req.protocol === "https";
+        const cookie = serializeSessionCookie(token, isSecure);
         res.appendHeader("Set-Cookie", cookie);
 
         if (config.getRawConfig().flags?.require_email_verification) {