Files
dotcom-arcade/backend/src/utils/trustedHeader.test.ts
T
branderson 057192be16 feat(auth): sign in users vouched for by a trusted proxy header
With TRUSTED_HEADER_USERNAME and TRUSTED_PROXIES set, POST
/api/auth/trusted-login signs in the user an authenticating reverse proxy
(such as Authentik forward auth) names in that header, believing it only on
connections from the proxy addresses. Users are matched by username, so an
existing account is adopted; otherwise one is created, the first as admin
as with registration. New accounts get a random password, and a user the
proxy vouches for can set their own password without the current one, to log
in when reached without the proxy.
2026-10-12 00:55:29 -07:00

72 lines
2.6 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { Request } from 'express';
vi.mock('./logger', () => ({
logger: { info: vi.fn(), error: vi.fn(), debug: vi.fn(), warn: vi.fn() },
}));
const config = vi.hoisted(() => ({
trustedHeaderUsername: 'X-authentik-username' as string | null,
trustedHeaderEmail: 'X-authentik-email' as string | null,
trustedProxies: ['192.0.2.10', '198.51.100.0/24', '2001:db8::/32'],
}));
vi.mock('../config', () => ({ config }));
function request(remoteAddress: string, headers: Record<string, string> = {}): Request {
return { socket: { remoteAddress }, headers } as unknown as Request;
}
describe('trusted header identity', () => {
beforeEach(() => {
vi.resetModules();
config.trustedHeaderUsername = 'X-authentik-username';
});
async function load() {
return import('./trustedHeader');
}
it('believes the header only on connections from a trusted proxy', async () => {
const { getTrustedIdentity } = await load();
const headers = { 'x-authentik-username': 'brad', 'x-authentik-email': 'brad@example.com' };
expect(getTrustedIdentity(request('192.0.2.10', headers))).toEqual({
username: 'brad',
email: 'brad@example.com',
});
expect(getTrustedIdentity(request('::ffff:198.51.100.7', headers))?.username).toBe('brad');
expect(getTrustedIdentity(request('2001:db8::5', headers))?.username).toBe('brad');
expect(getTrustedIdentity(request('192.0.2.11', headers))).toBeNull();
expect(getTrustedIdentity(request('10.0.0.5', headers))).toBeNull();
});
it('needs a username, and makes the email optional', async () => {
const { getTrustedIdentity } = await load();
expect(getTrustedIdentity(request('192.0.2.10'))).toBeNull();
expect(getTrustedIdentity(request('192.0.2.10', { 'x-authentik-username': ' ' }))).toBeNull();
expect(getTrustedIdentity(request('192.0.2.10', { 'x-authentik-username': 'brad' }))).toEqual({
username: 'brad',
email: null,
});
});
it('is off without a header name', async () => {
config.trustedHeaderUsername = null;
const { getTrustedIdentity, isTrustedHeaderEnabled } = await load();
expect(isTrustedHeaderEnabled()).toBe(false);
expect(
getTrustedIdentity(request('192.0.2.10', { 'x-authentik-username': 'brad' }))
).toBeNull();
});
it('skips invalid proxy entries', async () => {
const { buildBlockList } = await load();
const list = buildBlockList(['not-an-ip', '192.0.2.0/30']);
expect(list.check('192.0.2.1', 'ipv4')).toBe(true);
expect(list.check('192.0.2.9', 'ipv4')).toBe(false);
});
});