UNPKG

n8n

Version:

n8n Workflow Automation Tool

190 lines 7.39 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.JwksResolverService = void 0; const node_crypto_1 = require("node:crypto"); const backend_common_1 = require("@n8n/backend-common"); const di_1 = require("@n8n/di"); const n8n_workflow_1 = require("n8n-workflow"); const zod_1 = require("zod"); const token_exchange_schemas_1 = require("../token-exchange.schemas"); const FETCH_TIMEOUT_MS = 10_000; const DEFAULT_TTL_SECONDS = 3600; const MIN_TTL_SECONDS = 60; const MAX_TTL_SECONDS = 86_400; const supportedAlgorithmSet = new Set(token_exchange_schemas_1.JwtAlgorithmSchema.options); const SigningJwkSchema = zod_1.z .object({ kid: zod_1.z.string().min(1), kty: zod_1.z.enum(['RSA', 'EC', 'OKP']), alg: zod_1.z.string().optional(), use: zod_1.z .string() .optional() .refine((use) => use === undefined || use === 'sig', { message: 'Only signing keys (use: "sig" or absent) are accepted', }), crv: zod_1.z.string().optional(), }) .passthrough(); const JwkSetSchema = zod_1.z.object({ keys: zod_1.z.array(zod_1.z.unknown()).min(1), }); const CURVE_TO_ALGORITHM = { 'P-256': 'ES256', 'P-384': 'ES384', 'P-521': 'ES512', Ed25519: 'EdDSA', }; function inferAlgorithm(jwk) { if (jwk.alg) { return supportedAlgorithmSet.has(jwk.alg) ? jwk.alg : undefined; } switch (jwk.kty) { case 'RSA': return 'RS256'; case 'EC': case 'OKP': return jwk.crv ? CURVE_TO_ALGORITHM[jwk.crv] : undefined; default: return undefined; } } function parseMaxAge(cacheControl) { if (!cacheControl) return undefined; const match = /max-age=(\d+)/.exec(cacheControl); if (!match) return undefined; const value = parseInt(match[1], 10); return Number.isNaN(value) ? undefined : value; } let JwksResolverService = class JwksResolverService { constructor(logger) { this.logger = logger.scoped('token-exchange'); } async resolveKeys(source, options) { const fetcher = options?.fetcher ?? globalThis.fetch; const defaultTtl = options?.defaultTtlSeconds ?? DEFAULT_TTL_SECONDS; const { url } = source; this.logger.debug(`Fetching JWKS from "${url}"`); const { rawKeys, cacheControl } = await this.fetchJwkSet(url, fetcher); const ttlSeconds = this.computeTtl(cacheControl, source, defaultTtl); const keys = []; const skipped = []; for (const rawJwk of rawKeys) { const result = this.parseJwk(rawJwk, source); if ('resolved' in result) { keys.push(result.resolved); } else { skipped.push(result.skipped); } } if (keys.length === 0) { const reasons = skipped.map((s) => `${s.kid ?? 'unknown'}: ${s.reason}`).join('; '); throw new n8n_workflow_1.OperationalError(`JWKS response has no usable signing keys for "${url}" (${reasons})`); } this.logger.debug(`Resolved ${keys.length} key(s) from "${url}"`, { resolved: keys.length, skipped: skipped.length, ttlSeconds, }); return { keys, ttlSeconds, skipped }; } async fetchJwkSet(url, fetcher) { let response; try { response = await fetcher(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), headers: { Accept: 'application/json' }, redirect: 'error', }); } catch (error) { const message = error instanceof Error ? error.message : 'unknown error'; throw new n8n_workflow_1.OperationalError(`JWKS fetch failed for "${url}": ${message}`); } if (!response.ok) { throw new n8n_workflow_1.OperationalError(`JWKS fetch failed for "${url}": HTTP ${response.status}`); } let body; try { body = await response.json(); } catch { throw new n8n_workflow_1.OperationalError(`JWKS response is not valid JSON for "${url}"`); } const jwkSetResult = JwkSetSchema.safeParse(body); if (!jwkSetResult.success) { throw new n8n_workflow_1.OperationalError(`JWKS response has no keys for "${url}"`); } return { rawKeys: jwkSetResult.data.keys, cacheControl: response.headers.get('cache-control'), }; } parseJwk(rawJwk, source) { if (rawJwk === null || typeof rawJwk !== 'object') { return { skipped: { kid: undefined, reason: 'not an object' } }; } const jwkResult = SigningJwkSchema.safeParse(rawJwk); if (!jwkResult.success) { const raw = rawJwk; return { skipped: { kid: typeof raw.kid === 'string' ? raw.kid : undefined, reason: 'failed schema validation', }, }; } const jwk = jwkResult.data; const algorithm = inferAlgorithm(jwk); if (!algorithm) { return { skipped: { kid: jwk.kid, reason: `unsupported algorithm or key type (kty=${jwk.kty}, alg=${jwk.alg ?? 'none'}, crv=${jwk.crv ?? 'none'})`, }, }; } let keyObject; try { keyObject = (0, node_crypto_1.createPublicKey)({ format: 'jwk', key: jwk }); } catch { return { skipped: { kid: jwk.kid, reason: 'failed to create public key from JWK material' }, }; } return { resolved: { kid: jwk.kid, algorithms: [algorithm], keyMaterial: keyObject.export({ type: 'spki', format: 'pem' }), issuer: source.issuer, expectedAudience: source.expectedAudience, allowedRoles: source.allowedRoles, }, }; } computeTtl(cacheControl, source, defaultTtl) { const maxAge = parseMaxAge(cacheControl); const rawTtl = maxAge ?? source.cacheTtlSeconds ?? defaultTtl; return Math.max(MIN_TTL_SECONDS, Math.min(rawTtl, MAX_TTL_SECONDS)); } }; exports.JwksResolverService = JwksResolverService; exports.JwksResolverService = JwksResolverService = __decorate([ (0, di_1.Service)(), __metadata("design:paramtypes", [backend_common_1.Logger]) ], JwksResolverService); //# sourceMappingURL=jwks-resolver.js.map