WIP: admin auth

This commit is contained in:
Danny Avila 2025-08-30 04:41:51 -04:00
parent d04da60b3b
commit fbe0def2fa
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
8 changed files with 229 additions and 0 deletions

View file

@ -0,0 +1,77 @@
const { logger, signPayload } = require('@librechat/data-schemas');
const { generate2FATempToken } = require('~/server/services/twoFactorService');
const { setAuthTokens } = require('~/server/services/AuthService');
/**
* Generates admin-specific JWT token with isAdmin claim
* @param {Object} user - User object from database
* @returns {Promise<string>} - JWT token
*/
const generateAdminToken = async (user) => {
if (!user) {
throw new Error('No user provided');
}
let expires = 1000 * 60 * 15; // 15 minutes default
if (process.env.SESSION_EXPIRY !== undefined && process.env.SESSION_EXPIRY !== '') {
try {
const evaluated = eval(process.env.SESSION_EXPIRY);
if (evaluated) {
expires = evaluated;
}
} catch (error) {
logger.warn('Invalid SESSION_EXPIRY expression, using default:', error);
}
}
return await signPayload({
payload: {
id: user._id,
username: user.username,
provider: user.provider,
email: user.email,
isAdmin: true, // Admin-specific claim
},
secret: process.env.JWT_SECRET,
expirationTime: expires / 1000,
});
};
/**
* Admin login controller - handles authentication for admin users
* Returns admin-specific JWT with isAdmin claim
*/
const adminLoginController = async (req, res) => {
try {
if (!req.user) {
return res.status(400).json({ message: 'Invalid credentials' });
}
// User role validation is already done in requireAdminAuth middleware
// Handle 2FA if enabled
if (req.user.twoFactorEnabled) {
const tempToken = generate2FATempToken(req.user._id);
return res.status(200).json({ twoFAPending: true, tempToken });
}
const { password: _p, totpSecret: _t, __v, ...user } = req.user;
user.id = user._id.toString();
// Generate admin-specific token
const token = await generateAdminToken(req.user);
// Set standard auth cookies (refreshToken, etc.)
await setAuthTokens(req.user._id, res);
return res.status(200).send({ token, user, isAdmin: true });
} catch (err) {
logger.error('[adminLoginController]', err);
return res.status(500).json({ message: 'Something went wrong' });
}
};
module.exports = {
adminLoginController,
};

View file

@ -0,0 +1,39 @@
const { logger } = require('@librechat/data-schemas');
const { SystemRoles } = require('librechat-data-provider');
/**
* Admin token verification controller
* Verifies JWT token and returns user data if valid and has admin role
* Used by admin panel to verify authentication status
*/
const adminVerifyController = async (req, res) => {
try {
// User is already authenticated via requireAdminJwtAuth middleware
if (!req.user) {
return res.status(401).json({ message: 'Unauthorized' });
}
// Double-check admin role (redundant but ensures security)
if (!req.user.role || req.user.role !== SystemRoles.ADMIN) {
logger.warn('[adminVerifyController] Non-admin user attempting to verify:', req.user.email);
return res.status(403).json({ message: 'Access denied: Admin privileges required' });
}
// Return user data without sensitive fields
const { password: _p, totpSecret: _t, __v, ...user } = req.user;
user.id = user._id.toString();
return res.status(200).json({
valid: true,
user,
isAdmin: true,
});
} catch (err) {
logger.error('[adminVerifyController]', err);
return res.status(500).json({ message: 'Something went wrong' });
}
};
module.exports = {
adminVerifyController,
};

View file

@ -109,6 +109,7 @@ const startServer = async () => {
app.use('/oauth', routes.oauth);
/* API Endpoints */
app.use('/api/auth', routes.auth);
app.use('/api/admin', routes.adminAuth);
app.use('/api/actions', routes.actions);
app.use('/api/keys', routes.keys);
app.use('/api/user', routes.user);

View file

@ -1,12 +1,14 @@
const validatePasswordReset = require('./validatePasswordReset');
const validateRegistration = require('./validateRegistration');
const buildEndpointOption = require('./buildEndpointOption');
const requireAdminJwtAuth = require('./requireAdminJwtAuth');
const validateMessageReq = require('./validateMessageReq');
const checkDomainAllowed = require('./checkDomainAllowed');
const concurrentLimiter = require('./concurrentLimiter');
const validateEndpoint = require('./validateEndpoint');
const requireLocalAuth = require('./requireLocalAuth');
const canDeleteAccount = require('./canDeleteAccount');
const requireAdminAuth = require('./requireAdminAuth');
const accessResources = require('./accessResources');
const requireLdapAuth = require('./requireLdapAuth');
const abortMiddleware = require('./abortMiddleware');
@ -38,6 +40,8 @@ module.exports = {
moderateText,
validateModel,
requireJwtAuth,
requireAdminAuth,
requireAdminJwtAuth,
checkInviteUser,
requireLdapAuth,
requireLocalAuth,

View file

@ -0,0 +1,35 @@
const passport = require('passport');
const { logger } = require('@librechat/data-schemas');
const { SystemRoles } = require('librechat-data-provider');
/**
* Middleware for admin authentication using local strategy
* Validates credentials and ensures user has admin role
*/
const requireAdminAuth = (req, res, next) => {
passport.authenticate('local', (err, user, info) => {
if (err) {
logger.error('[requireAdminAuth] Error at passport.authenticate:', err);
return next(err);
}
if (!user) {
logger.debug('[requireAdminAuth] Error: No user');
return res.status(404).send(info);
}
if (info && info.message) {
logger.debug('[requireAdminAuth] Error: ' + info.message);
return res.status(422).send({ message: info.message });
}
// Check if user has admin role
if (!user.role || user.role !== SystemRoles.ADMIN) {
logger.debug('[requireAdminAuth] Error: User is not an admin');
return res.status(403).send({ message: 'Access denied: Admin privileges required' });
}
req.user = user;
next();
})(req, res, next);
};
module.exports = requireAdminAuth;

View file

@ -0,0 +1,42 @@
const cookies = require('cookie');
const passport = require('passport');
const { isEnabled } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const { SystemRoles } = require('librechat-data-provider');
/**
* Custom Middleware to handle JWT authentication for admin endpoints
* Validates JWT token and ensures user has admin role
*/
const requireAdminJwtAuth = (req, res, next) => {
// Check if token provider is specified in cookies
const cookieHeader = req.headers.cookie;
const tokenProvider = cookieHeader ? cookies.parse(cookieHeader).token_provider : null;
// Use OpenID authentication if token provider is OpenID and OPENID_REUSE_TOKENS is enabled
const authStrategy =
tokenProvider === 'openid' && isEnabled(process.env.OPENID_REUSE_TOKENS) ? 'openidJwt' : 'jwt';
passport.authenticate(authStrategy, { session: false }, (err, user, _info) => {
if (err) {
logger.error('[requireAdminJwtAuth] Authentication error:', err);
return res.status(500).json({ message: 'Authentication error' });
}
if (!user) {
logger.debug('[requireAdminJwtAuth] No user found');
return res.status(401).json({ message: 'Unauthorized' });
}
// Check if user has admin role
if (!user.role || user.role !== SystemRoles.ADMIN) {
logger.debug('[requireAdminJwtAuth] User is not an admin:', user.email);
return res.status(403).json({ message: 'Access denied: Admin privileges required' });
}
req.user = user;
next();
})(req, res, next);
};
module.exports = requireAdminJwtAuth;

View file

@ -0,0 +1,29 @@
const express = require('express');
const { adminVerifyController } = require('~/server/controllers/auth/AdminVerifyController');
const { adminLoginController } = require('~/server/controllers/auth/AdminLoginController');
const middleware = require('~/server/middleware');
const router = express.Router();
// Admin local authentication route
router.post(
'/login/local',
middleware.logHeaders,
middleware.loginLimiter,
middleware.checkBan,
middleware.requireAdminAuth, // Uses local auth strategy + admin role validation
adminLoginController,
);
// Admin token verification endpoint
router.get(
'/verify',
middleware.requireAdminJwtAuth, // Validates JWT + admin role
adminVerifyController,
);
// TODO: Future OAuth/OpenID routes will be added here
// router.get('/auth/openid', ...);
// router.get('/auth/openid/callback', ...);
module.exports = router;

View file

@ -1,6 +1,7 @@
const accessPermissions = require('./accessPermissions');
const assistants = require('./assistants');
const categories = require('./categories');
const adminAuth = require('./admin/auth');
const tokenizer = require('./tokenizer');
const endpoints = require('./endpoints');
const staticRoute = require('./static');
@ -32,6 +33,7 @@ module.exports = {
mcp,
edit,
auth,
adminAuth,
keys,
user,
tags,