Spaces:
Sleeping
Sleeping
Download src/server/security.ts from vooom/NewsAnalyze: direct link, hf CLI and curl.
- Browser
- Download file 4.88 kB
-
https://huggingface.co/spaces/vooom/NewsAnalyze/resolve/main/src/server/security.ts
- Command line
-
hf download hf://spaces/vooom/NewsAnalyze/src/server/security.ts
-
curl -L -o security.ts https://huggingface.co/spaces/vooom/NewsAnalyze/resolve/main/src/server/security.ts
4.88 kB
| /** | |
| * HTTP Security Middleware | |
| * CORS allow-list, admin token gate, Firebase ID token verification and rate limiting. | |
| * All behavior is driven by environment variables so deployments opt in explicitly. | |
| */ | |
| import crypto from "crypto"; | |
| import type { NextFunction, Request, Response } from "express"; | |
| import type { CorsOptions } from "cors"; | |
| import rateLimit from "express-rate-limit"; | |
| import { createRemoteJWKSet, jwtVerify } from "jose"; | |
| const FIREBASE_JWKS_URL = new URL( | |
| "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com" | |
| ); | |
| export const CORS_ALLOWED_HEADERS = [ | |
| "Content-Type", | |
| "Authorization", | |
| "X-Admin-Token", | |
| "X-LLM-API-Key", | |
| "X-LLM-Provider", | |
| "X-Unsloth-Key", | |
| "X-HF-Token", | |
| "X-GNews-Key", | |
| "X-AGNews-Key", | |
| "X-NewsAPI-Key", | |
| ]; | |
| export function parseAllowedOrigins(raw: string | undefined): string[] { | |
| return (raw || "") | |
| .split(",") | |
| .map((o) => o.trim().replace(/\/$/, "")) | |
| .filter(Boolean); | |
| } | |
| /** | |
| * Cross-origin access is denied unless the origin is listed in ALLOWED_ORIGINS. | |
| * Same-origin requests (the bundled web UI) never need CORS headers. | |
| */ | |
| export function buildCorsOptions(allowedOrigins: string[]): CorsOptions { | |
| return { | |
| origin: (origin, callback) => { | |
| if (!origin) return callback(null, false); | |
| callback(null, allowedOrigins.includes("*") || allowedOrigins.includes(origin)); | |
| }, | |
| methods: ["GET", "POST", "OPTIONS"], | |
| allowedHeaders: CORS_ALLOWED_HEADERS, | |
| }; | |
| } | |
| export function safeEqual(a: string, b: string): boolean { | |
| const bufA = Buffer.from(a); | |
| const bufB = Buffer.from(b); | |
| if (bufA.length !== bufB.length) return false; | |
| return crypto.timingSafeEqual(bufA, bufB); | |
| } | |
| /** | |
| * Gates administrative endpoints behind ADMIN_TOKEN (sent as `X-Admin-Token`). | |
| * When ADMIN_TOKEN is unset the endpoints are disabled entirely. | |
| */ | |
| export function requireAdmin(adminToken: string | undefined) { | |
| const expected = (adminToken || "").trim(); | |
| return (req: Request, res: Response, next: NextFunction) => { | |
| if (!expected) { | |
| res.status(403).json({ | |
| error: "Admin endpoints are disabled. Set the ADMIN_TOKEN environment variable on the server to enable them.", | |
| }); | |
| return; | |
| } | |
| const provided = String(req.headers["x-admin-token"] || "").trim(); | |
| if (!provided || !safeEqual(provided, expected)) { | |
| res.status(401).json({ error: "Invalid or missing X-Admin-Token header." }); | |
| return; | |
| } | |
| next(); | |
| }; | |
| } | |
| export interface FirebaseAuthConfig { | |
| projectId?: string; | |
| requireAuth: boolean; | |
| } | |
| /** | |
| * Verifies Firebase ID tokens sent as `Authorization: Bearer <token>`. | |
| * - A token that is present but invalid is always rejected. | |
| * - A missing token is rejected only when REQUIRE_AUTH=true. | |
| * - REQUIRE_AUTH=true without FIREBASE_PROJECT_ID fails closed. | |
| */ | |
| export function firebaseAuth(config: FirebaseAuthConfig) { | |
| const projectId = (config.projectId || "").trim(); | |
| const jwks = projectId ? createRemoteJWKSet(FIREBASE_JWKS_URL) : null; | |
| return async (req: Request, res: Response, next: NextFunction) => { | |
| const header = String(req.headers.authorization || ""); | |
| const token = header.startsWith("Bearer ") ? header.slice(7).trim() : ""; | |
| if (!token) { | |
| if (config.requireAuth) { | |
| res.status(401).json({ error: "Authentication required. Sign in to run this request." }); | |
| return; | |
| } | |
| next(); | |
| return; | |
| } | |
| if (!jwks) { | |
| if (config.requireAuth) { | |
| res.status(503).json({ error: "Server misconfigured: REQUIRE_AUTH is set but FIREBASE_PROJECT_ID is missing." }); | |
| return; | |
| } | |
| // Verification is not configured; treat the request as anonymous. | |
| next(); | |
| return; | |
| } | |
| try { | |
| const { payload } = await jwtVerify(token, jwks, { | |
| issuer: `https://securetoken.google.com/${projectId}`, | |
| audience: projectId, | |
| }); | |
| res.locals.userId = payload.sub; | |
| next(); | |
| } catch { | |
| res.status(401).json({ error: "Invalid or expired authentication token." }); | |
| } | |
| }; | |
| } | |
| function readPositiveInt(raw: string | undefined, fallback: number): number { | |
| const num = Number(raw); | |
| return Number.isInteger(num) && num > 0 ? num : fallback; | |
| } | |
| export function buildRateLimiters(env: NodeJS.ProcessEnv) { | |
| const general = rateLimit({ | |
| windowMs: 60 * 1000, | |
| limit: readPositiveInt(env.API_RATE_LIMIT_PER_MINUTE, 120), | |
| standardHeaders: "draft-7", | |
| legacyHeaders: false, | |
| message: { error: "Too many requests. Please slow down." }, | |
| }); | |
| const forecast = rateLimit({ | |
| windowMs: 15 * 60 * 1000, | |
| limit: readPositiveInt(env.FORECAST_RATE_LIMIT_PER_15_MIN, 15), | |
| standardHeaders: "draft-7", | |
| legacyHeaders: false, | |
| message: { error: "Forecast rate limit reached. Please try again in a few minutes." }, | |
| }); | |
| return { general, forecast }; | |
| } | |