🔒✉️ 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:
Marco Beretta 2024-02-05 08:14:52 +01:00 committed by GitHub
parent b5c2fb93c1
commit 25da90657d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 136 additions and 21 deletions

View file

@ -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,

View 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);
});
});