关于 node-add-oauth
用于为现有 n8n 节点添加标准 OAuth2 授权码支持的工作流技能,涵盖端点调研、凭据配置、请求路由、测试与验证。
主要功能
- 01检查节点与凭据惯例
- 02调研 OAuth2 端点和作用域
- 03创建并注册凭据定义
- 04更新节点与认证路由
- 05添加凭据和路由测试
- 06运行类型、Lint 与测试检查
适用场景
- 01为 n8n 集成添加 OAuth2 认证
- 02支持凭据的自定义作用域
- 03实现基于网关的 OAuth 路由
- 04通过测试验证 OAuth 凭据行为
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ n8n-io/ n8n --skill node-add-oauth安装后在 Claude Code 中输入/node-add-oauth确认已加载。
详细介绍
概述
为现有的 n8n 节点添加 OAuth2(授权码 / 3LO)支持。适用于任何支持标准 OAuth2 的第三方服务。
开始之前,请阅读 packages/nodes-base/credentials/ 下现有的类似 OAuth2 凭据文件及其测试,了解此代码库中使用的约定
(例如 DiscordOAuth2Api.credentials.ts、MicrosoftTeamsOAuth2Api.credentials.ts)。
步骤 0 — 解析参数
提取:
NODE_NAME:服务名称(例如GitHub、Notion)。尝试根据参数推断; 如果不明确,请询问用户。CUSTOM_SCOPES:凭据是否应支持用户自定义作用域。如果参数未明确说明这一点,请在继续之前询问用户:"用户是否应能自定义此凭据的 OAuth2 作用域,还是应固定作用域?"
步骤 1 — 探索节点
阅读以下内容(根据具体服务调整路径约定):
- 节点目录:
packages/nodes-base/nodes/{NODE_NAME}/- 查找
*.node.ts(主节点)及任何*Trigger.node.ts - 查找
GenericFunctions.ts(名称可能有所不同) - 检查是否存在
auth/version子目录
- 查找
- 现有凭据:
packages/nodes-base/credentials/— 查找现有的{NODE_NAME}*Api.credentials.ts文件,了解命名约定以及已使用的认证方式。 packages/nodes-base/package.json中的package.json— 查找此节点的现有凭据注册位置(搜索节点名称)。
步骤 2 — 调研 OAuth2 端点
查阅服务的 OAuth2 文档:
- 授权 URL
- 访问令牌 URL
- 必需的认证查询参数(例如
prompt=consent、access_type=offline) - 节点现有操作所需的默认作用域
- API 是否需要在令牌交换后查询 cloudId / workspace ID (Atlassian 风格的网关 API 需要;大多数服务不需要)
如果无法有把握地确定端点,请让用户提供。
步骤 3 — 创建凭据文件
文件:packages/nodes-base/credentials/{NODE_NAME}OAuth2Api.credentials.ts
import type { ICredentialType, INodeProperties } from 'n8n-workflow';
const defaultScopes = [/* minimum scopes for existing node operations */];
export class {NODE_NAME}OAuth2Api implements ICredentialType {
name = '{camelCase}OAuth2Api';
extends = ['oAuth2Api'];
displayName = '{Display Name} OAuth2 API';
documentationUrl = '{doc-slug}'; // matches docs.n8n.io/integrations/...
properties: INodeProperties[] = [
// Include service-specific fields the node needs to construct API calls
// (e.g. domain, workspace URL) — add BEFORE the hidden fields below.
{ displayName: 'Grant Type', name: 'grantType', type: 'hidden', default: 'authorizationCode' },
{ displayName: 'Authorization URL', name: 'authUrl', type: 'hidden', default: '{AUTH_URL}', required: true },
{ displayName: 'Access Token URL', name: 'accessTokenUrl', type: 'hidden', default: '{TOKEN_URL}', required: true },
// Only include authQueryParameters if the service requires extra query params:
{ displayName: 'Auth URI Query Parameters', name: 'authQueryParameters', type: 'hidden', default: '{QUERY_PARAMS}' },
{ displayName: 'Authentication', name: 'authentication', type: 'hidden', default: 'header' },
// ── Custom scopes block (ONLY when CUSTOM_SCOPES = yes) ──────────────
{
displayName: 'Custom Scopes',
name: 'customScopes',
type: 'boolean',
default: false,
description: 'Define custom scopes',
},
{
displayName:
'The default scopes needed for the node to work are already set. If you change these the node may not function correctly.',
name: 'customScopesNotice',
type: 'notice',
default: '',
displayOptions: { show: { customScopes: [true] } },
},
{
displayName: 'Enabled Scopes',
name: 'enabledScopes',
type: 'string',
displayOptions: { show: { customScopes: [true] } },
default: defaultScopes.join(' '),
description: 'Scopes that should be enabled',
},
// ── End custom scopes block ───────────────────────────────────────────
{
displayName: 'Scope',
name: 'scope',
type: 'hidden',
// Custom scopes: expression toggles between user value and defaults.
// Fixed scopes: use the literal defaultScopes string instead.
default:
'={{$self["customScopes"] ? $self["enabledScopes"] : "' + defaultScopes.join(' ') + '"}}',
},
];
}
规则:
- 不要添加
authenticate块 —oAuth2Api机制会自动注入 Bearer 令牌。 - 不要添加
test块 — OAuth 流程会验证凭据。 - 模块级的
defaultScopes是唯一事实来源:它会同时填充enabledScopes默认值和scope表达式回退值。只需在一个位置更新。 - 如果服务需要域名 / workspace URL 来构造 API 调用,请在隐藏字段之前添加一个可见的
string字段。
步骤 4 — 在 package.json 中注册凭据
文件:packages/nodes-base/package.json
找到 n8n.credentials 数组,并在此服务的其他凭据附近插入新条目(在该服务的区块内按字母顺序排列):
"dist/credentials/{NODE_NAME}OAuth2Api.credentials.js",
步骤 5 — 更新 GENERIC_OAUTH2_CREDENTIALS_WITH_EDITABLE_SCOPE(仅限自定义作用域)
仅当 CUSTOM_SCOPES = yes 时执行此步骤。
文件:packages/cli/src/constants.ts
将 '{camelCase}OAuth2Api' 添加到
GENERIC_OAUTH2_CREDENTIALS_WITH_EDITABLE_SCOPE 数组。否则,n8n 会在 OAuth2 重新连接时删除用户的自定义作用域。
export const GENERIC_OAUTH2_CREDENTIALS_WITH_EDITABLE_SCOPE = [
'oAuth2Api',
'googleOAuth2Api',
'microsoftOAuth2Api',
'highLevelOAuth2Api',
'mcpOAuth2Api',
'{camelCase}OAuth2Api', // ← add this
];
步骤 6 — 更新 GenericFunctions.ts
6a — 标准服务(令牌可直接用于实例 URL)
在现有的 else 回退分支之前添加一个 else if 分支:
} else if ({versionParam} === '{camelCase}OAuth2') {
domain = (await this.getCredentials('{camelCase}OAuth2Api')).{domainField} as string;
credentialType = '{camelCase}OAuth2Api';
} else {
6b — 需要查询 workspace / cloud ID 的网关服务
当 OAuth 令牌的作用域针对网关 URL 而非直接实例 URL 时(Atlassian 的 api.atlassian.com 是典型示例),请在主请求函数之前添加模块级缓存和查询辅助函数:
// Module-level cache: normalised domain → site/cloud ID
export const _cloudIdCache = new Map<string, string>();
async function getSiteId(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
credentialType: string,
domain: string,
): Promise<string> {
const normalizedDomain = domain.replace(/\/$/, '');
if (_cloudIdCache.has(normalizedDomain)) return _cloudIdCache.get(normalizedDomain)!;
const resources = (await this.helpers.requestWithAuthentication.call(this, credentialType, {
uri: '{ACCESSIBLE_RESOURCES_ENDPOINT}',
json: true,
})) as Array<{ id: string; url: string }>;
const site = resources.find((r) => r.url === normalizedDomain);
if (!site) {
throw new NodeOperationError(
this.getNode(),
`No accessible site found for domain: ${domain}. Make sure the domain matches your site URL exactly.`,
);
}
_cloudIdCache.set(normalizedDomain, site.id);
return site.id;
}
然后在主请求函数中:
} else if ({versionParam} === '{camelCase}OAuth2') {
const rawDomain = (await this.getCredentials('{camelCase}OAuth2Api')).domain as string;
credentialType = '{camelCase}OAuth2Api';
const siteId = await getSiteId.call(this, credentialType, rawDomain);
domain = `{GATEWAY_BASE_URL}/${siteId}`;
} else {
这样,现有的 uri: \${domain}/rest${endpoint}`` 构造会自动生成正确的网关 URL。
如果尚未从 n8n-workflow 导入,请添加 NodeOperationError。
步骤 7 — 更新节点文件
主节点(*.node.ts)
凭据数组 — 添加新凭据类型的条目:
{
name: '{camelCase}OAuth2Api',
required: true,
displayOptions: { show: { {versionParam}: ['{camelCase}OAuth2'] } },
},
版本/身份验证选项 — 添加到 {versionParam}(或同等内容)的选项列表中:
{ name: '{Display Name} (OAuth2)', value: '{camelCase}OAuth2' },
保持 default 不变——不得影响现有工作流。
触发器节点(如果存在,则为 *Trigger.node.ts)
进行相同的两项更改。保留该触发器节点凭据数组中其他凭据条目所使用的 displayName 标签模式。
步骤 8 — 编写凭据测试
文件:packages/nodes-base/credentials/test/{NODE_NAME}OAuth2Api.credentials.test.ts
使用 @n8n/client-oauth2 中的 ClientOAuth2 和 nock 模拟 HTTP 请求。遵循
MicrosoftTeamsOAuth2Api.credentials.test.ts 中的结构。
必需的测试用例:
- 元数据 — 名称、extends 数组、
enabledScopes默认值、身份验证 URL、令牌 URL、authQueryParameters默认值(如适用)。 - 授权 URI 中的默认作用域 — 调用
oauthClient.code.getUri(),断言每个默认作用域都存在。 - 使用默认作用域获取令牌 — 使用
nock模拟令牌端点,调用oauthClient.code.getToken(...),断言token.data.scope包含每个作用域。 - 授权 URI 中的自定义作用域(当 CUSTOM_SCOPES = no 时跳过)。
- 使用自定义作用域获取令牌(当 CUSTOM_SCOPES = no 时跳过)。
- 最少/不同的作用域集合(当 CUSTOM_SCOPES = no 时跳过)— 断言集合中不存在的作用域在 URI 和令牌响应中均不存在。
必需的生命周期钩子:
beforeAll(() => { nock.disableNetConnect(); });
afterAll(() => { nock.restore(); });
afterEach(() => { nock.cleanAll(); });
步骤 9 — 更新 GenericFunctions.test.ts
在凭据路由的 describe 块中:
- 如果添加了站点 ID 缓存(
_cloudIdCache),导入它,并在afterEach中调用_cloudIdCache.clear()(或等效方法)。 - 添加/更新 OAuth2 路由测试用例:
- 简单路由:断言
getCredentials使用正确的凭据名称调用,并且requestWithAuthentication使用正确的名称和 URI 调用。 - 网关查找:模拟
requestWithAuthentication,使其第一次调用时返回可访问资源负载,第二次调用时返回{}。断言第一次调用指向资源端点,第二次调用使用包含站点 ID 的网关基础 URL。
- 简单路由:断言
步骤 10 — 验证
# From packages/nodes-base/
pnpm test credentials/test/{NODE_NAME}OAuth2Api.credentials.test.ts
pnpm test nodes/{NODE_NAME}/__test__/GenericFunctions.test.ts
pnpm typecheck
pnpm lint
# Only when constants.ts was changed:
pushd ../cli && pnpm typecheck && popd
完成前修复所有类型错误。绝不要跳过 pnpm typecheck。