UNPKG

n8n

Version:

n8n Workflow Automation Tool

172 lines • 8.13 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); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.OAuth2UserInfoIdentifier = exports.UserInfoResponseSchema = exports.OAuth2UserInfoOptionsSchema = void 0; const backend_common_1 = require("@n8n/backend-common"); const constants_1 = require("@n8n/constants"); const di_1 = require("@n8n/di"); const axios_1 = __importDefault(require("axios")); const zod_1 = require("zod"); const cache_service_1 = require("../../../../services/cache/cache.service"); const identifier_interface_1 = require("./identifier-interface"); const oauth2_utils_1 = require("./oauth2-utils"); const MIN_TOKEN_CACHE_TIMEOUT = 30 * constants_1.Time.seconds.toMilliseconds; const MAX_TOKEN_CACHE_TIMEOUT = 5 * constants_1.Time.minutes.toMilliseconds; const DEFAULT_CACHE_TIMEOUT = 60 * constants_1.Time.seconds.toMilliseconds; const METADATA_CACHE_TIMEOUT = 1 * constants_1.Time.hours.toMilliseconds; exports.OAuth2UserInfoOptionsSchema = zod_1.z.object({ ...oauth2_utils_1.OAuth2OptionsSchema.shape, validation: zod_1.z.literal('oauth2-userinfo'), }); const OAuth2MetadataSchema = zod_1.z.object({ issuer: zod_1.z.string().url(), userinfo_endpoint: zod_1.z.string().url(), }); exports.UserInfoResponseSchema = zod_1.z .object({ sub: zod_1.z.string().optional(), }) .passthrough(); const CACHE_PREFIX = 'oauth2-userinfo-identifier'; let OAuth2UserInfoIdentifier = class OAuth2UserInfoIdentifier { constructor(logger, cache) { this.logger = logger; this.cache = cache; } async validateOptions(identifierOptions) { const options = this.parseOptions(identifierOptions); let metadata; try { metadata = await this.fetchMetadata(options, true); } catch (error) { if (error instanceof identifier_interface_1.IdentifierValidationError) { throw error; } this.logger.error(`Failed to reach OAuth2 metadata URL ${options.metadataUri}`, { error, }); throw new identifier_interface_1.IdentifierValidationError(`Could not reach metadata URL: ${error instanceof Error ? error.message : String(error)}`, { cause: error }); } if (!metadata.userinfo_endpoint) { this.logger.error('Metadata does not contain an userinfo endpoint'); throw new identifier_interface_1.IdentifierValidationError('Metadata does not contain an userinfo endpoint'); } } async resolve(context, identifierOptions) { const options = this.parseOptions(identifierOptions); const metadata = await this.fetchMetadata(options); const hashedToken = (0, oauth2_utils_1.sha256)(context.identity); const identifierCacheKey = `${CACHE_PREFIX}:subject:${metadata.issuer}:${hashedToken}`; const cached = await this.cache.get(identifierCacheKey); if (cached) { return cached; } let ttl = DEFAULT_CACHE_TIMEOUT; const { subject, ttl: ttlOverwrite } = await this.resolveBasedOnUserInfo(metadata, options, context); if (ttlOverwrite) { ttl = ttlOverwrite; } await this.cache.set(identifierCacheKey, subject, ttl); return subject; } parseOptions(options) { try { return exports.OAuth2UserInfoOptionsSchema.parse(options); } catch (error) { this.logger.error('Invalid OAuth2 identifier options', { error }); throw new identifier_interface_1.IdentifierValidationError('Invalid OAuth2 identifier options', { cause: error, }); } } async fetchMetadata(options, skipCache = false) { const cacheKey = `${CACHE_PREFIX}:metadata:${options.metadataUri}`; if (!skipCache) { const cached = await this.cache.get(cacheKey); if (cached) { return cached; } } const response = await axios_1.default.get(options.metadataUri, { validateStatus: () => true, timeout: 10 * constants_1.Time.seconds.toMilliseconds, }); if (response.status !== 200) { this.logger.error(`Failed to fetch OAuth2 metadata from ${options.metadataUri}, status code: ${response.status}`); throw new identifier_interface_1.IdentifierValidationError(`Failed to fetch OAuth2 metadata, status code: ${response.status}`); } try { const metadata = OAuth2MetadataSchema.parse(response.data); if (!skipCache) { await this.cache.set(cacheKey, metadata, METADATA_CACHE_TIMEOUT); } return metadata; } catch (error) { this.logger.error('Invalid OAuth2 metadata format', { error }); throw new identifier_interface_1.IdentifierValidationError('Invalid OAuth2 metadata format', { cause: error }); } } parseUserInfoResponse(data) { try { return exports.UserInfoResponseSchema.parse(data); } catch (error) { this.logger.error('Invalid userinfo response format', { error }); throw new identifier_interface_1.IdentifierValidationError('Invalid userinfo response format'); } } async resolveBasedOnUserInfo(metadata, options, context) { const response = await axios_1.default.get(metadata.userinfo_endpoint, { headers: { authorization: `Bearer ${context.identity}` }, validateStatus: () => true, timeout: 10 * constants_1.Time.seconds.toMilliseconds, }); if (response.status !== 200) { this.logger.error('UserInfo failed', { status: response.status, data: response.data, }); throw new identifier_interface_1.IdentifierValidationError('UserInfo query failed'); } const userData = this.parseUserInfoResponse(response.data); const subject = userData[options.subjectClaim]; if (!subject) { this.logger.error(`UserInfo response missing subject claim (${options.subjectClaim})`); throw new identifier_interface_1.IdentifierValidationError(`UserInfo response missing subject claim (${options.subjectClaim})`); } const subjectStr = String(subject); this.logger.debug('UserInfo successfully', { subject: subjectStr }); let ttl = undefined; if (userData.exp && typeof userData.exp === 'number') { const expiresIn = userData.exp * 1000 - Date.now(); if (expiresIn > 0) { ttl = Math.max(MIN_TOKEN_CACHE_TIMEOUT, Math.min(expiresIn, MAX_TOKEN_CACHE_TIMEOUT)); } else { ttl = MIN_TOKEN_CACHE_TIMEOUT; } } return { subject: subjectStr, ttl }; } }; exports.OAuth2UserInfoIdentifier = OAuth2UserInfoIdentifier; exports.OAuth2UserInfoIdentifier = OAuth2UserInfoIdentifier = __decorate([ (0, di_1.Service)(), __metadata("design:paramtypes", [backend_common_1.Logger, cache_service_1.CacheService]) ], OAuth2UserInfoIdentifier); //# sourceMappingURL=oauth2-userinfo-identifier.js.map