import type { NextFunction, Request, RequestHandler, Response } from 'express' import jwt from 'jsonwebtoken' interface LabJwtClaims extends jwt.JwtPayload { email?: string id?: number isAdmin?: boolean merit_slug?: unknown merit_slugs?: unknown name?: string picture?: string | null role?: string sub?: string } interface LabAccessResponse { allowed?: boolean reason?: string unlockUrl?: string } interface LabAuthConfig { enabled: boolean gnommowebUrl: string jwtSecret: string keycardMeritSlug: string keycardRequired: boolean signInUrl: string temporarySecretKey?: string userProfileUrl: string } declare global { namespace Express { interface Request { labUser?: LabJwtClaims labAccess?: LabAccessResponse } } } function envFlag(name: string, fallback: boolean) { const value = process.env[name] if (value === undefined) return fallback return ['1', 'true', 'yes', 'on'].includes(value.toLowerCase()) } function trimTrailingSlash(value: string) { return value.replace(/\/+$/, '') } export function labAuthConfig(): LabAuthConfig { const gnommowebUrl = trimTrailingSlash(process.env.GNOMMOWEB_URL ?? 'https://glitch.university') return { enabled: envFlag('LAB_AUTH_ENABLED', process.env.NODE_ENV === 'production'), gnommowebUrl, jwtSecret: process.env.JWT_SECRET ?? '', keycardMeritSlug: process.env.LAB_KEYCARD_MERIT_SLUG ?? 'lab-keycard', keycardRequired: envFlag('LAB_KEYCARD_REQUIRED', true), signInUrl: process.env.LAB_SIGN_IN_URL ?? `${gnommowebUrl}/auth/google`, temporarySecretKey: process.env.LAB_TEMP_SECRET_KEY ?? 'supersecret', userProfileUrl: process.env.LAB_USER_PROFILE_URL ?? `${gnommowebUrl}/api/user/profile` } } function wantsHtml(request: Request) { const accept = request.get('accept') ?? '' return accept.includes('text/html') || accept.includes('*/*') } function currentUrl(request: Request) { const proto = request.get('x-forwarded-proto') ?? request.protocol const host = request.get('x-forwarded-host') ?? request.get('host') ?? 'localhost' return `${proto}://${host}${request.originalUrl}` } function loginUrl(request: Request, config: LabAuthConfig) { const returnTo = currentUrl(request) if (config.signInUrl.includes('{returnTo}')) { return config.signInUrl.replace('{returnTo}', encodeURIComponent(returnTo)) } const url = new URL(config.signInUrl) url.searchParams.set('returnTo', returnTo) return url.toString() } function temporaryCookieOptions(request: Request) { const secure = request.secure || request.get('x-forwarded-proto') === 'https' return { httpOnly: true, maxAge: 1000 * 60 * 60 * 12, sameSite: 'lax' as const, secure } } function hasTemporaryAccess(request: Request, config: LabAuthConfig) { return Boolean(config.temporarySecretKey && request.cookies?.lab_temp_key === config.temporarySecretKey) } function isLocalLoginPath(request: Request) { return request.path === '/lab-login' || request.path === '/admin/lab-login' } function localLoginPage(config: LabAuthConfig, error = '') { return ` CA Lab Login
CA Lab Computer

Insert lab keycard

Sign in with your Glitch University JWT, or use the temporary lab key while access is being repaired.

${error ? `

${escapeHtml(error)}

` : ''}
` } function lockedPage(access: LabAccessResponse, config: LabAuthConfig) { const unlockUrl = access.unlockUrl ?? `${config.gnommowebUrl}/tech-tree` const reason = access.reason ?? 'Your Glitch University account does not have the CA Lab Keycard yet.' return ` CA Lab Locked
CA Lab Computer

Lab keycard required

${escapeHtml(reason)}

Earn the Cellular Automata lab merit on glitch.university, then return here.

Go to Glitch University
` } function escapeHtml(value: string) { return value .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", ''') } function isMeritSlug(value: unknown, requiredSlug: string) { return typeof value === 'string' && value.trim() === requiredSlug } function collectionHasMeritSlug(value: unknown, requiredSlug: string): boolean { if (!Array.isArray(value)) return false return value.some((entry) => { if (isMeritSlug(entry, requiredSlug)) return true if (!entry || typeof entry !== 'object') return false const record = entry as Record return isMeritSlug(record.slug, requiredSlug) }) } function payloadHasMeritSlug(payload: unknown, requiredSlug: string) { if (!payload || typeof payload !== 'object') return false const record = payload as Record const user = record.user && typeof record.user === 'object' ? record.user as Record : undefined return ( isMeritSlug(record.merit_slug, requiredSlug) || collectionHasMeritSlug(record.merit_slugs, requiredSlug) || collectionHasMeritSlug(record.merits, requiredSlug) || (user ? collectionHasMeritSlug(user.merits, requiredSlug) : false) ) } function jwtClaimsHaveKeycard(user: LabJwtClaims | undefined, config: LabAuthConfig) { if (!user) return false return isMeritSlug(user.merit_slug, config.keycardMeritSlug) || collectionHasMeritSlug(user.merit_slugs, config.keycardMeritSlug) } async function checkUserProfileForKeycard(token: string, config: LabAuthConfig): Promise { const response = await fetch(config.userProfileUrl, { headers: { accept: 'application/json', cookie: `auth_token=${encodeURIComponent(token)}` } }) if (!response.ok) { return { allowed: false, reason: response.status === 401 ? 'Sign in to glitch.university to access the CA lab.' : `The CA Lab Keycard could not be verified. The user profile endpoint returned HTTP ${response.status}.` } } const profile = await response.json() if (payloadHasMeritSlug(profile, config.keycardMeritSlug)) { return { allowed: true, reason: 'CA Lab Keycard found in gnommoweb user profile.' } } return { allowed: false, reason: 'Your Glitch University account does not have the CA Lab Keycard yet.' } } export function requireLabAccess(config: LabAuthConfig = labAuthConfig()): RequestHandler { return async (request: Request, response: Response, next: NextFunction) => { if (!config.enabled) { next() return } if (request.method === 'GET' && isLocalLoginPath(request)) { response.type('html').send(localLoginPage(config)) return } if (request.method === 'POST' && isLocalLoginPath(request)) { const jwtToken = typeof request.body?.jwt_token === 'string' ? request.body.jwt_token.trim() : '' const secretKey = typeof request.body?.secret_key === 'string' ? request.body.secret_key.trim() : '' if (config.temporarySecretKey && secretKey === config.temporarySecretKey) { response.cookie('lab_temp_key', config.temporarySecretKey, temporaryCookieOptions(request)) response.redirect('/admin') return } if (jwtToken) { response.cookie('auth_token', jwtToken, temporaryCookieOptions(request)) response.redirect('/admin') return } response.status(401).type('html').send(localLoginPage(config, 'Enter a JWT token or the temporary secret key.')) return } if (hasTemporaryAccess(request, config)) { request.labAccess = { allowed: true, reason: 'Temporary CA Lab secret key accepted.' } next() return } if (!config.jwtSecret) { response.status(500).json({ error: 'Lab authentication is enabled but JWT_SECRET is not configured' }) return } const token = request.cookies?.auth_token if (!token) { if (wantsHtml(request)) { response.status(401).type('html').send(localLoginPage(config)) return } response.status(401).json({ error: 'Authentication required', signInUrl: loginUrl(request, config) }) return } try { request.labUser = jwt.verify(token, config.jwtSecret) as LabJwtClaims } catch { if (wantsHtml(request)) { response.redirect(loginUrl(request, config)) return } response.status(401).json({ error: 'Invalid or expired token', signInUrl: loginUrl(request, config) }) return } if (!config.keycardRequired) { next() return } if (jwtClaimsHaveKeycard(request.labUser, config)) { request.labAccess = { allowed: true, reason: 'CA Lab Keycard found in JWT claims.' } next() return } let access: LabAccessResponse try { access = await checkUserProfileForKeycard(token, config) } catch { access = { allowed: false, reason: 'The CA Lab Keycard service is unavailable.' } } request.labAccess = access if (access.allowed === true) { next() return } if (wantsHtml(request)) { response.status(403).type('html').send(lockedPage(access, config)) return } response.status(403).json({ error: 'CA Lab Keycard required', reason: access.reason, unlockUrl: access.unlockUrl ?? `${config.gnommowebUrl}/tech-tree` }) } }