From 1bd874591a7e41133b647bc9eed696bd65432f44 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 11 Jun 2025 22:12:50 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20feat:=20Add=20Basic=20Token=20Ex?= =?UTF-8?q?change=20Method=20for=20Actions=20OAuth=20flow=20(#7844)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Enhanced the OAuth callback and action creation processes to include the `token_exchange_method` parameter. - Updated the `TokenService` to handle different token exchange methods, allowing for either 'default_post' or 'basic_auth_header' approaches. - Improved the handling of access tokens and refresh tokens based on the specified exchange method. --- api/server/routes/actions.js | 1 + api/server/services/ActionService.js | 2 ++ api/server/services/TokenService.js | 47 +++++++++++++++++++++------- 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/api/server/routes/actions.js b/api/server/routes/actions.js index dc474d1a67..242e52e4ae 100644 --- a/api/server/routes/actions.js +++ b/api/server/routes/actions.js @@ -53,6 +53,7 @@ router.get('/:action_id/oauth/callback', async (req, res) => { identifier, client_url: flowState.metadata.client_url, redirect_uri: flowState.metadata.redirect_uri, + token_exchange_method: flowState.metadata.token_exchange_method, /** Encrypted values */ encrypted_oauth_client_id: flowState.metadata.encrypted_oauth_client_id, encrypted_oauth_client_secret: flowState.metadata.encrypted_oauth_client_secret, diff --git a/api/server/services/ActionService.js b/api/server/services/ActionService.js index ed9b0c447d..6656b1f8c7 100644 --- a/api/server/services/ActionService.js +++ b/api/server/services/ActionService.js @@ -210,6 +210,7 @@ async function createActionTool({ userId: userId, client_url: metadata.auth.client_url, redirect_uri: `${process.env.DOMAIN_SERVER}/api/actions/${action_id}/oauth/callback`, + token_exchange_method: metadata.auth.token_exchange_method, /** Encrypted values */ encrypted_oauth_client_id: encrypted.oauth_client_id, encrypted_oauth_client_secret: encrypted.oauth_client_secret, @@ -264,6 +265,7 @@ async function createActionTool({ refresh_token, client_url: metadata.auth.client_url, encrypted_oauth_client_id: encrypted.oauth_client_id, + token_exchange_method: metadata.auth.token_exchange_method, encrypted_oauth_client_secret: encrypted.oauth_client_secret, }); const flowsCache = getLogStores(CacheKeys.FLOWS); diff --git a/api/server/services/TokenService.js b/api/server/services/TokenService.js index 3dd2e79ffa..5c52198f58 100644 --- a/api/server/services/TokenService.js +++ b/api/server/services/TokenService.js @@ -1,4 +1,5 @@ const axios = require('axios'); +const { TokenExchangeMethodEnum } = require('librechat-data-provider'); const { handleOAuthToken } = require('~/models/Token'); const { decryptV2 } = require('~/server/utils/crypto'); const { logAxiosError } = require('~/utils'); @@ -49,6 +50,7 @@ async function processAccessTokens(tokenData, { userId, identifier }) { * @param {string} fields.client_url - The URL of the OAuth provider. * @param {string} fields.identifier - The identifier for the token. * @param {string} fields.refresh_token - The refresh token to use. + * @param {string} fields.token_exchange_method - The token exchange method ('default_post' or 'basic_auth_header'). * @param {string} fields.encrypted_oauth_client_id - The client ID for the OAuth provider. * @param {string} fields.encrypted_oauth_client_secret - The client secret for the OAuth provider. * @returns {Promise<{ @@ -63,26 +65,36 @@ const refreshAccessToken = async ({ client_url, identifier, refresh_token, + token_exchange_method, encrypted_oauth_client_id, encrypted_oauth_client_secret, }) => { try { const oauth_client_id = await decryptV2(encrypted_oauth_client_id); const oauth_client_secret = await decryptV2(encrypted_oauth_client_secret); + + const headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json', + }; + const params = new URLSearchParams({ - client_id: oauth_client_id, - client_secret: oauth_client_secret, grant_type: 'refresh_token', refresh_token, }); + if (token_exchange_method === TokenExchangeMethodEnum.BasicAuthHeader) { + const basicAuth = Buffer.from(`${oauth_client_id}:${oauth_client_secret}`).toString('base64'); + headers['Authorization'] = `Basic ${basicAuth}`; + } else { + params.append('client_id', oauth_client_id); + params.append('client_secret', oauth_client_secret); + } + const response = await axios({ method: 'POST', url: client_url, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - Accept: 'application/json', - }, + headers, data: params.toString(), }); await processAccessTokens(response.data, { @@ -110,6 +122,7 @@ const refreshAccessToken = async ({ * @param {string} fields.identifier - The identifier for the token. * @param {string} fields.client_url - The URL of the OAuth provider. * @param {string} fields.redirect_uri - The redirect URI for the OAuth provider. + * @param {string} fields.token_exchange_method - The token exchange method ('default_post' or 'basic_auth_header'). * @param {string} fields.encrypted_oauth_client_id - The client ID for the OAuth provider. * @param {string} fields.encrypted_oauth_client_secret - The client secret for the OAuth provider. * @returns {Promise<{ @@ -125,27 +138,37 @@ const getAccessToken = async ({ identifier, client_url, redirect_uri, + token_exchange_method, encrypted_oauth_client_id, encrypted_oauth_client_secret, }) => { const oauth_client_id = await decryptV2(encrypted_oauth_client_id); const oauth_client_secret = await decryptV2(encrypted_oauth_client_secret); + + const headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json', + }; + const params = new URLSearchParams({ code, - client_id: oauth_client_id, - client_secret: oauth_client_secret, grant_type: 'authorization_code', redirect_uri, }); + if (token_exchange_method === TokenExchangeMethodEnum.BasicAuthHeader) { + const basicAuth = Buffer.from(`${oauth_client_id}:${oauth_client_secret}`).toString('base64'); + headers['Authorization'] = `Basic ${basicAuth}`; + } else { + params.append('client_id', oauth_client_id); + params.append('client_secret', oauth_client_secret); + } + try { const response = await axios({ method: 'POST', url: client_url, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - Accept: 'application/json', - }, + headers, data: params.toString(), });