import assert from 'node:assert/strict';
import { test } from 'node:test';
import type { webcrypto } from 'node:crypto';
import { VerifiedAccessIdentity, type TeamCertKey } from './access.ts';
const TEAM = 'tangled.cloudflareaccess.com';
const AUD = 'a'.repeat(32);
const env = { ACCESS_TEAM_DOMAIN: TEAM, ACCESS_AUD: AUD };
const generated = (await crypto.subtle.generateKey(
{ name: 'RSASSA-PKCS1-v1_5', modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' },
true,
['sign', 'verify'],
)) as webcrypto.CryptoKeyPair;
const jwk = {
private: (await crypto.subtle.exportKey('jwk', generated.privateKey)) as TeamCertKey,
public: (await crypto.subtle.exportKey('jwk', generated.publicKey)) as TeamCertKey,
};
let servedKeys: TeamCertKey[] = [{ ...jwk.public, kid: 'test-key', use: 'sig', alg: 'RS256' }];
globalThis.fetch = (async () => Response.json({ keys: servedKeys })) as typeof fetch;
const b64url = (bytes: Uint8Array): string => Buffer.from(bytes).toString('base64url');
const signedToken = async (claims: Record<string, unknown>, kid = 'test-key'): Promise<string> => {
const head = b64url(new TextEncoder().encode(JSON.stringify({ alg: 'RS256', kid, typ: 'JWT' })));
const payload = b64url(new TextEncoder().encode(JSON.stringify(claims)));
const key = await crypto.subtle.importKey(
'jwk',
jwk.private,
{ name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
false,
['sign'],
);
const signature = await crypto.subtle.sign(
'RSASSA-PKCS1-v1_5',
key,
new TextEncoder().encode(`${head}.${payload}`) as unknown as ArrayBuffer,
);
return `${head}.${payload}.${b64url(new Uint8Array(signature))}`;
};
const now = Math.floor(Date.now() / 1000);
const claims = (over: Record<string, unknown> = {}) => ({
iss: `https://${TEAM}`,
aud: AUD,
iat: now,
exp: now + 600,
email: 'lewis@cia.nel.pet',
...over,
});
test('team-signed token gives bearer email', async () => {
const identity = await VerifiedAccessIdentity.verify(await signedToken(claims()), env);
assert.equal(identity?.email, 'lewis@cia.nel.pet');
const arrayAud = await VerifiedAccessIdentity.verify(await signedToken(claims({ aud: [AUD] })), env);
assert.equal(arrayAud?.email, 'lewis@cia.nel.pet');
});
test('a token outside policy isnot a full ident', async () => {
const [head, payload] = (await signedToken(claims())).split('.');
const cases: Record<string, Promise<string>> = {
'tampered signature': Promise.resolve(`${head}.${payload}.AAAA`),
'another team': signedToken(claims({ iss: 'https://elsewhere.cloudflareaccess.com' })),
'another audience': signedToken(claims({ aud: 'b'.repeat(32) })),
'superstring audience': signedToken(claims({ aud: `${AUD}tail` })),
'prefixed audience': signedToken(claims({ aud: `head${AUD}` })),
expired: signedToken(claims({ exp: now - 3600 })),
'issued in the future': signedToken(claims({ iat: now + 3600 })),
'without exp': signedToken(claims({ exp: undefined })),
'without iat': signedToken(claims({ iat: undefined })),
};
for (const [name, token] of Object.entries(cases)) {
assert.equal(await VerifiedAccessIdentity.verify(await token, env), null, name);
}
});
test('token that is in rotated key kid will still be verifiable if served by certs', async () => {
servedKeys = [{ ...jwk.public, kid: 'rotated-key', use: 'sig', alg: 'RS256' }];
const token = await signedToken(claims(), 'rotated-key');
const identity = await VerifiedAccessIdentity.verify(token, env);
assert.equal(identity?.email, 'lewis@cia.nel.pet');
});
test('w/o team or aud configured should be closed', async () => {
assert.equal(await VerifiedAccessIdentity.verify(await signedToken(claims()), {}), null);
assert.equal(await VerifiedAccessIdentity.verify(null, env), null);
});