Skip to content
This repository was archived by the owner on Jun 16, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions api/auth/_cors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* Parses the ALLOWED_ORIGINS environment variable into an array of trusted origins.
* Handles comma-separated values, trims whitespace, and filters empty entries.
*
* @returns {string[]} Array of allowed origin URLs
*/
export const getAllowedOrigins = () => {
const envOrigins = process.env.ALLOWED_ORIGINS || "";

if (!envOrigins || typeof envOrigins !== "string") {
return [];
}

return envOrigins
.split(",")
.map((origin) => origin.trim())
.filter((origin) => origin.length > 0);
};

/**
* Checks if a given origin is allowed based on the allowlist and development mode.
* In non-production environments, common localhost origins are automatically allowed.
*
* @param {string} origin - The origin URL to validate
* @returns {boolean} True if the origin is allowed, false otherwise
*/
export const isAllowedOrigin = (origin) => {
if (!origin || typeof origin !== "string") {
return false;
}

const allowedOrigins = getAllowedOrigins();

// Check against explicit allowlist
if (allowedOrigins.includes(origin)) {
return true;
}

// Allow common development origins in non-production environments
const isDevelopment = process.env.NODE_ENV !== "production";
if (isDevelopment) {
const developmentOrigins = [
"http://localhost:3000",
"http://localhost:5173",
"http://127.0.0.1:3000",
"http://127.0.0.1:5173",
];
return developmentOrigins.includes(origin);
}

// Fail closed: reject untrusted origins in production
return false;
};

/**
* Builds CORS headers for a given request.
* Validates the Origin header against the allowlist and returns appropriate headers.
*
* Security decisions:
* - No wildcard origin (*) is ever returned
* - Origin is only reflected after validation against allowlist
* - Exact string matching is used (no regex)
* - Vary: Origin is always returned to prevent caching issues
* - Untrusted origins receive no ACAO header (fail closed)
*
* @param {Object} req - The HTTP request object
* @returns {Object} CORS headers object
*/
export const buildCorsHeaders = (req) => {
const requestOrigin = req.headers?.origin;
const isOriginAllowed = isAllowedOrigin(requestOrigin);

const headers = {
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Vary": "Origin",
};

// Only set ACAO for validated, trusted origins
if (isOriginAllowed && requestOrigin) {
headers["Access-Control-Allow-Origin"] = requestOrigin;
}

return headers;
};

export const corsResponse = (req, res, status, body) => {
const headers = buildCorsHeaders(req);
Object.entries(headers).forEach(([k, v]) => res.setHeader(k, v));
res.status(status).json(body);
};
25 changes: 25 additions & 0 deletions api/auth/_jwt-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Returns the JWT signing secret from environment variables.
*
* SECURITY: JWT_SECRET is mandatory. There is NO fallback secret.
* This prevents token signing with publicly known secrets and ensures
* fail-closed security behavior.
*
* @throws {Error} If JWT_SECRET is missing, empty, or whitespace-only
* @returns {string} The JWT signing secret
*/
export const getJwtSecret = () => {
const secret = process.env.JWT_SECRET;

if (!secret || !secret.trim()) {
throw new Error(
"JWT_SECRET environment variable is required. " +
"Generate a secure secret using: openssl rand -base64 32"
);
}

return secret;
};

export const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "7d";
export const JWT_COOKIE_MAX_AGE_SECONDS = 7 * 24 * 60 * 60;
58 changes: 58 additions & 0 deletions api/auth/_storage-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* api/auth/storage-config.js
*
* Centralized authentication storage configuration and validation.
*
* This module enforces fail-closed security for persistent authentication storage.
* In-memory user storage is permitted ONLY in development environments.
* Production requires DATABASE_URL to be configured.
*
* SECURITY REQUIREMENTS:
* - Fail closed, never fail open
* - No fallback to Map storage in production
* - No bypass flags
* - No silent warnings
* - No environment variable defaults
* - No hardcoded database URLs
*/

/**
* Checks if persistent storage is properly configured.
*
* @returns {boolean} True if DATABASE_URL is present and non-empty
*/
export const isPersistentStorageConfigured = () => {
return Boolean(process.env.DATABASE_URL?.trim());
};

/**
* Asserts that persistent storage is configured in production.
*
* Throws an error if:
* - NODE_ENV is "production" AND
* - DATABASE_URL is missing, empty, or whitespace-only
*
* This should be called during module initialization to fail fast
* before the application accepts any authentication requests.
*
* @throws {Error} If production storage is not configured
*/
export const assertPersistentStorageConfigured = () => {
if (process.env.NODE_ENV === "production" && !isPersistentStorageConfigured()) {
throw new Error(
"DATABASE_URL is required in production. In-memory authentication storage is not permitted."
);
}
};

/**
* Checks if in-memory storage is allowed for the current environment.
*
* In-memory storage is allowed ONLY when NODE_ENV is NOT "production".
* This preserves existing development and test workflows.
*
* @returns {boolean} True if in-memory storage is permitted
*/
export const isInMemoryStorageAllowed = () => {
return process.env.NODE_ENV !== "production";
};
Loading
Loading