mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +02:00
📧 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:
parent
48f6f8f2f8
commit
78d735f35c
6 changed files with 109 additions and 26 deletions
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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(),
|
||||||
},
|
},
|
||||||
|
|
1
packages/api/src/auth/index.ts
Normal file
1
packages/api/src/auth/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './openid';
|
49
packages/api/src/auth/openid.ts
Normal file
49
packages/api/src/auth/openid.ts
Normal 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 };
|
||||||
|
}
|
|
@ -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';
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue