20930a4816
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.
130 lines
4.2 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|