Files
flynn/src/gateway/auth.ts
T
William Valentin 6090508bad style: auto-fix ESLint issues (curly braces and formatting)
- Add curly braces to all if/else/for/while statements
- Fix indentation and trailing spaces
- Auto-fixed 372 linting errors using eslint --fix
- Remaining issues are warnings only (non-null assertions, explicit any types)
2026-02-11 10:30:24 -08:00

90 lines
3.0 KiB
TypeScript

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 <token> header
* b. ?token=<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 <token>)' };
}
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;
}