mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 17:00:15 +01:00
* ✨feat: OAuth for Actions * WIP: PoC flow state manager * refactor: Add identifier field to token model from action schema * chore: fix potential file type issues * ci: fix type issue with action metadata auth * fix: ensure FlowManagerOptions has a default ttl value * WIP: OAUTH actions * WIP: first pass OAuth Action * fix: standardize identifier usage in OAuth flow handling * fix: update token retrieval to include userId in query and use correct identifier * refacotr: update token retrieval to use userId for OAuth token query * feat: Tool Call Auth styling * fix: streamline token creation and add type field to token schema * refactor: cleanup OAuth flow by encrypting client credentials and ensuring oauth operations only run under condition * refactor: use encrypted credentials in OAuth callback * fix: update Token collection indexes to use expiresAt TTL index and not createdAt legacy index * refactor: enhance Token index cleanup by improving logging and removing redundant index creation logic * refactor: remove unused OAuth login route and related logic for improved clarity * refactor: replace fetch with axios for OAuth token exchange and improve error handling * refactor: better UX after authentication before oauth tool execution * refactor: implement cleanup handlers for FlowStateManager intervals to enhance resource management * refactor: encrypt OAuth tokens before storing and decrypt upon retrieval for enhanced security * refactor: enhance authentication success page with improved styling and countdown feature * refactor: add response_type parameter to OAuth redirect URI for improved compatibility * chore: update translation.json new localizations * chore: remove unused OGDialog import from OGDialogTemplate component * refactor: Actions Auth using new Dialog styling, use same component with Agents/Assistants * refactor: update removeNullishValues function to support removal of empty strings and adjust transform usage in schemas * chore: bump version of librechat-data-provider to 0.7.6991 * refactor: integrate removeNullishValues function to clean metadata before encryption in agent and assistant routes * refactor: update OAuth input fields to use 'password' type for better security * refactor: update localization placeholders for sign-in message to use double curly braces * refactor: add access_type parameter for offline access in createActionTool function * refactor: implement handleOAuthToken function for token management and encryption * feat: refresh token support * refactor: add default expiration for access token and error handling for missing token * feat: localizations for ActionAuth * refactor: set refresh token expiration to null to not expire if expiry never given * fix: prevent crash fromerror within async handleAbortError in AskController, EditController, and AgentController * feat: Action Callback URL * 🌍 i18n: Update translation.json with latest translations * refactor: handle errors in flow state checking to prevent unhandled promise rejections * fix: improve flow state concurrency to prevent multiple token creation calls * refactor: RequestExecutor to use separate axios instance * refactor: improve concurrency flows by keeping completed state until TTL expiry * refactor: increase TTL for flow state management and adjust monitoring interval * ci: mock axios instance creation in actions spec * feat: add Babel and Jest configuration files; implement FlowStateManager tests with concurrency handling * chore: add disableOAuth prop to ActionsAuth (not implemented for Assistants yet) --------- Co-authored-by: Danny Avila <danny@librechat.ai> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
136 lines
4.8 KiB
JavaScript
136 lines
4.8 KiB
JavaScript
const express = require('express');
|
|
const jwt = require('jsonwebtoken');
|
|
const { getAccessToken } = require('~/server/services/TokenService');
|
|
const { logger, getFlowStateManager } = require('~/config');
|
|
const { getLogStores } = require('~/cache');
|
|
|
|
const router = express.Router();
|
|
const JWT_SECRET = process.env.JWT_SECRET;
|
|
|
|
/**
|
|
* Handles the OAuth callback and exchanges the authorization code for tokens.
|
|
*
|
|
* @route GET /actions/:action_id/oauth/callback
|
|
* @param {string} req.params.action_id - The ID of the action.
|
|
* @param {string} req.query.code - The authorization code returned by the provider.
|
|
* @param {string} req.query.state - The state token to verify the authenticity of the request.
|
|
* @returns {void} Sends a success message after updating the action with OAuth tokens.
|
|
*/
|
|
router.get('/:action_id/oauth/callback', async (req, res) => {
|
|
const { action_id } = req.params;
|
|
const { code, state } = req.query;
|
|
|
|
const flowManager = await getFlowStateManager(getLogStores);
|
|
let identifier = action_id;
|
|
try {
|
|
let decodedState;
|
|
try {
|
|
decodedState = jwt.verify(state, JWT_SECRET);
|
|
} catch (err) {
|
|
await flowManager.failFlow(identifier, 'oauth', 'Invalid or expired state parameter');
|
|
return res.status(400).send('Invalid or expired state parameter');
|
|
}
|
|
|
|
if (decodedState.action_id !== action_id) {
|
|
await flowManager.failFlow(identifier, 'oauth', 'Mismatched action ID in state parameter');
|
|
return res.status(400).send('Mismatched action ID in state parameter');
|
|
}
|
|
|
|
if (!decodedState.user) {
|
|
await flowManager.failFlow(identifier, 'oauth', 'Invalid user ID in state parameter');
|
|
return res.status(400).send('Invalid user ID in state parameter');
|
|
}
|
|
identifier = `${decodedState.user}:${action_id}`;
|
|
const flowState = await flowManager.getFlowState(identifier, 'oauth');
|
|
if (!flowState) {
|
|
throw new Error('OAuth flow not found');
|
|
}
|
|
|
|
const tokenData = await getAccessToken({
|
|
code,
|
|
userId: decodedState.user,
|
|
identifier,
|
|
client_url: flowState.metadata.client_url,
|
|
redirect_uri: flowState.metadata.redirect_uri,
|
|
/** Encrypted values */
|
|
encrypted_oauth_client_id: flowState.metadata.encrypted_oauth_client_id,
|
|
encrypted_oauth_client_secret: flowState.metadata.encrypted_oauth_client_secret,
|
|
});
|
|
await flowManager.completeFlow(identifier, 'oauth', tokenData);
|
|
res.send(`
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Authentication Successful</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
<style>
|
|
body {
|
|
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont;
|
|
background-color: rgb(249, 250, 251);
|
|
margin: 0;
|
|
padding: 2rem;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
min-height: 100vh;
|
|
}
|
|
.card {
|
|
background-color: white;
|
|
border-radius: 0.5rem;
|
|
padding: 2rem;
|
|
max-width: 28rem;
|
|
width: 100%;
|
|
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
|
text-align: center;
|
|
}
|
|
.heading {
|
|
color: rgb(17, 24, 39);
|
|
font-size: 1.875rem;
|
|
font-weight: 700;
|
|
margin: 0 0 1rem;
|
|
}
|
|
.description {
|
|
color: rgb(75, 85, 99);
|
|
font-size: 0.875rem;
|
|
margin: 0.5rem 0;
|
|
}
|
|
.countdown {
|
|
color: rgb(99, 102, 241);
|
|
font-weight: 500;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="card">
|
|
<h1 class="heading">Authentication Successful</h1>
|
|
<p class="description">
|
|
Your authentication was successful. This window will close in
|
|
<span class="countdown" id="countdown">3</span> seconds.
|
|
</p>
|
|
</div>
|
|
<script>
|
|
let secondsLeft = 3;
|
|
const countdownElement = document.getElementById('countdown');
|
|
|
|
const countdown = setInterval(() => {
|
|
secondsLeft--;
|
|
countdownElement.textContent = secondsLeft;
|
|
|
|
if (secondsLeft <= 0) {
|
|
clearInterval(countdown);
|
|
window.close();
|
|
}
|
|
}, 1000);
|
|
</script>
|
|
</body>
|
|
</html>
|
|
`);
|
|
} catch (error) {
|
|
logger.error('Error in OAuth callback:', error);
|
|
await flowManager.failFlow(identifier, 'oauth', error);
|
|
res.status(500).send('Authentication failed. Please try again.');
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|