diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index b7e4be8e04a2b..11b2406e50e76 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -622,6 +622,7 @@ class App { this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { // Allow access also from frontend when developing res.header('Access-Control-Allow-Origin', 'http://localhost:8080'); + res.header('Access-Control-Allow-Credentials', true); res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); res.header( 'Access-Control-Allow-Headers', diff --git a/packages/cli/src/UserManagement/routes/auth.ts b/packages/cli/src/UserManagement/routes/auth.ts index a4b6e49e162c9..2e6e5a56c2f62 100644 --- a/packages/cli/src/UserManagement/routes/auth.ts +++ b/packages/cli/src/UserManagement/routes/auth.ts @@ -6,7 +6,7 @@ import { compare } from 'bcryptjs'; import * as jwt from 'jsonwebtoken'; import { IDataObject } from 'n8n-workflow'; import { Db, ResponseHelper } from '../..'; -import { issueJWT, resolveJwtContent } from '../auth/jwt'; +import { issueCookie, resolveJwtContent } from '../auth/jwt'; import { JwtPayload, N8nApp, PublicUser } from '../Interfaces'; import config = require('../../../config'); import { isInstanceOwnerSetup, sanitizeUser } from '../UserManagementHelper'; @@ -49,8 +49,7 @@ export function authenticationMethods(this: N8nApp): void { throw error; } - const userData = await issueJWT(user); - res.cookie('n8n-auth', userData.token, { maxAge: userData.expiresIn, httpOnly: true }); + await issueCookie(res, user); return sanitizeUser(user); }), @@ -97,14 +96,13 @@ export function authenticationMethods(this: N8nApp): void { throw new Error('Invalid database state - user has password set.'); } - const userData = await issueJWT(user); - res.cookie('n8n-auth', userData.token, { maxAge: userData.expiresIn, httpOnly: true }); + await issueCookie(res, user); return sanitizeUser(user); }), ); - this.app.get( + this.app.post( `/${this.restEndpoint}/logout`, ResponseHelper.send(async (req: Request, res: Response): Promise => { res.clearCookie('n8n-auth'); diff --git a/packages/cli/src/UserManagement/routes/index.ts b/packages/cli/src/UserManagement/routes/index.ts index d1cbee5868e5c..8f6a6f8acadff 100644 --- a/packages/cli/src/UserManagement/routes/index.ts +++ b/packages/cli/src/UserManagement/routes/index.ts @@ -45,6 +45,8 @@ export function addRoutes(this: N8nApp, ignoredEndpoints: string[], restEndpoint this.app.use((req: Request, res: Response, next: NextFunction) => { if ( + // skip authentication for preflight requests + req.method === 'OPTIONS' || req.url.includes('login') || req.url.includes('logout') || req.url === '/index.html' || diff --git a/packages/cli/src/UserManagement/routes/me.ts b/packages/cli/src/UserManagement/routes/me.ts index 66649dea173e3..636bfc269fd48 100644 --- a/packages/cli/src/UserManagement/routes/me.ts +++ b/packages/cli/src/UserManagement/routes/me.ts @@ -6,7 +6,7 @@ import express = require('express'); import validator from 'validator'; import { Db, ResponseHelper } from '../..'; -import { issueJWT } from '../auth/jwt'; +import { issueCookie } from '../auth/jwt'; import { N8nApp, PublicUser } from '../Interfaces'; import { validatePassword, sanitizeUser } from '../UserManagementHelper'; import type { AuthenticatedRequest, MeRequest } from '../../requests'; @@ -47,9 +47,7 @@ export function meNamespace(this: N8nApp): void { const user = await Db.collections.User!.save(newUser); - const userData = await issueJWT(user); - - res.cookie('n8n-auth', userData.token, { maxAge: userData.expiresIn, httpOnly: true }); + await issueCookie(res, user); return sanitizeUser(user); }, @@ -67,8 +65,7 @@ export function meNamespace(this: N8nApp): void { const user = await Db.collections.User!.save(req.user); - const userData = await issueJWT(user); - res.cookie('n8n-auth', userData.token, { maxAge: userData.expiresIn, httpOnly: true }); + await issueCookie(res, user); return { success: true }; }), diff --git a/packages/cli/src/UserManagement/routes/owner.ts b/packages/cli/src/UserManagement/routes/owner.ts index 35ec524e3f790..00d5323c6386a 100644 --- a/packages/cli/src/UserManagement/routes/owner.ts +++ b/packages/cli/src/UserManagement/routes/owner.ts @@ -9,7 +9,7 @@ import config = require('../../../config'); import { User } from '../../databases/entities/User'; import { validateEntity } from '../../GenericHelpers'; import { OwnerRequest } from '../../requests'; -import { issueJWT } from '../auth/jwt'; +import { issueCookie } from '../auth/jwt'; import { N8nApp } from '../Interfaces'; import { sanitizeUser } from '../UserManagementHelper'; @@ -75,8 +75,7 @@ export function ownerNamespace(this: N8nApp): void { { value: JSON.stringify(true) }, ); - const { token, expiresIn } = await issueJWT(owner); - res.cookie('n8n-auth', token, { maxAge: expiresIn, httpOnly: true }); + await issueCookie(res, owner); return sanitizeUser(owner); }), diff --git a/packages/cli/src/UserManagement/routes/passwordReset.ts b/packages/cli/src/UserManagement/routes/passwordReset.ts index e9f0c26fdcbf2..9be4b87a43213 100644 --- a/packages/cli/src/UserManagement/routes/passwordReset.ts +++ b/packages/cli/src/UserManagement/routes/passwordReset.ts @@ -12,7 +12,7 @@ import { N8nApp } from '../Interfaces'; import { validatePassword } from '../UserManagementHelper'; import * as UserManagementMailer from '../email'; import type { PasswordResetRequest } from '../../requests'; -import { issueJWT } from '../auth/jwt'; +import { issueCookie } from '../auth/jwt'; import { getBaseUrl } from '../../GenericHelpers'; export function passwordResetNamespace(this: N8nApp): void { @@ -104,8 +104,7 @@ export function passwordResetNamespace(this: N8nApp): void { resetPasswordToken: null, }); - const userData = await issueJWT(req.user); - res.cookie('n8n-auth', userData.token, { maxAge: userData.expiresIn, httpOnly: true }); + await issueCookie(res, req.user); }), ); } diff --git a/packages/cli/src/UserManagement/routes/users.ts b/packages/cli/src/UserManagement/routes/users.ts index bd0c9ab7ce6bf..54eed37821cae 100644 --- a/packages/cli/src/UserManagement/routes/users.ts +++ b/packages/cli/src/UserManagement/routes/users.ts @@ -14,7 +14,7 @@ import { User } from '../../databases/entities/User'; import { SharedWorkflow } from '../../databases/entities/SharedWorkflow'; import { SharedCredentials } from '../../databases/entities/SharedCredentials'; import { getInstance } from '../email/UserManagementMailer'; -import { issueJWT } from '../auth/jwt'; +import { issueCookie } from '../auth/jwt'; export function usersNamespace(this: N8nApp): void { this.app.post( @@ -205,8 +205,8 @@ export function usersNamespace(this: N8nApp): void { const updatedUser = await Db.collections.User!.save(invitee); - const userData = await issueJWT(updatedUser); - res.cookie('n8n-auth', userData.token, { maxAge: userData.expiresIn, httpOnly: true }); + await issueCookie(res, updatedUser); + return sanitizeUser(updatedUser); }), ); diff --git a/packages/cli/test/integration/auth.endpoints.test.ts b/packages/cli/test/integration/auth.endpoints.test.ts index 83f67512daa05..b989bced009bc 100644 --- a/packages/cli/test/integration/auth.endpoints.test.ts +++ b/packages/cli/test/integration/auth.endpoints.test.ts @@ -125,11 +125,11 @@ describe('auth endpoints', () => { expect(response.headers['set-cookie']).toBeUndefined(); }); - test('GET /logout should log user out', async () => { + test('POST /logout should log user out', async () => { const owner = await Db.collections.User!.findOneOrFail(); const ownerAgent = await utils.createAuthAgent(app, owner); - const response = await ownerAgent.get('/logout'); + const response = await ownerAgent.post('/logout'); expect(response.statusCode).toBe(200); expect(response.body).toEqual(LOGGED_OUT_RESPONSE_BODY); diff --git a/packages/editor-ui/src/api/helpers.ts b/packages/editor-ui/src/api/helpers.ts index be34b1c3a3113..1bb96a1740e27 100644 --- a/packages/editor-ui/src/api/helpers.ts +++ b/packages/editor-ui/src/api/helpers.ts @@ -49,6 +49,9 @@ async function request(config: {method: Method, baseURL: string, endpoint: strin baseURL, headers, }; + if (process.env.NODE_ENV !== 'production' && !baseURL.includes('api.n8n.io') ) { + options.withCredentials = true; + } if (['PATCH', 'POST', 'PUT'].includes(method)) { options.data = data; } else { @@ -82,7 +85,7 @@ export async function makeRestApiRequest(context: IRestApiContext, method: Metho method, baseURL: context.baseUrl, endpoint, - headers: {sessionid: context.sessionId}, + headers: { sessionid: context.sessionId }, data, }); diff --git a/packages/editor-ui/src/api/settings-mock.ts b/packages/editor-ui/src/api/settings-mock.ts deleted file mode 100644 index aace5283fa2f6..0000000000000 --- a/packages/editor-ui/src/api/settings-mock.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { IRestApiContext, IN8nUISettings } from '../Interface'; -import { makeRestApiRequest } from './helpers'; - -if (!window.localStorage.getItem('mock.settings.isUserManagementEnabled')) { - window.localStorage.setItem('mock.settings.isUserManagementEnabled', 'true'); -} - -if (!window.localStorage.getItem('mock.settings.showSetupOnFirstLoad')) { - window.localStorage.setItem('mock.settings.showSetupOnFirstLoad', 'true'); -} - -if (!window.localStorage.getItem('mock.settings.smtpSetup')) { - window.localStorage.setItem('mock.settings.smtpSetup', 'false'); -} - -if (!window.localStorage.getItem('mock.settings.personalizationSurveyEnabled')) { - window.localStorage.setItem('mock.settings.personalizationSurveyEnabled', 'true'); -} - -export async function getSettings(context: IRestApiContext): Promise { - const settings = await makeRestApiRequest(context, 'GET', '/settings'); - const isUMEnabled = window.localStorage.getItem('mock.settings.isUserManagementEnabled'); - const showSetupOnFirstLoad = window.localStorage.getItem('mock.settings.showSetupOnFirstLoad'); - const smtpSetup = window.localStorage.getItem('mock.settings.smtpSetup'); - const personalizationSurveyEnabled = window.localStorage.getItem('mock.settings.personalizationSurveyEnabled'); - window.localStorage.setItem('mock.settings.showSetupOnFirstLoad', 'false'); - settings.userManagement = { - enabled: isUMEnabled === 'true', - showSetupOnFirstLoad: showSetupOnFirstLoad === 'true', - smtpSetup: smtpSetup === 'true', - }; - settings.personalizationSurveyEnabled = personalizationSurveyEnabled === 'true'; - settings.tagsEnabled = true; - return settings; -} diff --git a/packages/editor-ui/src/api/users-mock.ts b/packages/editor-ui/src/api/users-mock.ts deleted file mode 100644 index 91f8f6283ba0d..0000000000000 --- a/packages/editor-ui/src/api/users-mock.ts +++ /dev/null @@ -1,294 +0,0 @@ -import { IPersonalizationSurveyAnswers, IRestApiContext, IUserResponse } from '@/Interface'; -import { IDataObject } from 'n8n-workflow'; - -const users = [ - { - id: '0', - globalRole: { - name: 'owner', - id: "1", - }, - }, - { - id: '10', - firstName: 'xi', - lastName: 'lll', - email: 'test9@gmail.com', - globalRole: { - name: 'member', - id: "2", - }, - }, - { - id: '2', - email: 'test2@gmail.com', - globalRole: { - name: 'member', - id: "2", - }, - }, - { - id: '3', - firstName: 'sup', - lastName: 'yo', - email: 'test3@gmail.com', - globalRole: { - name: 'member', - id: "2", - }, - }, - { - id: '4', - firstName: 'xx', - lastName: 'aaaa', - email: 'test4@gmail.com', - globalRole: { - name: 'member', - id: "2", - }, - }, - { - id: '5', - firstName: 'gg', - lastName: 'kk', - email: 'test5@gmail.com', - globalRole: { - name: 'member', - id: "2", - }, - }, - { - id: '7', - email: 'test7@gmail.com', - globalRole: { - name: 'member', - id: "2", - }, - }, - { - id: '8', - firstName: 'sup', - lastName: 'yo', - email: 'test8@gmail.com', - globalRole: { - name: 'member', - id: "2", - }, - }, - { - id: '9', - firstName: 'aaa', - lastName: 'yo', - email: 'test88@gmail.com', - globalRole: { - name: 'member', - id: "2", - }, - }, - { - id: "10", - firstName: 'verylongfirstnameofmymomandmydad', - lastName: 'verylonglastnameofmymomandmydads', - email: 'veryyyyyyyyyyyyyyyyylongemailllllllllllllllllllll@gmail.com', - globalRole: { - name: "member", - id: "2", - }, - }, -]; - -if (!window.localStorage.getItem('mock.users.currentUserId')) { - window.localStorage.setItem('mock.users.currentUserId', '0'); -} - -const getRandomId = () => `${Math.floor(Math.random() * 10000000000 + 100)}`; - -if (!window.localStorage.getItem('mock.users.users')) { - window.localStorage.setItem('mock.users.users', JSON.stringify(users)); -} - -if (!window.localStorage.getItem('mock.users.currentUserId')) { - window.localStorage.setItem('mock.users.currentUserId', 'null'); -} - -function getAllUsers() { - return JSON.parse(window.localStorage.getItem('mock.users.users') || '[]'); -} - -function getCurrUser() { - const id = window.localStorage.getItem('mock.users.currentUserId'); - if (!id) { - return null; - } - - const users = JSON.parse(window.localStorage.getItem('mock.users.users') || '[]'); - - return users.find((user: IUserResponse) => user.id === id); -} - -function addUser(user: IUserResponse) { - const users = JSON.parse(window.localStorage.getItem('mock.users.users') || '[]'); - users.push(user); - window.localStorage.setItem('mock.users.users', JSON.stringify(users)); -} - -function removeUser(userId: string) { - let users = JSON.parse(window.localStorage.getItem('mock.users.users') || '[]'); - users = users.filter((user: IUserResponse) => user.id !== userId); - window.localStorage.setItem('mock.users.users', JSON.stringify(users)); -} - -function log(context: IRestApiContext, method: string, path: string, params?: any): void { // tslint:disable-line:no-any - console.log(method, path, params); // eslint-disable-line no-console -} - -export async function loginCurrentUser(context: IRestApiContext): Promise { - log(context, 'GET', '/login'); - - return await Promise.resolve(getCurrUser()); -} - -export async function getCurrentUser(context: IRestApiContext): Promise { - log(context, 'GET', '/me'); - - return await Promise.resolve(getCurrUser()); -} - -export async function login(context: IRestApiContext, params: {email: string, password: string}): Promise { - log(context, 'POST', '/login', params); - - const users = getAllUsers(); - const user = users.find((user: IUserResponse) => user.email === params.email && user.firstName); - if (!user) { - throw new Error(`Cannot login with this email. Must use an existing email`); - } - window.localStorage.setItem('mock.users.currentUserId', user.id); - return await Promise.resolve(user); -} - -export async function logout(context: IRestApiContext): Promise { - log(context, 'POST', '/logout'); - // @ts-ignore - window.localStorage.setItem('mock.users.currentUserId', undefined); -} - -export async function setupOwner(context: IRestApiContext, params: { firstName: string; lastName: string; email: string; password: string;}): Promise { - log(context, 'POST', '/owner/setup', params as unknown as IDataObject); - const user = getCurrUser(); - removeUser('0'); - const newUser: IUserResponse = {...user, ...params}; - addUser(newUser); - - return await Promise.resolve(newUser); -} - -export async function validateSignupToken(context: IRestApiContext, params: {inviteeId: string, inviterId: string}): Promise<{inviter: {firstName: string, lastName: string}}> { - if (params.inviterId !== '123' || params.inviteeId !== '345') { - throw new Error('invalid token. try query ?inviterId=123&inviteeId=345'); - } - - log(context, 'GET', '/resolve-signup-token', params); - - return await Promise.resolve({ - inviter: { - firstName: 'Moh', - lastName: 'Salah', - }, - }); -} - -export async function signup(context: IRestApiContext, params: {inviterId: string; inviteeId: string; firstName: string; lastName: string; password: string}): Promise { - if (params.inviterId !== '123' || params.inviteeId !== '345') { - throw new Error('invalid token. try query ?inviterId=123&inviteeId=345'); - } - - log(context, 'POST', `/users/${params.inviteeId}`, params as unknown as IDataObject); - - const newUser: IUserResponse = {...params, email: `${params.firstName}@n8n.io`, id: getRandomId(), "globalRole": {name: 'member', id: '2'}}; - window.localStorage.setItem('mock.users.currentUserId', newUser.id); - addUser(newUser); - - return await Promise.resolve(newUser); -} - -export async function sendForgotPasswordEmail(context: IRestApiContext, params: {email: string}): Promise { - log(context, 'POST', '/forgot-password', params); -} - -export async function validatePasswordToken(context: IRestApiContext, params: {token: string, userId: string}): Promise { - log(context, 'GET', '/resolve-password-token', params); - - if (params.token !== '123' && params.userId !== '345') { - throw new Error('invalid token. try query ?token=123&userId=345'); - } -} - -export async function changePassword(context: IRestApiContext, params: {token: string, password: string, userId: string}): Promise { - if (params.token !== '123' && params.userId !== '345') { - throw new Error('invalid token. try query ?token=123&userId=345'); - } - - log(context, 'POST', '/change-password', params); -} - -export async function updateCurrentUser(context: IRestApiContext, params: {id: string, firstName: string, lastName: string, email: string}): Promise { - log(context, 'PATCH', `/me`, params as unknown as IDataObject); - const user = getCurrUser(); - removeUser(params.id); - const newUser = { - ...user, - ...params, - }; - addUser(newUser); - - return await Promise.resolve(newUser); -} - -export async function updateCurrentUserPassword(context: IRestApiContext, params: {password: string}): Promise { - log(context, 'PATCH', `/me/password`, {password: params.password}); -} - -export async function deleteUser(context: IRestApiContext, {id, transferId}: {id: string, transferId?: string}): Promise { - log(context, 'DELETE', `/users/${id}`, transferId ? { transferId } : {}); - removeUser(id); -} - -export async function getUsers(context: IRestApiContext): Promise { - log(context, 'GET', '/users'); - - return Promise.resolve(getAllUsers()); -} - -export async function inviteUsers(context: IRestApiContext, params: Array<{email: string}>): Promise>> { - log(context, 'POST', '/users', params); - - const users = params.map(({email}: {email: string}) => ({ - id: getRandomId(), - email, - })); - users.forEach((user) => addUser({ - ...user, - globalRole: { - name: 'member', - id: '2', - }, - })); - - return await Promise.resolve(users); -} - -export async function reinvite(context: IRestApiContext, {id}: {id: string}): Promise { - log(context, 'POST', `/users/${id}/reinvite`); -} - -export async function submitPersonalizationSurvey(context: IRestApiContext, params: IPersonalizationSurveyAnswers): Promise { - log(context, 'POST', `/me/survey`, params); - const user = getCurrUser(); - removeUser(user.id); - const newUser = { - ...user, - personalizationAnswers: params, - }; - addUser(newUser); - return Promise.resolve(); -} diff --git a/packages/editor-ui/src/api/users.ts b/packages/editor-ui/src/api/users.ts index 12cdc8231f0a3..817f4ca0a99c3 100644 --- a/packages/editor-ui/src/api/users.ts +++ b/packages/editor-ui/src/api/users.ts @@ -19,7 +19,7 @@ export async function logout(context: IRestApiContext): Promise { } export function setupOwner(context: IRestApiContext, params: { firstName: string; lastName: string; email: string; password: string;}): Promise { - return makeRestApiRequest(context, 'POST', '/owner/setup', params as unknown as IDataObject); + return makeRestApiRequest(context, 'POST', '/owner', params as unknown as IDataObject); } export function validateSignupToken(context: IRestApiContext, params: {inviterId: string; inviteeId: string}): Promise<{inviter: {firstName: string, lastName: string}}> { diff --git a/packages/editor-ui/src/modules/settings.ts b/packages/editor-ui/src/modules/settings.ts index 59982ddf6275f..2948e46a56991 100644 --- a/packages/editor-ui/src/modules/settings.ts +++ b/packages/editor-ui/src/modules/settings.ts @@ -7,8 +7,7 @@ import { IRootState, ISettingsState, } from '../Interface'; -import { getSettings } from '../api/settings-mock'; -import { getPromptsData, submitValueSurvey, submitContactInfo } from '../api/settings'; +import { getPromptsData, submitValueSurvey, submitContactInfo, getSettings } from '../api/settings'; import Vue from 'vue'; import { CONTACT_PROMPT_MODAL_KEY, VALUE_SURVEY_MODAL_KEY } from '@/constants'; import { ITelemetrySettings } from 'n8n-workflow'; diff --git a/packages/editor-ui/src/modules/userHelpers.ts b/packages/editor-ui/src/modules/userHelpers.ts index a61169311495a..87d093a25e3e3 100644 --- a/packages/editor-ui/src/modules/userHelpers.ts +++ b/packages/editor-ui/src/modules/userHelpers.ts @@ -139,6 +139,9 @@ export const isAuthorized = (permissions: IPermissions, {currentUser, isUMEnable return false; } } + else if (permissions.deny.role) { + return false; + } } if (permissions.allow) { diff --git a/packages/editor-ui/src/modules/users.ts b/packages/editor-ui/src/modules/users.ts index d9f3057645865..08c328c53e9cb 100644 --- a/packages/editor-ui/src/modules/users.ts +++ b/packages/editor-ui/src/modules/users.ts @@ -1,4 +1,4 @@ -import { changePassword, deleteUser, getCurrentUser, getUsers, inviteUsers, login, loginCurrentUser, logout, reinvite, sendForgotPasswordEmail, setupOwner, signup, submitPersonalizationSurvey, updateCurrentUser, updateCurrentUserPassword, validatePasswordToken, validateSignupToken } from '@/api/users-mock'; +import { changePassword, deleteUser, getCurrentUser, getUsers, inviteUsers, login, loginCurrentUser, logout, reinvite, sendForgotPasswordEmail, setupOwner, signup, submitPersonalizationSurvey, updateCurrentUser, updateCurrentUserPassword, validatePasswordToken, validateSignupToken } from '@/api/users'; import { PERSONALIZATION_MODAL_KEY } from '@/constants'; import Vue from 'vue'; import { ActionContext, Module } from 'vuex'; @@ -12,7 +12,7 @@ import { } from '../Interface'; import { getPersonalizedNodeTypes, isAuthorized, PERMISSIONS, ROLE } from './userHelpers'; -const isDefaultUser = (user: IUserResponse | null) => Boolean(user && !user.email); +const isDefaultUser = (user: IUserResponse | null) => Boolean(user && !user.email && user.globalRole && user.globalRole.name === ROLE.Owner); const isPendingUser = (user: IUserResponse | null) => Boolean(user && user.email && !user.firstName && !user.lastName);