fix(ChatTrigger Node): Scope n8n User Auth OAuth2 token to Hosted Chat mode (no-changelog) (#37080)

This commit is contained in:
MHMD
2026-08-27 08:35:15 +00:00
committed by GitHub
parent 35969d93c5
commit a8d6af95a6
2 changed files with 40 additions and 1 deletions
@@ -59,7 +59,11 @@ export async function validateAuth(context: IWebhookFunctions) {
// origin never sends. Checked first so the frame doesn't depend on that cookie.
// Verified against n8n's internal AS (not just decoded) so the token also seeds
// the run's identity for private-credential resolution.
if (isChatOAuth2Enabled()) {
// Restricted to hostedChat: that's the only mode with a page to run the frame on,
// so a token from it must never authenticate a webhook-mode call — e.g. a stale
// token replayed after the node's mode was switched from hostedChat to webhook.
const mode = context.getNodeParameter('mode', 'hostedChat') as 'hostedChat' | 'webhook';
if (isChatOAuth2Enabled() && mode === 'hostedChat') {
const chatToken = headers['x-auth-token'];
if (typeof chatToken === 'string' && chatToken) {
const resourceUrl = context.getWebhookResourceUrl('default');
@@ -182,6 +182,7 @@ describe('validateAuth', () => {
beforeEach(() => {
mockContext.getWebhookName.mockReturnValue('default');
mockContext.getNodeParameter.calledWith('mode', 'hostedChat').mockReturnValue('hostedChat');
mockContext.getWebhookResourceUrl.mockReturnValue(resourceUrl);
vi.stubEnv('N8N_ENV_FEAT_CHAT_TRIGGER_OAUTH2', 'true');
});
@@ -242,6 +243,40 @@ describe('validateAuth', () => {
});
expect(mockContext.validateN8nOAuth2Token).not.toHaveBeenCalled();
});
// Embedded (`webhook`) mode has no page to mint or carry this token, so a
// call to it must keep working the same way it always has (the plain
// session-cookie check) even if a hostedChat-minted token is replayed —
// e.g. after switching a node's mode from hostedChat to webhook.
it('falls back to the cookie check when mode is webhook, even with a valid token', async () => {
mockContext.getNodeParameter.calledWith('mode', 'hostedChat').mockReturnValue('webhook');
mockContext.getHeaderData.mockReturnValue({
'x-auth-token': 'as-token',
});
await expect(validateAuth(mockContext)).rejects.toMatchObject({
responseCode: 401,
message: 'User not authenticated!',
});
expect(mockContext.validateN8nOAuth2Token).not.toHaveBeenCalled();
});
it('still passes via the cookie check in webhook mode when a valid token is also present', async () => {
mockContext.getNodeParameter.calledWith('mode', 'hostedChat').mockReturnValue('webhook');
mockContext.getHeaderData.mockReturnValue({
'x-auth-token': 'as-token',
cookie: 'n8n-auth=valid.jwt.token',
});
mockContext.validateCookieAuth.mockResolvedValue({
id: 'user-1',
email: 'user@example.com',
firstName: 'Test',
lastName: 'User',
});
await expect(validateAuth(mockContext)).resolves.toBeUndefined();
expect(mockContext.validateCookieAuth).toHaveBeenCalledWith('valid.jwt.token');
});
});
});
});