nexstream-api / backend /src /utils /network /auth.util.ts
circleci
deploy ccb248c
65fc551
Raw
History Blame Contribute Delete
3.38 kB
import { Request, Response, NextFunction } from 'express';
import { timingSafeEqual } from 'node:crypto';
function isLocalRequest(req: Request): boolean {
// proxied/spoofed requests are not local
if (req.headers['x-forwarded-for']) return false;
const ip = req.socket.remoteAddress?.replace(/^::ffff:/u, '');
return ip === '127.0.0.1' || ip === '::1';
}
export type AuthMode = 'open' | 'apikey' | 'deny';
// resolve effective auth posture
export function resolveAuthMode(
env: NodeJS.ProcessEnv = process.env
): AuthMode {
const explicit = (env.AUTH_MODE ?? '').toLowerCase().trim();
if (explicit === 'open') return 'open';
if (explicit === 'apikey') return 'apikey';
// a configured key implies apikey mode
if (env.API_KEY) return 'apikey';
// prod fails closed; dev stays open
if (env.NODE_ENV === 'production') return 'deny';
return 'open';
}
// fail fast on unsafe prod config
export function assertProdConfig(env: NodeJS.ProcessEnv = process.env): void {
if (env.NODE_ENV !== 'production') return;
if (!env.PROXY_SIGNING_SECRET) {
throw new Error(
'PROXY_SIGNING_SECRET is required when NODE_ENV=production'
);
}
const mode = resolveAuthMode(env);
if (mode === 'deny') {
console.warn(
'[auth] DENY MODE — no API_KEY and no AUTH_MODE set. ' +
'Non-localhost requests are blocked. ' +
'Set API_KEY to require a key, or AUTH_MODE=open for an open public instance.'
);
} else if (mode === 'open') {
console.warn(
'[auth] OPEN MODE — all requests allowed without authentication.'
);
}
}
function extractKey(req: Request): string | undefined {
const auth = req.headers.authorization;
if (auth?.startsWith('Bearer ')) return auth.slice(7);
const headerKey = req.headers['x-api-key'];
if (typeof headerKey === 'string') return headerKey;
const queryKey = req.query.key ?? req.query.apiKey;
return typeof queryKey === 'string' ? queryKey : undefined;
}
function keysMatch(provided: string, expected: string): boolean {
const providedBuf = Buffer.from(provided);
const expectedBuf = Buffer.from(expected);
if (providedBuf.length !== expectedBuf.length) return false;
return timingSafeEqual(providedBuf, expectedBuf);
}
export function requireApiKey(
req: Request,
res: Response,
next: NextFunction
): void {
const mode = resolveAuthMode();
// open: no auth required
if (mode === 'open') {
next();
return;
}
// local self-host stays frictionless
if (isLocalRequest(req)) {
next();
return;
}
// deny: fail closed for public requests
if (mode === 'deny') {
res.status(403).json({ error: 'Public access is not configured' });
return;
}
// apikey: require a matching key
const expected = process.env.API_KEY;
const provided = extractKey(req);
if (expected && provided && keysMatch(provided, expected)) {
next();
return;
}
res.status(401).json({ error: 'Unauthorized' });
}
// metrics gate: localhost or valid key
export function requireLocalOrApiKey(
req: Request,
res: Response,
next: NextFunction
): void {
if (isLocalRequest(req)) {
next();
return;
}
const expected = process.env.API_KEY;
const provided = extractKey(req);
if (expected && provided && keysMatch(provided, expected)) {
next();
return;
}
res.status(403).json({ error: 'Forbidden' });
}