From 75ce181a688c0e61d3262f66a736c7699c4543bf Mon Sep 17 00:00:00 2001 From: Ilango Date: Wed, 25 Feb 2026 19:10:12 +0530 Subject: [PATCH] Updates to environment and initialization logic (#5683) * fix: make TOKEN_HASH_SECRET required and add check on server start * fix: make EXPRESS_SESSION_SECRET required and add check on server start * fix: make jwt variables required and validate on server start * fix: formatting * fix: cypress tests * fix: remove env file for cypress and refactor * revert: changes to workflow and env examples * update: how missing/weak default secrets are handled * update: how auth secrets are setup and read * fix: update env examples for docker * fix: weak defaults list * fix: weak defaults list --------- Co-authored-by: yau-wd Co-authored-by: yau-wd <246233045+yau-wd@users.noreply.github.com> --- docker/.env.example | 40 +++++-- docker/worker/.env.example | 40 +++++-- packages/server/.env.example | 23 ++-- .../enterprise/middleware/passport/index.ts | 36 ++++--- .../src/enterprise/utils/authSecrets.ts | 100 ++++++++++++++++++ .../src/enterprise/utils/tempTokenUtils.ts | 11 +- packages/server/src/index.ts | 5 + packages/server/src/utils/index.ts | 75 +++++++++++++ 8 files changed, 277 insertions(+), 53 deletions(-) create mode 100644 packages/server/src/enterprise/utils/authSecrets.ts diff --git a/docker/.env.example b/docker/.env.example index 214481f2f..8c1f28d07 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -4,7 +4,7 @@ PORT=3000 ############################################## DATABASE #################################################### ############################################################################################################ -DATABASE_PATH=/root/.flowise +# DATABASE_PATH=/your_database_path/.flowise # DATABASE_TYPE=postgres # DATABASE_PORT=5432 # DATABASE_HOST="" @@ -21,12 +21,14 @@ DATABASE_PATH=/root/.flowise ############################################################################################################ # SECRETKEY_STORAGE_TYPE=local #(local | aws) -SECRETKEY_PATH=/root/.flowise +# SECRETKEY_PATH=/your_secret_path/.flowise # FLOWISE_SECRETKEY_OVERWRITE=myencryptionkey # (if you want to overwrite the secret key) # SECRETKEY_AWS_ACCESS_KEY= # SECRETKEY_AWS_SECRET_KEY= # SECRETKEY_AWS_REGION=us-west-2 # SECRETKEY_AWS_NAME=FlowiseEncryptionKey +# SECRETKEY_AWS_AUTH_PREFIX=Flowise # (when SECRETKEY_STORAGE_TYPE=aws, prefix for auth secret names e.g. FlowiseTokenHashSecret) +# Auth secrets (TOKEN_HASH_SECRET, EXPRESS_SESSION_SECRET, JWT_*) can be set here or left unset to use file/AWS storage ############################################################################################################ @@ -34,7 +36,7 @@ SECRETKEY_PATH=/root/.flowise ############################################################################################################ # DEBUG=true -LOG_PATH=/root/.flowise/logs +# LOG_PATH=/your_log_path/.flowise/logs # LOG_LEVEL=info #(error | warn | info | verbose | debug) # LOG_SANITIZE_BODY_FIELDS=password,pwd,pass,secret,token,apikey,api_key,accesstoken,access_token,refreshtoken,refresh_token,clientsecret,client_secret,privatekey,private_key,secretkey,secret_key,auth,authorization,credential,credentials # LOG_SANITIZE_HEADER_FIELDS=authorization,x-api-key,x-auth-token,cookie @@ -48,7 +50,7 @@ LOG_PATH=/root/.flowise/logs ############################################################################################################ # STORAGE_TYPE=local (local | s3 | gcs) -BLOB_STORAGE_PATH=/root/.flowise/storage +# BLOB_STORAGE_PATH=/your_storage_path/.flowise/storage # S3_STORAGE_BUCKET_NAME=flowise # S3_STORAGE_ACCESS_KEY_ID= # S3_STORAGE_SECRET_ACCESS_KEY= @@ -91,20 +93,29 @@ BLOB_STORAGE_PATH=/root/.flowise/storage # ALLOW_UNAUTHORIZED_CERTS=false # SENDER_EMAIL=team@example.com -JWT_AUTH_TOKEN_SECRET='AABBCCDDAABBCCDDAABBCCDDAABBCCDDAABBCCDD' -JWT_REFRESH_TOKEN_SECRET='AABBCCDDAABBCCDDAABBCCDDAABBCCDDAABBCCDD' -JWT_ISSUER='ISSUER' -JWT_AUDIENCE='AUDIENCE' +# Auth secrets: set via env (backwards compat) or leave unset to use file/AWS storage (SECRETKEY_PATH or SECRETKEY_STORAGE_TYPE=aws) +# Generate a secure 32-byte secret using: openssl rand -hex 32 +# JWT_AUTH_TOKEN_SECRET= +# JWT_REFRESH_TOKEN_SECRET= + +JWT_ISSUER=Flowise +JWT_AUDIENCE=Flowise JWT_TOKEN_EXPIRY_IN_MINUTES=360 JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200 + # EXPIRE_AUTH_TOKENS_ON_RESTART=true # (if you need to expire all tokens on app restart) -# EXPRESS_SESSION_SECRET=flowise + +# Generate a secure 32-byte secret using: openssl rand -hex 32 (or leave unset for file/AWS storage) +# EXPRESS_SESSION_SECRET= + # SECURE_COOKIES= # INVITE_TOKEN_EXPIRY_IN_HOURS=24 # PASSWORD_RESET_TOKEN_EXPIRY_IN_MINS=15 # PASSWORD_SALT_HASH_ROUNDS=10 -# TOKEN_HASH_SECRET='popcorn' + +# Generate a secure 32-byte secret using: openssl rand -hex 32 (or leave unset for file/AWS storage) +# TOKEN_HASH_SECRET= # WORKSPACE_INVITE_TEMPLATE_PATH=/path/to/custom/workspace_invite.hbs @@ -167,7 +178,6 @@ JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200 # REDIS_KEEP_ALIVE= # ENABLE_BULLMQ_DASHBOARD= - ############################################################################################################ ############################################## SECURITY #################################################### ############################################################################################################ @@ -176,3 +186,11 @@ JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200 # CUSTOM_MCP_SECURITY_CHECK=true # CUSTOM_MCP_PROTOCOL=sse #(stdio | sse) # TRUST_PROXY=true #(true | false | 1 | loopback| linklocal | uniquelocal | IP addresses | loopback, IP addresses) + + +############################################################################################################ +########################################### DOCUMENT LOADERS ############################################### +############################################################################################################ + +# PUPPETEER_EXECUTABLE_FILE_PATH='C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' +# PLAYWRIGHT_EXECUTABLE_FILE_PATH='C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' diff --git a/docker/worker/.env.example b/docker/worker/.env.example index c87324808..d5914865d 100644 --- a/docker/worker/.env.example +++ b/docker/worker/.env.example @@ -4,7 +4,7 @@ WORKER_PORT=5566 ############################################## DATABASE #################################################### ############################################################################################################ -DATABASE_PATH=/root/.flowise +# DATABASE_PATH=/your_database_path/.flowise # DATABASE_TYPE=postgres # DATABASE_PORT=5432 # DATABASE_HOST="" @@ -21,12 +21,14 @@ DATABASE_PATH=/root/.flowise ############################################################################################################ # SECRETKEY_STORAGE_TYPE=local #(local | aws) -SECRETKEY_PATH=/root/.flowise +# SECRETKEY_PATH=/your_secret_path/.flowise # FLOWISE_SECRETKEY_OVERWRITE=myencryptionkey # (if you want to overwrite the secret key) # SECRETKEY_AWS_ACCESS_KEY= # SECRETKEY_AWS_SECRET_KEY= # SECRETKEY_AWS_REGION=us-west-2 # SECRETKEY_AWS_NAME=FlowiseEncryptionKey +# SECRETKEY_AWS_AUTH_PREFIX=Flowise # (when SECRETKEY_STORAGE_TYPE=aws, prefix for auth secret names e.g. FlowiseTokenHashSecret) +# Auth secrets (TOKEN_HASH_SECRET, EXPRESS_SESSION_SECRET, JWT_*) can be set here or left unset to use file/AWS storage ############################################################################################################ @@ -34,7 +36,7 @@ SECRETKEY_PATH=/root/.flowise ############################################################################################################ # DEBUG=true -LOG_PATH=/root/.flowise/logs +# LOG_PATH=/your_log_path/.flowise/logs # LOG_LEVEL=info #(error | warn | info | verbose | debug) # LOG_SANITIZE_BODY_FIELDS=password,pwd,pass,secret,token,apikey,api_key,accesstoken,access_token,refreshtoken,refresh_token,clientsecret,client_secret,privatekey,private_key,secretkey,secret_key,auth,authorization,credential,credentials # LOG_SANITIZE_HEADER_FIELDS=authorization,x-api-key,x-auth-token,cookie @@ -48,7 +50,7 @@ LOG_PATH=/root/.flowise/logs ############################################################################################################ # STORAGE_TYPE=local (local | s3 | gcs) -BLOB_STORAGE_PATH=/root/.flowise/storage +# BLOB_STORAGE_PATH=/your_storage_path/.flowise/storage # S3_STORAGE_BUCKET_NAME=flowise # S3_STORAGE_ACCESS_KEY_ID= # S3_STORAGE_SECRET_ACCESS_KEY= @@ -91,20 +93,29 @@ BLOB_STORAGE_PATH=/root/.flowise/storage # ALLOW_UNAUTHORIZED_CERTS=false # SENDER_EMAIL=team@example.com -JWT_AUTH_TOKEN_SECRET='AABBCCDDAABBCCDDAABBCCDDAABBCCDDAABBCCDD' -JWT_REFRESH_TOKEN_SECRET='AABBCCDDAABBCCDDAABBCCDDAABBCCDDAABBCCDD' -JWT_ISSUER='ISSUER' -JWT_AUDIENCE='AUDIENCE' +# Auth secrets: set via env (backwards compat) or leave unset to use file/AWS storage (SECRETKEY_PATH or SECRETKEY_STORAGE_TYPE=aws) +# Generate a secure 32-byte secret using: openssl rand -hex 32 +# JWT_AUTH_TOKEN_SECRET= +# JWT_REFRESH_TOKEN_SECRET= + +JWT_ISSUER=Flowise +JWT_AUDIENCE=Flowise JWT_TOKEN_EXPIRY_IN_MINUTES=360 JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200 + # EXPIRE_AUTH_TOKENS_ON_RESTART=true # (if you need to expire all tokens on app restart) -# EXPRESS_SESSION_SECRET=flowise + +# Generate a secure 32-byte secret using: openssl rand -hex 32 (or leave unset for file/AWS storage) +# EXPRESS_SESSION_SECRET= + # SECURE_COOKIES= # INVITE_TOKEN_EXPIRY_IN_HOURS=24 # PASSWORD_RESET_TOKEN_EXPIRY_IN_MINS=15 # PASSWORD_SALT_HASH_ROUNDS=10 -# TOKEN_HASH_SECRET='popcorn' + +# Generate a secure 32-byte secret using: openssl rand -hex 32 (or leave unset for file/AWS storage) +# TOKEN_HASH_SECRET= # WORKSPACE_INVITE_TEMPLATE_PATH=/path/to/custom/workspace_invite.hbs @@ -167,7 +178,6 @@ JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200 # REDIS_KEEP_ALIVE= # ENABLE_BULLMQ_DASHBOARD= - ############################################################################################################ ############################################## SECURITY #################################################### ############################################################################################################ @@ -176,3 +186,11 @@ JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200 # CUSTOM_MCP_SECURITY_CHECK=true # CUSTOM_MCP_PROTOCOL=sse #(stdio | sse) # TRUST_PROXY=true #(true | false | 1 | loopback| linklocal | uniquelocal | IP addresses | loopback, IP addresses) + + +############################################################################################################ +########################################### DOCUMENT LOADERS ############################################### +############################################################################################################ + +# PUPPETEER_EXECUTABLE_FILE_PATH='C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' +# PLAYWRIGHT_EXECUTABLE_FILE_PATH='C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' diff --git a/packages/server/.env.example b/packages/server/.env.example index a387a12dc..8c1f28d07 100644 --- a/packages/server/.env.example +++ b/packages/server/.env.example @@ -27,6 +27,8 @@ PORT=3000 # SECRETKEY_AWS_SECRET_KEY= # SECRETKEY_AWS_REGION=us-west-2 # SECRETKEY_AWS_NAME=FlowiseEncryptionKey +# SECRETKEY_AWS_AUTH_PREFIX=Flowise # (when SECRETKEY_STORAGE_TYPE=aws, prefix for auth secret names e.g. FlowiseTokenHashSecret) +# Auth secrets (TOKEN_HASH_SECRET, EXPRESS_SESSION_SECRET, JWT_*) can be set here or left unset to use file/AWS storage ############################################################################################################ @@ -91,20 +93,29 @@ PORT=3000 # ALLOW_UNAUTHORIZED_CERTS=false # SENDER_EMAIL=team@example.com -JWT_AUTH_TOKEN_SECRET='AABBCCDDAABBCCDDAABBCCDDAABBCCDDAABBCCDD' -JWT_REFRESH_TOKEN_SECRET='AABBCCDDAABBCCDDAABBCCDDAABBCCDDAABBCCDD' -JWT_ISSUER='ISSUER' -JWT_AUDIENCE='AUDIENCE' +# Auth secrets: set via env (backwards compat) or leave unset to use file/AWS storage (SECRETKEY_PATH or SECRETKEY_STORAGE_TYPE=aws) +# Generate a secure 32-byte secret using: openssl rand -hex 32 +# JWT_AUTH_TOKEN_SECRET= +# JWT_REFRESH_TOKEN_SECRET= + +JWT_ISSUER=Flowise +JWT_AUDIENCE=Flowise JWT_TOKEN_EXPIRY_IN_MINUTES=360 JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200 + # EXPIRE_AUTH_TOKENS_ON_RESTART=true # (if you need to expire all tokens on app restart) -# EXPRESS_SESSION_SECRET=flowise + +# Generate a secure 32-byte secret using: openssl rand -hex 32 (or leave unset for file/AWS storage) +# EXPRESS_SESSION_SECRET= + # SECURE_COOKIES= # INVITE_TOKEN_EXPIRY_IN_HOURS=24 # PASSWORD_RESET_TOKEN_EXPIRY_IN_MINS=15 # PASSWORD_SALT_HASH_ROUNDS=10 -# TOKEN_HASH_SECRET='popcorn' + +# Generate a secure 32-byte secret using: openssl rand -hex 32 (or leave unset for file/AWS storage) +# TOKEN_HASH_SECRET= # WORKSPACE_INVITE_TEMPLATE_PATH=/path/to/custom/workspace_invite.hbs diff --git a/packages/server/src/enterprise/middleware/passport/index.ts b/packages/server/src/enterprise/middleware/passport/index.ts index ea7b75ed4..2fe913580 100644 --- a/packages/server/src/enterprise/middleware/passport/index.ts +++ b/packages/server/src/enterprise/middleware/passport/index.ts @@ -19,6 +19,13 @@ import { OrganizationUserErrorMessage, OrganizationUserService } from '../../ser import { OrganizationService } from '../../services/organization.service' import { RoleErrorMessage, RoleService } from '../../services/role.service' import { WorkspaceUserService } from '../../services/workspace-user.service' +import { + getExpressSessionSecret, + getJWTAudience, + getJWTAuthTokenSecret, + getJWTIssuer, + getJWTRefreshTokenSecret +} from '../../utils/authSecrets' import { decryptToken, encryptToken, generateSafeCopy } from '../../utils/tempTokenUtils' import { getAuthStrategy } from './AuthStrategy' import { initializeDBClientAndStore, initializeRedisClientAndStore } from './SessionPersistance' @@ -26,12 +33,7 @@ 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 @@ -46,16 +48,11 @@ const secureCookie = : 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', + secret: getExpressSessionSecret(), resave: false, saveUninitialized: false, cookie: { @@ -101,6 +98,11 @@ const _initializePassportMiddleware = async (app: express.Application) => { export const initializeJwtCookieMiddleware = async (app: express.Application, identityManager: IdentityManager) => { await _initializePassportMiddleware(app) + const jwtOptions = { + secretOrKey: getJWTAuthTokenSecret(), + audience: getJWTAudience(), + issuer: getJWTIssuer() + } const strategy = getAuthStrategy(jwtOptions) passport.use(strategy) passport.use( @@ -234,7 +236,7 @@ export const initializeJwtCookieMiddleware = async (app: express.Application, id const refreshToken = req.cookies.refreshToken if (!refreshToken) return res.sendStatus(401) - jwt.verify(refreshToken, jwtRefreshSecret, async (err: any, payload: any) => { + jwt.verify(refreshToken, getJWTRefreshTokenSecret(), 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 @@ -368,7 +370,7 @@ export const generateJwtAuthToken = (user: any) => { 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) + return _generateJwtToken(user, expiryInMinutes, getJWTAuthTokenSecret()) } export const generateJwtRefreshToken = (user: any) => { @@ -390,17 +392,17 @@ export const generateJwtRefreshToken = (user: any) => { ? parseInt(process.env.JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES) : 129600 // 90 days } - return _generateJwtToken(user, expiryInMinutes, jwtRefreshSecret) + return _generateJwtToken(user, expiryInMinutes, getJWTRefreshTokenSecret()) } const _generateJwtToken = (user: Partial, expiryInMinutes: number, secret: string) => { const encryptedUserInfo = encryptToken(user?.id + ':' + user?.activeWorkspaceId) - return sign({ id: user?.id, username: user?.name, meta: encryptedUserInfo }, secret!, { + 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 + audience: getJWTAudience(), + issuer: getJWTIssuer() }) } diff --git a/packages/server/src/enterprise/utils/authSecrets.ts b/packages/server/src/enterprise/utils/authSecrets.ts new file mode 100644 index 000000000..8498db5e8 --- /dev/null +++ b/packages/server/src/enterprise/utils/authSecrets.ts @@ -0,0 +1,100 @@ +import { getOrCreateStoredSecret } from '../../utils' + +/** + * Weak default values that were previously hardcoded when env vars were not set. + * If the user has set a var to one of these, we treat it as "not set" and use file/AWS storage instead. + */ +const WEAK_DEFAULTS: Record = { + JWT_AUTH_TOKEN_SECRET: 'AABBCCDDAABBCCDDAABBCCDDAABBCCDDAABBCCDD', + JWT_REFRESH_TOKEN_SECRET: 'AABBCCDDAABBCCDDAABBCCDDAABBCCDDAABBCCDD', + EXPRESS_SESSION_SECRET: 'flowise', + TOKEN_HASH_SECRET: 'popcorn' +} + +let tokenHashSecret: string | undefined +let expressSessionSecret: string | undefined +let jwtAuthTokenSecret: string | undefined +let jwtRefreshTokenSecret: string | undefined +let jwtIssuer: string | undefined +let jwtAudience: string | undefined + +const NOT_INITIALIZED = 'Auth secrets not initialized. Call initAuthSecrets() first.' + +/** + * Initialize auth secrets from env (backwards compat) → AWS Secrets Manager → filesystem. + * Each secret is generated with crypto.randomBytes(32) when created (or 'flowise' for JWT_ISSUER/JWT_AUDIENCE). + * Call once after getEncryptionKey() in initDatabase(). + */ +export async function initAuthSecrets(): Promise { + tokenHashSecret = await getOrCreateStoredSecret({ + envKey: 'TOKEN_HASH_SECRET', + fileName: 'token_hash_secret.key', + awsSecretIdSuffix: 'TokenHashSecret', + weakDefault: WEAK_DEFAULTS.TOKEN_HASH_SECRET + }) + + expressSessionSecret = await getOrCreateStoredSecret({ + envKey: 'EXPRESS_SESSION_SECRET', + fileName: 'express_session_secret.key', + awsSecretIdSuffix: 'ExpressSessionSecret', + weakDefault: WEAK_DEFAULTS.EXPRESS_SESSION_SECRET + }) + + jwtAuthTokenSecret = await getOrCreateStoredSecret({ + envKey: 'JWT_AUTH_TOKEN_SECRET', + fileName: 'jwt_auth_token_secret.key', + awsSecretIdSuffix: 'JWTAuthTokenSecret', + weakDefault: WEAK_DEFAULTS.JWT_AUTH_TOKEN_SECRET + }) + + jwtRefreshTokenSecret = await getOrCreateStoredSecret({ + envKey: 'JWT_REFRESH_TOKEN_SECRET', + fileName: 'jwt_refresh_token_secret.key', + awsSecretIdSuffix: 'JWTRefreshTokenSecret', + weakDefault: WEAK_DEFAULTS.JWT_REFRESH_TOKEN_SECRET + }) + + jwtIssuer = await getOrCreateStoredSecret({ + envKey: 'JWT_ISSUER', + fileName: 'jwt_issuer.key', + awsSecretIdSuffix: 'JWTIssuer', + defaultValueForNew: 'flowise' + }) + + jwtAudience = await getOrCreateStoredSecret({ + envKey: 'JWT_AUDIENCE', + fileName: 'jwt_audience.key', + awsSecretIdSuffix: 'JWTAudience', + defaultValueForNew: 'flowise' + }) +} + +export function getTokenHashSecret(): string { + if (tokenHashSecret === undefined) throw new Error(NOT_INITIALIZED) + return tokenHashSecret +} + +export function getExpressSessionSecret(): string { + if (expressSessionSecret === undefined) throw new Error(NOT_INITIALIZED) + return expressSessionSecret +} + +export function getJWTAuthTokenSecret(): string { + if (jwtAuthTokenSecret === undefined) throw new Error(NOT_INITIALIZED) + return jwtAuthTokenSecret +} + +export function getJWTRefreshTokenSecret(): string { + if (jwtRefreshTokenSecret === undefined) throw new Error(NOT_INITIALIZED) + return jwtRefreshTokenSecret +} + +export function getJWTIssuer(): string { + if (jwtIssuer === undefined) throw new Error(NOT_INITIALIZED) + return jwtIssuer +} + +export function getJWTAudience(): string { + if (jwtAudience === undefined) throw new Error(NOT_INITIALIZED) + return jwtAudience +} diff --git a/packages/server/src/enterprise/utils/tempTokenUtils.ts b/packages/server/src/enterprise/utils/tempTokenUtils.ts index 6e6ba8b76..02f317df4 100644 --- a/packages/server/src/enterprise/utils/tempTokenUtils.ts +++ b/packages/server/src/enterprise/utils/tempTokenUtils.ts @@ -2,6 +2,7 @@ import { LoggedInUser } from '../Interface.Enterprise' import * as crypto from 'crypto' import moment from 'moment' import { customAlphabet } from 'nanoid' +import { getTokenHashSecret } from './authSecrets' const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 64) @@ -28,10 +29,7 @@ export const generateTempToken = () => { // Encrypt token with password using crypto.Cipheriv export const encryptToken = (stringToEncrypt: string) => { - const key = crypto - .createHash('sha256') - .update(process.env.TOKEN_HASH_SECRET || 'Secre$t') - .digest() + const key = crypto.createHash('sha256').update(getTokenHashSecret()).digest() const IV_LENGTH = 16 const iv = crypto.randomBytes(IV_LENGTH) @@ -47,10 +45,7 @@ export const encryptToken = (stringToEncrypt: string) => { // Decrypt token using the inverse of encryption crypto algorithm export const decryptToken = (stringToDecrypt: string): string | undefined => { try { - const key = crypto - .createHash('sha256') - .update(process.env.TOKEN_HASH_SECRET || 'Secre$t') - .digest() + const key = crypto.createHash('sha256').update(getTokenHashSecret()).digest() let textParts = stringToDecrypt.split(':') let iv = Buffer.from(textParts.shift() as string, 'hex') diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index b3b2abc2e..bbb31910e 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -14,6 +14,7 @@ import { Organization } from './enterprise/database/entities/organization.entity import { Workspace } from './enterprise/database/entities/workspace.entity' import { LoggedInUser } from './enterprise/Interface.Enterprise' import { initializeJwtCookieMiddleware, verifyToken, verifyTokenForBullMQDashboard } from './enterprise/middleware/passport' +import { initAuthSecrets } from './enterprise/utils/authSecrets' import { IdentityManager } from './IdentityManager' import { MODE, Platform } from './Interface' import { IMetricsProvider } from './Interface.Metrics' @@ -104,6 +105,10 @@ export class App { await getEncryptionKey() logger.info('🔑 [server]: Encryption key initialized successfully') + // Initialize auth secrets (env → AWS Secrets Manager → filesystem) + await initAuthSecrets() + logger.info('🔐 [server]: Auth initialized successfully') + // Initialize Rate Limit this.rateLimiterManager = RateLimiterManager.getInstance() await this.rateLimiterManager.initializeRateLimiters(await getDataSource().getRepository(ChatFlow).find()) diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index f8b228e0c..f74917d0c 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -1661,6 +1661,81 @@ export const generateEncryptKey = (): string => { return randomBytes(24).toString('base64') } +/** + * Returns the directory where auth secrets are stored (same as encryption key directory). + * Used for file-based storage of TOKEN_HASH_SECRET, EXPRESS_SESSION_SECRET, JWT_*, etc. + */ +export const getAuthSecretsDirectory = (): string => { + return process.env.SECRETKEY_PATH ? process.env.SECRETKEY_PATH : path.join(getUserHome(), '.flowise') +} + +/** + * Generate a 32-byte auth secret (OWASP recommendation). + * Used for auth secrets only; encryption key remains 24 bytes. + */ +export const generateAuthSecret = (): string => { + return randomBytes(32).toString('base64') +} + +export interface GetOrCreateStoredSecretOptions { + envKey: string + fileName: string + awsSecretIdSuffix: string + /** When generating a new secret, use this value instead of random (e.g. 'flowise' for JWT_ISSUER/JWT_AUDIENCE) */ + defaultValueForNew?: string + /** If env is set to this value, treat as unset (backwards compat for weak defaults) */ + weakDefault?: string +} + +/** + * Resolve an auth secret: env (backwards compat) → AWS Secrets Manager → filesystem (read or generate+write). + * Used by initAuthSecrets() for each of the six auth secrets. + */ +export const getOrCreateStoredSecret = async (options: GetOrCreateStoredSecretOptions): Promise => { + const { envKey, fileName, awsSecretIdSuffix, defaultValueForNew, weakDefault } = options + const envVal = process.env[envKey] + const useEnv = envVal && envVal.trim() !== '' && (weakDefault === undefined || envVal !== weakDefault) + if (useEnv) { + return envVal!.trim() + } + + if (USE_AWS_SECRETS_MANAGER && secretsManagerClient) { + const prefix = process.env.SECRETKEY_AWS_AUTH_PREFIX || 'Flowise' + const secretId = prefix + awsSecretIdSuffix + try { + const command = new GetSecretValueCommand({ SecretId: secretId }) + const response = await secretsManagerClient.send(command) + if (response.SecretString) { + return response.SecretString + } + } catch (error: any) { + if (error.name === 'ResourceNotFoundException') { + const newValue = defaultValueForNew !== undefined ? defaultValueForNew : generateAuthSecret() + const createCommand = new CreateSecretCommand({ + Name: secretId, + SecretString: newValue + }) + await secretsManagerClient.send(createCommand) + return newValue + } + throw error + } + } + + const dir = getAuthSecretsDirectory() + const filePath = path.join(dir, fileName) + try { + return await fs.promises.readFile(filePath, 'utf8') + } catch { + const value = defaultValueForNew !== undefined ? defaultValueForNew : generateAuthSecret() + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }) + } + await fs.promises.writeFile(filePath, value) + return value + } +} + /** * Transform ICredentialBody from req to Credential entity * @param {ICredentialReqBody} body