436 lines
19 KiB
TypeScript
436 lines
19 KiB
TypeScript
import { HttpStatusCode } from 'axios'
|
|
import { RedisStore } from 'connect-redis'
|
|
import express, { NextFunction, Request, Response } from 'express'
|
|
import session from 'express-session'
|
|
import { StatusCodes } from 'http-status-codes'
|
|
import jwt, { JwtPayload, sign } from 'jsonwebtoken'
|
|
import passport from 'passport'
|
|
import { VerifiedCallback } from 'passport-jwt'
|
|
import { InternalFlowiseError } from '../../../errors/internalFlowiseError'
|
|
import { IdentityManager } from '../../../IdentityManager'
|
|
import { Platform } from '../../../Interface'
|
|
import { getRunningExpressApp } from '../../../utils/getRunningExpressApp'
|
|
import { OrganizationUserStatus } from '../../database/entities/organization-user.entity'
|
|
import { GeneralRole } from '../../database/entities/role.entity'
|
|
import { WorkspaceUser, WorkspaceUserStatus } from '../../database/entities/workspace-user.entity'
|
|
import { ErrorMessage, IAssignedWorkspace, LoggedInUser } from '../../Interface.Enterprise'
|
|
import { AccountService } from '../../services/account.service'
|
|
import { OrganizationUserErrorMessage, OrganizationUserService } from '../../services/organization-user.service'
|
|
import { OrganizationService } from '../../services/organization.service'
|
|
import { RoleErrorMessage, RoleService } from '../../services/role.service'
|
|
import { WorkspaceUserService } from '../../services/workspace-user.service'
|
|
import { decryptToken, encryptToken, generateSafeCopy } from '../../utils/tempTokenUtils'
|
|
import { getAuthStrategy } from './AuthStrategy'
|
|
import { initializeDBClientAndStore, initializeRedisClientAndStore } from './SessionPersistance'
|
|
import { v4 as uuidv4 } from 'uuid'
|
|
|
|
const localStrategy = require('passport-local').Strategy
|
|
|
|
const jwtAudience = process.env.JWT_AUDIENCE || 'AUDIENCE'
|
|
const jwtIssuer = process.env.JWT_ISSUER || 'ISSUER'
|
|
|
|
const expireAuthTokensOnRestart = process.env.EXPIRE_AUTH_TOKENS_ON_RESTART === 'true'
|
|
const jwtAuthTokenSecret = process.env.JWT_AUTH_TOKEN_SECRET || 'auth_token'
|
|
const jwtRefreshSecret = process.env.JWT_REFRESH_TOKEN_SECRET || process.env.JWT_AUTH_TOKEN_SECRET || 'refresh_token'
|
|
|
|
// Allow explicit override of cookie security settings
|
|
// This is useful when running behind a reverse proxy/load balancer that terminates SSL
|
|
const secureCookie =
|
|
process.env.SECURE_COOKIES === 'false'
|
|
? false
|
|
: process.env.SECURE_COOKIES === 'true'
|
|
? true
|
|
: process.env.APP_URL?.startsWith('https')
|
|
? true
|
|
: false
|
|
const jwtOptions = {
|
|
secretOrKey: jwtAuthTokenSecret,
|
|
audience: jwtAudience,
|
|
issuer: jwtIssuer
|
|
}
|
|
|
|
const _initializePassportMiddleware = async (app: express.Application) => {
|
|
// Configure session middleware
|
|
let options: any = {
|
|
secret: process.env.EXPRESS_SESSION_SECRET || 'flowise',
|
|
resave: false,
|
|
saveUninitialized: false,
|
|
cookie: {
|
|
secure: secureCookie,
|
|
httpOnly: true,
|
|
sameSite: 'lax' // Add sameSite attribute
|
|
}
|
|
}
|
|
|
|
// if the auth tokens are not to be expired on restart, then configure the session store
|
|
if (!expireAuthTokensOnRestart) {
|
|
// configure session store based on the mode
|
|
if (process.env.MODE === 'queue') {
|
|
const redisStore = initializeRedisClientAndStore()
|
|
options.store = redisStore as RedisStore
|
|
} else {
|
|
// for the database store, choose store basis the DB configuration from .env
|
|
const dbSessionStore = initializeDBClientAndStore()
|
|
if (dbSessionStore) {
|
|
options.store = dbSessionStore
|
|
}
|
|
}
|
|
}
|
|
|
|
app.use(session(options))
|
|
app.use(passport.initialize())
|
|
app.use(passport.session())
|
|
|
|
if (options.store) {
|
|
const appServer = getRunningExpressApp()
|
|
appServer.sessionStore = options.store
|
|
}
|
|
|
|
passport.serializeUser((user: any, done) => {
|
|
done(null, user)
|
|
})
|
|
|
|
passport.deserializeUser((user: any, done) => {
|
|
done(null, user)
|
|
})
|
|
}
|
|
|
|
export const initializeJwtCookieMiddleware = async (app: express.Application, identityManager: IdentityManager) => {
|
|
await _initializePassportMiddleware(app)
|
|
|
|
const strategy = getAuthStrategy(jwtOptions)
|
|
passport.use(strategy)
|
|
passport.use(
|
|
'login',
|
|
new localStrategy(
|
|
{
|
|
usernameField: 'email',
|
|
passwordField: 'password',
|
|
session: true
|
|
},
|
|
async (email: string, password: string, done: VerifiedCallback) => {
|
|
let queryRunner
|
|
try {
|
|
queryRunner = getRunningExpressApp().AppDataSource.createQueryRunner()
|
|
await queryRunner.connect()
|
|
const accountService = new AccountService()
|
|
const body: any = {
|
|
user: {
|
|
email: email,
|
|
credential: password
|
|
}
|
|
}
|
|
const response = await accountService.login(body)
|
|
const workspaceUser: WorkspaceUser =
|
|
Array.isArray(response.workspaceDetails) && response.workspaceDetails.length > 0
|
|
? response.workspaceDetails[0]
|
|
: (response.workspaceDetails as WorkspaceUser)
|
|
const workspaceUserService = new WorkspaceUserService()
|
|
workspaceUser.status = WorkspaceUserStatus.ACTIVE
|
|
workspaceUser.lastLogin = new Date().toISOString()
|
|
workspaceUser.updatedBy = workspaceUser.userId
|
|
const organizationUserService = new OrganizationUserService()
|
|
const { organizationUser } = await organizationUserService.readOrganizationUserByWorkspaceIdUserId(
|
|
workspaceUser.workspaceId,
|
|
workspaceUser.userId,
|
|
queryRunner
|
|
)
|
|
if (!organizationUser)
|
|
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, OrganizationUserErrorMessage.ORGANIZATION_USER_NOT_FOUND)
|
|
organizationUser.status = OrganizationUserStatus.ACTIVE
|
|
await workspaceUserService.updateWorkspaceUser(workspaceUser, queryRunner)
|
|
await organizationUserService.updateOrganizationUser(organizationUser)
|
|
|
|
const workspaceUsers = await workspaceUserService.readWorkspaceUserByUserId(organizationUser.userId, queryRunner)
|
|
const assignedWorkspaces: IAssignedWorkspace[] = workspaceUsers.map((workspaceUser) => {
|
|
return {
|
|
id: workspaceUser.workspace.id,
|
|
name: workspaceUser.workspace.name,
|
|
role: workspaceUser.role?.name,
|
|
organizationId: workspaceUser.workspace.organizationId
|
|
} as IAssignedWorkspace
|
|
})
|
|
|
|
let roleService = new RoleService()
|
|
const ownerRole = await roleService.readGeneralRoleByName(GeneralRole.OWNER, queryRunner)
|
|
const role = await roleService.readRoleById(workspaceUser.roleId, queryRunner)
|
|
if (!role) throw new InternalFlowiseError(StatusCodes.NOT_FOUND, RoleErrorMessage.ROLE_NOT_FOUND)
|
|
|
|
const orgService = new OrganizationService()
|
|
const organization = await orgService.readOrganizationById(organizationUser.organizationId, queryRunner)
|
|
if (!organization) {
|
|
return done('Organization not found')
|
|
}
|
|
const subscriptionId = organization.subscriptionId as string
|
|
const customerId = organization.customerId as string
|
|
const features = await identityManager.getFeaturesByPlan(subscriptionId)
|
|
const productId = await identityManager.getProductIdFromSubscription(subscriptionId)
|
|
|
|
const loggedInUser: LoggedInUser = {
|
|
id: workspaceUser.userId,
|
|
email: response.user.email,
|
|
name: response.user?.name,
|
|
roleId: workspaceUser.roleId,
|
|
activeOrganizationId: organization.id,
|
|
activeOrganizationSubscriptionId: subscriptionId,
|
|
activeOrganizationCustomerId: customerId,
|
|
activeOrganizationProductId: productId,
|
|
isOrganizationAdmin: workspaceUser.roleId === ownerRole.id,
|
|
activeWorkspaceId: workspaceUser.workspaceId,
|
|
activeWorkspace: workspaceUser.workspace.name,
|
|
assignedWorkspaces,
|
|
permissions: [...JSON.parse(role.permissions)],
|
|
features
|
|
}
|
|
return done(null, loggedInUser, { message: 'Logged in Successfully' })
|
|
} catch (error) {
|
|
return done(error)
|
|
} finally {
|
|
if (queryRunner) await queryRunner.release()
|
|
}
|
|
}
|
|
)
|
|
)
|
|
|
|
app.post('/api/v1/auth/resolve', async (req, res) => {
|
|
// check for the organization, if empty redirect to the organization setup page for OpenSource and Enterprise Versions
|
|
// for Cloud (Horizontal) version, redirect to the signin page
|
|
const expressApp = getRunningExpressApp()
|
|
const platform = expressApp.identityManager.getPlatformType()
|
|
if (platform === Platform.CLOUD) {
|
|
return res.status(HttpStatusCode.Ok).json({ redirectUrl: '/signin' })
|
|
}
|
|
const orgService = new OrganizationService()
|
|
const queryRunner = expressApp.AppDataSource.createQueryRunner()
|
|
await queryRunner.connect()
|
|
const registeredOrganizationCount = await orgService.countOrganizations(queryRunner)
|
|
await queryRunner.release()
|
|
if (registeredOrganizationCount === 0) {
|
|
switch (platform) {
|
|
case Platform.ENTERPRISE:
|
|
if (!identityManager.isLicenseValid()) {
|
|
return res.status(HttpStatusCode.Ok).json({ redirectUrl: '/license-expired' })
|
|
}
|
|
return res.status(HttpStatusCode.Ok).json({ redirectUrl: '/organization-setup' })
|
|
default:
|
|
return res.status(HttpStatusCode.Ok).json({ redirectUrl: '/organization-setup' })
|
|
}
|
|
}
|
|
switch (platform) {
|
|
case Platform.ENTERPRISE:
|
|
if (!identityManager.isLicenseValid()) {
|
|
return res.status(HttpStatusCode.Ok).json({ redirectUrl: '/license-expired' })
|
|
}
|
|
return res.status(HttpStatusCode.Ok).json({ redirectUrl: '/signin' })
|
|
default:
|
|
return res.status(HttpStatusCode.Ok).json({ redirectUrl: '/signin' })
|
|
}
|
|
})
|
|
|
|
app.post('/api/v1/auth/refreshToken', async (req, res) => {
|
|
const refreshToken = req.cookies.refreshToken
|
|
if (!refreshToken) return res.sendStatus(401)
|
|
|
|
jwt.verify(refreshToken, jwtRefreshSecret, async (err: any, payload: any) => {
|
|
if (err || !payload) return res.status(401).json({ message: ErrorMessage.REFRESH_TOKEN_EXPIRED })
|
|
// @ts-ignore
|
|
const loggedInUser = req.user as LoggedInUser
|
|
let isSSO = false
|
|
let newTokenResponse: any = {}
|
|
if (loggedInUser && loggedInUser.ssoRefreshToken) {
|
|
try {
|
|
newTokenResponse = await identityManager.getRefreshToken(loggedInUser.ssoProvider, loggedInUser.ssoRefreshToken)
|
|
if (newTokenResponse.error) {
|
|
return res.status(401).json({ message: ErrorMessage.REFRESH_TOKEN_EXPIRED })
|
|
}
|
|
isSSO = true
|
|
} catch (error) {
|
|
return res.status(401).json({ message: ErrorMessage.REFRESH_TOKEN_EXPIRED })
|
|
}
|
|
}
|
|
const meta = decryptToken(payload.meta)
|
|
if (!meta) {
|
|
return res.status(401).json({ message: ErrorMessage.REFRESH_TOKEN_EXPIRED })
|
|
}
|
|
if (isSSO) {
|
|
loggedInUser.ssoToken = newTokenResponse.access_token
|
|
if (newTokenResponse.refresh_token) {
|
|
loggedInUser.ssoRefreshToken = newTokenResponse.refresh_token
|
|
}
|
|
return setTokenOrCookies(res, loggedInUser, false, req, false, true)
|
|
} else {
|
|
return setTokenOrCookies(res, loggedInUser, false, req)
|
|
}
|
|
})
|
|
})
|
|
|
|
app.post('/api/v1/auth/login', (req, res, next?) => {
|
|
passport.authenticate('login', async (err: any, user: LoggedInUser) => {
|
|
try {
|
|
if (err || !user) {
|
|
return next ? next(err) : res.status(401).json(err)
|
|
}
|
|
if (identityManager.isEnterprise() && !identityManager.isLicenseValid()) {
|
|
return res.status(401).json({ redirectUrl: '/license-expired' })
|
|
}
|
|
|
|
req.session.regenerate((regenerateErr) => {
|
|
if (regenerateErr) {
|
|
return next ? next(regenerateErr) : res.status(500).json({ message: 'Session regeneration failed' })
|
|
}
|
|
|
|
req.login(user, { session: true }, async (error) => {
|
|
if (error) {
|
|
return next ? next(error) : res.status(401).json(error)
|
|
}
|
|
return setTokenOrCookies(res, user, true, req)
|
|
})
|
|
})
|
|
} catch (error: any) {
|
|
return next ? next(error) : res.status(401).json(error)
|
|
}
|
|
})(req, res, next)
|
|
})
|
|
}
|
|
|
|
export const setTokenOrCookies = (
|
|
res: Response,
|
|
user: any,
|
|
regenerateRefreshToken: boolean,
|
|
req?: Request,
|
|
redirect?: boolean,
|
|
isSSO?: boolean
|
|
) => {
|
|
const token = generateJwtAuthToken(user)
|
|
let refreshToken: string = ''
|
|
if (regenerateRefreshToken) {
|
|
refreshToken = generateJwtRefreshToken(user)
|
|
} else {
|
|
refreshToken = req?.cookies?.refreshToken
|
|
}
|
|
const returnUser = generateSafeCopy(user)
|
|
returnUser.isSSO = !isSSO ? false : isSSO
|
|
|
|
if (redirect) {
|
|
// 1. Generate a random token
|
|
const ssoToken = uuidv4()
|
|
|
|
// 2. Store returnUser in your session store, keyed by ssoToken, with a short expiry
|
|
storeSSOUserPayload(ssoToken, returnUser)
|
|
// 3. Redirect with token only
|
|
const dashboardUrl = `/sso-success?token=${ssoToken}`
|
|
|
|
// Return the token as a cookie in our response.
|
|
let resWithCookies = res
|
|
.cookie('token', token, {
|
|
httpOnly: true,
|
|
secure: secureCookie,
|
|
sameSite: 'lax'
|
|
})
|
|
.cookie('refreshToken', refreshToken, {
|
|
httpOnly: true,
|
|
secure: secureCookie,
|
|
sameSite: 'lax'
|
|
})
|
|
resWithCookies.redirect(dashboardUrl)
|
|
} else {
|
|
// Return the token as a cookie in our response.
|
|
res.cookie('token', token, {
|
|
httpOnly: true,
|
|
secure: secureCookie,
|
|
sameSite: 'lax'
|
|
})
|
|
.cookie('refreshToken', refreshToken, {
|
|
httpOnly: true,
|
|
secure: secureCookie,
|
|
sameSite: 'lax'
|
|
})
|
|
.type('json')
|
|
.send({ ...returnUser })
|
|
}
|
|
}
|
|
|
|
export const generateJwtAuthToken = (user: any) => {
|
|
let expiryInMinutes = -1
|
|
if (user?.ssoToken) {
|
|
const jwtHeader = jwt.decode(user.ssoToken, { complete: true })
|
|
if (jwtHeader) {
|
|
const utcSeconds = (jwtHeader.payload as any).exp
|
|
let d = new Date(0) // The 0 there is the key, which sets the date to the epoch
|
|
d.setUTCSeconds(utcSeconds)
|
|
// get the minutes difference from current time
|
|
expiryInMinutes = Math.abs(d.getTime() - new Date().getTime()) / 60000
|
|
}
|
|
}
|
|
if (expiryInMinutes === -1) {
|
|
expiryInMinutes = process.env.JWT_TOKEN_EXPIRY_IN_MINUTES ? parseInt(process.env.JWT_TOKEN_EXPIRY_IN_MINUTES) : 60
|
|
}
|
|
return _generateJwtToken(user, expiryInMinutes, jwtAuthTokenSecret)
|
|
}
|
|
|
|
export const generateJwtRefreshToken = (user: any) => {
|
|
let expiryInMinutes = -1
|
|
if (user.ssoRefreshToken) {
|
|
const jwtHeader = jwt.decode(user.ssoRefreshToken, { complete: false })
|
|
if (jwtHeader && typeof jwtHeader !== 'string') {
|
|
const utcSeconds = (jwtHeader as JwtPayload).exp
|
|
if (utcSeconds) {
|
|
let d = new Date(0) // The 0 there is the key, which sets the date to the epoch
|
|
d.setUTCSeconds(utcSeconds)
|
|
// get the minutes difference from current time
|
|
expiryInMinutes = Math.abs(d.getTime() - new Date().getTime()) / 60000
|
|
}
|
|
}
|
|
}
|
|
if (expiryInMinutes === -1) {
|
|
expiryInMinutes = process.env.JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES
|
|
? parseInt(process.env.JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES)
|
|
: 129600 // 90 days
|
|
}
|
|
return _generateJwtToken(user, expiryInMinutes, jwtRefreshSecret)
|
|
}
|
|
|
|
const _generateJwtToken = (user: Partial<LoggedInUser>, expiryInMinutes: number, secret: string) => {
|
|
const encryptedUserInfo = encryptToken(user?.id + ':' + user?.activeWorkspaceId)
|
|
return sign({ id: user?.id, username: user?.name, meta: encryptedUserInfo }, secret!, {
|
|
expiresIn: expiryInMinutes + 'm', // Expiry in minutes
|
|
notBefore: '0', // Cannot use before now, can be configured to be deferred.
|
|
algorithm: 'HS256', // HMAC using SHA-256 hash algorithm
|
|
audience: jwtAudience, // The audience of the token
|
|
issuer: jwtIssuer // The issuer of the token
|
|
})
|
|
}
|
|
|
|
export const verifyToken = (req: Request, res: Response, next: NextFunction) => {
|
|
passport.authenticate('jwt', { session: true }, (err: any, user: LoggedInUser, info: object) => {
|
|
if (err) {
|
|
return next(err)
|
|
}
|
|
|
|
// @ts-ignore
|
|
if (info && info.name === 'TokenExpiredError') {
|
|
if (req.cookies && req.cookies.refreshToken) {
|
|
return res.status(401).json({ message: ErrorMessage.TOKEN_EXPIRED, retry: true })
|
|
}
|
|
return res.status(401).json({ message: ErrorMessage.INVALID_MISSING_TOKEN })
|
|
}
|
|
|
|
if (!user) {
|
|
return res.status(401).json({ message: ErrorMessage.INVALID_MISSING_TOKEN })
|
|
}
|
|
|
|
const identityManager = getRunningExpressApp().identityManager
|
|
if (identityManager.isEnterprise() && !identityManager.isLicenseValid()) {
|
|
return res.status(401).json({ redirectUrl: '/license-expired' })
|
|
}
|
|
|
|
req.user = user
|
|
next()
|
|
})(req, res, next)
|
|
}
|
|
|
|
const storeSSOUserPayload = (ssoToken: string, returnUser: any) => {
|
|
const app = getRunningExpressApp()
|
|
app.cachePool.addSSOTokenCache(ssoToken, returnUser)
|
|
}
|