mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 17:00:15 +01:00
🔒✉️ feat: allow only certain domain (#1562)
* feat: allow only certain domain * Update dotenv.md * refactor( registrationController) & handle ALLOWED_REGISTRATION_DOMAINS not specified * cleanup and moved to AuthService for better error handling * refactor: replace environment variable with librechat config item, add typedef for custom config, update docs for new registration object and allowedDomains values * ci(AuthService): test for `isDomainAllowed` --------- Co-authored-by: Danny Avila <messagedaniel@protonmail.com>
This commit is contained in:
parent
b5c2fb93c1
commit
25da90657d
8 changed files with 136 additions and 21 deletions
4
api/cache/getCustomConfig.js
vendored
4
api/cache/getCustomConfig.js
vendored
|
|
@ -4,7 +4,9 @@ const getLogStores = require('./getLogStores');
|
|||
|
||||
/**
|
||||
* Retrieves the configuration object
|
||||
* @function getCustomConfig */
|
||||
* @function getCustomConfig
|
||||
* @returns {Promise<TCustomConfig | null>}
|
||||
* */
|
||||
async function getCustomConfig() {
|
||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||
let customConfig = await cache.get(CacheKeys.CUSTOM_CONFIG);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
const crypto = require('crypto');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const { registerSchema, errorsToString } = require('~/strategies/validators');
|
||||
const getCustomConfig = require('~/cache/getCustomConfig');
|
||||
const Token = require('~/models/schema/tokenSchema');
|
||||
const { sendEmail } = require('~/server/utils');
|
||||
const Session = require('~/models/Session');
|
||||
|
|
@ -12,6 +13,27 @@ const domains = {
|
|||
server: process.env.DOMAIN_SERVER,
|
||||
};
|
||||
|
||||
async function isDomainAllowed(email) {
|
||||
if (!email) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const domain = email.split('@')[1];
|
||||
|
||||
if (!domain) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const customConfig = await getCustomConfig();
|
||||
if (!customConfig) {
|
||||
return true;
|
||||
} else if (!customConfig?.registration?.allowedDomains) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return customConfig.registration.allowedDomains.includes(domain);
|
||||
}
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
/**
|
||||
|
|
@ -80,6 +102,12 @@ const registerUser = async (user) => {
|
|||
return { status: 500, message: 'Something went wrong' };
|
||||
}
|
||||
|
||||
if (!(await isDomainAllowed(email))) {
|
||||
const errorMessage = 'Registration from this domain is not allowed.';
|
||||
logger.error(`[registerUser] [Registration not allowed] [Email: ${user.email}]`);
|
||||
return { status: 403, message: errorMessage };
|
||||
}
|
||||
|
||||
//determine if this is the first registered user (not counting anonymous_user)
|
||||
const isFirstRegisteredUser = (await User.countDocuments({})) === 0;
|
||||
|
||||
|
|
@ -239,6 +267,7 @@ const setAuthTokens = async (userId, res, sessionId = null) => {
|
|||
module.exports = {
|
||||
registerUser,
|
||||
logoutUser,
|
||||
isDomainAllowed,
|
||||
requestPasswordReset,
|
||||
resetPassword,
|
||||
setAuthTokens,
|
||||
|
|
|
|||
39
api/server/services/AuthService.spec.js
Normal file
39
api/server/services/AuthService.spec.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
const getCustomConfig = require('~/cache/getCustomConfig');
|
||||
const { isDomainAllowed } = require('./AuthService');
|
||||
|
||||
jest.mock('~/cache/getCustomConfig', () => jest.fn());
|
||||
|
||||
describe('isDomainAllowed', () => {
|
||||
it('should allow domain when customConfig is not available', async () => {
|
||||
getCustomConfig.mockResolvedValue(null);
|
||||
await expect(isDomainAllowed('test@domain1.com')).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('should allow domain when allowedDomains is not defined in customConfig', async () => {
|
||||
getCustomConfig.mockResolvedValue({});
|
||||
await expect(isDomainAllowed('test@domain1.com')).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('should reject an email if it is falsy', async () => {
|
||||
getCustomConfig.mockResolvedValue({});
|
||||
await expect(isDomainAllowed('')).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('should allow a domain if it is included in the allowedDomains', async () => {
|
||||
getCustomConfig.mockResolvedValue({
|
||||
registration: {
|
||||
allowedDomains: ['domain1.com', 'domain2.com'],
|
||||
},
|
||||
});
|
||||
await expect(isDomainAllowed('user@domain1.com')).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('should reject a domain if it is not included in the allowedDomains', async () => {
|
||||
getCustomConfig.mockResolvedValue({
|
||||
registration: {
|
||||
allowedDomains: ['domain1.com', 'domain2.com'],
|
||||
},
|
||||
});
|
||||
await expect(isDomainAllowed('user@domain3.com')).resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -26,6 +26,12 @@
|
|||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports TCustomConfig
|
||||
* @typedef {import('librechat-data-provider').TCustomConfig} TCustomConfig
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports TMessage
|
||||
* @typedef {import('librechat-data-provider').TMessage} TMessage
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue