mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 08:12:00 +02:00
WIP: admin auth
This commit is contained in:
parent
d04da60b3b
commit
fbe0def2fa
8 changed files with 229 additions and 0 deletions
77
api/server/controllers/auth/AdminLoginController.js
Normal file
77
api/server/controllers/auth/AdminLoginController.js
Normal 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,
|
||||||
|
};
|
39
api/server/controllers/auth/AdminVerifyController.js
Normal file
39
api/server/controllers/auth/AdminVerifyController.js
Normal 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,
|
||||||
|
};
|
|
@ -109,6 +109,7 @@ const startServer = async () => {
|
||||||
app.use('/oauth', routes.oauth);
|
app.use('/oauth', routes.oauth);
|
||||||
/* API Endpoints */
|
/* API Endpoints */
|
||||||
app.use('/api/auth', routes.auth);
|
app.use('/api/auth', routes.auth);
|
||||||
|
app.use('/api/admin', routes.adminAuth);
|
||||||
app.use('/api/actions', routes.actions);
|
app.use('/api/actions', routes.actions);
|
||||||
app.use('/api/keys', routes.keys);
|
app.use('/api/keys', routes.keys);
|
||||||
app.use('/api/user', routes.user);
|
app.use('/api/user', routes.user);
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
const validatePasswordReset = require('./validatePasswordReset');
|
const validatePasswordReset = require('./validatePasswordReset');
|
||||||
const validateRegistration = require('./validateRegistration');
|
const validateRegistration = require('./validateRegistration');
|
||||||
const buildEndpointOption = require('./buildEndpointOption');
|
const buildEndpointOption = require('./buildEndpointOption');
|
||||||
|
const requireAdminJwtAuth = require('./requireAdminJwtAuth');
|
||||||
const validateMessageReq = require('./validateMessageReq');
|
const validateMessageReq = require('./validateMessageReq');
|
||||||
const checkDomainAllowed = require('./checkDomainAllowed');
|
const checkDomainAllowed = require('./checkDomainAllowed');
|
||||||
const concurrentLimiter = require('./concurrentLimiter');
|
const concurrentLimiter = require('./concurrentLimiter');
|
||||||
const validateEndpoint = require('./validateEndpoint');
|
const validateEndpoint = require('./validateEndpoint');
|
||||||
const requireLocalAuth = require('./requireLocalAuth');
|
const requireLocalAuth = require('./requireLocalAuth');
|
||||||
const canDeleteAccount = require('./canDeleteAccount');
|
const canDeleteAccount = require('./canDeleteAccount');
|
||||||
|
const requireAdminAuth = require('./requireAdminAuth');
|
||||||
const accessResources = require('./accessResources');
|
const accessResources = require('./accessResources');
|
||||||
const requireLdapAuth = require('./requireLdapAuth');
|
const requireLdapAuth = require('./requireLdapAuth');
|
||||||
const abortMiddleware = require('./abortMiddleware');
|
const abortMiddleware = require('./abortMiddleware');
|
||||||
|
@ -38,6 +40,8 @@ module.exports = {
|
||||||
moderateText,
|
moderateText,
|
||||||
validateModel,
|
validateModel,
|
||||||
requireJwtAuth,
|
requireJwtAuth,
|
||||||
|
requireAdminAuth,
|
||||||
|
requireAdminJwtAuth,
|
||||||
checkInviteUser,
|
checkInviteUser,
|
||||||
requireLdapAuth,
|
requireLdapAuth,
|
||||||
requireLocalAuth,
|
requireLocalAuth,
|
||||||
|
|
35
api/server/middleware/requireAdminAuth.js
Normal file
35
api/server/middleware/requireAdminAuth.js
Normal 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;
|
42
api/server/middleware/requireAdminJwtAuth.js
Normal file
42
api/server/middleware/requireAdminJwtAuth.js
Normal 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;
|
29
api/server/routes/admin/auth.js
Normal file
29
api/server/routes/admin/auth.js
Normal 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;
|
|
@ -1,6 +1,7 @@
|
||||||
const accessPermissions = require('./accessPermissions');
|
const accessPermissions = require('./accessPermissions');
|
||||||
const assistants = require('./assistants');
|
const assistants = require('./assistants');
|
||||||
const categories = require('./categories');
|
const categories = require('./categories');
|
||||||
|
const adminAuth = require('./admin/auth');
|
||||||
const tokenizer = require('./tokenizer');
|
const tokenizer = require('./tokenizer');
|
||||||
const endpoints = require('./endpoints');
|
const endpoints = require('./endpoints');
|
||||||
const staticRoute = require('./static');
|
const staticRoute = require('./static');
|
||||||
|
@ -32,6 +33,7 @@ module.exports = {
|
||||||
mcp,
|
mcp,
|
||||||
edit,
|
edit,
|
||||||
auth,
|
auth,
|
||||||
|
adminAuth,
|
||||||
keys,
|
keys,
|
||||||
user,
|
user,
|
||||||
tags,
|
tags,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue