mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 00:40:14 +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
|
* Retrieves the configuration object
|
||||||
* @function getCustomConfig */
|
* @function getCustomConfig
|
||||||
|
* @returns {Promise<TCustomConfig | null>}
|
||||||
|
* */
|
||||||
async function getCustomConfig() {
|
async function getCustomConfig() {
|
||||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||||
let customConfig = await cache.get(CacheKeys.CUSTOM_CONFIG);
|
let customConfig = await cache.get(CacheKeys.CUSTOM_CONFIG);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
const { registerSchema, errorsToString } = require('~/strategies/validators');
|
const { registerSchema, errorsToString } = require('~/strategies/validators');
|
||||||
|
const getCustomConfig = require('~/cache/getCustomConfig');
|
||||||
const Token = require('~/models/schema/tokenSchema');
|
const Token = require('~/models/schema/tokenSchema');
|
||||||
const { sendEmail } = require('~/server/utils');
|
const { sendEmail } = require('~/server/utils');
|
||||||
const Session = require('~/models/Session');
|
const Session = require('~/models/Session');
|
||||||
|
|
@ -12,6 +13,27 @@ const domains = {
|
||||||
server: process.env.DOMAIN_SERVER,
|
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';
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -80,6 +102,12 @@ const registerUser = async (user) => {
|
||||||
return { status: 500, message: 'Something went wrong' };
|
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)
|
//determine if this is the first registered user (not counting anonymous_user)
|
||||||
const isFirstRegisteredUser = (await User.countDocuments({})) === 0;
|
const isFirstRegisteredUser = (await User.countDocuments({})) === 0;
|
||||||
|
|
||||||
|
|
@ -239,6 +267,7 @@ const setAuthTokens = async (userId, res, sessionId = null) => {
|
||||||
module.exports = {
|
module.exports = {
|
||||||
registerUser,
|
registerUser,
|
||||||
logoutUser,
|
logoutUser,
|
||||||
|
isDomainAllowed,
|
||||||
requestPasswordReset,
|
requestPasswordReset,
|
||||||
resetPassword,
|
resetPassword,
|
||||||
setAuthTokens,
|
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
|
* @memberof typedefs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @exports TCustomConfig
|
||||||
|
* @typedef {import('librechat-data-provider').TCustomConfig} TCustomConfig
|
||||||
|
* @memberof typedefs
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @exports TMessage
|
* @exports TMessage
|
||||||
* @typedef {import('librechat-data-provider').TMessage} TMessage
|
* @typedef {import('librechat-data-provider').TMessage} TMessage
|
||||||
|
|
|
||||||
|
|
@ -4,21 +4,6 @@ description: Comprehensive guide for configuring the `librechat.yaml` file AKA t
|
||||||
weight: -10
|
weight: -10
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- # Table of Contents
|
|
||||||
|
|
||||||
- [Intro](#librechat-configuration-guide)
|
|
||||||
- [Setup](#setup)
|
|
||||||
- [Docker Setup](#docker-setup)
|
|
||||||
- [Config Structure](#config-structure)
|
|
||||||
- [1. Version](#1-version)
|
|
||||||
- [2. Cache Settings](#2-cache-settings)
|
|
||||||
- [3. Endpoints](#3-endpoints)
|
|
||||||
- [Endpoint Object Structure](#endpoint-object-structure)
|
|
||||||
- [Additional Notes](#additional-notes)
|
|
||||||
- [Default Parameters](#default-parameters)
|
|
||||||
- [Breakdown of Default Params](#breakdown-of-default-params)
|
|
||||||
- [Example Config](#example-config) -->
|
|
||||||
|
|
||||||
# LibreChat Configuration Guide
|
# LibreChat Configuration Guide
|
||||||
|
|
||||||
Welcome to the guide for configuring the **librechat.yaml** file in LibreChat.
|
Welcome to the guide for configuring the **librechat.yaml** file in LibreChat.
|
||||||
|
|
@ -43,8 +28,11 @@ Stay tuned for ongoing enhancements to customize your LibreChat instance!
|
||||||
- [Version](#version)
|
- [Version](#version)
|
||||||
- [Cache Settings](#cache-settings)
|
- [Cache Settings](#cache-settings)
|
||||||
- [File Strategy](#file-strategy)
|
- [File Strategy](#file-strategy)
|
||||||
|
- [Registration](#registration)
|
||||||
- [Endpoints](#endpoints)
|
- [Endpoints](#endpoints)
|
||||||
- [Endpoint Object Structure](#endpoint-object-structure)
|
- [Registration Object Structure](#registration-object-structure)
|
||||||
|
- [**allowedDomains**:](#allowedDomains)
|
||||||
|
- [Custom Endpoint Object Structure](#custom-endpoint-object-structure)
|
||||||
- [**name**:](#name)
|
- [**name**:](#name)
|
||||||
- [**apiKey**:](#apikey)
|
- [**apiKey**:](#apikey)
|
||||||
- [**baseURL**:](#baseurl)
|
- [**baseURL**:](#baseurl)
|
||||||
|
|
@ -120,6 +108,15 @@ docker-compose up # no need to rebuild
|
||||||
- **Description**: Determines where to save user uploaded/generated files. Defaults to `"local"` if omitted.
|
- **Description**: Determines where to save user uploaded/generated files. Defaults to `"local"` if omitted.
|
||||||
- **Example**: `fileStrategy: "firebase"`
|
- **Example**: `fileStrategy: "firebase"`
|
||||||
|
|
||||||
|
### Registration
|
||||||
|
- **Key**: `registration`
|
||||||
|
- **Type**: Object
|
||||||
|
- **Description**: Configures registration-related settings for the application.
|
||||||
|
- **Sub-Key**: `allowedDomains`
|
||||||
|
- **Type**: Array of Strings
|
||||||
|
- **Description**: Specifies a list of allowed email domains for user registration. Users attempting to register with email domains not listed here will be restricted from registering.
|
||||||
|
- [Registration Object Structure](#registration-object-structure)
|
||||||
|
|
||||||
### Endpoints
|
### Endpoints
|
||||||
- **Key**: `endpoints`
|
- **Key**: `endpoints`
|
||||||
- **Type**: Object
|
- **Type**: Object
|
||||||
|
|
@ -127,9 +124,34 @@ docker-compose up # no need to rebuild
|
||||||
- **Sub-Key**: `custom`
|
- **Sub-Key**: `custom`
|
||||||
- **Type**: Array of Objects
|
- **Type**: Array of Objects
|
||||||
- **Description**: Each object in the array represents a unique endpoint configuration.
|
- **Description**: Each object in the array represents a unique endpoint configuration.
|
||||||
|
- [Custom Endpoint Object Structure](#custom-endpoint-object-structure)
|
||||||
- **Required**
|
- **Required**
|
||||||
|
|
||||||
## Endpoint Object Structure
|
## Registration Object Structure
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Example Registration Object Structure
|
||||||
|
registration:
|
||||||
|
allowedDomains:
|
||||||
|
- "gmail.com"
|
||||||
|
- "protonmail.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
### **allowedDomains**:
|
||||||
|
|
||||||
|
> A list specifying allowed email domains for registration.
|
||||||
|
|
||||||
|
- Type: Array of Strings
|
||||||
|
- Example:
|
||||||
|
```yaml
|
||||||
|
allowedDomains:
|
||||||
|
- "gmail.com"
|
||||||
|
- "protonmail.com"
|
||||||
|
```
|
||||||
|
- **Required**
|
||||||
|
- **Note**: Users with email domains not listed will be restricted from registering.
|
||||||
|
|
||||||
|
## Custom Endpoint Object Structure
|
||||||
Each endpoint in the `custom` array should have the following structure:
|
Each endpoint in the `custom` array should have the following structure:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|
@ -345,8 +367,12 @@ Custom endpoints share logic with the OpenAI endpoint, and thus have default par
|
||||||
## Example Config
|
## Example Config
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
version: 1.0.1
|
version: 1.0.2
|
||||||
cache: true
|
cache: true
|
||||||
|
# Example Registration Object Structure
|
||||||
|
registration:
|
||||||
|
allowedDomains:
|
||||||
|
- "gmail.com"
|
||||||
endpoints:
|
endpoints:
|
||||||
custom:
|
custom:
|
||||||
# Mistral AI API
|
# Mistral AI API
|
||||||
|
|
|
||||||
|
|
@ -665,7 +665,8 @@ see: **[User/Auth System](../configuration/user_auth_system.md)**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ALLOW_EMAIL_LOGIN=true
|
ALLOW_EMAIL_LOGIN=true
|
||||||
ALLOW_REGISTRATION=true
|
ALLOW_REGISTRATION=true
|
||||||
|
ALLOWED_REGISTRATION_DOMAINS=
|
||||||
ALLOW_SOCIAL_LOGIN=false
|
ALLOW_SOCIAL_LOGIN=false
|
||||||
ALLOW_SOCIAL_REGISTRATION=false
|
ALLOW_SOCIAL_REGISTRATION=false
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
# Configuration version (required)
|
# Configuration version (required)
|
||||||
version: 1.0.1
|
version: 1.0.2
|
||||||
|
|
||||||
# Cache settings: Set to true to enable caching
|
# Cache settings: Set to true to enable caching
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
|
# Example Registration Object Structure (optional)
|
||||||
|
# registration:
|
||||||
|
# allowedDomains:
|
||||||
|
# - "gmail.com"
|
||||||
|
|
||||||
# Definition of custom endpoints
|
# Definition of custom endpoints
|
||||||
endpoints:
|
endpoints:
|
||||||
custom:
|
custom:
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,11 @@ export const configSchema = z.object({
|
||||||
version: z.string(),
|
version: z.string(),
|
||||||
cache: z.boolean(),
|
cache: z.boolean(),
|
||||||
fileStrategy: fileSourceSchema.optional(),
|
fileStrategy: fileSourceSchema.optional(),
|
||||||
|
registration: z
|
||||||
|
.object({
|
||||||
|
allowedDomains: z.array(z.string()).optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
endpoints: z
|
endpoints: z
|
||||||
.object({
|
.object({
|
||||||
custom: z.array(endpointSchema.partial()),
|
custom: z.array(endpointSchema.partial()),
|
||||||
|
|
@ -37,6 +42,8 @@ export const configSchema = z.object({
|
||||||
.strict(),
|
.strict(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type TCustomConfig = z.infer<typeof configSchema>;
|
||||||
|
|
||||||
export enum KnownEndpoints {
|
export enum KnownEndpoints {
|
||||||
mistral = 'mistral',
|
mistral = 'mistral',
|
||||||
openrouter = 'openrouter',
|
openrouter = 'openrouter',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue