diff --git a/packages/cli/BREAKING-CHANGES.md b/packages/cli/BREAKING-CHANGES.md index 540749ed43599..afa1ed17e144d 100644 --- a/packages/cli/BREAKING-CHANGES.md +++ b/packages/cli/BREAKING-CHANGES.md @@ -2,6 +2,16 @@ This list shows all the versions which include breaking changes and how to upgrade. +## 1.32.0 + +### What changed? + +N8n auth cookie has `Secure` flag set by default now. + +### When is action necessary? + +If you are running n8n without HTTP**S** on a domain other than `localhost`, you need to either setup HTTPS, or you can disable the secure flag by setting the env variable `N8N_SECURE_COOKIE` to `false`. + ## 1.27.0 ### What changed? diff --git a/packages/cli/src/auth/jwt.ts b/packages/cli/src/auth/jwt.ts index affc9ea75f8b8..fca7e2a272eb9 100644 --- a/packages/cli/src/auth/jwt.ts +++ b/packages/cli/src/auth/jwt.ts @@ -90,5 +90,6 @@ export async function issueCookie(res: Response, user: User): Promise { maxAge: userData.expiresIn * Time.seconds.toMilliseconds, httpOnly: true, sameSite: 'lax', + secure: config.getEnv('secure_cookie'), }); } diff --git a/packages/cli/src/config/index.ts b/packages/cli/src/config/index.ts index 755d1105828df..2ad950837a03b 100644 --- a/packages/cli/src/config/index.ts +++ b/packages/cli/src/config/index.ts @@ -15,6 +15,7 @@ if (inE2ETests) { process.env.N8N_LOG_LEVEL = 'silent'; process.env.N8N_PUBLIC_API_DISABLED = 'true'; process.env.SKIP_STATISTICS_EVENTS = 'true'; + process.env.N8N_SECURE_COOKIE = 'false'; } else { dotenv.config(); } diff --git a/packages/cli/src/config/schema.ts b/packages/cli/src/config/schema.ts index 0fde528a9877b..a2ea6c47c6f8f 100644 --- a/packages/cli/src/config/schema.ts +++ b/packages/cli/src/config/schema.ts @@ -538,6 +538,12 @@ export const schema = { env: 'N8N_PROTOCOL', doc: 'HTTP Protocol via which n8n can be reached', }, + secure_cookie: { + doc: 'This sets the `Secure` flag on n8n auth cookie', + format: Boolean, + default: true, + env: 'N8N_SECURE_COOKIE', + }, ssl_key: { format: String, default: '', diff --git a/packages/cli/test/unit/controllers/me.controller.test.ts b/packages/cli/test/unit/controllers/me.controller.test.ts index 0ebca0bd77e16..f0f9b8458623a 100644 --- a/packages/cli/test/unit/controllers/me.controller.test.ts +++ b/packages/cli/test/unit/controllers/me.controller.test.ts @@ -1,7 +1,7 @@ -import type { CookieOptions, Response } from 'express'; +import type { Response } from 'express'; import { Container } from 'typedi'; import jwt from 'jsonwebtoken'; -import { mock, anyObject, captor } from 'jest-mock-extended'; +import { mock, anyObject } from 'jest-mock-extended'; import type { PublicUser } from '@/Interfaces'; import type { User } from '@db/entities/User'; import { MeController } from '@/controllers/me.controller'; @@ -11,10 +11,10 @@ import { UserService } from '@/services/user.service'; import { ExternalHooks } from '@/ExternalHooks'; import { InternalHooks } from '@/InternalHooks'; import { License } from '@/License'; -import { badPasswords } from '../shared/testData'; -import { mockInstance } from '../../shared/mocking'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { UserRepository } from '@/databases/repositories/user.repository'; +import { badPasswords } from '../shared/testData'; +import { mockInstance } from '../../shared/mocking'; describe('MeController', () => { const externalHooks = mockInstance(ExternalHooks); @@ -63,10 +63,16 @@ describe('MeController', () => { expect(userService.update).toHaveBeenCalled(); - const cookieOptions = captor(); - expect(res.cookie).toHaveBeenCalledWith(AUTH_COOKIE_NAME, 'signed-token', cookieOptions); - expect(cookieOptions.value.httpOnly).toBe(true); - expect(cookieOptions.value.sameSite).toBe('lax'); + expect(res.cookie).toHaveBeenCalledWith( + AUTH_COOKIE_NAME, + 'signed-token', + expect.objectContaining({ + maxAge: expect.any(Number), + httpOnly: true, + sameSite: 'lax', + secure: false, + }), + ); expect(externalHooks.run).toHaveBeenCalledWith('user.profile.update', [ user.email, @@ -175,10 +181,16 @@ describe('MeController', () => { expect(req.user.password).not.toBe(passwordHash); - const cookieOptions = captor(); - expect(res.cookie).toHaveBeenCalledWith(AUTH_COOKIE_NAME, 'new-signed-token', cookieOptions); - expect(cookieOptions.value.httpOnly).toBe(true); - expect(cookieOptions.value.sameSite).toBe('lax'); + expect(res.cookie).toHaveBeenCalledWith( + AUTH_COOKIE_NAME, + 'new-signed-token', + expect.objectContaining({ + maxAge: expect.any(Number), + httpOnly: true, + sameSite: 'lax', + secure: false, + }), + ); expect(externalHooks.run).toHaveBeenCalledWith('user.password.update', [ req.user.email, diff --git a/packages/cli/test/unit/middleware/auth.test.ts b/packages/cli/test/unit/middleware/auth.test.ts index dfd6e0e8c13e4..d46a17d0e4fea 100644 --- a/packages/cli/test/unit/middleware/auth.test.ts +++ b/packages/cli/test/unit/middleware/auth.test.ts @@ -156,6 +156,7 @@ describe('refreshExpiringCookie', () => { httpOnly: true, maxAge: jwtSessionDurationHours * Time.hours.toMilliseconds, sameSite: 'lax', + secure: false, }); }); });