📧 fix: Missing Email fallback in openIdJwtLogin (#9311)

* 📧 fix: Missing Email fallback in `openIdJwtLogin`

* chore: Add auth module export to index
This commit is contained in:
Danny Avila 2025-08-27 12:59:40 -04:00 committed by GitHub
parent 48f6f8f2f8
commit 78d735f35c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 109 additions and 26 deletions

View file

@ -1,10 +1,11 @@
const { SystemRoles } = require('librechat-data-provider');
const { HttpsProxyAgent } = require('https-proxy-agent');
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
const { updateUser, findUser } = require('~/models');
const { logger } = require('~/config');
const jwksRsa = require('jwks-rsa'); const jwksRsa = require('jwks-rsa');
const { isEnabled } = require('~/server/utils'); const { logger } = require('@librechat/data-schemas');
const { HttpsProxyAgent } = require('https-proxy-agent');
const { SystemRoles } = require('librechat-data-provider');
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
const { isEnabled, findOpenIDUser } = require('@librechat/api');
const { updateUser, findUser } = require('~/models');
/** /**
* @function openIdJwtLogin * @function openIdJwtLogin
* @param {import('openid-client').Configuration} openIdConfig - Configuration object for the JWT strategy. * @param {import('openid-client').Configuration} openIdConfig - Configuration object for the JWT strategy.
@ -13,6 +14,14 @@ const { isEnabled } = require('~/server/utils');
* It uses the jwks-rsa library to retrieve the signing key from a JWKS endpoint. * It uses the jwks-rsa library to retrieve the signing key from a JWKS endpoint.
* The strategy extracts the JWT from the Authorization header as a Bearer token. * The strategy extracts the JWT from the Authorization header as a Bearer token.
* The JWT is then verified using the signing key, and the user is retrieved from the database. * The JWT is then verified using the signing key, and the user is retrieved from the database.
*
* Includes email fallback mechanism:
* 1. Primary lookup: Search user by openidId (sub claim)
* 2. Fallback lookup: If not found, search by email claim
* 3. User migration: If found by email without openidId, migrate the user by adding openidId
* 4. Provider validation: Ensures users registered with other providers cannot use OpenID
*
* This enables seamless migration for existing users when SharePoint integration is enabled.
*/ */
const openIdJwtLogin = (openIdConfig) => { const openIdJwtLogin = (openIdConfig) => {
let jwksRsaOptions = { let jwksRsaOptions = {
@ -34,19 +43,41 @@ const openIdJwtLogin = (openIdConfig) => {
}, },
async (payload, done) => { async (payload, done) => {
try { try {
const user = await findUser({ openidId: payload?.sub }); const { user, error, migration } = await findOpenIDUser({
openidId: payload?.sub,
email: payload?.email,
strategyName: 'openIdJwtLogin',
findUser,
});
if (error) {
done(null, false, { message: error });
return;
}
if (user) { if (user) {
user.id = user._id.toString(); user.id = user._id.toString();
const updateData = {};
if (migration) {
updateData.provider = 'openid';
updateData.openidId = payload?.sub;
}
if (!user.role) { if (!user.role) {
user.role = SystemRoles.USER; user.role = SystemRoles.USER;
await updateUser(user.id, { role: user.role }); updateData.role = user.role;
} }
if (Object.keys(updateData).length > 0) {
await updateUser(user.id, updateData);
}
done(null, user); done(null, user);
} else { } else {
logger.warn( logger.warn(
'[openIdJwtLogin] openId JwtStrategy => no user found with the sub claims: ' + '[openIdJwtLogin] openId JwtStrategy => no user found with the sub claims: ' +
payload?.sub, payload?.sub +
(payload?.email ? ' or email: ' + payload.email : ''),
); );
done(null, false); done(null, false);
} }

View file

@ -7,7 +7,13 @@ const { HttpsProxyAgent } = require('https-proxy-agent');
const { hashToken, logger } = require('@librechat/data-schemas'); const { hashToken, logger } = require('@librechat/data-schemas');
const { CacheKeys, ErrorTypes } = require('librechat-data-provider'); const { CacheKeys, ErrorTypes } = require('librechat-data-provider');
const { Strategy: OpenIDStrategy } = require('openid-client/passport'); const { Strategy: OpenIDStrategy } = require('openid-client/passport');
const { isEnabled, logHeaders, safeStringify, getBalanceConfig } = require('@librechat/api'); const {
isEnabled,
logHeaders,
safeStringify,
findOpenIDUser,
getBalanceConfig,
} = require('@librechat/api');
const { getStrategyFunctions } = require('~/server/services/Files/strategies'); const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { findUser, createUser, updateUser } = require('~/models'); const { findUser, createUser, updateUser } = require('~/models');
const { getAppConfig } = require('~/server/services/Config'); const { getAppConfig } = require('~/server/services/Config');
@ -333,23 +339,16 @@ async function setupOpenId() {
async (tokenset, done) => { async (tokenset, done) => {
try { try {
const claims = tokenset.claims(); const claims = tokenset.claims();
let user = await findUser({ openidId: claims.sub }); const result = await findOpenIDUser({
logger.info( openidId: claims.sub,
`[openidStrategy] user ${user ? 'found' : 'not found'} with openidId: ${claims.sub}`, email: claims.email,
); strategyName: 'openidStrategy',
findUser,
});
let user = result.user;
const error = result.error;
if (!user) { if (error) {
user = await findUser({ email: claims.email });
logger.info(
`[openidStrategy] user ${user ? 'found' : 'not found'} with email: ${
claims.email
} for openidId: ${claims.sub}`,
);
}
if (user != null && user.provider !== 'openid') {
logger.info(
`[openidStrategy] Attempted OpenID login by user ${user.email}, was registered with "${user.provider}" provider`,
);
return done(null, false, { return done(null, false, {
message: ErrorTypes.AUTH_FAILED, message: ErrorTypes.AUTH_FAILED,
}); });

View file

@ -31,6 +31,7 @@ jest.mock('@librechat/data-schemas', () => ({
...jest.requireActual('@librechat/api'), ...jest.requireActual('@librechat/api'),
logger: { logger: {
info: jest.fn(), info: jest.fn(),
warn: jest.fn(),
debug: jest.fn(), debug: jest.fn(),
error: jest.fn(), error: jest.fn(),
}, },

View file

@ -0,0 +1 @@
export * from './openid';

View file

@ -0,0 +1,49 @@
import { logger } from '@librechat/data-schemas';
import type { IUser, UserMethods } from '@librechat/data-schemas';
/**
* Finds or migrates a user for OpenID authentication
* @returns user object (with migration fields if needed), error message, and whether migration is needed
*/
export async function findOpenIDUser({
openidId,
email,
findUser,
strategyName = 'openid',
}: {
openidId: string;
findUser: UserMethods['findUser'];
email?: string;
strategyName?: string;
}): Promise<{ user: IUser | null; error: string | null; migration: boolean }> {
let user = await findUser({ openidId });
logger.info(`[${strategyName}] user ${user ? 'found' : 'not found'} with openidId: ${openidId}`);
// If user not found by openidId, try to find by email
if (!user && email) {
user = await findUser({ email });
logger.warn(
`[${strategyName}] user ${user ? 'found' : 'not found'} with email: ${email} for openidId: ${openidId}`,
);
// If user found by email, check if they're allowed to use OpenID provider
if (user && user.provider && user.provider !== 'openid') {
logger.warn(
`[${strategyName}] Attempted OpenID login by user ${user.email}, was registered with "${user.provider}" provider`,
);
return { user: null, error: 'AUTH_FAILED', migration: false };
}
// If user found by email but doesn't have openidId, prepare for migration
if (user && !user.openidId) {
logger.info(
`[${strategyName}] Preparing user ${user.email} for migration to OpenID with sub: ${openidId}`,
);
user.provider = 'openid';
user.openidId = openidId;
return { user, error: null, migration: true };
}
}
return { user, error: null, migration: false };
}

View file

@ -1,4 +1,6 @@
export * from './app'; export * from './app';
/* Auth */
export * from './auth';
/* MCP */ /* MCP */
export * from './mcp/MCPManager'; export * from './mcp/MCPManager';
export * from './mcp/connection'; export * from './mcp/connection';