feat: add query-param token auth and optional HTTP auth to gateway
Support ?token= query parameter as a fallback for WebSocket clients that cannot set Authorization headers (e.g. browsers). Add authHttp option to GatewayServer so token auth can be applied to HTTP requests too, returning 401 with WWW-Authenticate header on failure.
This commit is contained in:
+32
-10
@@ -14,27 +14,39 @@ export interface AuthResult {
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates a WebSocket upgrade request.
|
||||
* Authenticates a WebSocket upgrade request or HTTP request.
|
||||
*
|
||||
* Auth is checked in this order:
|
||||
* 1. If token is configured, validate Authorization header (Bearer token)
|
||||
* 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) {
|
||||
return { authenticated: false, error: 'Authorization header required' };
|
||||
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)' };
|
||||
}
|
||||
}
|
||||
|
||||
const parts = authHeader.split(' ');
|
||||
if (parts.length !== 2 || parts[0] !== 'Bearer') {
|
||||
return { authenticated: false, error: 'Invalid Authorization format (expected: Bearer <token>)' };
|
||||
}
|
||||
|
||||
if (parts[1] !== config.token) {
|
||||
if (!tokenValid) {
|
||||
return { authenticated: false, error: 'Invalid token' };
|
||||
}
|
||||
|
||||
@@ -57,6 +69,16 @@ export function authenticateRequest(req: IncomingMessage, config: AuthConfig): A
|
||||
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'];
|
||||
|
||||
Reference in New Issue
Block a user