mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-23 10:54:11 +01:00
started with Multi-Tenant OpenID.
TODO: working code but needs some refactoring and cleaning up.
This commit is contained in:
parent
d786bf263c
commit
6577144554
10 changed files with 350 additions and 58 deletions
|
|
@ -52,10 +52,9 @@ router.get('/', async function (req, res) {
|
|||
!!process.env.APPLE_KEY_ID &&
|
||||
!!process.env.APPLE_PRIVATE_KEY_PATH,
|
||||
openidLoginEnabled:
|
||||
!!process.env.OPENID_CLIENT_ID &&
|
||||
!!process.env.OPENID_CLIENT_SECRET &&
|
||||
!!process.env.OPENID_ISSUER &&
|
||||
!!process.env.OPENID_ENABLED &&
|
||||
!!process.env.OPENID_SESSION_SECRET,
|
||||
openidMultiTenantEnabled: !!process.env.OPENID_MULTI_TENANT,
|
||||
openidLabel: process.env.OPENID_BUTTON_LABEL || 'Continue with OpenID',
|
||||
openidImageUrl: process.env.OPENID_IMAGE_URL,
|
||||
serverDomain: process.env.DOMAIN_SERVER || 'http://localhost:3080',
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ const passport = require('passport');
|
|||
const { loginLimiter, checkBan, checkDomainAllowed } = require('~/server/middleware');
|
||||
const { setAuthTokens } = require('~/server/services/AuthService');
|
||||
const { logger } = require('~/config');
|
||||
const { chooseOpenIdStrategy } = require('~/server/utils/openidHelper');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
|
|
@ -30,7 +31,7 @@ const oauthHandler = async (req, res) => {
|
|||
|
||||
router.get('/error', (req, res) => {
|
||||
// A single error message is pushed by passport when authentication fails.
|
||||
logger.error('Error in OAuth authentication:', { message: req.session.messages.pop() });
|
||||
logger.error('Error in OAuth authentication:', { message: req.session?.messages?.pop() });
|
||||
res.redirect(`${domains.client}/login`);
|
||||
});
|
||||
|
||||
|
|
@ -83,20 +84,32 @@ router.get(
|
|||
/**
|
||||
* OpenID Routes
|
||||
*/
|
||||
router.get(
|
||||
'/openid',
|
||||
passport.authenticate('openid', {
|
||||
session: false,
|
||||
}),
|
||||
);
|
||||
router.get('/openid', async (req, res, next) => {
|
||||
try {
|
||||
const strategy = await chooseOpenIdStrategy(req);
|
||||
console.log('OpenID login using strategy:', strategy);
|
||||
passport.authenticate(strategy, {
|
||||
session: false,
|
||||
})(req, res, next);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get(
|
||||
'/openid/callback',
|
||||
passport.authenticate('openid', {
|
||||
failureRedirect: `${domains.client}/oauth/error`,
|
||||
failureMessage: true,
|
||||
session: false,
|
||||
}),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const strategy = await chooseOpenIdStrategy(req);
|
||||
passport.authenticate(strategy, {
|
||||
failureRedirect: `${domains.client}/oauth/error`,
|
||||
failureMessage: true,
|
||||
session: false,
|
||||
})(req, res, next);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
oauthHandler,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ const { isEnabled } = require('~/server/utils');
|
|||
const { logger } = require('~/config');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Express.Application} app
|
||||
*/
|
||||
const configureSocialLogins = (app) => {
|
||||
|
|
@ -35,10 +34,7 @@ const configureSocialLogins = (app) => {
|
|||
passport.use(appleLogin());
|
||||
}
|
||||
if (
|
||||
process.env.OPENID_CLIENT_ID &&
|
||||
process.env.OPENID_CLIENT_SECRET &&
|
||||
process.env.OPENID_ISSUER &&
|
||||
process.env.OPENID_SCOPE &&
|
||||
process.env.OPENID_ENABLED &&
|
||||
process.env.OPENID_SESSION_SECRET
|
||||
) {
|
||||
const sessionOptions = {
|
||||
|
|
|
|||
52
api/server/utils/openidHelper.js
Normal file
52
api/server/utils/openidHelper.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
const { logger } = require('~/config');
|
||||
const { getCustomConfig } = require('~/server/services/Config');
|
||||
|
||||
/**
|
||||
* Loads the tenant configurations from the custom configuration.
|
||||
* @returns {Promise<Array>} Array of tenant configurations.
|
||||
*/
|
||||
async function getOpenIdTenants() {
|
||||
try {
|
||||
const customConfig = await getCustomConfig();
|
||||
if (customConfig?.openid?.tenants) {
|
||||
return customConfig.openid.tenants;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Failed to load custom configuration for OpenID tenants:', err);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Chooses the OpenID strategy name based on the email domain.
|
||||
* It consults the global tenant mapping (built in setupOpenId).
|
||||
* @param {import('express').Request} req - The Express request object.
|
||||
* @returns {Promise<string>} - The chosen strategy name.
|
||||
*/
|
||||
async function chooseOpenIdStrategy(req) {
|
||||
if (req.query.email) {
|
||||
const email = req.query.email;
|
||||
const domain = email.split('@')[1].toLowerCase();
|
||||
const tenants = await getOpenIdTenants();
|
||||
|
||||
// Iterate over the tenants and return the strategy name of the first matching tenant
|
||||
for (const tenant of tenants) {
|
||||
if (tenant.domains) {
|
||||
const tenantDomains = tenant.domains.split(',').map(s => s.trim().toLowerCase());
|
||||
if (tenantDomains.includes(domain)) {
|
||||
// Look up the registered strategy via the global mapping.
|
||||
if (tenant.name && tenant.name.trim() && global.__openidTenantMapping) {
|
||||
const mapped = global.__openidTenantMapping.get(tenant.name.trim().toLowerCase());
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
}
|
||||
return 'openid'; // Fallback if no mapping exists.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 'openid';
|
||||
}
|
||||
|
||||
module.exports = { getOpenIdTenants, chooseOpenIdStrategy };
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
const fetch = require('node-fetch');
|
||||
const passport = require('passport');
|
||||
const jwtDecode = require('jsonwebtoken/decode');
|
||||
const { decode: jwtDecode } = require('jsonwebtoken');
|
||||
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||
const { Issuer, Strategy: OpenIDStrategy, custom } = require('openid-client');
|
||||
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
||||
|
|
@ -8,6 +8,7 @@ const { findUser, createUser, updateUser } = require('~/models/userMethods');
|
|||
const { hashToken } = require('~/server/utils/crypto');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const { logger } = require('~/config');
|
||||
const { getOpenIdTenants } = require('~/server/utils/openidHelper');
|
||||
|
||||
let crypto;
|
||||
try {
|
||||
|
|
@ -105,16 +106,18 @@ function convertToUsername(input, defaultValue = '') {
|
|||
return defaultValue;
|
||||
}
|
||||
|
||||
async function setupOpenId() {
|
||||
/**
|
||||
* Sets up a single OpenID strategy for the given tenant configuration.
|
||||
* @param {Object} tenant - The tenant’s OpenID config (issuer, clientId, etc.).
|
||||
* @param {string} tenant.issuer
|
||||
* @param {string} tenant.clientId
|
||||
* @param {string} tenant.clientSecret
|
||||
* @param {string} strategyName - Unique name for the strategy.
|
||||
*/
|
||||
async function setupSingleStrategy(tenant, strategyName) {
|
||||
try {
|
||||
if (process.env.PROXY) {
|
||||
const proxyAgent = new HttpsProxyAgent(process.env.PROXY);
|
||||
custom.setHttpOptionsDefaults({
|
||||
agent: proxyAgent,
|
||||
});
|
||||
logger.info(`[openidStrategy] proxy agent added: ${process.env.PROXY}`);
|
||||
}
|
||||
const issuer = await Issuer.discover(process.env.OPENID_ISSUER);
|
||||
// Discover the issuer (this performs the .well-known lookup).
|
||||
const issuer = await Issuer.discover(tenant.issuer);
|
||||
/* Supported Algorithms, openid-client v5 doesn't set it automatically as discovered from server.
|
||||
- id_token_signed_response_alg // defaults to 'RS256'
|
||||
- request_object_signing_alg // defaults to 'RS256'
|
||||
|
|
@ -124,8 +127,8 @@ async function setupOpenId() {
|
|||
*/
|
||||
/** @type {import('openid-client').ClientMetadata} */
|
||||
const clientMetadata = {
|
||||
client_id: process.env.OPENID_CLIENT_ID,
|
||||
client_secret: process.env.OPENID_CLIENT_SECRET,
|
||||
client_id: tenant.clientId,
|
||||
client_secret: tenant.clientSecret,
|
||||
redirect_uris: [process.env.DOMAIN_SERVER + process.env.OPENID_CALLBACK_URL],
|
||||
};
|
||||
if (isEnabled(process.env.OPENID_SET_FIRST_SUPPORTED_ALGORITHM)) {
|
||||
|
|
@ -146,7 +149,7 @@ async function setupOpenId() {
|
|||
async (tokenset, userinfo, done) => {
|
||||
try {
|
||||
logger.info(`[openidStrategy] verify login openidId: ${userinfo.sub}`);
|
||||
logger.debug('[openidStrategy] very login tokenset and userinfo', { tokenset, userinfo });
|
||||
logger.debug('[openidStrategy] verify login tokenset and userinfo', { tokenset, userinfo });
|
||||
|
||||
let user = await findUser({ openidId: userinfo.sub });
|
||||
logger.info(
|
||||
|
|
@ -265,7 +268,65 @@ async function setupOpenId() {
|
|||
},
|
||||
);
|
||||
|
||||
passport.use('openid', openidLogin);
|
||||
passport.use(strategyName, openidLogin);
|
||||
logger.info(`Configured OpenID strategy [${strategyName}] for issuer: ${tenant.issuer}`);
|
||||
} catch (err) {
|
||||
logger.error(`[openidStrategy] Error configuring strategy "${strategyName}":`, err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the YAML configuration and registers strategies for multi-tenant OpenID Connect.
|
||||
*/
|
||||
async function setupOpenId() {
|
||||
try {
|
||||
// If a proxy is configured, set it for openid-client.
|
||||
|
||||
// Set global HTTP options for openid-client
|
||||
if (process.env.PROXY) {
|
||||
const proxyAgent = new HttpsProxyAgent(process.env.PROXY);
|
||||
custom.setHttpOptionsDefaults({
|
||||
agent: proxyAgent,
|
||||
timeout: 10000, // 10,000ms = 10 seconds
|
||||
});
|
||||
logger.info(`[openidStrategy] Proxy agent added: ${process.env.PROXY} with timeout 10000ms`);
|
||||
} else {
|
||||
custom.setHttpOptionsDefaults({
|
||||
timeout: 10000, // Increase the default timeout
|
||||
});
|
||||
logger.info('[openidStrategy] Set default timeout to 10000ms');
|
||||
}
|
||||
|
||||
const tenants = await getOpenIdTenants();
|
||||
|
||||
// Global mapping: tenant name (lowercase) -> strategy name.
|
||||
const tenantMapping = new Map();
|
||||
|
||||
// If there is one tenant with no domains specified, register it as the default "openid" strategy.
|
||||
if (tenants.length === 1 && (!tenants[0].domains || tenants[0].domains.trim() === '')) {
|
||||
await setupSingleStrategy(tenants[0].openid, 'openid');
|
||||
tenantMapping.set(tenants[0].name?.trim().toLowerCase() || 'openid', 'openid');
|
||||
logger.info('Configured single-tenant OpenID strategy as "openid"');
|
||||
} else {
|
||||
// Otherwise, iterate over each tenant.
|
||||
for (const tenantCfg of tenants) {
|
||||
const openidCfg = tenantCfg.openid;
|
||||
let strategyName = 'openid';
|
||||
if (tenantCfg.name && tenantCfg.name.trim()) {
|
||||
strategyName = `openid_${tenantCfg.name.trim()}`;
|
||||
}else {
|
||||
logger.warn(
|
||||
`[openidStrategy] Tenant with issuer ${openidCfg.issuer} has no domains specified; defaulting strategy name to "openid".`,
|
||||
);
|
||||
}
|
||||
await setupSingleStrategy(openidCfg, strategyName);
|
||||
if (tenantCfg.name && tenantCfg.name.trim()) {
|
||||
tenantMapping.set(tenantCfg.name.trim().toLowerCase(), strategyName);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Store the tenant mapping globally so that the helper can choose the correct strategy.
|
||||
global.__openidTenantMapping = tenantMapping;
|
||||
} catch (err) {
|
||||
logger.error('[openidStrategy]', err);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue