diff --git a/api/server/controllers/auth/AdminLoginController.js b/api/server/controllers/auth/AdminLoginController.js new file mode 100644 index 0000000000..6744444570 --- /dev/null +++ b/api/server/controllers/auth/AdminLoginController.js @@ -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} - 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, +}; diff --git a/api/server/controllers/auth/AdminVerifyController.js b/api/server/controllers/auth/AdminVerifyController.js new file mode 100644 index 0000000000..ff835316a3 --- /dev/null +++ b/api/server/controllers/auth/AdminVerifyController.js @@ -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, +}; diff --git a/api/server/index.js b/api/server/index.js index e458b0349e..43ab763a97 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -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); diff --git a/api/server/middleware/index.js b/api/server/middleware/index.js index 55ee465674..7d8e000e18 100644 --- a/api/server/middleware/index.js +++ b/api/server/middleware/index.js @@ -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, diff --git a/api/server/middleware/requireAdminAuth.js b/api/server/middleware/requireAdminAuth.js new file mode 100644 index 0000000000..03e2b2cf17 --- /dev/null +++ b/api/server/middleware/requireAdminAuth.js @@ -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; diff --git a/api/server/middleware/requireAdminJwtAuth.js b/api/server/middleware/requireAdminJwtAuth.js new file mode 100644 index 0000000000..346799b933 --- /dev/null +++ b/api/server/middleware/requireAdminJwtAuth.js @@ -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; diff --git a/api/server/routes/admin/auth.js b/api/server/routes/admin/auth.js new file mode 100644 index 0000000000..003784635a --- /dev/null +++ b/api/server/routes/admin/auth.js @@ -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; diff --git a/api/server/routes/index.js b/api/server/routes/index.js index adaca3859a..6b5c1b6c2f 100644 --- a/api/server/routes/index.js +++ b/api/server/routes/index.js @@ -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,