From 65c8a27db277579a517dd9bc89dbdd4d8242dbd4 Mon Sep 17 00:00:00 2001 From: Yuliia Pominchuk Date: Mon, 3 Aug 2026 23:14:41 +0200 Subject: [PATCH] chore: Webhook OAuth tester --- .../modules/oauth-server/oauth.controller.ts | 196 ++++++++++++++++++ 1 file changed, 196 insertions(+) diff --git a/packages/cli/src/modules/oauth-server/oauth.controller.ts b/packages/cli/src/modules/oauth-server/oauth.controller.ts index e7011ef4814..690b20c2cb1 100644 --- a/packages/cli/src/modules/oauth-server/oauth.controller.ts +++ b/packages/cli/src/modules/oauth-server/oauth.controller.ts @@ -15,6 +15,7 @@ import { import { Container } from '@n8n/di'; import type { Response, Request, RequestHandler, Router } from 'express'; +import { isWebhookOAuth2Enabled } from '@/constants/oauth2-triggers'; import type { ProtectedResource } from '@/services/protected-resource.registry'; import { ProtectedResourceRegistry } from '@/services/protected-resource.registry'; import { UrlService } from '@/services/url.service'; @@ -289,6 +290,30 @@ export class OAuthController { res.json(this.buildProtectedResourceMetadata(resource)); } + /** + * Dev-only, same-origin OAuth flow tester for `n8nOAuth2` webhook triggers. + * Lets a non-technical tester paste a webhook URL, run DCR + PKCE + consent + * entirely in the browser (no CORS, since the page is served by the instance + * itself and the user is already logged in), then see the minted token's + * audience and the webhook's response. + * + * Gated behind the same feature flag as the webhook OAuth resolver, so it is + * absent (404) unless `N8N_ENV_FEAT_WEBHOOK_PRIVATE_CREDENTIALS` is enabled — + * it must never be reachable on a production instance. + */ + @Get('/oauth/webhook-tester', { skipAuth: true }) + webhookTester(_req: Request, res: Response) { + if (!isWebhookOAuth2Enabled()) { + res.status(404).json({ message: 'Not found' }); + return; + } + res.setHeader( + 'Content-Security-Policy', + "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self'; base-uri 'self'; form-action 'self'", + ); + res.type('html').send(WEBHOOK_TESTER_HTML); + } + private buildProtectedResourceMetadata(resource: ProtectedResource): Record { const baseUrl = this.urlService.getInstanceBaseUrl(); const metadata: Record = { @@ -304,3 +329,174 @@ export class OAuthController { return metadata; } } + +/** + * Static page for {@link OAuthController.webhookTester}. Self-contained (inline + * CSS + JS, no build step) so it can be served straight from the controller. All + * requests it makes (`/oauth/register`, `/oauth/authorize`, `/oauth/token`, the + * `.well-known` discovery doc and the webhook itself) are same-origin, so the + * browser needs no CORS grant. The pasted webhook's path is always resolved + * against this instance's origin, so it only tests webhooks on this instance. + */ +const WEBHOOK_TESTER_HTML = ` + + + + +Webhook OAuth tester + + + +

Webhook OAuth tester

+

Dev tool. Make sure you are logged into this n8n instance as the user you want to test as, and that the workflow with an n8n User Auth (OAuth2) Webhook trigger is active.

+ +
+ + + + +
+
+ + + +
+ +
+
+ + + +`;