Files
flynn/src/gateway/auth.test.ts
T
2026-02-16 12:47:34 -08:00

214 lines
7.0 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { authenticateRequest, authorizeNodeMethod } 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');
});
});
});
describe('authorizeNodeMethod', () => {
it('allows non-node methods', () => {
const result = authorizeNodeMethod({ enabled: false, method: 'system.health' });
expect(result.authenticated).toBe(true);
});
it('blocks node methods when node RPC is disabled', () => {
const result = authorizeNodeMethod({ enabled: false, method: 'node.capabilities.get' });
expect(result.authenticated).toBe(false);
expect(result.error).toContain('disabled');
});
it('allows node.register without prior registration', () => {
const result = authorizeNodeMethod({ enabled: true, method: 'node.register' });
expect(result.authenticated).toBe(true);
});
it('requires role for scoped node methods', () => {
const result = authorizeNodeMethod({ enabled: true, method: 'node.capabilities.get' });
expect(result.authenticated).toBe(false);
expect(result.error).toContain('not registered');
});
it('enforces allowed role list and method scopes', () => {
const deniedRole = authorizeNodeMethod({
enabled: true,
method: 'node.capabilities.get',
nodeRole: 'observer',
allowedRoles: ['companion'],
});
expect(deniedRole.authenticated).toBe(false);
const deniedMethod = authorizeNodeMethod({
enabled: true,
method: 'node.admin.reset',
nodeRole: 'companion',
allowedRoles: ['companion'],
roleScopes: { companion: ['node.capabilities.get'] },
});
expect(deniedMethod.authenticated).toBe(false);
const allowed = authorizeNodeMethod({
enabled: true,
method: 'node.capabilities.get',
nodeRole: 'companion',
allowedRoles: ['companion'],
roleScopes: { companion: ['node.capabilities.get'] },
});
expect(allowed.authenticated).toBe(true);
const allowedLocation = authorizeNodeMethod({
enabled: true,
method: 'node.location.set',
nodeRole: 'companion',
allowedRoles: ['companion'],
roleScopes: { companion: ['node.capabilities.get', 'node.location.set'] },
});
expect(allowedLocation.authenticated).toBe(true);
const deniedStatus = authorizeNodeMethod({
enabled: true,
method: 'node.status.set',
nodeRole: 'observer',
allowedRoles: ['companion', 'observer'],
roleScopes: {
companion: ['node.capabilities.get', 'node.status.set'],
observer: ['node.capabilities.get'],
},
});
expect(deniedStatus.authenticated).toBe(false);
const allowedPush = authorizeNodeMethod({
enabled: true,
method: 'node.push_token.set',
nodeRole: 'companion',
allowedRoles: ['companion'],
roleScopes: {
companion: ['node.capabilities.get', 'node.push_token.set'],
},
});
expect(allowedPush.authenticated).toBe(true);
});
});