UNPKG

mongoku

Version:

[![CI](https://github.com/huggingface/Mongoku/actions/workflows/ci.yml/badge.svg)](https://github.com/huggingface/Mongoku/actions/workflows/ci.yml)

249 lines (246 loc) 8.34 kB
import { b as base } from './server-Crjo4w1q.js'; import './root-otUAnOAR.js'; import { p as private_env } from './shared-server-BmU87nph.js'; import { createHmac, timingSafeEqual, randomBytes, createHash } from 'node:crypto'; const DEFAULT_SESSION_DURATION = 86400; const OAUTH_CIMD_CLIENT_ID = "__CIMD__"; let cachedConfig; function parseSessionDuration(rawDuration) { if (!rawDuration) { return DEFAULT_SESSION_DURATION; } const parsed = Number(rawDuration); if (!Number.isInteger(parsed) || parsed <= 0) { return DEFAULT_SESSION_DURATION; } return parsed; } async function fetchOpenIDConfiguration(issuerUrl) { const wellKnown = issuerUrl.replace(/\/+$/, "") + "/.well-known/openid-configuration"; const response = await fetch(wellKnown); if (!response.ok) { throw new Error(`Failed to fetch OpenID configuration from ${wellKnown} (${response.status})`); } const config = await response.json(); if (!config.authorization_endpoint || !config.token_endpoint) { throw new Error(`OpenID configuration at ${wellKnown} is missing required endpoints`); } return config; } async function getOAuthConfig() { if (cachedConfig !== void 0) { return cachedConfig; } const clientId = private_env.MONGOKU_OAUTH_CLIENT_ID; if (!clientId) { cachedConfig = null; return null; } const issuerUrl = private_env.MONGOKU_OAUTH_ISSUER_URL; const sessionSecret = private_env.MONGOKU_OAUTH_SESSION_SECRET; if (!issuerUrl || !sessionSecret) { throw new Error( "OAuth is partially configured. When MONGOKU_OAUTH_CLIENT_ID is set, MONGOKU_OAUTH_ISSUER_URL and MONGOKU_OAUTH_SESSION_SECRET are also required." ); } const oidc = await fetchOpenIDConfiguration(issuerUrl); const allowedSubsRaw = private_env.MONGOKU_OAUTH_ALLOWED_SUBS; const allowedSubs = allowedSubsRaw ? new Set( allowedSubsRaw.split(",").map((s) => s.trim()).filter(Boolean) ) : void 0; const requiredClaimRaw = private_env.MONGOKU_OAUTH_REQUIRED_CLAIM; let requiredClaim; if (requiredClaimRaw) { const eqIndex = requiredClaimRaw.indexOf("="); if (eqIndex <= 0) { throw new Error('MONGOKU_OAUTH_REQUIRED_CLAIM must be in the format "field=value" (e.g. "authority=admin")'); } requiredClaim = { field: requiredClaimRaw.slice(0, eqIndex), value: requiredClaimRaw.slice(eqIndex + 1) }; } cachedConfig = { clientId, issuerUrl, authorizationUrl: oidc.authorization_endpoint, tokenUrl: oidc.token_endpoint, scopes: private_env.MONGOKU_OAUTH_SCOPES ?? "openid profile email", sessionSecret, sessionDuration: parseSessionDuration(private_env.MONGOKU_OAUTH_SESSION_DURATION), allowedSubs, requiredClaim }; return cachedConfig; } function resolveOAuthClientId(config, origin) { if (config.clientId !== OAUTH_CIMD_CLIENT_ID) { return config.clientId; } return new URL(`${base}/.well-known/cimd.json`, origin).toString(); } function base64url(buffer) { return buffer.toString("base64url"); } function generateCodeVerifier() { return base64url(randomBytes(64)); } function generateCodeChallenge(verifier) { return base64url(createHash("sha256").update(verifier).digest()); } function generateState() { return base64url(randomBytes(32)); } function buildAuthorizationUrl(config, origin, callbackUrl, codeChallenge, state) { const url = new URL(config.authorizationUrl); url.searchParams.set("client_id", resolveOAuthClientId(config, origin)); url.searchParams.set("response_type", "code"); url.searchParams.set("redirect_uri", callbackUrl); url.searchParams.set("scope", config.scopes); url.searchParams.set("state", state); url.searchParams.set("code_challenge", codeChallenge); url.searchParams.set("code_challenge_method", "S256"); return url.toString(); } async function exchangeCode(config, origin, code, codeVerifier, callbackUrl) { const body = new URLSearchParams({ grant_type: "authorization_code", code, redirect_uri: callbackUrl, client_id: resolveOAuthClientId(config, origin), code_verifier: codeVerifier }); const response = await fetch(config.tokenUrl, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: body.toString() }); if (!response.ok) { const text = await response.text(); throw new Error(`Token exchange failed (${response.status}): ${text}`); } return response.json(); } function extractUserFromIdToken(idToken) { try { const parts = idToken.split("."); if (parts.length !== 3) { return { user: {}, claims: {} }; } const claims = JSON.parse(Buffer.from(parts[1], "base64url").toString()); return { user: { sub: claims.sub, name: claims.name || claims.preferred_username, email: claims.email }, claims }; } catch { return { user: {}, claims: {} }; } } function checkRequiredClaim(claims, required) { const actual = claims[required.field]; if (Array.isArray(actual)) { return actual.includes(required.value); } return String(actual) === required.value; } function createSessionCookie(config, user) { const sessionDuration = Number.isInteger(config.sessionDuration) && config.sessionDuration > 0 ? config.sessionDuration : DEFAULT_SESSION_DURATION; const payload = { ...user, exp: Math.floor(Date.now() / 1e3) + sessionDuration }; const payloadStr = Buffer.from(JSON.stringify(payload)).toString("base64url"); const signature = createHmac("sha256", config.sessionSecret).update(payloadStr).digest("base64url"); return `${payloadStr}.${signature}`; } function verifySession(config, cookie) { const dotIndex = cookie.lastIndexOf("."); if (dotIndex === -1) { return null; } const payloadStr = cookie.slice(0, dotIndex); const signature = cookie.slice(dotIndex + 1); const expectedSignature = createHmac("sha256", config.sessionSecret).update(payloadStr).digest("base64url"); const a = Buffer.from(signature); const b = Buffer.from(expectedSignature); if (a.length !== b.length || !timingSafeEqual(a, b)) { return null; } try { const payload = JSON.parse(Buffer.from(payloadStr, "base64url").toString()); if (!Number.isFinite(payload.exp)) { return null; } if (payload.exp < Math.floor(Date.now() / 1e3)) { return null; } return payload; } catch { return null; } } function getCallbackUrl(origin) { return `${origin}${base}/auth/callback`; } const OAUTH_AUTH_PREFIX = `${base}/auth`; function sanitizeOAuthReturnPath(requestUrl, raw) { if (raw == null || raw === "") { return null; } const trimmed = raw.trim(); if (trimmed.startsWith("//")) { return null; } let pathWithSearch; if (/^https?:\/\//i.test(trimmed)) { let parsed; try { parsed = new URL(trimmed); } catch { return null; } if (parsed.origin !== requestUrl.origin) { return null; } pathWithSearch = parsed.pathname + parsed.search; } else if (trimmed.startsWith("/")) { try { const parsed = new URL(trimmed, requestUrl.origin); if (parsed.origin !== requestUrl.origin) { return null; } pathWithSearch = parsed.pathname + parsed.search; } catch { return null; } } else { return null; } if (base !== "") { if (pathWithSearch !== base && !pathWithSearch.startsWith(`${base}/`)) { return null; } } else if (!pathWithSearch.startsWith("/")) { return null; } if (pathWithSearch === OAUTH_AUTH_PREFIX || pathWithSearch.startsWith(`${OAUTH_AUTH_PREFIX}/`)) { return null; } return pathWithSearch; } const OAUTH_RETURN_COOKIE = "mongoku_oauth_return"; function cookieOptions(url, maxAge) { return { httpOnly: true, secure: url.protocol === "https:", sameSite: "lax", path: base || "/", ...maxAge !== void 0 && { maxAge } }; } export { OAUTH_CIMD_CLIENT_ID as O, getCallbackUrl as a, extractUserFromIdToken as b, cookieOptions as c, checkRequiredClaim as d, exchangeCode as e, createSessionCookie as f, getOAuthConfig as g, OAUTH_RETURN_COOKIE as h, generateCodeVerifier as i, generateCodeChallenge as j, generateState as k, buildAuthorizationUrl as l, sanitizeOAuthReturnPath as s, verifySession as v }; //# sourceMappingURL=oauth-D6jTWKFd.js.map