From 4b805e051f7b952bda3b2407d4ac334f518ff460 Mon Sep 17 00:00:00 2001 From: Ilango Date: Fri, 7 Aug 2026 11:24:29 +0530 Subject: [PATCH] fix: flowise-707 (#6712) --- .../src/database/migrations/mariadb/index.ts | 4 +- .../src/database/migrations/mysql/index.ts | 4 +- .../src/database/migrations/postgres/index.ts | 4 +- .../src/database/migrations/sqlite/index.ts | 4 +- .../database/entities/user.entity.ts | 6 + .../1780000000000-AddSSOIdentityBinding.ts | 14 ++ .../1780000000000-AddSSOIdentityBinding.ts | 14 ++ .../1780000000000-AddSSOIdentityBinding.ts | 13 ++ .../1780000000000-AddSSOIdentityBinding.ts | 14 ++ .../src/enterprise/services/user.service.ts | 8 +- .../server/src/enterprise/sso/SSOBase.test.ts | 144 +++++++++++++++++- packages/server/src/enterprise/sso/SSOBase.ts | 23 ++- packages/ui/src/routes/MainRoutes.jsx | 2 +- 13 files changed, 242 insertions(+), 12 deletions(-) create mode 100644 packages/server/src/enterprise/database/migrations/mariadb/1780000000000-AddSSOIdentityBinding.ts create mode 100644 packages/server/src/enterprise/database/migrations/mysql/1780000000000-AddSSOIdentityBinding.ts create mode 100644 packages/server/src/enterprise/database/migrations/postgres/1780000000000-AddSSOIdentityBinding.ts create mode 100644 packages/server/src/enterprise/database/migrations/sqlite/1780000000000-AddSSOIdentityBinding.ts diff --git a/packages/server/src/database/migrations/mariadb/index.ts b/packages/server/src/database/migrations/mariadb/index.ts index 82ed7dc48..e7fdbd760 100644 --- a/packages/server/src/database/migrations/mariadb/index.ts +++ b/packages/server/src/database/migrations/mariadb/index.ts @@ -47,6 +47,7 @@ import { AddCustomMcpServer1766000000000 } from './1766000000000-AddCustomMcpSer import { AddWebhookSecretToChatFlow1776240000003 } from './1776240000003-AddWebhookSecretToChatFlow' import { AddMcpServerConfigToChatFlow1767000000000 } from './1767000000000-AddMcpServerConfigToChatFlow' import { AddScheduleEntities1772000000000 } from './1772000000000-AddScheduleEntities' +import { AddSSOIdentityBinding1780000000000 } from '../../../enterprise/database/migrations/mariadb/1780000000000-AddSSOIdentityBinding' import { AddAuthTables1720230151482 } from '../../../enterprise/database/migrations/mariadb/1720230151482-AddAuthTables' import { AddWorkspace1725437498242 } from '../../../enterprise/database/migrations/mariadb/1725437498242-AddWorkspace' @@ -120,5 +121,6 @@ export const mariadbMigrations = [ AddCustomMcpServer1766000000000, AddWebhookSecretToChatFlow1776240000003, AddMcpServerConfigToChatFlow1767000000000, - AddScheduleEntities1772000000000 + AddScheduleEntities1772000000000, + AddSSOIdentityBinding1780000000000 ] diff --git a/packages/server/src/database/migrations/mysql/index.ts b/packages/server/src/database/migrations/mysql/index.ts index 58c08d4c7..68b640b5c 100644 --- a/packages/server/src/database/migrations/mysql/index.ts +++ b/packages/server/src/database/migrations/mysql/index.ts @@ -48,6 +48,7 @@ import { AddWebhookSecretToChatFlow1776240000002 } from './1776240000002-AddWebh import { AddCustomMcpServer1766000000000 } from './1766000000000-AddCustomMcpServer' import { AddMcpServerConfigToChatFlow1767000000000 } from './1767000000000-AddMcpServerConfigToChatFlow' import { AddScheduleEntities1772000000000 } from './1772000000000-AddScheduleEntities' +import { AddSSOIdentityBinding1780000000000 } from '../../../enterprise/database/migrations/mysql/1780000000000-AddSSOIdentityBinding' import { AddAuthTables1720230151482 } from '../../../enterprise/database/migrations/mysql/1720230151482-AddAuthTables' import { AddWorkspace1720230151484 } from '../../../enterprise/database/migrations/mysql/1720230151484-AddWorkspace' @@ -122,5 +123,6 @@ export const mysqlMigrations = [ AddWebhookSecretToChatFlow1776240000002, AddCustomMcpServer1766000000000, AddMcpServerConfigToChatFlow1767000000000, - AddScheduleEntities1772000000000 + AddScheduleEntities1772000000000, + AddSSOIdentityBinding1780000000000 ] diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts index 750d7daf6..cfca76b85 100644 --- a/packages/server/src/database/migrations/postgres/index.ts +++ b/packages/server/src/database/migrations/postgres/index.ts @@ -46,6 +46,7 @@ import { AddWebhookSecretToChatFlow1776240000001 } from './1776240000001-AddWebh import { AddCustomMcpServer1766000000000 } from './1766000000000-AddCustomMcpServer' import { AddMcpServerConfigToChatFlow1767000000000 } from './1767000000000-AddMcpServerConfigToChatFlow' import { AddScheduleEntities1772000000000 } from './1772000000000-AddScheduleEntities' +import { AddSSOIdentityBinding1780000000000 } from '../../../enterprise/database/migrations/postgres/1780000000000-AddSSOIdentityBinding' import { AddAuthTables1720230151482 } from '../../../enterprise/database/migrations/postgres/1720230151482-AddAuthTables' import { AddWorkspace1720230151484 } from '../../../enterprise/database/migrations/postgres/1720230151484-AddWorkspace' @@ -118,5 +119,6 @@ export const postgresMigrations = [ AddWebhookSecretToChatFlow1776240000001, AddCustomMcpServer1766000000000, AddMcpServerConfigToChatFlow1767000000000, - AddScheduleEntities1772000000000 + AddScheduleEntities1772000000000, + AddSSOIdentityBinding1780000000000 ] diff --git a/packages/server/src/database/migrations/sqlite/index.ts b/packages/server/src/database/migrations/sqlite/index.ts index db277bacd..c8d33e2fc 100644 --- a/packages/server/src/database/migrations/sqlite/index.ts +++ b/packages/server/src/database/migrations/sqlite/index.ts @@ -44,6 +44,7 @@ import { AddWebhookSecretToChatFlow1776240000000 } from './1776240000000-AddWebh import { AddCustomMcpServer1766000000000 } from './1766000000000-AddCustomMcpServer' import { AddMcpServerConfigToChatFlow1767000000000 } from './1767000000000-AddMcpServerConfigToChatFlow' import { AddScheduleEntities1772000000000 } from './1772000000000-AddScheduleEntities' +import { AddSSOIdentityBinding1780000000000 } from '../../../enterprise/database/migrations/sqlite/1780000000000-AddSSOIdentityBinding' import { AddAuthTables1720230151482 } from '../../../enterprise/database/migrations/sqlite/1720230151482-AddAuthTables' import { AddWorkspace1720230151484 } from '../../../enterprise/database/migrations/sqlite/1720230151484-AddWorkspace' @@ -114,5 +115,6 @@ export const sqliteMigrations = [ AddWebhookSecretToChatFlow1776240000000, AddCustomMcpServer1766000000000, AddMcpServerConfigToChatFlow1767000000000, - AddScheduleEntities1772000000000 + AddScheduleEntities1772000000000, + AddSSOIdentityBinding1780000000000 ] diff --git a/packages/server/src/enterprise/database/entities/user.entity.ts b/packages/server/src/enterprise/database/entities/user.entity.ts index 3bb455aef..75628a589 100644 --- a/packages/server/src/enterprise/database/entities/user.entity.ts +++ b/packages/server/src/enterprise/database/entities/user.entity.ts @@ -36,6 +36,12 @@ export class User { @Column({ type: 'varchar', length: 20, default: UserStatus.UNVERIFIED }) status: string + @Column({ type: 'varchar', length: 100, nullable: true }) + ssoProvider?: string | null + + @Column({ type: 'varchar', length: 255, nullable: true }) + ssoSubjectId?: string | null + @CreateDateColumn() createdDate?: Date diff --git a/packages/server/src/enterprise/database/migrations/mariadb/1780000000000-AddSSOIdentityBinding.ts b/packages/server/src/enterprise/database/migrations/mariadb/1780000000000-AddSSOIdentityBinding.ts new file mode 100644 index 000000000..59619ae8f --- /dev/null +++ b/packages/server/src/enterprise/database/migrations/mariadb/1780000000000-AddSSOIdentityBinding.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' +import { ensureColumnExists } from './mariaDbCustomFunctions' + +export class AddSSOIdentityBinding1780000000000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await ensureColumnExists(queryRunner, 'user', 'ssoProvider', 'varchar(100)') + await ensureColumnExists(queryRunner, 'user', 'ssoSubjectId', 'varchar(255)') + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`user\` DROP COLUMN \`ssoProvider\`;`) + await queryRunner.query(`ALTER TABLE \`user\` DROP COLUMN \`ssoSubjectId\`;`) + } +} diff --git a/packages/server/src/enterprise/database/migrations/mysql/1780000000000-AddSSOIdentityBinding.ts b/packages/server/src/enterprise/database/migrations/mysql/1780000000000-AddSSOIdentityBinding.ts new file mode 100644 index 000000000..db3a06818 --- /dev/null +++ b/packages/server/src/enterprise/database/migrations/mysql/1780000000000-AddSSOIdentityBinding.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' +import { ensureColumnExists } from './mysqlCustomFunctions' + +export class AddSSOIdentityBinding1780000000000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await ensureColumnExists(queryRunner, 'user', 'ssoProvider', 'varchar(100)') + await ensureColumnExists(queryRunner, 'user', 'ssoSubjectId', 'varchar(255)') + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`user\` DROP COLUMN \`ssoProvider\`;`) + await queryRunner.query(`ALTER TABLE \`user\` DROP COLUMN \`ssoSubjectId\`;`) + } +} diff --git a/packages/server/src/enterprise/database/migrations/postgres/1780000000000-AddSSOIdentityBinding.ts b/packages/server/src/enterprise/database/migrations/postgres/1780000000000-AddSSOIdentityBinding.ts new file mode 100644 index 000000000..0f61649a3 --- /dev/null +++ b/packages/server/src/enterprise/database/migrations/postgres/1780000000000-AddSSOIdentityBinding.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddSSOIdentityBinding1780000000000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user" ADD COLUMN IF NOT EXISTS "ssoProvider" varchar;`) + await queryRunner.query(`ALTER TABLE "user" ADD COLUMN IF NOT EXISTS "ssoSubjectId" varchar;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "ssoProvider";`) + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "ssoSubjectId";`) + } +} diff --git a/packages/server/src/enterprise/database/migrations/sqlite/1780000000000-AddSSOIdentityBinding.ts b/packages/server/src/enterprise/database/migrations/sqlite/1780000000000-AddSSOIdentityBinding.ts new file mode 100644 index 000000000..cb1fae790 --- /dev/null +++ b/packages/server/src/enterprise/database/migrations/sqlite/1780000000000-AddSSOIdentityBinding.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' +import { ensureColumnExists } from './sqlliteCustomFunctions' + +export class AddSSOIdentityBinding1780000000000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await ensureColumnExists(queryRunner, 'user', 'ssoProvider', 'varchar') + await ensureColumnExists(queryRunner, 'user', 'ssoSubjectId', 'varchar') + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "ssoProvider";`) + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "ssoSubjectId";`) + } +} diff --git a/packages/server/src/enterprise/services/user.service.ts b/packages/server/src/enterprise/services/user.service.ts index c0da3ad81..757799bcb 100644 --- a/packages/server/src/enterprise/services/user.service.ts +++ b/packages/server/src/enterprise/services/user.service.ts @@ -27,7 +27,9 @@ export const enum UserErrorMessage { INCORRECT_USER_EMAIL_OR_CREDENTIALS = 'Incorrect Email or Password', PASSWORDS_DO_NOT_MATCH = 'Passwords do not match', EMAIL_CHANGE_USE_CONFIRM_LINK = 'Use the confirm email change link from your email to complete this action.', - USER_INVITED_PENDING_ACTIVATION = 'You have a pending invitation. Please check your email and complete the invitation to activate your account before signing in.' + USER_INVITED_PENDING_ACTIVATION = 'You have a pending invitation. Please check your email and complete the invitation to activate your account before signing in.', + SSO_IDENTITY_MISMATCH = 'This account is linked to a different sign-in method. Please use your original sign-in method, or contact your administrator to update it.', + SSO_SUBJECT_ID_MISSING = 'Your identity provider did not return a valid user identifier. Please contact your administrator.' } export class UserService { private telemetry: Telemetry @@ -66,6 +68,10 @@ export class UserService { return await queryRunner.manager.findOneBy(User, { tempToken: token }) } + public async bindSSOIdentity(userId: string, ssoProvider: string, ssoSubjectId: string, queryRunner: QueryRunner) { + return await queryRunner.manager.update(User, { id: userId }, { ssoProvider, ssoSubjectId }) + } + public validateUserStatus(status: string | undefined) { if (status && !Object.values(UserStatus).includes(status as UserStatus)) throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, UserErrorMessage.INVALID_USER_STATUS) diff --git a/packages/server/src/enterprise/sso/SSOBase.test.ts b/packages/server/src/enterprise/sso/SSOBase.test.ts index adde8678c..281a90031 100644 --- a/packages/server/src/enterprise/sso/SSOBase.test.ts +++ b/packages/server/src/enterprise/sso/SSOBase.test.ts @@ -5,26 +5,59 @@ import { UserStatus } from '../database/entities/user.entity' import { UserErrorMessage } from '../services/user.service' const mockReadUserByEmail = jest.fn() as jest.MockedFunction<(_email: string, _queryRunner: QueryRunner) => Promise> +const mockBindSSOIdentity = jest.fn() as jest.MockedFunction< + (_userId: string, _ssoProvider: string, _ssoSubjectId: string, _queryRunner: QueryRunner) => Promise +> const mockCreateQueryRunner = jest.fn() as jest.MockedFunction<() => any> const mockGetPlatformType = jest.fn() as jest.MockedFunction<() => Platform> +const mockGetFeaturesByPlan = jest.fn() as jest.MockedFunction<(_subscriptionId: string) => Promise> +const mockGetProductIdFromSubscription = jest.fn() as jest.MockedFunction<(_subscriptionId: string) => Promise> +const mockReadWorkspaceUserByLastLogin = jest.fn() as jest.MockedFunction<(_userId: string, _queryRunner: QueryRunner) => Promise> +const mockReadWorkspaceUserByUserId = jest.fn() as jest.MockedFunction<(_userId: string, _queryRunner: QueryRunner) => Promise> +const mockReadGeneralRoleByName = jest.fn() as jest.MockedFunction<(_name: string, _queryRunner: QueryRunner) => Promise> +const mockReadRoleById = jest.fn() as jest.MockedFunction<(_id: string, _queryRunner: QueryRunner) => Promise> +const mockReadOrganizationById = jest.fn() as jest.MockedFunction<(_id: string, _queryRunner: QueryRunner) => Promise> +const mockRecordLoginActivity = jest.fn() as jest.MockedFunction<(..._args: any[]) => Promise> jest.mock('../../utils/getRunningExpressApp', () => ({ getRunningExpressApp: jest.fn().mockImplementation(() => ({ AppDataSource: { createQueryRunner: mockCreateQueryRunner }, - identityManager: { getPlatformType: mockGetPlatformType } + identityManager: { + getPlatformType: mockGetPlatformType, + getFeaturesByPlan: mockGetFeaturesByPlan, + getProductIdFromSubscription: mockGetProductIdFromSubscription + } })) })) jest.mock('../services/user.service', () => ({ - UserService: jest.fn().mockImplementation(() => ({ readUserByEmail: mockReadUserByEmail })) + UserService: jest.fn().mockImplementation(() => ({ + readUserByEmail: mockReadUserByEmail, + bindSSOIdentity: mockBindSSOIdentity + })) })) jest.mock('../services/organization.service', () => ({ - OrganizationService: jest.fn().mockImplementation(() => ({})) + OrganizationService: jest.fn().mockImplementation(() => ({ readOrganizationById: mockReadOrganizationById })) })) jest.mock('../services/workspace-user.service', () => ({ - WorkspaceUserService: jest.fn().mockImplementation(() => ({})) + WorkspaceUserService: jest.fn().mockImplementation(() => ({ + readWorkspaceUserByLastLogin: mockReadWorkspaceUserByLastLogin, + readWorkspaceUserByUserId: mockReadWorkspaceUserByUserId + })) +})) + +jest.mock('../services/role.service', () => ({ + RoleService: jest.fn().mockImplementation(() => ({ + readGeneralRoleByName: mockReadGeneralRoleByName, + readRoleById: mockReadRoleById + })) +})) + +jest.mock('../services/audit', () => ({ + __esModule: true, + default: { recordLoginActivity: mockRecordLoginActivity } })) import SSOBase from './SSOBase' @@ -43,15 +76,32 @@ function makeQueryRunner() { return { connect: jest.fn(), release: jest.fn(), isReleased: false } } +const WORKSPACE_USER = { + userId: 'user-1', + roleId: 'role-1', + workspaceId: 'workspace-1', + workspace: { id: 'workspace-1', name: 'Default Workspace', organizationId: 'org-1' } +} + +function mockHappyPathDependencies() { + mockReadWorkspaceUserByLastLogin.mockResolvedValue(WORKSPACE_USER) + mockReadWorkspaceUserByUserId.mockResolvedValue([{ ...WORKSPACE_USER, role: { name: 'Owner' } }]) + mockReadGeneralRoleByName.mockResolvedValue({ id: 'role-1' }) + mockReadRoleById.mockResolvedValue({ id: 'role-1', permissions: '[]' }) + mockReadOrganizationById.mockResolvedValue({ id: 'org-1', subscriptionId: 'sub-1', customerId: 'cust-1' }) + mockGetFeaturesByPlan.mockResolvedValue({}) + mockGetProductIdFromSubscription.mockResolvedValue('product-1') +} + describe('SSOBase.verifyAndLogin', () => { beforeEach(() => { jest.clearAllMocks() + mockGetPlatformType.mockReturnValue(Platform.ENTERPRISE) }) it('rejects an INVITED user instead of auto-activating them (invite-token bypass fix)', async () => { const queryRunner = makeQueryRunner() mockCreateQueryRunner.mockReturnValue(queryRunner) - mockGetPlatformType.mockReturnValue(Platform.ENTERPRISE) mockReadUserByEmail.mockResolvedValue({ id: 'user-1', email: 'invitee@example.com', @@ -79,4 +129,88 @@ describe('SSOBase.verifyAndLogin', () => { ) expect(queryRunner.release).toHaveBeenCalled() }) + + it('binds an ACTIVE user to their SSO identity on first login (trust-on-first-use)', async () => { + const queryRunner = makeQueryRunner() + mockCreateQueryRunner.mockReturnValue(queryRunner) + mockReadUserByEmail.mockResolvedValue({ + id: 'user-1', + email: 'user@example.com', + status: UserStatus.ACTIVE, + ssoProvider: null, + ssoSubjectId: null + }) + mockHappyPathDependencies() + + const sso = new TestSSO({} as any) + const done = jest.fn() + + await sso.verifyAndLogin({} as any, 'user@example.com', done, { id: 'sub-123', displayName: 'User' } as any, 'at', 'rt') + + expect(mockBindSSOIdentity).toHaveBeenCalledWith('user-1', 'Test SSO', 'sub-123', queryRunner) + expect(done).toHaveBeenCalledWith(null, expect.objectContaining({ id: 'user-1' }), { message: 'Logged in Successfully' }) + }) + + it('allows a returning SSO login whose identity matches the stored binding', async () => { + const queryRunner = makeQueryRunner() + mockCreateQueryRunner.mockReturnValue(queryRunner) + mockReadUserByEmail.mockResolvedValue({ + id: 'user-1', + email: 'user@example.com', + status: UserStatus.ACTIVE, + ssoProvider: 'Test SSO', + ssoSubjectId: 'sub-123' + }) + mockHappyPathDependencies() + + const sso = new TestSSO({} as any) + const done = jest.fn() + + await sso.verifyAndLogin({} as any, 'user@example.com', done, { id: 'sub-123', displayName: 'User' } as any, 'at', 'rt') + + expect(mockBindSSOIdentity).not.toHaveBeenCalled() + expect(done).toHaveBeenCalledWith(null, expect.objectContaining({ id: 'user-1' }), { message: 'Logged in Successfully' }) + }) + + it('rejects a login whose SSO identity does not match the stored binding (cross-provider account takeover attempt)', async () => { + const queryRunner = makeQueryRunner() + mockCreateQueryRunner.mockReturnValue(queryRunner) + mockReadUserByEmail.mockResolvedValue({ + id: 'user-1', + email: 'victim@example.com', + status: UserStatus.ACTIVE, + ssoProvider: 'Google SSO', + ssoSubjectId: 'google-sub-1' + }) + + const sso = new TestSSO({} as any) + const done = jest.fn() + + await sso.verifyAndLogin({} as any, 'victim@example.com', done, { id: 'auth0-sub-999', displayName: 'Attacker' } as any, 'at', 'rt') + + expect(mockBindSSOIdentity).not.toHaveBeenCalled() + expect(mockRecordLoginActivity).toHaveBeenCalled() + expect(done).toHaveBeenCalledWith({ name: 'SSO_LOGIN_FAILED', message: UserErrorMessage.SSO_IDENTITY_MISMATCH }, undefined) + expect(queryRunner.release).toHaveBeenCalled() + }) + + it('rejects a login when the provider does not supply a stable subject id', async () => { + const queryRunner = makeQueryRunner() + mockCreateQueryRunner.mockReturnValue(queryRunner) + mockReadUserByEmail.mockResolvedValue({ + id: 'user-1', + email: 'user@example.com', + status: UserStatus.ACTIVE, + ssoProvider: null, + ssoSubjectId: null + }) + + const sso = new TestSSO({} as any) + const done = jest.fn() + + await sso.verifyAndLogin({} as any, 'user@example.com', done, { displayName: 'No Subject' } as any, 'at', 'rt') + + expect(mockBindSSOIdentity).not.toHaveBeenCalled() + expect(done).toHaveBeenCalledWith({ name: 'SSO_LOGIN_FAILED', message: UserErrorMessage.SSO_SUBJECT_ID_MISSING }, undefined) + }) }) diff --git a/packages/server/src/enterprise/sso/SSOBase.ts b/packages/server/src/enterprise/sso/SSOBase.ts index aaa1bc22d..80e1c53a0 100644 --- a/packages/server/src/enterprise/sso/SSOBase.ts +++ b/packages/server/src/enterprise/sso/SSOBase.ts @@ -1,8 +1,9 @@ // SSOBase.ts import express from 'express' import passport from 'passport' -import { IAssignedWorkspace, LoggedInUser } from '../Interface.Enterprise' +import { IAssignedWorkspace, LoggedInUser, LoginActivityCode } from '../Interface.Enterprise' import { getRunningExpressApp } from '../../utils/getRunningExpressApp' +import auditService from '../services/audit' import { UserErrorMessage, UserService } from '../services/user.service' import { WorkspaceUserService } from '../services/workspace-user.service' import { WorkspaceUser } from '../database/entities/workspace-user.entity' @@ -71,6 +72,26 @@ abstract class SSOBase { // identity provider's email assertion, which is not equivalent proof. throw new InternalFlowiseError(StatusCodes.FORBIDDEN, UserErrorMessage.USER_INVITED_PENDING_ACTIVATION) } + + const providerSubjectId = profile?.id + if (!providerSubjectId) { + throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, UserErrorMessage.SSO_SUBJECT_ID_MISSING) + } + + if (user.ssoProvider && user.ssoSubjectId) { + if (user.ssoProvider !== ssoProviderName || user.ssoSubjectId !== providerSubjectId) { + await auditService.recordLoginActivity( + user.email, + LoginActivityCode.INVALID_LOGIN_MODE, + UserErrorMessage.SSO_IDENTITY_MISMATCH, + ssoProviderName + ) + throw new InternalFlowiseError(StatusCodes.FORBIDDEN, UserErrorMessage.SSO_IDENTITY_MISMATCH) + } + } else { + await userService.bindSSOIdentity(user.id, ssoProviderName, providerSubjectId, queryRunner) + } + let wsUserOrUsers = await workspaceUserService.readWorkspaceUserByLastLogin(user?.id, queryRunner) wu = Array.isArray(wsUserOrUsers) && wsUserOrUsers.length > 0 ? wsUserOrUsers[0] : (wsUserOrUsers as WorkspaceUser) } diff --git a/packages/ui/src/routes/MainRoutes.jsx b/packages/ui/src/routes/MainRoutes.jsx index ecd8276e9..9b86f16b2 100644 --- a/packages/ui/src/routes/MainRoutes.jsx +++ b/packages/ui/src/routes/MainRoutes.jsx @@ -53,7 +53,7 @@ const Evaluators = Loadable(lazy(() => import('@/views/evaluators'))) const Account = Loadable(lazy(() => import('@/views/account'))) // files routing -const Files = Loadable(lazy(() => import('@/views/files'))) +// const Files = Loadable(lazy(() => import('@/views/files'))) // logs routing const Logs = Loadable(lazy(() => import('@/views/serverlogs')))