Files
flynn/src/gateway/auth.test.ts
T
William Valentin 20930a4816 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.
2026-02-06 16:51:41 -08:00

130 lines
4.2 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { authenticateRequest } from './auth.js';
import type { IncomingMessage } from 'http';
function mockRequest(headers: Record<string, string> = {}): IncomingMessage {
return { headers } as unknown as IncomingMessage;
}
function mockRequestWithUrl(url: string, headers: Record<string, string> = {}): IncomingMessage {
return { url, headers } as unknown as IncomingMessage;
}
describe('authenticateRequest', () => {
describe('no auth configured', () => {
it('allows all connections', () => {
const result = authenticateRequest(mockRequest(), {});
expect(result.authenticated).toBe(true);
expect(result.identity).toBe('anonymous');
});
});
describe('token auth', () => {
const config = { token: 'secret-token-123' };
it('accepts valid Bearer token', () => {
const result = authenticateRequest(
mockRequest({ authorization: 'Bearer secret-token-123' }),
config,
);
expect(result.authenticated).toBe(true);
expect(result.identity).toBe('token-user');
});
it('rejects missing Authorization header', () => {
const result = authenticateRequest(mockRequest(), config);
expect(result.authenticated).toBe(false);
expect(result.error).toContain('Authorization required');
});
it('rejects invalid token', () => {
const result = authenticateRequest(
mockRequest({ authorization: 'Bearer wrong-token' }),
config,
);
expect(result.authenticated).toBe(false);
expect(result.error).toContain('Invalid token');
});
it('rejects non-Bearer format', () => {
const result = authenticateRequest(
mockRequest({ authorization: 'Basic dXNlcjpwYXNz' }),
config,
);
expect(result.authenticated).toBe(false);
expect(result.error).toContain('Invalid Authorization format');
});
it('uses Tailscale identity when both token and tailscale are configured', () => {
const result = authenticateRequest(
mockRequest({
authorization: 'Bearer secret-token-123',
'tailscale-user-login': 'will@example.com',
}),
{ token: 'secret-token-123', tailscaleIdentity: true },
);
expect(result.authenticated).toBe(true);
expect(result.identity).toBe('will@example.com');
});
});
describe('tailscale identity', () => {
const config = { tailscaleIdentity: true };
it('extracts identity from Tailscale-User-Login header', () => {
const result = authenticateRequest(
mockRequest({ 'tailscale-user-login': 'will@example.com' }),
config,
);
expect(result.authenticated).toBe(true);
expect(result.identity).toBe('will@example.com');
});
it('allows connections without Tailscale header (local access)', () => {
const result = authenticateRequest(mockRequest(), config);
expect(result.authenticated).toBe(true);
expect(result.identity).toBe('anonymous');
});
});
describe('query parameter token', () => {
const config = { token: 'secret-token-123' };
it('accepts valid token in query parameter', () => {
const result = authenticateRequest(
mockRequestWithUrl('/?token=secret-token-123'),
config,
);
expect(result.authenticated).toBe(true);
expect(result.identity).toBe('token-user');
});
it('rejects invalid token in query parameter', () => {
const result = authenticateRequest(
mockRequestWithUrl('/?token=wrong'),
config,
);
expect(result.authenticated).toBe(false);
expect(result.error).toContain('Invalid token');
});
it('prefers header over query parameter', () => {
const result = authenticateRequest(
mockRequestWithUrl('/?token=wrong', { authorization: 'Bearer secret-token-123' }),
config,
);
expect(result.authenticated).toBe(true);
expect(result.identity).toBe('token-user');
});
it('rejects when neither header nor query parameter provided', () => {
const result = authenticateRequest(
mockRequestWithUrl('/'),
config,
);
expect(result.authenticated).toBe(false);
expect(result.error).toContain('Authorization required');
});
});
});