mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 08:20:14 +01:00
* feat: Add OpenID Connect federated provider token support
Implements support for passing federated provider tokens (Cognito, Azure AD, Auth0)
as variables in LibreChat's librechat.yaml configuration for both custom endpoints
and MCP servers.
Features:
- New LIBRECHAT_OPENID_* template variables for federated provider tokens
- JWT claims parsing from ID tokens without verification (for claim extraction)
- Token validation with expiration checking
- Support for multiple token storage locations (federatedTokens, openidTokens)
- Integration with existing template variable system
- Comprehensive test suite with Cognito-specific scenarios
- Provider-agnostic design supporting Cognito, Azure AD, Auth0, etc.
Security:
- Server-side only token processing
- Automatic token expiration validation
- Graceful fallbacks for missing/invalid tokens
- No client-side token exposure
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Add federated token propagation to OIDC authentication strategies
Adds federatedTokens object to user during authentication to enable
federated provider token template variables in LibreChat configuration.
Changes:
- OpenID JWT Strategy: Extract raw JWT from Authorization header and
attach as federatedTokens.access_token to enable {{LIBRECHAT_OPENID_TOKEN}}
placeholder resolution
- OpenID Strategy: Attach tokenset tokens as federatedTokens object to
standardize token access across both authentication strategies
This enables proper token propagation for custom endpoints and MCP
servers that require federated provider tokens for authorization.
Resolves missing token issue reported by @ramden in PR #9931
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Denis Ramic <denis.ramic@nfon.com>
Co-Authored-By: Claude <noreply@anthropic.com>
* test: Add federatedTokens validation tests for OIDC strategies
Adds comprehensive test coverage for the federated token propagation
feature implemented in the authentication strategies.
Tests added:
- Verify federatedTokens object is attached to user with correct structure
(access_token, refresh_token, expires_at)
- Verify both tokenset and federatedTokens are present in user object
- Ensure tokens from OIDC provider are correctly propagated
Also fixes existing test suite by adding missing mocks:
- isEmailDomainAllowed function mock
- findOpenIDUser function mock
These tests validate the fix from commit 5874ba29f that enables
{{LIBRECHAT_OPENID_TOKEN}} template variable functionality.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: Remove implementation documentation file
The PR description already contains all necessary implementation details.
This documentation file is redundant and was requested to be removed.
* fix: skip s256 check
* fix(openid): handle missing refresh token in Cognito token refresh response
When OPENID_REUSE_TOKENS=true, the token refresh flow was failing because
Cognito (and most OAuth providers) don't return a new refresh token in the
refresh grant response - they only return new access and ID tokens.
Changes:
- Modified setOpenIDAuthTokens() to accept optional existingRefreshToken parameter
- Updated validation to only require access_token (refresh_token now optional)
- Added logic to reuse existing refresh token when not provided in tokenset
- Updated refreshController to pass original refresh token as fallback
- Added comments explaining standard OAuth 2.0 refresh token behavior
This fixes the "Token is not present. User is not authenticated." error that
occurred during silent token refresh with Cognito as the OpenID provider.
Fixes: Authentication loop with OPENID_REUSE_TOKENS=true and AWS Cognito
* fix(openid): extract refresh token from cookies for template variable replacement
When OPENID_REUSE_TOKENS=true, the openIdJwtStrategy populates user.federatedTokens
to enable template variable replacement (e.g., {{LIBRECHAT_OPENID_ACCESS_TOKEN}}).
However, the refresh_token field was incorrectly sourced from payload.refresh_token,
which is always undefined because:
1. JWTs don't contain refresh tokens in their payload
2. The JWT itself IS the access token
3. Refresh tokens are separate opaque tokens stored in HTTP-only cookies
This caused extractOpenIDTokenInfo() to receive incomplete federatedTokens,
resulting in template variables remaining unreplaced in headers.
**Root Cause:**
- Line 90: `refresh_token: payload.refresh_token` (always undefined)
- JWTs only contain access token data in their claims
- Refresh tokens are separate, stored securely in cookies
**Solution:**
- Import `cookie` module to parse cookies from request
- Extract refresh token from `refreshToken` cookie
- Populate federatedTokens with both access token (JWT) and refresh token (from cookie)
**Impact:**
- Template variables like {{LIBRECHAT_OPENID_ACCESS_TOKEN}} now work correctly
- Headers in librechat.yaml are properly replaced with actual tokens
- MCP server authentication with federated tokens now functional
**Technical Details:**
- passReqToCallback=true in JWT strategy provides req object access
- Refresh token extracted via cookies.parse(req.headers.cookie).refreshToken
- Falls back gracefully if cookie header or refreshToken is missing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: re-resolve headers on each request to pick up fresh federatedTokens
- OpenAIClient now re-resolves headers in chatCompletion() before each API call
- This ensures template variables like {{LIBRECHAT_OPENID_TOKEN}} are replaced
with actual token values from req.user.federatedTokens
- initialize.js now stores original template headers instead of pre-resolved ones
- Fixes template variable replacement when OPENID_REUSE_TOKENS=true
The issue was that headers were only resolved once during client initialization,
before openIdJwtStrategy had populated user.federatedTokens. Now headers are
re-resolved on every request with the current user's fresh tokens.
* debug: add logging to track header resolution in OpenAIClient
* debug: log tokenset structure after refresh to diagnose missing access_token
* fix: set federatedTokens on user object after OAuth refresh
- After successful OAuth token refresh, the user object was not being
updated with federatedTokens
- This caused template variable resolution to fail on subsequent requests
- Now sets user.federatedTokens with access_token, id_token, refresh_token
and expires_at from the refreshed tokenset
- Fixes template variables like {{LIBRECHAT_OPENID_TOKEN}} not being
replaced after token refresh
- Related to PR #9931 (OpenID federated token support)
* fix(openid): pass user object through agent chain for template variable resolution
Root cause: buildAgentContext in agents/run.ts called resolveHeaders without
the user parameter, preventing OpenID federated token template variables from
being resolved in agent runtime parameters.
Changes:
- packages/api/src/agents/run.ts: Add user parameter to createRun signature
- packages/api/src/agents/run.ts: Pass user to resolveHeaders in buildAgentContext
- api/server/controllers/agents/client.js: Pass user when calling createRun
- api/server/services/Endpoints/bedrock/options.js: Add resolveHeaders call with debug logging
- api/server/services/Endpoints/custom/initialize.js: Add debug logging
- packages/api/src/utils/env.ts: Add comprehensive debug logging and stack traces
- packages/api/src/utils/oidc.ts: Fix eslint errors (unused type, explicit any)
This ensures template variables like {{LIBRECHAT_OPENID_TOKEN}} and
{{LIBRECHAT_USER_OPENIDID}} are properly resolved in both custom endpoint
headers and Bedrock AgentCore runtime parameters.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: remove debug logging from OpenID token template feature
Removed excessive debug logging that was added during development to make
the PR more suitable for upstream review:
- Removed 7 debug statements from OpenAIClient.js
- Removed all console.log statements from packages/api/src/utils/env.ts
- Removed debug logging from bedrock/options.js
- Removed debug logging from custom/initialize.js
- Removed debug statement from AuthController.js
This reduces the changeset by ~50 lines while maintaining full functionality
of the OpenID federated token template variable feature.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* test(openid): add comprehensive unit tests for template variable substitution
- Add 34 unit tests for OIDC token utilities (oidc.spec.ts)
- Test coverage for token extraction, validation, and placeholder processing
- Integration tests for full OpenID token flow
- All tests pass with comprehensive edge case coverage
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
* test: fix OpenID federated tokens test failures
- Add serverMetadata() mock to openid-client mock configuration
* Fixes TypeError in openIdJwtStrategy.js where serverMetadata() was being called
* Mock now returns jwks_uri and end_session_endpoint as expected by the code
- Update outdated initialize.spec.js test
* Remove test expecting resolveHeaders call during initialization
* Header resolution was refactored to be deferred until LLM request time
* Update test to verify options are returned correctly with useLegacyContent flag
Fixes #9931 CI failures for backend unit tests
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: fix package-lock.json conflict
* chore: sync package-log with upstream
* chore: cleanup
* fix: use createSafeUser
* fix: fix createSafeUser signature
* chore: remove comments
* chore: purge comments
* fix: update Jest testPathPattern to testPathPatterns for Jest 30+ compatibility
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Denis Ramic <denis.ramic@nfon.com>
Co-authored-by: kristjanaapro <kristjana@apro.is>
chore: import order and add back JSDoc for OpenID JWT callback
547 lines
16 KiB
JavaScript
547 lines
16 KiB
JavaScript
const bcrypt = require('bcryptjs');
|
|
const jwt = require('jsonwebtoken');
|
|
const { webcrypto } = require('node:crypto');
|
|
const { logger } = require('@librechat/data-schemas');
|
|
const { isEnabled, checkEmailConfig, isEmailDomainAllowed } = require('@librechat/api');
|
|
const { ErrorTypes, SystemRoles, errorsToString } = require('librechat-data-provider');
|
|
const {
|
|
findUser,
|
|
findToken,
|
|
createUser,
|
|
updateUser,
|
|
countUsers,
|
|
getUserById,
|
|
findSession,
|
|
createToken,
|
|
deleteTokens,
|
|
deleteSession,
|
|
createSession,
|
|
generateToken,
|
|
deleteUserById,
|
|
generateRefreshToken,
|
|
} = require('~/models');
|
|
const { registerSchema } = require('~/strategies/validators');
|
|
const { getAppConfig } = require('~/server/services/Config');
|
|
const { sendEmail } = require('~/server/utils');
|
|
|
|
const domains = {
|
|
client: process.env.DOMAIN_CLIENT,
|
|
server: process.env.DOMAIN_SERVER,
|
|
};
|
|
|
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
const genericVerificationMessage = 'Please check your email to verify your email address.';
|
|
|
|
/**
|
|
* Logout user
|
|
*
|
|
* @param {ServerRequest} req
|
|
* @param {string} refreshToken
|
|
* @returns
|
|
*/
|
|
const logoutUser = async (req, refreshToken) => {
|
|
try {
|
|
const userId = req.user._id;
|
|
const session = await findSession({ userId: userId, refreshToken });
|
|
|
|
if (session) {
|
|
try {
|
|
await deleteSession({ sessionId: session._id });
|
|
} catch (deleteErr) {
|
|
logger.error('[logoutUser] Failed to delete session.', deleteErr);
|
|
return { status: 500, message: 'Failed to delete session.' };
|
|
}
|
|
}
|
|
|
|
try {
|
|
req.session.destroy();
|
|
} catch (destroyErr) {
|
|
logger.debug('[logoutUser] Failed to destroy session.', destroyErr);
|
|
}
|
|
|
|
return { status: 200, message: 'Logout successful' };
|
|
} catch (err) {
|
|
return { status: 500, message: err.message };
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Creates Token and corresponding Hash for verification
|
|
* @returns {[string, string]}
|
|
*/
|
|
const createTokenHash = () => {
|
|
const token = Buffer.from(webcrypto.getRandomValues(new Uint8Array(32))).toString('hex');
|
|
const hash = bcrypt.hashSync(token, 10);
|
|
return [token, hash];
|
|
};
|
|
|
|
/**
|
|
* Send Verification Email
|
|
* @param {Partial<IUser>} user
|
|
* @returns {Promise<void>}
|
|
*/
|
|
const sendVerificationEmail = async (user) => {
|
|
const [verifyToken, hash] = createTokenHash();
|
|
|
|
const verificationLink = `${
|
|
domains.client
|
|
}/verify?token=${verifyToken}&email=${encodeURIComponent(user.email)}`;
|
|
await sendEmail({
|
|
email: user.email,
|
|
subject: 'Verify your email',
|
|
payload: {
|
|
appName: process.env.APP_TITLE || 'LibreChat',
|
|
name: user.name || user.username || user.email,
|
|
verificationLink: verificationLink,
|
|
year: new Date().getFullYear(),
|
|
},
|
|
template: 'verifyEmail.handlebars',
|
|
});
|
|
|
|
await createToken({
|
|
userId: user._id,
|
|
email: user.email,
|
|
token: hash,
|
|
createdAt: Date.now(),
|
|
expiresIn: 900,
|
|
});
|
|
|
|
logger.info(`[sendVerificationEmail] Verification link issued. [Email: ${user.email}]`);
|
|
};
|
|
|
|
/**
|
|
* Verify Email
|
|
* @param {ServerRequest} req
|
|
*/
|
|
const verifyEmail = async (req) => {
|
|
const { email, token } = req.body;
|
|
const decodedEmail = decodeURIComponent(email);
|
|
|
|
const user = await findUser({ email: decodedEmail }, 'email _id emailVerified');
|
|
|
|
if (!user) {
|
|
logger.warn(`[verifyEmail] [User not found] [Email: ${decodedEmail}]`);
|
|
return new Error('User not found');
|
|
}
|
|
|
|
if (user.emailVerified) {
|
|
logger.info(`[verifyEmail] Email already verified [Email: ${decodedEmail}]`);
|
|
return { message: 'Email already verified', status: 'success' };
|
|
}
|
|
|
|
let emailVerificationData = await findToken({ email: decodedEmail }, { sort: { createdAt: -1 } });
|
|
|
|
if (!emailVerificationData) {
|
|
logger.warn(`[verifyEmail] [No email verification data found] [Email: ${decodedEmail}]`);
|
|
return new Error('Invalid or expired password reset token');
|
|
}
|
|
|
|
const isValid = bcrypt.compareSync(token, emailVerificationData.token);
|
|
|
|
if (!isValid) {
|
|
logger.warn(
|
|
`[verifyEmail] [Invalid or expired email verification token] [Email: ${decodedEmail}]`,
|
|
);
|
|
return new Error('Invalid or expired email verification token');
|
|
}
|
|
|
|
const updatedUser = await updateUser(emailVerificationData.userId, { emailVerified: true });
|
|
|
|
if (!updatedUser) {
|
|
logger.warn(`[verifyEmail] [User update failed] [Email: ${decodedEmail}]`);
|
|
return new Error('Failed to update user verification status');
|
|
}
|
|
|
|
await deleteTokens({ token: emailVerificationData.token });
|
|
logger.info(`[verifyEmail] Email verification successful [Email: ${decodedEmail}]`);
|
|
return { message: 'Email verification was successful', status: 'success' };
|
|
};
|
|
|
|
/**
|
|
* Register a new user.
|
|
* @param {IUser} user <email, password, name, username>
|
|
* @param {Partial<IUser>} [additionalData={}]
|
|
* @returns {Promise<{status: number, message: string, user?: IUser}>}
|
|
*/
|
|
const registerUser = async (user, additionalData = {}) => {
|
|
const { error } = registerSchema.safeParse(user);
|
|
if (error) {
|
|
const errorMessage = errorsToString(error.errors);
|
|
logger.info(
|
|
'Route: register - Validation Error',
|
|
{ name: 'Request params:', value: user },
|
|
{ name: 'Validation error:', value: errorMessage },
|
|
);
|
|
|
|
return { status: 404, message: errorMessage };
|
|
}
|
|
|
|
const { email, password, name, username, provider } = user;
|
|
|
|
let newUserId;
|
|
try {
|
|
const appConfig = await getAppConfig();
|
|
if (!isEmailDomainAllowed(email, appConfig?.registration?.allowedDomains)) {
|
|
const errorMessage =
|
|
'The email address provided cannot be used. Please use a different email address.';
|
|
logger.error(`[registerUser] [Registration not allowed] [Email: ${user.email}]`);
|
|
return { status: 403, message: errorMessage };
|
|
}
|
|
|
|
const existingUser = await findUser({ email }, 'email _id');
|
|
|
|
if (existingUser) {
|
|
logger.info(
|
|
'Register User - Email in use',
|
|
{ name: 'Request params:', value: user },
|
|
{ name: 'Existing user:', value: existingUser },
|
|
);
|
|
|
|
// Sleep for 1 second
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
return { status: 200, message: genericVerificationMessage };
|
|
}
|
|
|
|
//determine if this is the first registered user (not counting anonymous_user)
|
|
const isFirstRegisteredUser = (await countUsers()) === 0;
|
|
|
|
const salt = bcrypt.genSaltSync(10);
|
|
const newUserData = {
|
|
provider: provider ?? 'local',
|
|
email,
|
|
username,
|
|
name,
|
|
avatar: null,
|
|
role: isFirstRegisteredUser ? SystemRoles.ADMIN : SystemRoles.USER,
|
|
password: bcrypt.hashSync(password, salt),
|
|
...additionalData,
|
|
};
|
|
|
|
const emailEnabled = checkEmailConfig();
|
|
const disableTTL = isEnabled(process.env.ALLOW_UNVERIFIED_EMAIL_LOGIN);
|
|
|
|
const newUser = await createUser(newUserData, appConfig.balance, disableTTL, true);
|
|
newUserId = newUser._id;
|
|
if (emailEnabled && !newUser.emailVerified) {
|
|
await sendVerificationEmail({
|
|
_id: newUserId,
|
|
email,
|
|
name,
|
|
});
|
|
} else {
|
|
await updateUser(newUserId, { emailVerified: true });
|
|
}
|
|
|
|
return { status: 200, message: genericVerificationMessage };
|
|
} catch (err) {
|
|
logger.error('[registerUser] Error in registering user:', err);
|
|
if (newUserId) {
|
|
const result = await deleteUserById(newUserId);
|
|
logger.warn(
|
|
`[registerUser] [Email: ${email}] [Temporary User deleted: ${JSON.stringify(result)}]`,
|
|
);
|
|
}
|
|
return { status: 500, message: 'Something went wrong' };
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Request password reset
|
|
* @param {ServerRequest} req
|
|
*/
|
|
const requestPasswordReset = async (req) => {
|
|
const { email } = req.body;
|
|
const appConfig = await getAppConfig();
|
|
if (!isEmailDomainAllowed(email, appConfig?.registration?.allowedDomains)) {
|
|
const error = new Error(ErrorTypes.AUTH_FAILED);
|
|
error.code = ErrorTypes.AUTH_FAILED;
|
|
error.message = 'Email domain not allowed';
|
|
return error;
|
|
}
|
|
const user = await findUser({ email }, 'email _id');
|
|
const emailEnabled = checkEmailConfig();
|
|
|
|
logger.warn(`[requestPasswordReset] [Password reset request initiated] [Email: ${email}]`);
|
|
|
|
if (!user) {
|
|
logger.warn(`[requestPasswordReset] [No user found] [Email: ${email}] [IP: ${req.ip}]`);
|
|
return {
|
|
message: 'If an account with that email exists, a password reset link has been sent to it.',
|
|
};
|
|
}
|
|
|
|
await deleteTokens({ userId: user._id });
|
|
|
|
const [resetToken, hash] = createTokenHash();
|
|
|
|
await createToken({
|
|
userId: user._id,
|
|
token: hash,
|
|
createdAt: Date.now(),
|
|
expiresIn: 900,
|
|
});
|
|
|
|
const link = `${domains.client}/reset-password?token=${resetToken}&userId=${user._id}`;
|
|
|
|
if (emailEnabled) {
|
|
await sendEmail({
|
|
email: user.email,
|
|
subject: 'Password Reset Request',
|
|
payload: {
|
|
appName: process.env.APP_TITLE || 'LibreChat',
|
|
name: user.name || user.username || user.email,
|
|
link: link,
|
|
year: new Date().getFullYear(),
|
|
},
|
|
template: 'requestPasswordReset.handlebars',
|
|
});
|
|
logger.info(
|
|
`[requestPasswordReset] Link emailed. [Email: ${email}] [ID: ${user._id}] [IP: ${req.ip}]`,
|
|
);
|
|
} else {
|
|
logger.info(
|
|
`[requestPasswordReset] Link issued. [Email: ${email}] [ID: ${user._id}] [IP: ${req.ip}]`,
|
|
);
|
|
return { link };
|
|
}
|
|
|
|
return {
|
|
message: 'If an account with that email exists, a password reset link has been sent to it.',
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Reset Password
|
|
*
|
|
* @param {*} userId
|
|
* @param {String} token
|
|
* @param {String} password
|
|
* @returns
|
|
*/
|
|
const resetPassword = async (userId, token, password) => {
|
|
let passwordResetToken = await findToken(
|
|
{
|
|
userId,
|
|
},
|
|
{ sort: { createdAt: -1 } },
|
|
);
|
|
|
|
if (!passwordResetToken) {
|
|
return new Error('Invalid or expired password reset token');
|
|
}
|
|
|
|
const isValid = bcrypt.compareSync(token, passwordResetToken.token);
|
|
|
|
if (!isValid) {
|
|
return new Error('Invalid or expired password reset token');
|
|
}
|
|
|
|
const hash = bcrypt.hashSync(password, 10);
|
|
const user = await updateUser(userId, { password: hash });
|
|
|
|
if (checkEmailConfig()) {
|
|
await sendEmail({
|
|
email: user.email,
|
|
subject: 'Password Reset Successfully',
|
|
payload: {
|
|
appName: process.env.APP_TITLE || 'LibreChat',
|
|
name: user.name || user.username || user.email,
|
|
year: new Date().getFullYear(),
|
|
},
|
|
template: 'passwordReset.handlebars',
|
|
});
|
|
}
|
|
|
|
await deleteTokens({ token: passwordResetToken.token });
|
|
logger.info(`[resetPassword] Password reset successful. [Email: ${user.email}]`);
|
|
return { message: 'Password reset was successful' };
|
|
};
|
|
|
|
/**
|
|
* Set Auth Tokens
|
|
* @param {String | ObjectId} userId
|
|
* @param {ServerResponse} res
|
|
* @param {ISession | null} [session=null]
|
|
* @returns
|
|
*/
|
|
const setAuthTokens = async (userId, res, _session = null) => {
|
|
try {
|
|
let session = _session;
|
|
let refreshToken;
|
|
let refreshTokenExpires;
|
|
|
|
if (session && session._id && session.expiration != null) {
|
|
refreshTokenExpires = session.expiration.getTime();
|
|
refreshToken = await generateRefreshToken(session);
|
|
} else {
|
|
const result = await createSession(userId);
|
|
session = result.session;
|
|
refreshToken = result.refreshToken;
|
|
refreshTokenExpires = session.expiration.getTime();
|
|
}
|
|
|
|
const user = await getUserById(userId);
|
|
const token = await generateToken(user);
|
|
|
|
res.cookie('refreshToken', refreshToken, {
|
|
expires: new Date(refreshTokenExpires),
|
|
httpOnly: true,
|
|
secure: isProduction,
|
|
sameSite: 'strict',
|
|
});
|
|
res.cookie('token_provider', 'librechat', {
|
|
expires: new Date(refreshTokenExpires),
|
|
httpOnly: true,
|
|
secure: isProduction,
|
|
sameSite: 'strict',
|
|
});
|
|
return token;
|
|
} catch (error) {
|
|
logger.error('[setAuthTokens] Error in setting authentication tokens:', error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @function setOpenIDAuthTokens
|
|
* Set OpenID Authentication Tokens
|
|
* //type tokenset from openid-client
|
|
* @param {import('openid-client').TokenEndpointResponse & import('openid-client').TokenEndpointResponseHelpers} tokenset
|
|
* - The tokenset object containing access and refresh tokens
|
|
* @param {Object} res - response object
|
|
* @param {string} [userId] - Optional MongoDB user ID for image path validation
|
|
* @returns {String} - access token
|
|
*/
|
|
const setOpenIDAuthTokens = (tokenset, res, userId, existingRefreshToken) => {
|
|
try {
|
|
if (!tokenset) {
|
|
logger.error('[setOpenIDAuthTokens] No tokenset found in request');
|
|
return;
|
|
}
|
|
const { REFRESH_TOKEN_EXPIRY } = process.env ?? {};
|
|
const expiryInMilliseconds = REFRESH_TOKEN_EXPIRY
|
|
? eval(REFRESH_TOKEN_EXPIRY)
|
|
: 1000 * 60 * 60 * 24 * 7; // 7 days default
|
|
const expirationDate = new Date(Date.now() + expiryInMilliseconds);
|
|
if (tokenset == null) {
|
|
logger.error('[setOpenIDAuthTokens] No tokenset found in request');
|
|
return;
|
|
}
|
|
if (!tokenset.access_token) {
|
|
logger.error('[setOpenIDAuthTokens] No access token found in tokenset');
|
|
return;
|
|
}
|
|
|
|
const refreshToken = tokenset.refresh_token || existingRefreshToken;
|
|
|
|
if (!refreshToken) {
|
|
logger.error('[setOpenIDAuthTokens] No refresh token available');
|
|
return;
|
|
}
|
|
|
|
res.cookie('refreshToken', refreshToken, {
|
|
expires: expirationDate,
|
|
httpOnly: true,
|
|
secure: isProduction,
|
|
sameSite: 'strict',
|
|
});
|
|
res.cookie('openid_access_token', tokenset.access_token, {
|
|
expires: expirationDate,
|
|
httpOnly: true,
|
|
secure: isProduction,
|
|
sameSite: 'strict',
|
|
});
|
|
res.cookie('token_provider', 'openid', {
|
|
expires: expirationDate,
|
|
httpOnly: true,
|
|
secure: isProduction,
|
|
sameSite: 'strict',
|
|
});
|
|
if (userId && isEnabled(process.env.OPENID_REUSE_TOKENS)) {
|
|
/** JWT-signed user ID cookie for image path validation when OPENID_REUSE_TOKENS is enabled */
|
|
const signedUserId = jwt.sign({ id: userId }, process.env.JWT_REFRESH_SECRET, {
|
|
expiresIn: expiryInMilliseconds / 1000,
|
|
});
|
|
res.cookie('openid_user_id', signedUserId, {
|
|
expires: expirationDate,
|
|
httpOnly: true,
|
|
secure: isProduction,
|
|
sameSite: 'strict',
|
|
});
|
|
}
|
|
return tokenset.access_token;
|
|
} catch (error) {
|
|
logger.error('[setOpenIDAuthTokens] Error in setting authentication tokens:', error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Resend Verification Email
|
|
* @param {Object} req
|
|
* @param {Object} req.body
|
|
* @param {String} req.body.email
|
|
* @returns {Promise<{status: number, message: string}>}
|
|
*/
|
|
const resendVerificationEmail = async (req) => {
|
|
try {
|
|
const { email } = req.body;
|
|
await deleteTokens({ email });
|
|
const user = await findUser({ email }, 'email _id name');
|
|
|
|
if (!user) {
|
|
logger.warn(`[resendVerificationEmail] [No user found] [Email: ${email}]`);
|
|
return { status: 200, message: genericVerificationMessage };
|
|
}
|
|
|
|
const [verifyToken, hash] = createTokenHash();
|
|
|
|
const verificationLink = `${
|
|
domains.client
|
|
}/verify?token=${verifyToken}&email=${encodeURIComponent(user.email)}`;
|
|
|
|
await sendEmail({
|
|
email: user.email,
|
|
subject: 'Verify your email',
|
|
payload: {
|
|
appName: process.env.APP_TITLE || 'LibreChat',
|
|
name: user.name || user.username || user.email,
|
|
verificationLink: verificationLink,
|
|
year: new Date().getFullYear(),
|
|
},
|
|
template: 'verifyEmail.handlebars',
|
|
});
|
|
|
|
await createToken({
|
|
userId: user._id,
|
|
email: user.email,
|
|
token: hash,
|
|
createdAt: Date.now(),
|
|
expiresIn: 900,
|
|
});
|
|
|
|
logger.info(`[resendVerificationEmail] Verification link issued. [Email: ${user.email}]`);
|
|
|
|
return {
|
|
status: 200,
|
|
message: genericVerificationMessage,
|
|
};
|
|
} catch (error) {
|
|
logger.error(`[resendVerificationEmail] Error resending verification email: ${error.message}`);
|
|
return {
|
|
status: 500,
|
|
message: 'Something went wrong.',
|
|
};
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
logoutUser,
|
|
verifyEmail,
|
|
registerUser,
|
|
setAuthTokens,
|
|
resetPassword,
|
|
setOpenIDAuthTokens,
|
|
requestPasswordReset,
|
|
resendVerificationEmail,
|
|
};
|