mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
💂 fix: Enforce ALLOW_EMAIL_LOGIN on the Backend Login Route (#14180)
* 🔒 fix: Enforce ALLOW_EMAIL_LOGIN on Backend Login Route
ALLOW_EMAIL_LOGIN=false previously only hid the login form; POST
/api/auth/login stayed mounted and accepted valid credentials. Add a
validateEmailLogin middleware (mirroring validateRegistration /
validatePasswordReset) that rejects login with 403 when the flag is
disabled, with an ALLOW_EMAIL_LOGIN_OVERRIDE escape hatch for
intentional direct API login (each use logged with request IP).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: Gate admin local login by email login flag
* fix: Move email login gate into api package
* test: Avoid mutating readonly request ip
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
cb5454d364
commit
73c43ded25
12 changed files with 203 additions and 0 deletions
|
|
@ -2,6 +2,7 @@ const validatePasswordReset = require('./validatePasswordReset');
|
|||
const setTwoFactorTempUser = require('./setTwoFactorTempUser');
|
||||
const validateRegistration = require('./validateRegistration');
|
||||
const buildEndpointOption = require('./buildEndpointOption');
|
||||
const validateEmailLogin = require('./validateEmailLogin');
|
||||
const validateMessageReq = require('./validateMessageReq');
|
||||
const { prepareMessageRequestValidation, sendValidationResponse } = require('./messageValidation');
|
||||
const checkDomainAllowed = require('./checkDomainAllowed');
|
||||
|
|
@ -53,4 +54,5 @@ module.exports = {
|
|||
buildEndpointOption,
|
||||
validateRegistration,
|
||||
validatePasswordReset,
|
||||
validateEmailLogin,
|
||||
};
|
||||
|
|
|
|||
3
api/server/middleware/validateEmailLogin.js
Normal file
3
api/server/middleware/validateEmailLogin.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
const { validateEmailLogin } = require('@librechat/api');
|
||||
|
||||
module.exports = validateEmailLogin;
|
||||
|
|
@ -73,6 +73,7 @@ router.post(
|
|||
middleware.logHeaders,
|
||||
middleware.loginLimiter,
|
||||
middleware.checkBan,
|
||||
middleware.validateEmailLogin,
|
||||
middleware.requireLocalAuth,
|
||||
tenantContextMiddleware,
|
||||
requireAdminAccess,
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ jest.mock('~/server/middleware', () => ({
|
|||
logHeaders: jest.fn((req, res, next) => next()),
|
||||
loginLimiter: jest.fn((req, res, next) => next()),
|
||||
checkBan: jest.fn((req, res, next) => next()),
|
||||
validateEmailLogin: jest.fn((req, res, next) => next()),
|
||||
requireLocalAuth: jest.fn((req, res, next) => next()),
|
||||
requireJwtAuth: jest.fn((req, res, next) => next()),
|
||||
checkDomainAllowed: jest.fn((req, res, next) => next()),
|
||||
|
|
@ -106,6 +107,7 @@ const openIdClient = require('openid-client');
|
|||
const { logger } = require('@librechat/data-schemas');
|
||||
const { isEnabled, applyAdminRefresh, buildOpenIDRefreshParams } = require('@librechat/api');
|
||||
const { getOpenIdConfig } = require('~/strategies');
|
||||
const middleware = require('~/server/middleware');
|
||||
const adminAuthRouter = require('./auth');
|
||||
|
||||
const ORIGINAL_OPENID_SCOPE = process.env.OPENID_SCOPE;
|
||||
|
|
@ -248,3 +250,44 @@ describe('admin auth OpenID refresh route', () => {
|
|||
expect(debugOutput).not.toContain('https://api.example.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('admin local login route', () => {
|
||||
let app;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
app = express();
|
||||
app.use(express.json());
|
||||
app.use('/api/admin', adminAuthRouter);
|
||||
});
|
||||
|
||||
it('applies the email login gate before local auth', async () => {
|
||||
const response = await request(app).post('/api/admin/login/local').send({
|
||||
email: 'admin@example.com',
|
||||
password: 'password',
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(middleware.validateEmailLogin).toHaveBeenCalledTimes(1);
|
||||
expect(middleware.requireLocalAuth).toHaveBeenCalledTimes(1);
|
||||
expect(middleware.validateEmailLogin.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
middleware.requireLocalAuth.mock.invocationCallOrder[0],
|
||||
);
|
||||
});
|
||||
|
||||
it('stops before local auth when the email login gate rejects the request', async () => {
|
||||
middleware.validateEmailLogin.mockImplementationOnce((req, res) =>
|
||||
res.status(403).json({ message: 'Email login is not allowed.' }),
|
||||
);
|
||||
|
||||
const response = await request(app).post('/api/admin/login/local').send({
|
||||
email: 'admin@example.com',
|
||||
password: 'password',
|
||||
});
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body).toEqual({ message: 'Email login is not allowed.' });
|
||||
expect(middleware.requireLocalAuth).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ jest.mock('~/server/middleware', () => {
|
|||
setTwoFactorTempUser: (...args) => mockSetTwoFactorTempUser(...args),
|
||||
twoFactorTempLimiter: (...args) => mockTwoFactorTempLimiter(...args),
|
||||
checkBan: (...args) => mockCheckBan(...args),
|
||||
validateEmailLogin: pass,
|
||||
requireLocalAuth: pass,
|
||||
requireLdapAuth: pass,
|
||||
registerLimiter: pass,
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ jest.mock('~/server/middleware', () => {
|
|||
setTwoFactorTempUser: pass,
|
||||
twoFactorTempLimiter: pass,
|
||||
checkBan: pass,
|
||||
validateEmailLogin: pass,
|
||||
requireLocalAuth: pass,
|
||||
requireLdapAuth: pass,
|
||||
registerLimiter: pass,
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ router.post(
|
|||
middleware.logHeaders,
|
||||
middleware.loginLimiter,
|
||||
middleware.checkBan,
|
||||
middleware.validateEmailLogin,
|
||||
ldapAuth ? middleware.requireLdapAuth : middleware.requireLocalAuth,
|
||||
setBalanceConfig,
|
||||
loginController,
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ jest.mock('~/server/middleware', () => {
|
|||
setTwoFactorTempUser: pass,
|
||||
twoFactorTempLimiter: pass,
|
||||
checkBan: (...args) => mockCheckBan(...args),
|
||||
validateEmailLogin: pass,
|
||||
requireLocalAuth: pass,
|
||||
requireLdapAuth: pass,
|
||||
registerLimiter: pass,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue