mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
* chore: Update @modelcontextprotocol/sdk to version 1.12.3 in package.json and package-lock.json - Bump version of @modelcontextprotocol/sdk to 1.12.3 to incorporate recent updates. - Update dependencies for ajv and cross-spawn to their latest versions. - Add ajv as a new dependency in the sdk module. - Include json-schema-traverse as a new dependency in the sdk module. * feat: @librechat/auth * feat: Add crypto module exports to auth package - Introduced a new crypto module by creating index.ts in the crypto directory. - Updated the main index.ts of the auth package to export from the new crypto module. * feat: Update package dependencies and build scripts for auth package - Added @librechat/auth as a dependency in package.json and package-lock.json. - Updated build scripts to include the auth package in both frontend and bun build processes. - Removed unused mongoose and openid-client dependencies from package-lock.json for cleaner dependency management. * refactor: Migrate crypto utility functions to @librechat/auth - Replaced local crypto utility imports with the new @librechat/auth package across multiple files. - Removed the obsolete crypto.js file and its exports. - Updated relevant services and models to utilize the new encryption and decryption methods from @librechat/auth. * feat: Enhance OAuth token handling and update dependencies in auth package * chore: Remove Token model and TokenService due to restructuring of OAuth handling - Deleted the Token.js model and TokenService.js, which were responsible for managing OAuth tokens. - This change is part of a broader refactor to streamline OAuth token management and improve code organization. * refactor: imports from '@librechat/auth' to '@librechat/api' and add OAuth token handling functionality * refactor: Simplify logger usage in MCP and FlowStateManager classes * chore: fix imports * feat: Add OAuth configuration schema to MCP with token exchange method support * feat: FIRST PASS Implement MCP OAuth flow with token management and error handling - Added a new route for handling OAuth callbacks and token retrieval. - Integrated OAuth token storage and retrieval mechanisms. - Enhanced MCP connection to support automatic OAuth flow initiation on 401 errors. - Implemented dynamic client registration and metadata discovery for OAuth. - Updated MCPManager to manage OAuth tokens and handle authentication requirements. - Introduced comprehensive logging for OAuth processes and error handling. * refactor: Update MCPConnection and MCPManager to utilize new URL handling - Added a `url` property to MCPConnection for better URL management. - Refactored MCPManager to use the new `url` property instead of a deprecated method for OAuth handling. - Changed logging from info to debug level for flow manager and token methods initialization. - Improved comments for clarity on existing tokens and OAuth event listener setup. * refactor: Improve connection timeout error messages in MCPConnection and MCPManager and use initTimeout for connection - Updated the connection timeout error messages to include the duration of the timeout. - Introduced a configurable `connectTimeout` variable in both MCPConnection and MCPManager for better flexibility. * chore: cleanup MCP OAuth Token exchange handling; fix: erroneous use of flowsCache and remove verbose logs * refactor: Update MCPManager and MCPTokenStorage to use TokenMethods for token management - Removed direct token storage handling in MCPManager and replaced it with TokenMethods for better abstraction. - Refactored MCPTokenStorage methods to accept parameters for token operations, enhancing flexibility and readability. - Improved logging messages related to token persistence and retrieval processes. * refactor: Update MCP OAuth handling to use static methods and improve flow management - Refactored MCPOAuthHandler to utilize static methods for initiating and completing OAuth flows, enhancing clarity and reducing instance dependencies. - Updated MCPManager to pass flowManager explicitly to OAuth handling methods, improving flexibility in flow state management. - Enhanced comments and logging for better understanding of OAuth processes and flow state retrieval. * refactor: Integrate token methods into createMCPTool for enhanced token management * refactor: Change logging from info to debug level in MCPOAuthHandler for improved log management * chore: clean up logging * feat: first pass, auth URL from MCP OAuth flow * chore: Improve logging format for OAuth authentication URL display * chore: cleanup mcp manager comments * feat: add connection reconnection logic in MCPManager * refactor: reorganize token storage handling in MCP - Moved token storage logic from MCPManager to a new MCPTokenStorage class for better separation of concerns. - Updated imports to reflect the new token storage structure. - Enhanced methods for storing, retrieving, updating, and deleting OAuth tokens, improving overall token management. * chore: update comment for SYSTEM_USER_ID in MCPManager for clarity * feat: implement refresh token functionality in MCP - Added refresh token handling in MCPManager to support token renewal for both app-level and user-specific connections. - Introduced a refreshTokens function to facilitate token refresh logic. - Enhanced MCPTokenStorage to manage client information and refresh token processes. - Updated logging for better traceability during token operations. * chore: cleanup @librechat/auth * feat: implement MCP server initialization in a separate service - Added a new service to handle the initialization of MCP servers, improving code organization and readability. - Refactored the server startup logic to utilize the new initializeMCP function. - Removed redundant MCP initialization code from the main server file. * fix: don't log auth url for user connections * feat: enhance OAuth flow with success and error handling components - Updated OAuth callback routes to redirect to new success and error pages instead of sending status messages. - Introduced `OAuthSuccess` and `OAuthError` components to provide user feedback during authentication. - Added localization support for success and error messages in the translation files. - Implemented countdown functionality in the success component for a better user experience. * fix: refresh token handling for user connections, add missing URL and methods - add standard enum for system user id and helper for determining app-lvel vs. user-level connections * refactor: update token handling in MCPManager and MCPTokenStorage * fix: improve error logging in OAuth authentication handler * fix: concurrency issues for both login url emission and concurrency of oauth flows for shared flows (same user, same server, multiple calls for same server) * fix: properly fail shared flows for concurrent server calls and prevent duplication of tokens * chore: remove unused auth package directory from update configuration * ci: fix mocks in samlStrategy tests * ci: add mcpConfig to AppService test setup * chore: remove obsolete MCP OAuth implementation documentation * fix: update build script for API to use correct command * chore: bump version of @librechat/api to 1.2.4 * fix: update abort signal handling in createMCPTool function * fix: add optional clientInfo parameter to refreshTokensFunction metadata * refactor: replace app.locals.availableTools with getCachedTools in multiple services and controllers for improved tool management * fix: concurrent refresh token handling issue * refactor: add signal parameter to getUserConnection method for improved abort handling * chore: JSDoc typing for `loadEphemeralAgent` * refactor: update isConnectionActive method to use destructured parameters for improved readability * feat: implement caching for MCP tools to handle app-level disconnects for loading list of tools * ci: fix agent test
512 lines
15 KiB
JavaScript
512 lines
15 KiB
JavaScript
const bcrypt = require('bcryptjs');
|
|
const { webcrypto } = require('node:crypto');
|
|
const { isEnabled } = require('@librechat/api');
|
|
const { logger } = require('@librechat/data-schemas');
|
|
const { SystemRoles, errorsToString } = require('librechat-data-provider');
|
|
const {
|
|
findUser,
|
|
createUser,
|
|
updateUser,
|
|
findToken,
|
|
countUsers,
|
|
getUserById,
|
|
findSession,
|
|
createToken,
|
|
deleteTokens,
|
|
deleteSession,
|
|
createSession,
|
|
generateToken,
|
|
deleteUserById,
|
|
generateRefreshToken,
|
|
} = require('~/models');
|
|
const { isEmailDomainAllowed } = require('~/server/services/domains');
|
|
const { checkEmailConfig, sendEmail } = require('~/server/utils');
|
|
const { getBalanceConfig } = require('~/server/services/Config');
|
|
const { registerSchema } = require('~/strategies/validators');
|
|
|
|
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<MongoUser> & { _id: ObjectId, email: string, name: string}} 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 {Express.Request} 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 });
|
|
|
|
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 {MongoUser} user <email, password, name, username>
|
|
* @param {Partial<MongoUser>} [additionalData={}]
|
|
* @returns {Promise<{status: number, message: string, user?: MongoUser}>}
|
|
*/
|
|
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 } = user;
|
|
|
|
let newUserId;
|
|
try {
|
|
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 };
|
|
}
|
|
|
|
if (!(await isEmailDomainAllowed(email))) {
|
|
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 };
|
|
}
|
|
|
|
//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: '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 balanceConfig = await getBalanceConfig();
|
|
|
|
const newUser = await createUser(newUserData, balanceConfig, 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 {Express.Request} req
|
|
*/
|
|
const requestPasswordReset = async (req) => {
|
|
const { email } = req.body;
|
|
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,
|
|
});
|
|
|
|
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 {Object} res
|
|
* @param {String} sessionId
|
|
* @returns
|
|
*/
|
|
const setAuthTokens = async (userId, res, sessionId = null) => {
|
|
try {
|
|
const user = await getUserById(userId);
|
|
const token = await generateToken(user);
|
|
|
|
let session;
|
|
let refreshToken;
|
|
let refreshTokenExpires;
|
|
|
|
if (sessionId) {
|
|
session = await findSession({ sessionId: sessionId }, { lean: false });
|
|
refreshTokenExpires = session.expiration.getTime();
|
|
refreshToken = await generateRefreshToken(session);
|
|
} else {
|
|
const result = await createSession(userId);
|
|
session = result.session;
|
|
refreshToken = result.refreshToken;
|
|
refreshTokenExpires = session.expiration.getTime();
|
|
}
|
|
|
|
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
|
|
* @returns {String} - access token
|
|
*/
|
|
const setOpenIDAuthTokens = (tokenset, res) => {
|
|
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 || !tokenset.refresh_token) {
|
|
logger.error('[setOpenIDAuthTokens] No access or refresh token found in tokenset');
|
|
return;
|
|
}
|
|
res.cookie('refreshToken', tokenset.refresh_token, {
|
|
expires: expirationDate,
|
|
httpOnly: true,
|
|
secure: isProduction,
|
|
sameSite: 'strict',
|
|
});
|
|
res.cookie('token_provider', 'openid', {
|
|
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,
|
|
requestPasswordReset,
|
|
resendVerificationEmail,
|
|
setOpenIDAuthTokens,
|
|
};
|