CI (SIAX Cloud) / integration (push) Skipped
CI (SIAX Cloud) / sonar (push) Skipped
masterplan-lock / check (push) Successful in 5s
Security Scan (SIAX Cloud) / security (push) Successful in 15s
CI (SIAX Cloud) / security (push) Successful in 16s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 7s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 21s
CI (SIAX Cloud) / security (pull_request) Successful in 25s
CI (SIAX Cloud) / quality (push) Successful in 1m17s
CI (SIAX Cloud) / integration (pull_request) Successful in 54s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m7s
215 lines
8.0 KiB
TypeScript
215 lines
8.0 KiB
TypeScript
/**
|
|
* Unit tests for src/auth/context.ts -- the auth seam. Verification is
|
|
* injected (opts.verify) so these tests are pure and network-free.
|
|
*
|
|
* Three groups:
|
|
* 1. the ENV DEFAULT (the fail-closed contract: absent AUTH_ENFORCE means
|
|
* an unauthenticated request is REJECTED, and production refuses to run
|
|
* with auth switched off),
|
|
* 2. enforced-mode decision logic,
|
|
* 3. the explicit `AUTH_ENFORCE=false` local-development escape hatch.
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import {
|
|
resolveAuthContext,
|
|
authEnforced,
|
|
assertAuthConfigSafe,
|
|
AuthMisconfigurationError,
|
|
AUTH_ENFORCE_VAR,
|
|
type OptRequestContext,
|
|
} from '@/src/auth/context';
|
|
import type { VerifiedPrincipal } from '@/src/auth/zitadel';
|
|
|
|
const principal: VerifiedPrincipal = {
|
|
sub: 'user_1',
|
|
tenantId: 'tenant_x',
|
|
claims: { sub: 'user_1', iss: 'https://id-customers.siax.io' },
|
|
};
|
|
|
|
const okVerify = vi.fn(async () => principal);
|
|
const failVerify = vi.fn(async () => {
|
|
throw new Error('token_verification_failed');
|
|
});
|
|
|
|
function h(init: Record<string, string>): Headers {
|
|
return new Headers(init);
|
|
}
|
|
|
|
describe('auth default -- fail-closed (no AUTH_ENFORCE in the environment)', () => {
|
|
const saved = { auth: process.env.AUTH_ENFORCE, node: process.env.NODE_ENV };
|
|
|
|
beforeEach(() => {
|
|
delete process.env.AUTH_ENFORCE;
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (saved.auth === undefined) delete process.env.AUTH_ENFORCE;
|
|
else process.env.AUTH_ENFORCE = saved.auth;
|
|
if (saved.node === undefined) delete process.env.NODE_ENV;
|
|
else process.env.NODE_ENV = saved.node;
|
|
});
|
|
|
|
it('REGRESSION: with the variable unset, an unauthenticated request is REJECTED (401)', async () => {
|
|
// No `enforce` option -> the decision comes from the environment, which is
|
|
// exactly the fresh-container / DR-restore / dropped-env-var case.
|
|
const d = await resolveAuthContext(h({ 'x-tenant-id': 'tenant_attacker' }), { verify: okVerify });
|
|
expect(d.ok).toBe(false);
|
|
if (!d.ok) {
|
|
expect(d.status).toBe(401);
|
|
expect(d.code).toBe('OPT_UNAUTHENTICATED');
|
|
}
|
|
});
|
|
|
|
it('with the variable unset, an invalid token is REJECTED (401) rather than ignored', async () => {
|
|
const d = await resolveAuthContext(h({ authorization: 'Bearer bad.tok.en' }), { verify: failVerify });
|
|
expect(d.ok).toBe(false);
|
|
if (!d.ok) expect(d.status).toBe(401);
|
|
});
|
|
|
|
it('authEnforced() is true when the variable is absent, empty, or misspelled', () => {
|
|
expect(authEnforced({})).toBe(true);
|
|
expect(authEnforced({ AUTH_ENFORCE: '' })).toBe(true);
|
|
expect(authEnforced({ AUTH_ENFORCE: 'flase' })).toBe(true);
|
|
expect(authEnforced({ AUTH_ENFORCE: '0' })).toBe(true);
|
|
expect(authEnforced({ AUTH_ENFORCE: 'no' })).toBe(true);
|
|
expect(authEnforced({ AUTH_ENFORCE: 'true' })).toBe(true);
|
|
});
|
|
|
|
it('only the literal string "false" opts out', () => {
|
|
expect(authEnforced({ AUTH_ENFORCE: 'false' })).toBe(false);
|
|
expect(authEnforced({ AUTH_ENFORCE: 'FALSE' })).toBe(false);
|
|
expect(authEnforced({ AUTH_ENFORCE: ' false ' })).toBe(false);
|
|
});
|
|
|
|
it('assertAuthConfigSafe throws in production when enforcement is off, naming the variable', () => {
|
|
expect(() => assertAuthConfigSafe({ NODE_ENV: 'production', AUTH_ENFORCE: 'false' })).toThrow(
|
|
AuthMisconfigurationError,
|
|
);
|
|
expect(() => assertAuthConfigSafe({ NODE_ENV: 'production', AUTH_ENFORCE: 'false' })).toThrow(
|
|
new RegExp(AUTH_ENFORCE_VAR),
|
|
);
|
|
});
|
|
|
|
it('assertAuthConfigSafe is silent when production enforces, or when off outside production', () => {
|
|
expect(() => assertAuthConfigSafe({ NODE_ENV: 'production' })).not.toThrow();
|
|
expect(() => assertAuthConfigSafe({ NODE_ENV: 'production', AUTH_ENFORCE: 'true' })).not.toThrow();
|
|
expect(() => assertAuthConfigSafe({ NODE_ENV: 'development', AUTH_ENFORCE: 'false' })).not.toThrow();
|
|
});
|
|
|
|
it('a production process with AUTH_ENFORCE=false cannot serve a request at all (throws)', async () => {
|
|
process.env.NODE_ENV = 'production';
|
|
process.env.AUTH_ENFORCE = 'false';
|
|
await expect(
|
|
resolveAuthContext(h({ 'x-tenant-id': 'tenant_attacker' }), { verify: okVerify }),
|
|
).rejects.toBeInstanceOf(AuthMisconfigurationError);
|
|
});
|
|
});
|
|
|
|
describe('resolveAuthContext -- enforced mode (the default)', () => {
|
|
it('401s when no bearer token is present', async () => {
|
|
const d = await resolveAuthContext(h({ 'x-tenant-id': 'tenant_x' }), { enforce: true, verify: okVerify });
|
|
expect(d.ok).toBe(false);
|
|
if (!d.ok) {
|
|
expect(d.status).toBe(401);
|
|
expect(d.code).toBe('OPT_UNAUTHENTICATED');
|
|
}
|
|
});
|
|
|
|
it('401s when the token fails verification', async () => {
|
|
const d = await resolveAuthContext(h({ authorization: 'Bearer bad.tok.en' }), {
|
|
enforce: true,
|
|
verify: failVerify,
|
|
});
|
|
expect(d.ok).toBe(false);
|
|
if (!d.ok) expect(d.status).toBe(401);
|
|
});
|
|
|
|
it('attaches the verified principal and derives tenant from the TOKEN', async () => {
|
|
const d = await resolveAuthContext(h({ authorization: 'Bearer good.tok.en' }), {
|
|
enforce: true,
|
|
verify: okVerify,
|
|
});
|
|
expect(d.ok).toBe(true);
|
|
if (d.ok) {
|
|
expect(d.ctx.principal).toEqual(principal);
|
|
expect(d.ctx.tenantId).toBe('tenant_x');
|
|
expect(d.ctx.actorId).toBe('user_1');
|
|
}
|
|
});
|
|
|
|
it('403s when X-Tenant-Id disagrees with the token tenant', async () => {
|
|
const d = await resolveAuthContext(
|
|
h({ authorization: 'Bearer good.tok.en', 'x-tenant-id': 'tenant_evil' }),
|
|
{ enforce: true, verify: okVerify },
|
|
);
|
|
expect(d.ok).toBe(false);
|
|
if (!d.ok) {
|
|
expect(d.status).toBe(403);
|
|
expect(d.code).toBe('OPT_ACCESS_DENIED');
|
|
expect(d.details).toMatchObject({ header_tenant: 'tenant_evil', token_tenant: 'tenant_x' });
|
|
}
|
|
});
|
|
|
|
it('rejects a forged actor header and accepts only the verified subject', async () => {
|
|
const forged = await resolveAuthContext(
|
|
h({ authorization: 'Bearer good.tok.en', 'x-actor-id': 'user_other' }),
|
|
{ enforce: true, verify: okVerify },
|
|
);
|
|
expect(forged).toMatchObject({ ok: false, status: 403, code: 'OPT_ACCESS_DENIED' });
|
|
const matching = await resolveAuthContext(
|
|
h({ authorization: 'Bearer good.tok.en', 'x-actor-id': 'user_1' }),
|
|
{ enforce: true, verify: okVerify },
|
|
);
|
|
expect(matching.ok).toBe(true);
|
|
if (matching.ok) expect(matching.ctx.actorId).toBe('user_1');
|
|
});
|
|
|
|
it('allows a matching X-Tenant-Id', async () => {
|
|
const d = await resolveAuthContext(
|
|
h({ authorization: 'Bearer good.tok.en', 'x-tenant-id': 'tenant_x' }),
|
|
{ enforce: true, verify: okVerify },
|
|
);
|
|
expect(d.ok).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('resolveAuthContext -- explicit AUTH_ENFORCE=false escape hatch (local dev only)', () => {
|
|
it('never rejects when no token is present', async () => {
|
|
const d = await resolveAuthContext(h({ 'x-tenant-id': 'tenant_header' }), {
|
|
enforce: false,
|
|
verify: okVerify,
|
|
});
|
|
expect(d.ok).toBe(true);
|
|
if (d.ok) {
|
|
expect((d.ctx as OptRequestContext).principal).toBeUndefined();
|
|
expect(d.ctx.tenantId).toBe('tenant_header');
|
|
}
|
|
});
|
|
|
|
it('never rejects even when a present token fails verification (logs a warning)', async () => {
|
|
const logger = { warn: vi.fn() };
|
|
const d = await resolveAuthContext(h({ authorization: 'Bearer bad.tok.en' }), {
|
|
enforce: false,
|
|
verify: failVerify,
|
|
logger,
|
|
});
|
|
expect(d.ok).toBe(true);
|
|
if (d.ok) expect(d.ctx.principal).toBeUndefined();
|
|
expect(logger.warn).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('attaches the principal when a present token verifies, without overriding a supplied tenant', async () => {
|
|
const d = await resolveAuthContext(
|
|
h({ authorization: 'Bearer good.tok.en', 'x-tenant-id': 'tenant_header' }),
|
|
{ enforce: false, verify: okVerify },
|
|
);
|
|
expect(d.ok).toBe(true);
|
|
if (d.ok) {
|
|
expect(d.ctx.principal).toEqual(principal);
|
|
// the caller-supplied header tenant is preserved in this mode
|
|
expect(d.ctx.tenantId).toBe('tenant_header');
|
|
}
|
|
});
|
|
});
|