mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-19 16:55:02 +02:00
77 lines
2.5 KiB
TypeScript
77 lines
2.5 KiB
TypeScript
import {
|
|
createPrivateKey,
|
|
createSign,
|
|
randomUUID,
|
|
X509Certificate,
|
|
type KeyObject,
|
|
} from 'node:crypto';
|
|
|
|
import { formatPemBlock } from './format-pem-block';
|
|
|
|
// private_key_jwt (RFC 7521/7523): the client proves its identity with a JWT
|
|
// signed by its private key instead of a shared secret. The `x5t` header (SHA-1
|
|
// thumbprint of the certificate) tells the server which public key verifies it.
|
|
export const CLIENT_ASSERTION_TYPE = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer';
|
|
|
|
const ASSERTION_TTL_SECONDS = 300;
|
|
|
|
function base64url(input: Buffer | string): string {
|
|
return Buffer.from(input).toString('base64url');
|
|
}
|
|
|
|
function certificateThumbprint(certificate: string): string {
|
|
let parsed: X509Certificate;
|
|
try {
|
|
parsed = new X509Certificate(formatPemBlock(certificate));
|
|
} catch (error) {
|
|
throw new Error(
|
|
'The Certificate field must contain a PEM certificate (-----BEGIN CERTIFICATE-----).',
|
|
{ cause: error },
|
|
);
|
|
}
|
|
return Buffer.from(parsed.fingerprint.replace(/:/g, ''), 'hex').toString('base64url');
|
|
}
|
|
|
|
export interface BuildClientAssertionOptions {
|
|
clientId: string;
|
|
/** Token endpoint; used as the JWT `aud`. */
|
|
accessTokenUri: string;
|
|
/** RSA private key (PEM). Signing is RS256-only; EC/Ed25519 keys are not supported. */
|
|
privateKey: string;
|
|
certificate: string;
|
|
}
|
|
|
|
export function buildClientAssertion(options: BuildClientAssertionOptions): string {
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const header = { alg: 'RS256', typ: 'JWT', x5t: certificateThumbprint(options.certificate) };
|
|
const payload = {
|
|
aud: options.accessTokenUri,
|
|
iss: options.clientId,
|
|
sub: options.clientId,
|
|
jti: randomUUID(),
|
|
iat: now,
|
|
nbf: now,
|
|
exp: now + ASSERTION_TTL_SECONDS,
|
|
};
|
|
|
|
let privateKey: KeyObject;
|
|
try {
|
|
privateKey = createPrivateKey(formatPemBlock(options.privateKey));
|
|
} catch (error) {
|
|
throw new Error(
|
|
'The Private Key field must contain a PEM private key (-----BEGIN PRIVATE KEY-----).',
|
|
{ cause: error },
|
|
);
|
|
}
|
|
|
|
// `createSign('RSA-SHA256')` also signs EC/Ed25519 keys, producing a signature
|
|
// that contradicts the pinned `alg: RS256` header. Reject non-RSA keys up front.
|
|
if (privateKey.asymmetricKeyType !== 'rsa') {
|
|
throw new Error('Certificate authentication requires an RSA private key');
|
|
}
|
|
|
|
const signingInput = `${base64url(JSON.stringify(header))}.${base64url(JSON.stringify(payload))}`;
|
|
const signature = createSign('RSA-SHA256').update(signingInput).sign(privateKey);
|
|
return `${signingInput}.${base64url(signature)}`;
|
|
}
|