diff --git a/.env.example b/.env.example index 2b811c79de..a58a37efb6 100644 --- a/.env.example +++ b/.env.example @@ -453,8 +453,8 @@ OPENID_REUSE_TOKENS= OPENID_JWKS_URL_CACHE_ENABLED= OPENID_JWKS_URL_CACHE_TIME= # 600000 ms eq to 10 minutes leave empty to disable caching #Set to true to trigger token exchange flow to acquire access token for the userinfo endpoint. -OPENID_ON_BEHALF_FLOW_FOR_USERINFRO_REQUIRED= -OPENID_ON_BEHALF_FLOW_USERINFRO_SCOPE = "user.read" # example for Scope Needed for Microsoft Graph API +OPENID_ON_BEHALF_FLOW_FOR_USERINFO_REQUIRED= +OPENID_ON_BEHALF_FLOW_USERINFO_SCOPE="user.read" # example for Scope Needed for Microsoft Graph API # Set to true to use the OpenID Connect end session endpoint for logout OPENID_USE_END_SESSION_ENDPOINT= diff --git a/api/app/clients/prompts/createContextHandlers.js b/api/app/clients/prompts/createContextHandlers.js index 57847bea3e..b3ea9164e7 100644 --- a/api/app/clients/prompts/createContextHandlers.js +++ b/api/app/clients/prompts/createContextHandlers.js @@ -1,6 +1,7 @@ const axios = require('axios'); -const { isEnabled } = require('~/server/utils'); -const { logger } = require('~/config'); +const { isEnabled } = require('@librechat/api'); +const { logger } = require('@librechat/data-schemas'); +const { generateShortLivedToken } = require('~/server/services/AuthService'); const footer = `Use the context as your learned knowledge to better answer the user. @@ -18,7 +19,7 @@ function createContextHandlers(req, userMessageContent) { const queryPromises = []; const processedFiles = []; const processedIds = new Set(); - const jwtToken = req.headers.authorization.split(' ')[1]; + const jwtToken = generateShortLivedToken(req.user.id); const useFullContext = isEnabled(process.env.RAG_USE_FULL_CONTEXT); const query = async (file) => { diff --git a/api/app/clients/tools/util/fileSearch.js b/api/app/clients/tools/util/fileSearch.js index 19d3a79edb..050a0fd896 100644 --- a/api/app/clients/tools/util/fileSearch.js +++ b/api/app/clients/tools/util/fileSearch.js @@ -1,9 +1,10 @@ const { z } = require('zod'); const axios = require('axios'); const { tool } = require('@langchain/core/tools'); +const { logger } = require('@librechat/data-schemas'); const { Tools, EToolResources } = require('librechat-data-provider'); +const { generateShortLivedToken } = require('~/server/services/AuthService'); const { getFiles } = require('~/models/File'); -const { logger } = require('~/config'); /** * @@ -59,7 +60,7 @@ const createFileSearchTool = async ({ req, files, entity_id }) => { if (files.length === 0) { return 'No files to search. Instruct the user to add files for the search.'; } - const jwtToken = req.headers.authorization.split(' ')[1]; + const jwtToken = generateShortLivedToken(req.user.id); if (!jwtToken) { return 'There was an error authenticating the file search request.'; } diff --git a/api/server/services/AuthService.js b/api/server/services/AuthService.js index 6061277437..8c7cbf7d92 100644 --- a/api/server/services/AuthService.js +++ b/api/server/services/AuthService.js @@ -1,4 +1,5 @@ const bcrypt = require('bcryptjs'); +const jwt = require('jsonwebtoken'); const { webcrypto } = require('node:crypto'); const { isEnabled } = require('@librechat/api'); const { logger } = require('@librechat/data-schemas'); @@ -499,6 +500,18 @@ const resendVerificationEmail = async (req) => { }; } }; +/** + * Generate a short-lived JWT token + * @param {String} userId - The ID of the user + * @param {String} [expireIn='5m'] - The expiration time for the token (default is 5 minutes) + * @returns {String} - The generated JWT token + */ +const generateShortLivedToken = (userId, expireIn = '5m') => { + return jwt.sign({ id: userId }, process.env.JWT_SECRET, { + expiresIn: expireIn, + algorithm: 'HS256', + }); +}; module.exports = { logoutUser, @@ -506,7 +519,8 @@ module.exports = { registerUser, setAuthTokens, resetPassword, + setOpenIDAuthTokens, requestPasswordReset, resendVerificationEmail, - setOpenIDAuthTokens, + generateShortLivedToken, }; diff --git a/api/server/services/Files/Local/crud.js b/api/server/services/Files/Local/crud.js index 7df528c5e1..455d4e0c4f 100644 --- a/api/server/services/Files/Local/crud.js +++ b/api/server/services/Files/Local/crud.js @@ -1,10 +1,11 @@ const fs = require('fs'); const path = require('path'); const axios = require('axios'); +const { logger } = require('@librechat/data-schemas'); const { EModelEndpoint } = require('librechat-data-provider'); +const { generateShortLivedToken } = require('~/server/services/AuthService'); const { getBufferMetadata } = require('~/server/utils'); const paths = require('~/config/paths'); -const { logger } = require('~/config'); /** * Saves a file to a specified output path with a new filename. @@ -206,7 +207,7 @@ const deleteLocalFile = async (req, file) => { const cleanFilepath = file.filepath.split('?')[0]; if (file.embedded && process.env.RAG_API_URL) { - const jwtToken = req.headers.authorization.split(' ')[1]; + const jwtToken = generateShortLivedToken(req.user.id); axios.delete(`${process.env.RAG_API_URL}/documents`, { headers: { Authorization: `Bearer ${jwtToken}`, diff --git a/api/server/services/Files/VectorDB/crud.js b/api/server/services/Files/VectorDB/crud.js index 1aeabc6c46..d7018f7669 100644 --- a/api/server/services/Files/VectorDB/crud.js +++ b/api/server/services/Files/VectorDB/crud.js @@ -4,6 +4,7 @@ const FormData = require('form-data'); const { logAxiosError } = require('@librechat/api'); const { logger } = require('@librechat/data-schemas'); const { FileSources } = require('librechat-data-provider'); +const { generateShortLivedToken } = require('~/server/services/AuthService'); /** * Deletes a file from the vector database. This function takes a file object, constructs the full path, and @@ -23,7 +24,8 @@ const deleteVectors = async (req, file) => { return; } try { - const jwtToken = req.headers.authorization.split(' ')[1]; + const jwtToken = generateShortLivedToken(req.user.id); + return await axios.delete(`${process.env.RAG_API_URL}/documents`, { headers: { Authorization: `Bearer ${jwtToken}`, @@ -70,7 +72,7 @@ async function uploadVectors({ req, file, file_id, entity_id }) { } try { - const jwtToken = req.headers.authorization.split(' ')[1]; + const jwtToken = generateShortLivedToken(req.user.id); const formData = new FormData(); formData.append('file_id', file_id); formData.append('file', fs.createReadStream(file.path)); diff --git a/api/strategies/openidStrategy.js b/api/strategies/openidStrategy.js index 2449872a9d..63a1aafd5a 100644 --- a/api/strategies/openidStrategy.js +++ b/api/strategies/openidStrategy.js @@ -118,7 +118,7 @@ class CustomOpenIDStrategy extends OpenIDStrategy { */ const exchangeAccessTokenIfNeeded = async (config, accessToken, sub, fromCache = false) => { const tokensCache = getLogStores(CacheKeys.OPENID_EXCHANGED_TOKENS); - const onBehalfFlowRequired = isEnabled(process.env.OPENID_ON_BEHALF_FLOW_FOR_USERINFRO_REQUIRED); + const onBehalfFlowRequired = isEnabled(process.env.OPENID_ON_BEHALF_FLOW_FOR_USERINFO_REQUIRED); if (onBehalfFlowRequired) { if (fromCache) { const cachedToken = await tokensCache.get(sub); @@ -130,7 +130,7 @@ const exchangeAccessTokenIfNeeded = async (config, accessToken, sub, fromCache = config, 'urn:ietf:params:oauth:grant-type:jwt-bearer', { - scope: process.env.OPENID_ON_BEHALF_FLOW_USERINFRO_SCOPE || 'user.read', + scope: process.env.OPENID_ON_BEHALF_FLOW_USERINFO_SCOPE || 'user.read', assertion: accessToken, requested_token_use: 'on_behalf_of', },