feat: Add custom fields & role assignment to OpenID strategy (#5612)

* started with Support for Customizable OpenID Profile Fields via Environment Variable

* kept as much of the original code as possible but still added the custom data mapper

* kept as much of the original code as possible but still added the custom data mapper

* resolved merge conflicts

* resolved merge conflicts

* resolved merge conflicts

* resolved merge conflicts

* removed some unneeded comments

* fix: conflicted issue

---------

Co-authored-by: Talstra Ruben SRSNL <ruben.talstra@stadlerrail.com>
This commit is contained in:
Ruben Talstra 2025-02-11 16:42:05 +01:00 committed by GitHub
parent 404b27d045
commit 2ef6e4462d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 250 additions and 2 deletions

View file

@ -8,6 +8,8 @@ const { findUser, createUser, updateUser } = require('~/models/userMethods');
const { hashToken } = require('~/server/utils/crypto');
const { isEnabled } = require('~/server/utils');
const { logger } = require('~/config');
const { SystemRoles } = require('librechat-data-provider');
const OpenIdDataMapper = require('./OpenId/openidDataMapper');
let crypto;
try {
@ -105,6 +107,45 @@ function convertToUsername(input, defaultValue = '') {
return defaultValue;
}
/**
* Decodes a JWT token safely.
* @param {string} token
* @returns {Object|null}
*/
function safeDecode(token) {
try {
const decoded = jwtDecode(token);
if (decoded && typeof decoded === 'object') {
return decoded;
}
logger.error('[openidStrategy] Decoded token is not an object.');
return null;
} catch (error) {
logger.error('[openidStrategy] safeDecode: Error decoding token:', error);
return null;
}
}
/**
* Extracts roles from a decoded token based on the provided path.
* @param {Object} decodedToken
* @param {string} parameterPath
* @returns {string[]}
*/
function extractRolesFromToken(decodedToken, parameterPath) {
if (!decodedToken) {
return [];
}
const roles = parameterPath.split('.').reduce((obj, key) => (obj?.[key] ?? null), decodedToken);
if (!Array.isArray(roles)) {
logger.error('[openidStrategy] extractRolesFromToken: Roles extracted from token are not in array format.');
return [];
}
return roles;
}
async function setupOpenId() {
try {
if (process.env.PROXY) {
@ -136,6 +177,7 @@ async function setupOpenId() {
const requiredRole = process.env.OPENID_REQUIRED_ROLE;
const requiredRoleParameterPath = process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH;
const requiredRoleTokenKind = process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND;
const adminRole = process.env.OPENID_ADMIN_ROLE;
const openidLogin = new OpenIDStrategy(
{
client,
@ -194,6 +236,30 @@ async function setupOpenId() {
}
}
let customOpenIdData = new Map();
if (process.env.OPENID_CUSTOM_DATA) {
const dataMapper = OpenIdDataMapper.getMapper(process.env.OPENID_PROVIDER.toLowerCase());
customOpenIdData = await dataMapper.mapCustomData(tokenset.access_token, process.env.OPENID_CUSTOM_DATA);
const tokenBasedRoles =
requiredRole &&
extractRolesFromToken(
safeDecode(requiredRoleTokenKind === 'access' ? tokenset.access_token : tokenset.id_token),
requiredRoleParameterPath,
);
if (tokenBasedRoles && tokenBasedRoles.length) {
customOpenIdData.set('roles', tokenBasedRoles);
} else {
logger.warn('[openidStrategy] tokenBasedRoles is missing or invalid.');
}
}
const token = requiredRoleTokenKind === 'access' ? tokenset.access_token : tokenset.id_token;
const decodedToken = safeDecode(token);
const tokenBasedRoles = extractRolesFromToken(decodedToken, requiredRoleParameterPath);
const isAdmin = tokenBasedRoles.includes(adminRole);
const assignedRole = isAdmin ? SystemRoles.ADMIN : SystemRoles.USER;
logger.debug(`[openidStrategy] Assigned system role: ${assignedRole} (isAdmin: ${isAdmin})`);
let username = '';
if (process.env.OPENID_USERNAME_CLAIM) {
username = userinfo[process.env.OPENID_USERNAME_CLAIM];
@ -211,6 +277,8 @@ async function setupOpenId() {
email: userinfo.email || '',
emailVerified: userinfo.email_verified || false,
name: fullName,
role: assignedRole,
customOpenIdData: customOpenIdData,
};
user = await createUser(user, true, true);
} else {
@ -218,6 +286,8 @@ async function setupOpenId() {
user.openidId = userinfo.sub;
user.username = username;
user.name = fullName;
user.role = assignedRole;
user.customOpenIdData = customOpenIdData;
}
if (userinfo.picture && !user.avatar?.includes('manual=true')) {
@ -271,4 +341,4 @@ async function setupOpenId() {
}
}
module.exports = setupOpenId;
module.exports = setupOpenId;