UNPKG

n8n

Version:

n8n Workflow Automation Tool

570 lines 28.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); 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 __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); 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 }; }; var OauthService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.OauthService = exports.OauthVersion = exports.skipAuthOnOAuthCallback = void 0; exports.shouldSkipAuthOnOAuthCallback = shouldSkipAuthOnOAuthCallback; const backend_common_1 = require("@n8n/backend-common"); const config_1 = require("@n8n/config"); const db_1 = require("@n8n/db"); const di_1 = require("@n8n/di"); const csrf_1 = __importDefault(require("csrf")); const n8n_core_1 = require("n8n-core"); const n8n_workflow_1 = require("n8n-workflow"); const constants_1 = require("../constants"); const credentials_finder_service_1 = require("../credentials/credentials-finder.service"); const credentials_helper_1 = require("../credentials-helper"); const auth_error_1 = require("../errors/response-errors/auth.error"); const bad_request_error_1 = require("../errors/response-errors/bad-request.error"); const not_found_error_1 = require("../errors/response-errors/not-found.error"); const validate_oauth_url_1 = require("../oauth/validate-oauth-url"); const url_service_1 = require("../services/url.service"); const WorkflowExecuteAdditionalData = __importStar(require("../workflow-execute-additional-data")); const client_oauth2_1 = require("@n8n/client-oauth2"); const axios_1 = __importDefault(require("axios")); const oauth2_dynamic_client_registration_schema_1 = require("../controllers/oauth/oauth2-dynamic-client-registration.schema"); const pkce_challenge_1 = __importDefault(require("pkce-challenge")); const qs = __importStar(require("querystring")); const split_1 = __importDefault(require("lodash/split")); const external_hooks_1 = require("../external-hooks"); const crypto_1 = require("crypto"); const oauth_1_0a_1 = __importDefault(require("oauth-1.0a")); const types_1 = require("./types"); Object.defineProperty(exports, "OauthVersion", { enumerable: true, get: function () { return types_1.OauthVersion; } }); const dynamic_credentials_proxy_1 = require("../credentials/dynamic-credentials-proxy"); function shouldSkipAuthOnOAuthCallback() { const value = process.env.N8N_SKIP_AUTH_ON_OAUTH_CALLBACK?.toLowerCase() ?? 'false'; return value === 'true'; } exports.skipAuthOnOAuthCallback = shouldSkipAuthOnOAuthCallback(); let OauthService = OauthService_1 = class OauthService { constructor(logger, credentialsHelper, credentialsRepository, credentialsFinderService, urlService, globalConfig, externalHooks, cipher, dynamicCredentialsProxy) { this.logger = logger; this.credentialsHelper = credentialsHelper; this.credentialsRepository = credentialsRepository; this.credentialsFinderService = credentialsFinderService; this.urlService = urlService; this.globalConfig = globalConfig; this.externalHooks = externalHooks; this.cipher = cipher; this.dynamicCredentialsProxy = dynamicCredentialsProxy; } validateOAuthUrlOrThrow(url) { try { (0, validate_oauth_url_1.validateOAuthUrl)(url); } catch (e) { this.logger.error('Invalid OAuth URL', { url, error: e }); throw e; } } getBaseUrl(oauthVersion) { const restUrl = `${this.urlService.getInstanceBaseUrl()}/${this.globalConfig.endpoints.rest}`; return `${restUrl}/oauth${oauthVersion}-credential`; } async getCredentialForUpdate(req) { const { id: credentialId } = req.query; if (!credentialId) { throw new bad_request_error_1.BadRequestError('Required credential ID is missing'); } const credential = await this.credentialsFinderService.findCredentialForUser(credentialId, req.user, ['credential:update']); if (!credential) { this.logger.error('OAuth credential authorization failed because the current user does not have the correct permissions', { userId: req.user.id, credentialId }); throw new not_found_error_1.NotFoundError(constants_1.RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL); } return credential; } async getAdditionalData() { return await WorkflowExecuteAdditionalData.getBase(); } async getDecryptedDataForAuthUri(credential, additionalData) { return await this.getDecryptedData(credential, additionalData, false); } async getDecryptedDataForCallback(credential, additionalData) { return await this.getDecryptedData(credential, additionalData, true); } async getDecryptedData(credential, additionalData, raw) { return await this.credentialsHelper.getDecrypted(additionalData, credential, credential.type, 'internal', undefined, raw); } async applyDefaultsAndOverwrites(credential, decryptedData, additionalData) { return (await this.credentialsHelper.applyDefaultsAndOverwrites(additionalData, decryptedData, credential.type, 'internal', undefined, undefined)); } async encryptAndSaveData(credential, toUpdate, toDelete = []) { if (toUpdate.oauthTokenData && typeof toUpdate.oauthTokenData === 'object') { const identifier = OauthService_1.extractAccountIdentifier(toUpdate.oauthTokenData); if (identifier) { toUpdate.accountIdentifier = identifier; } } const credentials = new n8n_core_1.Credentials(credential, credential.type, credential.data); await credentials.updateData(toUpdate, toDelete); await this.credentialsRepository.update(credential.id, { ...credentials.getDataToSave(), updatedAt: new Date(), }); } static extractAccountIdentifier(tokenData) { for (const key of ['email', 'login', 'username', 'user', 'account']) { if (typeof tokenData[key] === 'string' && tokenData[key]) { return tokenData[key]; } } if (typeof tokenData.id_token === 'string') { const parts = tokenData.id_token.split('.'); if (parts.length === 3) { try { const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString()); if (typeof payload.email === 'string' && payload.email) { return payload.email; } if (typeof payload.preferred_username === 'string' && payload.preferred_username) { return payload.preferred_username; } } catch { } } } const authedUser = tokenData.authed_user; if (authedUser && typeof authedUser === 'object') { const user = authedUser; if (typeof user.id === 'string' && user.id) { return user.id; } } return undefined; } async getCredentialWithoutUser(credentialId) { return await this.credentialsRepository.findOneBy({ id: credentialId }); } async createCsrfState(data) { const token = new csrf_1.default(); const csrfSecret = token.secretSync(); const state = { token: token.create(csrfSecret), createdAt: Date.now(), data: await this.cipher.encryptV2(JSON.stringify(data)), }; const base64State = Buffer.from(JSON.stringify(state)).toString('base64'); return [csrfSecret, base64State]; } async decodeCsrfState(encodedState, req) { const errorMessage = 'Invalid state format'; const decodedState = Buffer.from(encodedState, 'base64').toString(); const decoded = (0, n8n_workflow_1.jsonParse)(decodedState, { errorMessage, }); const decryptedState = (0, n8n_workflow_1.jsonParse)(await this.cipher.decryptV2(decoded.data), { errorMessage, }); if (typeof decryptedState.cid !== 'string' || typeof decoded.token !== 'string') { throw new n8n_workflow_1.UnexpectedError(errorMessage); } if (decryptedState.origin === 'dynamic-credential') { return [ { ...decoded, ...decryptedState }, await this.getCredentialWithoutUser(decryptedState.cid), ]; } if (exports.skipAuthOnOAuthCallback) { return [ { ...decoded, ...decryptedState }, await this.getCredentialWithoutUser(decryptedState.cid), ]; } if (req.user?.id === undefined || decryptedState.userId !== req.user.id) { throw new auth_error_1.AuthError('Unauthorized'); } const credential = await this.credentialsFinderService.findCredentialForUser(decryptedState.cid, req.user, ['credential:update']); return [{ ...decoded, ...decryptedState }, credential]; } verifyCsrfState(decrypted, state) { const token = new csrf_1.default(); return (Date.now() - state.createdAt <= types_1.MAX_CSRF_AGE && decrypted.csrfSecret !== undefined && token.verify(decrypted.csrfSecret, state.token)); } async resolveCredential(req) { const { state: encodedState } = req.query; const [state, credential] = await this.decodeCsrfState(encodedState, req); if (!credential) { throw new not_found_error_1.NotFoundError(constants_1.RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL); } const additionalData = await this.getAdditionalData(); const decryptedDataOriginal = await this.getDecryptedDataForCallback(credential, additionalData); const oauthCredentials = await this.applyDefaultsAndOverwrites(credential, decryptedDataOriginal, additionalData); if (!this.verifyCsrfState(decryptedDataOriginal, state)) { throw new n8n_workflow_1.UnexpectedError('The OAuth callback state is invalid!'); } return [credential, decryptedDataOriginal, oauthCredentials, state]; } renderCallbackError(res, message, reason) { res.render('oauth-error-callback', { error: { message, reason } }); } async getOAuthCredentials(credential) { const additionalData = await this.getAdditionalData(); const decryptedDataOriginal = await this.getDecryptedDataForAuthUri(credential, additionalData); if (decryptedDataOriginal?.scope && credential.type.includes('OAuth2') && !constants_1.GENERIC_OAUTH2_CREDENTIALS_WITH_EDITABLE_SCOPE.includes(credential.type) && !this.hasEditableScopeProperty(credential.type)) { delete decryptedDataOriginal.scope; } const oauthCredentials = await this.applyDefaultsAndOverwrites(credential, decryptedDataOriginal, additionalData); return oauthCredentials; } hasEditableScopeProperty(credentialType) { try { const properties = this.credentialsHelper.getCredentialsProperties(credentialType); const scopeProperty = properties.find((property) => property.name === 'scope'); return scopeProperty !== undefined && scopeProperty.type !== 'hidden'; } catch { return false; } } async generateAOauth2AuthUri(credential, csrfData) { const oauthCredentials = await this.getOAuthCredentials(credential); const toUpdate = {}; if (oauthCredentials.useDynamicClientRegistration && oauthCredentials.serverUrl) { this.validateOAuthUrlOrThrow(oauthCredentials.serverUrl); let authorizationServerUrl; try { const protectedResourceMetadata = await this.discoverProtectedResourceMetadata(oauthCredentials.serverUrl); authorizationServerUrl = protectedResourceMetadata.authorization_servers[0]; this.validateOAuthUrlOrThrow(authorizationServerUrl); this.logger.debug('Protected resource discovery succeeded', { resourceUrl: oauthCredentials.serverUrl, authorizationServerUrl, }); } catch (error) { if (error instanceof bad_request_error_1.BadRequestError && error.message.includes('OAuth url')) { throw error; } this.logger.debug('Protected resource discovery failed, assuming serverUrl is authorization server', { serverUrl: oauthCredentials.serverUrl, error: error.message, }); authorizationServerUrl = oauthCredentials.serverUrl; } const issuerUrl = new URL(authorizationServerUrl); const pathComponent = issuerUrl.pathname.replace(/\/$/, ''); const pathIsWellKnown = pathComponent.startsWith('/.well-known'); const discoveryUrls = pathComponent && !pathIsWellKnown ? [ `${issuerUrl.origin}/.well-known/oauth-authorization-server${pathComponent}`, `${issuerUrl.origin}/.well-known/openid-configuration${pathComponent}`, `${authorizationServerUrl}/.well-known/openid-configuration`, ] : [ `${issuerUrl.origin}/.well-known/oauth-authorization-server`, `${issuerUrl.origin}/.well-known/openid-configuration`, ]; let data; let lastError; for (const url of discoveryUrls) { try { this.validateOAuthUrlOrThrow(url); const response = await axios_1.default.get(url, { validateStatus: (status) => status === 200, }); data = response.data; break; } catch (error) { lastError = error; } } if (!data) { throw new bad_request_error_1.BadRequestError(`Failed to discover OAuth2 authorization server metadata. Tried: ${discoveryUrls.join(', ')}. Last error: ${lastError?.message}`); } const metadataValidation = oauth2_dynamic_client_registration_schema_1.oAuthAuthorizationServerMetadataSchema.safeParse(data); if (!metadataValidation.success) { throw new bad_request_error_1.BadRequestError(`Invalid OAuth2 server metadata: ${metadataValidation.error.issues.map((e) => e.message).join(', ')}`); } const { authorization_endpoint, token_endpoint, registration_endpoint, scopes_supported } = metadataValidation.data; oauthCredentials.authUrl = authorization_endpoint; oauthCredentials.accessTokenUrl = token_endpoint; toUpdate.authUrl = authorization_endpoint; toUpdate.accessTokenUrl = token_endpoint; const scope = scopes_supported ? scopes_supported.join(' ') : undefined; if (scope) { oauthCredentials.scope = scope; toUpdate.scope = scope; } const { grantType, authentication } = this.selectGrantTypeAndAuthenticationMethod(metadataValidation.data.grant_types_supported ?? ['authorization_code', 'implicit'], metadataValidation.data.token_endpoint_auth_methods_supported ?? ['client_secret_basic'], metadataValidation.data.code_challenge_methods_supported ?? []); oauthCredentials.grantType = grantType; toUpdate.grantType = grantType; if (authentication) { oauthCredentials.authentication = authentication; toUpdate.authentication = authentication; } const { grant_types, token_endpoint_auth_method } = this.mapGrantTypeAndAuthenticationMethod(grantType, authentication); const registerPayload = { redirect_uris: [`${this.getBaseUrl(2)}/callback`], token_endpoint_auth_method, grant_types, response_types: ['code'], client_name: 'n8n', client_uri: 'https://n8n.io/', scope, }; await this.externalHooks.run('oauth2.dynamicClientRegistration', [registerPayload]); const { data: registerResult } = await axios_1.default.post(registration_endpoint, registerPayload); const registrationValidation = oauth2_dynamic_client_registration_schema_1.dynamicClientRegistrationResponseSchema.safeParse(registerResult); if (!registrationValidation.success) { throw new bad_request_error_1.BadRequestError(`Invalid client registration response: ${registrationValidation.error.issues.map((e) => e.message).join(', ')}`); } const { client_id, client_secret } = registrationValidation.data; oauthCredentials.clientId = client_id; toUpdate.clientId = client_id; if (client_secret) { oauthCredentials.clientSecret = client_secret; toUpdate.clientSecret = client_secret; } } this.validateOAuthUrlOrThrow(oauthCredentials.authUrl ?? ''); this.validateOAuthUrlOrThrow(oauthCredentials.accessTokenUrl ?? ''); const [csrfSecret, state] = await this.createCsrfState(csrfData); const oAuthOptions = { ...this.convertCredentialToOptions(oauthCredentials), state, }; if (oauthCredentials.authQueryParameters) { oAuthOptions.query = qs.parse(oauthCredentials.authQueryParameters); } await this.externalHooks.run('oauth2.authenticate', [oAuthOptions]); toUpdate.csrfSecret = csrfSecret; if (oauthCredentials.grantType === 'pkce') { const { code_verifier, code_challenge } = await (0, pkce_challenge_1.default)(); oAuthOptions.query = { ...oAuthOptions.query, code_challenge, code_challenge_method: 'S256', }; toUpdate.codeVerifier = code_verifier; } await this.encryptAndSaveData(credential, toUpdate); const oAuthObj = new client_oauth2_1.ClientOAuth2(oAuthOptions); const returnUri = oAuthObj.code.getUri(); this.logger.debug('OAuth2 authorization url created for credential', { csrfData, credentialId: credential.id, }); return returnUri.toString(); } async generateAOauth1AuthUri(credential, csrfData) { const oauthCredentials = await this.getOAuthCredentials(credential); this.validateOAuthUrlOrThrow(oauthCredentials.authUrl ?? ''); this.validateOAuthUrlOrThrow(oauthCredentials.requestTokenUrl ?? ''); this.validateOAuthUrlOrThrow(oauthCredentials.accessTokenUrl ?? ''); const [csrfSecret, state] = await this.createCsrfState(csrfData); const signatureMethod = oauthCredentials.signatureMethod; const oAuthOptions = { consumer: { key: oauthCredentials.consumerKey, secret: oauthCredentials.consumerSecret, }, signature_method: signatureMethod, hash_function(base, key) { const algorithm = types_1.algorithmMap[signatureMethod] ?? 'sha1'; return (0, crypto_1.createHmac)(algorithm, key).update(base).digest('base64'); }, }; const oauthRequestData = { oauth_callback: `${this.getBaseUrl(1)}/callback?state=${state}`, }; await this.externalHooks.run('oauth1.authenticate', [oAuthOptions, oauthRequestData]); const oauth = new oauth_1_0a_1.default(oAuthOptions); const options = { method: 'POST', url: oauthCredentials.requestTokenUrl, data: oauthRequestData, }; const data = oauth.toHeader(oauth.authorize(options)); const axiosConfig = { method: options.method, url: options.url, headers: { ...data, }, }; const { data: response } = await axios_1.default.request(axiosConfig); if (typeof response !== 'string') { throw new bad_request_error_1.BadRequestError('Expected string response from OAuth1 request token endpoint, but received invalid response type'); } const paramsParser = new URLSearchParams(response); const responseJson = Object.fromEntries(paramsParser.entries()); if (!responseJson.oauth_token) { throw new bad_request_error_1.BadRequestError('OAuth1 request token response is missing required oauth_token parameter'); } const returnUri = `${oauthCredentials.authUrl}?oauth_token=${responseJson.oauth_token}`; await this.encryptAndSaveData(credential, { csrfSecret }, []); this.logger.debug('OAuth1 authorization url created for credential', { csrfData, credentialId: credential.id, }); return returnUri; } convertCredentialToOptions(credential) { const options = { clientId: credential.clientId, clientSecret: credential.clientSecret ?? '', accessTokenUri: credential.accessTokenUrl ?? '', authorizationUri: credential.authUrl ?? '', authentication: credential.authentication ?? 'header', redirectUri: `${this.getBaseUrl(2)}/callback`, scopes: (0, split_1.default)(credential.scope ?? 'openid', ','), scopesSeparator: credential.scope?.includes(',') ? ',' : ' ', ignoreSSLIssues: credential.ignoreSSLIssues ?? false, }; if (credential.additionalBodyProperties && typeof credential.additionalBodyProperties === 'string') { const parsedBody = (0, n8n_workflow_1.jsonParse)(credential.additionalBodyProperties); if (parsedBody) { options.body = parsedBody; } } return options; } async discoverProtectedResourceMetadata(resourceUrl) { this.validateOAuthUrlOrThrow(resourceUrl); const url = new URL(resourceUrl); const pathComponent = url.pathname.replace(/\/$/, ''); const discoveryUrls = pathComponent ? [ `${url.origin}/.well-known/oauth-protected-resource${pathComponent}`, `${url.origin}/.well-known/oauth-protected-resource`, ] : [ `${url.origin}/.well-known/oauth-protected-resource`, ]; for (const discoveryUrl of discoveryUrls) { try { this.validateOAuthUrlOrThrow(discoveryUrl); const { data } = await axios_1.default.get(discoveryUrl, { validateStatus: (status) => status === 200, }); if (data && Array.isArray(data.authorization_servers) && data.authorization_servers.length > 0) { return data; } } catch (error) { } } throw new bad_request_error_1.BadRequestError(`Failed to discover protected resource metadata. Tried: ${discoveryUrls.join(', ')}`); } selectGrantTypeAndAuthenticationMethod(grantTypes, tokenEndpointAuthMethods, codeChallengeMethods) { if (grantTypes.includes('authorization_code')) { if (codeChallengeMethods.includes('S256')) { return { grantType: 'pkce' }; } if (tokenEndpointAuthMethods.includes('client_secret_basic')) { return { grantType: 'authorizationCode', authentication: 'header' }; } if (tokenEndpointAuthMethods.includes('client_secret_post')) { return { grantType: 'authorizationCode', authentication: 'body' }; } } if (grantTypes.includes('client_credentials')) { if (tokenEndpointAuthMethods.includes('client_secret_basic')) { return { grantType: 'clientCredentials', authentication: 'header' }; } if (tokenEndpointAuthMethods.includes('client_secret_post')) { return { grantType: 'clientCredentials', authentication: 'body' }; } } throw new bad_request_error_1.BadRequestError('No supported grant type and authentication method found'); } mapGrantTypeAndAuthenticationMethod(grantType, authentication) { if (grantType === 'pkce') { return { grant_types: ['authorization_code', 'refresh_token'], token_endpoint_auth_method: 'none', }; } const tokenEndpointAuthMethod = authentication === 'header' ? 'client_secret_basic' : 'client_secret_post'; if (grantType === 'authorizationCode') { return { grant_types: ['authorization_code', 'refresh_token'], token_endpoint_auth_method: tokenEndpointAuthMethod, }; } return { grant_types: ['client_credentials'], token_endpoint_auth_method: tokenEndpointAuthMethod, }; } async saveDynamicCredential(credential, oauthTokenData, authHeader, credentialResolverId, authMetadata = {}) { const credentials = new n8n_core_1.Credentials(credential, credential.type, credential.data); await credentials.updateData(oauthTokenData, ['csrfSecret']); const credentialStoreMetadata = { id: credential.id, name: credential.name, type: credential.type, isResolvable: credential.isResolvable, resolverId: credentialResolverId, }; await this.dynamicCredentialsProxy.storeIfNeeded(credentialStoreMetadata, oauthTokenData, { version: 1, identity: authHeader, metadata: authMetadata }, await credentials.getData(), { credentialResolverId }); } }; exports.OauthService = OauthService; exports.OauthService = OauthService = OauthService_1 = __decorate([ (0, di_1.Service)(), __metadata("design:paramtypes", [backend_common_1.Logger, credentials_helper_1.CredentialsHelper, db_1.CredentialsRepository, credentials_finder_service_1.CredentialsFinderService, url_service_1.UrlService, config_1.GlobalConfig, external_hooks_1.ExternalHooks, n8n_core_1.Cipher, dynamic_credentials_proxy_1.DynamicCredentialsProxy]) ], OauthService); //# sourceMappingURL=oauth.service.js.map