import type { IncomingMessage } from 'http'; export interface AuthConfig { /** Static bearer token. If set, all connections must provide it. */ token?: string; /** Trust Tailscale-User-Login header for identity. */ tailscaleIdentity?: boolean; } export interface AuthResult { authenticated: boolean; identity?: string; error?: string; } /** * Authenticates a WebSocket upgrade request or HTTP request. * * Auth is checked in this order: * 1. If token is configured, validate via: * a. Authorization: Bearer header * b. ?token= query parameter (useful for browser WebSocket clients) * 2. If tailscaleIdentity is enabled, extract identity from Tailscale-User-Login header * 3. If no auth is configured, allow all connections */ export function authenticateRequest(req: IncomingMessage, config: AuthConfig): AuthResult { // If token auth is configured, it's required if (config.token) { let tokenValid = false; // Check Authorization header first const authHeader = req.headers['authorization']; if (authHeader) { const parts = authHeader.split(' '); if (parts.length !== 2 || parts[0] !== 'Bearer') { return { authenticated: false, error: 'Invalid Authorization format (expected: Bearer )' }; } tokenValid = parts[1] === config.token; } else { // Check query parameter as fallback (useful for WebSocket from browsers) const queryToken = extractQueryToken(req); if (queryToken !== undefined) { tokenValid = queryToken === config.token; } else { return { authenticated: false, error: 'Authorization required (Bearer token header or ?token= query parameter)' }; } } if (!tokenValid) { return { authenticated: false, error: 'Invalid token' }; } // Token is valid — check for Tailscale identity too const identity = extractTailscaleIdentity(req, config); return { authenticated: true, identity: identity ?? 'token-user' }; } // If Tailscale identity is configured (no token), use it if (config.tailscaleIdentity) { const identity = extractTailscaleIdentity(req, config); if (identity) { return { authenticated: true, identity }; } // Tailscale identity configured but header not present — still allow (might be local) return { authenticated: true, identity: 'anonymous' }; } // No auth configured — allow all return { authenticated: true, identity: 'anonymous' }; } function extractQueryToken(req: IncomingMessage): string | undefined { try { const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`); const token = url.searchParams.get('token'); return token ?? undefined; } catch { return undefined; } } function extractTailscaleIdentity(req: IncomingMessage, config: AuthConfig): string | undefined { if (!config.tailscaleIdentity) {return undefined;} const header = req.headers['tailscale-user-login']; if (typeof header === 'string' && header.length > 0) { return header; } return undefined; }