n8n/packages/@n8n/client-oauth2/test/client-assertion.test.ts

93 lines
3.0 KiB
TypeScript

import { formatPemBlock } from '@n8n/utils/format-pem-block';
import { createVerify, generateKeyPairSync, X509Certificate } from 'node:crypto';
import { buildClientAssertion } from '@/client-assertion';
import * as config from './config';
// SHA-1 (not SHA-256) thumbprint of config.certificate, base64url-encoded
const EXPECTED_X5T = 'aK3rs6ongTQ6RIYSBIx5LFJD-Q4';
function decodeSegment(segment: string) {
return JSON.parse(Buffer.from(segment, 'base64url').toString('utf8')) as Record<string, unknown>;
}
describe('buildClientAssertion', () => {
it('signs an RS256 JWT with the x5t header and expected claims', () => {
const assertion = buildClientAssertion({
clientId: config.clientId,
accessTokenUri: config.accessTokenUri,
privateKey: config.privateKey,
certificate: config.certificate,
});
const [headerSeg, payloadSeg, signatureSeg] = assertion.split('.');
const header = decodeSegment(headerSeg);
const payload = decodeSegment(payloadSeg);
expect(header).toMatchObject({ alg: 'RS256', typ: 'JWT', x5t: EXPECTED_X5T });
expect(payload).toMatchObject({
aud: config.accessTokenUri,
iss: config.clientId,
sub: config.clientId,
});
expect(payload.jti).toEqual(expect.any(String));
expect(payload.exp as number).toBeGreaterThan(payload.iat as number);
const verified = createVerify('RSA-SHA256')
.update(`${headerSeg}.${payloadSeg}`)
.verify(
new X509Certificate(formatPemBlock(config.certificate)).publicKey,
Buffer.from(signatureSeg, 'base64url'),
);
expect(verified).toBe(true);
});
it('throws a clear error when the certificate is not a valid X.509 certificate', () => {
expect(() =>
buildClientAssertion({
clientId: config.clientId,
accessTokenUri: config.accessTokenUri,
privateKey: config.privateKey,
certificate: 'not-a-valid-certificate',
}),
).toThrow('The Certificate field must contain a PEM certificate');
});
it('throws a clear error when the private key is invalid', () => {
expect(() =>
buildClientAssertion({
clientId: config.clientId,
accessTokenUri: config.accessTokenUri,
privateKey: 'not-a-valid-key',
certificate: config.certificate,
}),
).toThrow('The Private Key field must contain a PEM private key');
});
it('throws a clear error when the certificate and private key fields are swapped', () => {
expect(() =>
buildClientAssertion({
clientId: config.clientId,
accessTokenUri: config.accessTokenUri,
privateKey: config.certificate,
certificate: config.privateKey,
}),
).toThrow('The Certificate field must contain a PEM certificate');
});
it('throws when a well-formed non-RSA (EC) private key is provided', () => {
const { privateKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' });
expect(() =>
buildClientAssertion({
clientId: config.clientId,
accessTokenUri: config.accessTokenUri,
privateKey: privateKey.export({ type: 'pkcs8', format: 'pem' }).toString(),
certificate: config.certificate,
}),
).toThrow('requires an RSA private key');
});
});