Overview
LibreChat supports multiple authentication strategies to accommodate different use cases and security requirements:
JWT (JSON Web Tokens) - Primary authentication method
Local Authentication - Username/password with Passport.js
OAuth 2.0 - Social login providers (Google, GitHub, Discord, etc.)
OpenID Connect - Enterprise SSO integration
LDAP - Active Directory and LDAP servers
SAML 2.0 - Enterprise identity providers
Authentication Flow
Standard Login Flow
JWT Authentication
JWT Strategy Configuration
LibreChat uses Passport.js with JWT strategy:
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
const jwtLogin = () =>
new JwtStrategy(
{
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET,
},
async (payload, done) => {
try {
const user = await getUserById(payload?.id, '-password -__v -totpSecret -backupCodes');
if (user) {
user.id = user._id.toString();
if (!user.role) {
user.role = SystemRoles.USER;
await updateUser(user.id, { role: user.role });
}
done(null, user);
} else {
done(null, false);
}
} catch (err) {
done(err, false);
}
},
);
Using JWT Authentication
Include the JWT token in the Authorization header:
curl -X GET https://api.example.com/api/user \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
JavaScript
Python
TypeScript
const response = await fetch ( 'http://localhost:3080/api/user' , {
headers: {
'Authorization' : `Bearer ${ token } ` ,
'Content-Type' : 'application/json'
}
});
JWT Middleware
Protected routes use the requireJwtAuth middleware:
const requireJwtAuth = (req, res, next) => {
const cookieHeader = req.headers.cookie;
const tokenProvider = cookieHeader ? cookies.parse(cookieHeader).token_provider : null;
// Switch to OpenID JWT if using OpenID tokens
if (tokenProvider === 'openid' && isEnabled(process.env.OPENID_REUSE_TOKENS)) {
return passport.authenticate('openidJwt', { session: false })(req, res, next);
}
return passport.authenticate('jwt', { session: false })(req, res, next);
};
Token Expiry
Configure token lifetimes via environment variables:
SESSION_EXPIRY = 900000 # JWT token: 15 minutes (in milliseconds)
REFRESH_TOKEN_EXPIRY = 604800000 # Refresh token: 7 days (in milliseconds)
Access tokens (JWT) are short-lived for security. Use refresh tokens to obtain new access tokens without re-authentication.
Local Authentication
Login Endpoint
router.post(
'/login',
middleware.logHeaders,
middleware.loginLimiter,
middleware.checkBan,
ldapAuth ? middleware.requireLdapAuth : middleware.requireLocalAuth,
setBalanceConfig,
loginController,
);
Login Request
curl -X POST http://localhost:3080/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "your-password"
}'
Login Response
{
"token" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ,
"user" : {
"id" : "507f1f77bcf86cd799439011" ,
"email" : "user@example.com" ,
"name" : "John Doe" ,
"username" : "johndoe" ,
"role" : "USER" ,
"provider" : "local" ,
"avatar" : null ,
"emailVerified" : true
}
}
The response also sets HTTP-only cookies:
Set-Cookie: refreshToken=...; HttpOnly; Secure; SameSite=Strict
Set-Cookie: token_provider=librechat; HttpOnly; Secure; SameSite=Strict
Local Strategy Implementation
The local strategy validates username and password:
module.exports = () =>
new PassportLocalStrategy(
{
usernameField: 'email',
passwordField: 'password',
session: false,
passReqToCallback: true,
},
async (req, email, password, done) => {
try {
// Find user
const user = await findUser({ email: email.trim() }, '+password');
if (!user) {
return done(null, false, { message: 'Email does not exist.' });
}
// Verify password
const isMatch = await comparePassword(user, password);
if (!isMatch) {
return done(null, false, { message: 'Incorrect password.' });
}
// Check email verification
if (!user.emailVerified && !unverifiedAllowed) {
return done(null, user, { message: 'Email not verified.' });
}
return done(null, user);
} catch (err) {
return done(err);
}
},
);
Token Refresh
Refresh Token Flow
When the access token expires, use the refresh token to obtain a new one:
const refreshController = async (req, res) => {
const parsedCookies = req.headers.cookie ? cookies.parse(req.headers.cookie) : {};
const refreshToken = parsedCookies.refreshToken;
if (!refreshToken) {
return res.status(200).send('Refresh token not provided');
}
try {
// Verify refresh token
const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
const user = await getUserById(payload.id, '-password -__v -totpSecret -backupCodes');
if (!user) {
return res.status(401).redirect('/login');
}
// Find session
const session = await findSession({
userId: payload.id,
refreshToken: refreshToken,
});
if (session && session.expiration > new Date()) {
// Generate new access token
const token = await setAuthTokens(payload.id, res, session);
res.status(200).send({ token, user });
} else {
res.status(401).send('Refresh token expired or not found');
}
} catch (err) {
res.status(403).send('Invalid refresh token');
}
};
Refresh Request Example
curl -X POST http://localhost:3080/api/auth/refresh \
-H "Cookie: refreshToken=your-refresh-token"
OAuth 2.0 Authentication
LibreChat supports multiple OAuth providers:
Supported Providers
Google OAuth 2.0
GitHub OAuth
Discord OAuth
Facebook Login
Apple Sign In
OAuth Configuration
Enable social login:
OAuth Flow
// Google OAuth
router.get(
'/google',
passport.authenticate('google', {
scope: ['openid', 'profile', 'email'],
session: false,
}),
);
router.get(
'/google/callback',
passport.authenticate('google', {
failureRedirect: `${domains.client}/oauth/error`,
failureMessage: true,
session: false,
scope: ['openid', 'profile', 'email'],
}),
setBalanceConfig,
checkDomainAllowed,
oauthHandler,
);
OAuth Login Flow
Initiate OAuth - Redirect user to provider:
User Authorizes - User grants permissions on provider site
Callback - Provider redirects to callback URL:
GET /oauth/google/callback?code=...
Token Exchange - Server exchanges code for tokens
User Creation/Login - Find or create user account
Session Creation - Generate JWT and refresh tokens
OAuth tokens are handled server-side. The client receives standard LibreChat JWT tokens.
OpenID Connect
OpenID Configuration
For enterprise SSO with OpenID Connect:
# OpenID Provider Configuration
OPENID_ISSUER = https://accounts.google.com
OPENID_CLIENT_ID = your-client-id
OPENID_CLIENT_SECRET = your-client-secret
OPENID_CALLBACK_URL = https://your-domain.com/oauth/openid/callback
OPENID_SCOPE = "openid profile email"
# Token Management
OPENID_REUSE_TOKENS = true
OpenID Token Flow
const setOpenIDAuthTokens = (tokenset, req, res, userId, existingRefreshToken) => {
const refreshToken = tokenset.refresh_token || existingRefreshToken;
const appAuthToken = tokenset.id_token || tokenset.access_token;
// Store tokens in session (avoids large cookies)
if (req.session) {
req.session.openidTokens = {
accessToken: tokenset.access_token,
idToken: tokenset.id_token,
refreshToken: refreshToken,
expiresAt: expirationDate.getTime(),
};
}
// Set cookies
res.cookie('refreshToken', refreshToken, {
expires: expirationDate,
httpOnly: true,
secure: shouldUseSecureCookie(),
sameSite: 'strict',
});
res.cookie('token_provider', 'openid', {
expires: expirationDate,
httpOnly: true,
secure: shouldUseSecureCookie(),
sameSite: 'strict',
});
return appAuthToken;
};
OpenID tokens are stored server-side in express-session to avoid HTTP/2 header size limits for users with many group memberships.
LDAP Authentication
LDAP Configuration
Enable LDAP authentication:
LDAP_URL = ldap://localhost:389
LDAP_USER_SEARCH_BASE = ou = users, dc = example, dc = com
LDAP_BIND_DN = cn = admin, dc = example, dc = com
LDAP_BIND_CREDENTIALS = admin-password
LDAP_USER_SEARCH_FILTER = ( mail = {{username}} )
LDAP Strategy Setup
// Initialize LDAP if configured
if (process.env.LDAP_URL && process.env.LDAP_USER_SEARCH_BASE) {
passport.use(ldapLogin);
}
// Use LDAP auth for login
const ldapAuth = !!process.env.LDAP_URL && !!process.env.LDAP_USER_SEARCH_BASE;
router.post(
'/login',
ldapAuth ? middleware.requireLdapAuth : middleware.requireLocalAuth,
loginController,
);
Two-Factor Authentication (2FA)
2FA Endpoints
// Enable 2FA
router.get('/2fa/enable', middleware.requireJwtAuth, enable2FA);
// Verify TOTP code
router.post('/2fa/verify', middleware.requireJwtAuth, verify2FA);
// Verify with temporary token
router.post('/2fa/verify-temp', middleware.checkBan, verify2FAWithTempToken);
// Confirm 2FA setup
router.post('/2fa/confirm', middleware.requireJwtAuth, confirm2FA);
// Disable 2FA
router.post('/2fa/disable', middleware.requireJwtAuth, disable2FA);
// Regenerate backup codes
router.post('/2fa/backup/regenerate', middleware.requireJwtAuth, regenerateBackupCodes);
Enable 2FA Flow
Request Setup
GET /api/auth/2fa/enable
Authorization: Bearer {token}
Receive QR Code
{
"secret" : "JBSWY3DPEHPK3PXP" ,
"qrCode" : "data:image/png;base64,..." ,
"backupCodes" : [ "12345678" , "87654321" , ... ]
}
Verify Code
POST /api/auth/2fa/verify
Authorization: Bearer {token}
Content-Type: application/json
{
"code" : "123456"
}
Confirm 2FA
POST /api/auth/2fa/confirm
Authorization: Bearer {token}
Login with 2FA
When 2FA is enabled, the login flow changes:
Initial Login - Returns temporary token
Verify 2FA Code - Submit TOTP code or backup code
Receive JWT - Get full access token
User Registration
Registration Endpoint
router.post(
'/register',
middleware.registerLimiter,
middleware.checkBan,
middleware.checkInviteUser,
middleware.validateRegistration,
registrationController,
);
Registration Request
curl -X POST http://localhost:3080/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "newuser@example.com",
"password": "SecurePass123!",
"name": "John Doe",
"username": "johndoe"
}'
Email Verification
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, // 15 minutes
});
};
By default, users must verify their email before logging in. Set ALLOW_UNVERIFIED_EMAIL_LOGIN=true to bypass verification.
Password Reset
Request Password Reset
POST /api/auth/requestPasswordReset
Content-Type: application/json
{
"email" : "user@example.com"
}
Reset Password
POST /api/auth/resetPassword
Content-Type: application/json
{
"userId" : "507f1f77bcf86cd799439011",
"token" : "reset-token-from-email",
"password" : "NewSecurePassword123!"
}
Logout
Logout Endpoint
router.post('/logout', middleware.requireJwtAuth, logoutController);
Logout Request
curl -X POST http://localhost:3080/api/auth/logout \
-H "Authorization: Bearer {token}" \
-H "Cookie: refreshToken={refresh-token}"
Logout performs the following:
Invalidates the refresh token session
Destroys the express session
Returns success message
const logoutUser = async (req, refreshToken) => {
try {
const userId = req.user._id;
const session = await findSession({ userId, refreshToken });
if (session) {
await deleteSession({ sessionId: session._id });
}
req.session.destroy();
return { status: 200, message: 'Logout successful' };
} catch (err) {
return { status: 500, message: err.message };
}
};
Session Management
Session Configuration
SESSION_EXPIRY = 900000 # 15 minutes
REFRESH_TOKEN_EXPIRY = 604800000 # 7 days
Session Storage
Sessions are stored in MongoDB with automatic expiration:
const createSession = async (userId, { expiresIn }) => {
const session = await Session.create({
userId,
refreshToken: hashedToken,
expiration: new Date(Date.now() + expiresIn)
});
return { session, refreshToken };
};
Security Best Practices
Environment Variables
Never commit secrets to version control. Use environment variables for all sensitive data.
# Strong random secrets (minimum 32 characters)
JWT_SECRET = your-strong-random-secret-here
JWT_REFRESH_SECRET = your-refresh-secret-here
# Use secure cookies in production
SECURE_COOKIE = true
Token Security
Short-lived Access Tokens - 15 minutes by default
HTTP-Only Cookies - Prevent XSS attacks
Secure Flag - HTTPS only in production
SameSite Strict - CSRF protection
Token Rotation - New refresh token on each refresh
Password Security
// Passwords are hashed with bcrypt (10 rounds)
const salt = bcrypt . genSaltSync ( 10 );
const hashedPassword = bcrypt . hashSync ( password , salt );
Rate Limiting
Authentication endpoints are rate-limited:
Login : Limited to prevent brute force
Registration : Prevent spam accounts
Password Reset : Prevent abuse
Rate limit thresholds are configurable in the middleware/limiters module.
Next Steps