mirror of
https://github.com/n8n-io/n8n.git
synced 2026-08-28 17:22:01 +08:00
fix(ChatTrigger Node): Scope n8n User Auth OAuth2 token to Hosted Chat mode (no-changelog) (#37080)
This commit is contained in:
@@ -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');
|
||||
|
||||
+35
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user