Files
CASAN/packages/casan-control-panel/backend/test/auth-provider.test.ts
T

111 lines
4.8 KiB
TypeScript

import { createHmac, createSign, generateKeyPairSync } from 'node:crypto';
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
JwtAuthProvider,
validateAuthConfig,
type ControlPlaneAuthConfig,
} from '../src/common/auth-provider.js';
const secret = 'test-only-secret-with-sufficient-length';
const baseConfig: ControlPlaneAuthConfig = {
mode: 'jwt',
profile: 'test',
bind: '0.0.0.0',
issuer: 'https://issuer.test',
audience: 'casan-control-plane',
algorithm: 'HS256',
hmacSecret: secret,
clockSkewSeconds: 30,
roleClaim: 'groups',
tenantClaim: 'casan_tenant',
projectClaim: 'casan_project',
};
function token(payload: Record<string, unknown>, signingSecret = secret): string {
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
const signature = createHmac('sha256', signingSecret).update(`${header}.${body}`).digest('base64url');
return `${header}.${body}.${signature}`;
}
function rsToken(payload: Record<string, unknown>, privateKey: string): string {
const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url');
const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
const signer = createSign('RSA-SHA256');
signer.update(`${header}.${body}`);
signer.end();
return `${header}.${body}.${signer.sign(privateKey).toString('base64url')}`;
}
function claims(now: number, overrides: Record<string, unknown> = {}): Record<string, unknown> {
return {
iss: baseConfig.issuer,
aud: baseConfig.audience,
sub: 'verified-user',
exp: now + 300,
iat: now,
groups: ['project-admin'],
casan_tenant: 'tenant-a',
casan_project: 'project-a',
...overrides,
};
}
test('JWT provider verifies claims and ignores spoofed forwarded identity headers', () => {
const now = 1_800_000_000;
const provider = new JwtAuthProvider(baseConfig);
const decision = provider.authenticate({
authorization: `Bearer ${token(claims(now))}`,
'x-forwarded-user': 'attacker',
'x-casan-role': 'org-admin',
'x-casan-tenant': 'victim',
}, now);
assert.equal(decision.allowed, true);
assert.equal(decision.claims?.subject, 'verified-user');
assert.equal(decision.claims?.tenant, 'tenant-a');
assert.deepEqual(decision.claims?.roles, ['project-admin']);
});
test('JWT provider rejects missing, invalid, expired, wrong-audience and wrong-issuer tokens', () => {
const now = 1_800_000_000;
const provider = new JwtAuthProvider(baseConfig);
assert.equal(provider.authenticate({}, now).reasonCode, 'auth_bearer_token_required');
assert.equal(provider.authenticate({ authorization: `Bearer ${token(claims(now), 'wrong-secret')}` }, now).reasonCode, 'auth_signature_invalid');
assert.equal(provider.authenticate({ authorization: `Bearer ${token(claims(now, { exp: now - 31 }))}` }, now).reasonCode, 'auth_token_expired');
assert.equal(provider.authenticate({ authorization: `Bearer ${token(claims(now, { aud: 'wrong' }))}` }, now).reasonCode, 'auth_audience_invalid');
assert.equal(provider.authenticate({ authorization: `Bearer ${token(claims(now, { iss: 'https://wrong.test' }))}` }, now).reasonCode, 'auth_issuer_invalid');
});
test('production-compatible RS256 verification accepts a valid asymmetric token', () => {
const now = 1_800_000_000;
const keys = generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
});
const provider = new JwtAuthProvider({
...baseConfig,
profile: 'production',
algorithm: 'RS256',
publicKey: keys.publicKey,
hmacSecret: undefined,
});
const decision = provider.authenticate({ authorization: `Bearer ${rsToken(claims(now), keys.privateKey)}` }, now);
assert.equal(decision.allowed, true);
assert.equal(decision.claims?.subject, 'verified-user');
});
test('production and non-loopback startup refuse local or symmetric identity modes', () => {
assert.deepEqual(
validateAuthConfig({ ...baseConfig, mode: 'local', profile: 'production' }),
['verified_identity_required'],
);
assert.ok(validateAuthConfig({ ...baseConfig, profile: 'production' }).includes('auth_asymmetric_key_required_in_production'));
assert.ok(validateAuthConfig({ ...baseConfig, mode: 'local', profile: 'development' }).includes('verified_identity_required'));
assert.ok(validateAuthConfig({ ...baseConfig, mode: 'local', profile: 'strict', bind: '127.0.0.1' }).includes('verified_identity_required'));
assert.ok(validateAuthConfig({
...baseConfig, profile: 'production', algorithm: 'RS256', publicKey: 'not-a-public-key', hmacSecret: undefined,
}).includes('auth_rsa_public_key_invalid'));
});