2025-06-09 11:27:23 -04:00
const undici = require ( 'undici' ) ;
2025-10-09 08:35:22 +01:00
const { get } = require ( 'lodash' ) ;
2024-05-17 13:03:31 -05:00
const fetch = require ( 'node-fetch' ) ;
2023-12-14 07:49:27 -05:00
const passport = require ( 'passport' ) ;
2025-05-30 22:18:13 -04:00
const client = require ( 'openid-client' ) ;
2024-04-02 03:08:17 -04:00
const jwtDecode = require ( 'jsonwebtoken/decode' ) ;
2024-06-15 15:41:34 +02:00
const { HttpsProxyAgent } = require ( 'https-proxy-agent' ) ;
2025-05-30 22:18:13 -04:00
const { hashToken , logger } = require ( '@librechat/data-schemas' ) ;
2025-05-22 14:19:24 +02:00
const { Strategy : OpenIDStrategy } = require ( 'openid-client/passport' ) ;
2026-01-11 14:46:23 -05:00
const { CacheKeys , ErrorTypes , SystemRoles } = require ( 'librechat-data-provider' ) ;
2025-08-27 12:59:40 -04:00
const {
isEnabled ,
logHeaders ,
safeStringify ,
findOpenIDUser ,
getBalanceConfig ,
2025-09-27 21:20:19 -04:00
isEmailDomainAllowed ,
2025-08-27 12:59:40 -04:00
} = require ( '@librechat/api' ) ;
2024-04-19 09:12:55 -04:00
const { getStrategyFunctions } = require ( '~/server/services/Files/strategies' ) ;
2025-05-30 22:18:13 -04:00
const { findUser , createUser , updateUser } = require ( '~/models' ) ;
2025-08-26 12:10:18 -04:00
const { getAppConfig } = require ( '~/server/services/Config' ) ;
2025-05-22 14:19:24 +02:00
const getLogStores = require ( '~/cache/getLogStores' ) ;
2023-06-24 21:45:52 -05:00
2025-05-22 14:19:24 +02:00
/ * *
* @ typedef { import ( 'openid-client' ) . ClientMetadata } ClientMetadata
* @ typedef { import ( 'openid-client' ) . Configuration } Configuration
* * /
2025-06-09 11:27:23 -04:00
/ * *
* @ param { string } url
* @ param { client . CustomFetchOptions } options
* /
async function customFetch ( url , options ) {
const urlStr = url . toString ( ) ;
logger . debug ( ` [openidStrategy] Request to: ${ urlStr } ` ) ;
const debugOpenId = isEnabled ( process . env . DEBUG _OPENID _REQUESTS ) ;
if ( debugOpenId ) {
logger . debug ( ` [openidStrategy] Request method: ${ options . method || 'GET' } ` ) ;
logger . debug ( ` [openidStrategy] Request headers: ${ logHeaders ( options . headers ) } ` ) ;
if ( options . body ) {
let bodyForLogging = '' ;
if ( options . body instanceof URLSearchParams ) {
bodyForLogging = options . body . toString ( ) ;
} else if ( typeof options . body === 'string' ) {
bodyForLogging = options . body ;
} else {
bodyForLogging = safeStringify ( options . body ) ;
}
logger . debug ( ` [openidStrategy] Request body: ${ bodyForLogging } ` ) ;
}
}
try {
/** @type {undici.RequestInit} */
let fetchOptions = options ;
if ( process . env . PROXY ) {
logger . info ( ` [openidStrategy] proxy agent configured: ${ process . env . PROXY } ` ) ;
fetchOptions = {
... options ,
2025-07-01 22:30:06 +02:00
dispatcher : new undici . ProxyAgent ( process . env . PROXY ) ,
2025-06-09 11:27:23 -04:00
} ;
}
const response = await undici . fetch ( url , fetchOptions ) ;
if ( debugOpenId ) {
logger . debug ( ` [openidStrategy] Response status: ${ response . status } ${ response . statusText } ` ) ;
logger . debug ( ` [openidStrategy] Response headers: ${ logHeaders ( response . headers ) } ` ) ;
}
if ( response . status === 200 && response . headers . has ( 'www-authenticate' ) ) {
const wwwAuth = response . headers . get ( 'www-authenticate' ) ;
logger . warn ( ` [openidStrategy] Non-standard WWW-Authenticate header found in successful response (200 OK): ${ wwwAuth } .
This violates RFC 7235 and may cause issues with strict OAuth clients . Removing header for compatibility . ` );
/** Cloned response without the WWW-Authenticate header */
const responseBody = await response . arrayBuffer ( ) ;
const newHeaders = new Headers ( ) ;
for ( const [ key , value ] of response . headers . entries ( ) ) {
if ( key . toLowerCase ( ) !== 'www-authenticate' ) {
newHeaders . append ( key , value ) ;
}
}
return new Response ( responseBody , {
status : response . status ,
statusText : response . statusText ,
headers : newHeaders ,
} ) ;
}
return response ;
} catch ( error ) {
logger . error ( ` [openidStrategy] Fetch error: ${ error . message } ` ) ;
throw error ;
}
}
2025-05-22 14:19:24 +02:00
/** @typedef {Configuration | null} */
let openidConfig = null ;
2025-11-25 17:01:19 -05:00
/ * *
* Custom OpenID Strategy
*
* Note : Originally overrode currentUrl ( ) to work around Express 4 ' s req . host not including port .
* With Express 5 , req . host now includes the port by default , but we continue to use DOMAIN _SERVER
* for consistency and explicit configuration control .
* More info : https : //github.com/panva/openid-client/pull/713
* /
2025-05-22 14:19:24 +02:00
class CustomOpenIDStrategy extends OpenIDStrategy {
currentUrl ( req ) {
const hostAndProtocol = process . env . DOMAIN _SERVER ;
return new URL ( ` ${ hostAndProtocol } ${ req . originalUrl ? ? req . url } ` ) ;
}
2025-08-16 17:14:01 +00:00
2025-05-25 23:40:37 -04:00
authorizationRequestParams ( req , options ) {
const params = super . authorizationRequestParams ( req , options ) ;
if ( options ? . state && ! params . has ( 'state' ) ) {
params . set ( 'state' , options . state ) ;
}
2025-08-05 02:49:36 +08:00
if ( process . env . OPENID _AUDIENCE ) {
params . set ( 'audience' , process . env . OPENID _AUDIENCE ) ;
logger . debug (
` [openidStrategy] Adding audience to authorization request: ${ process . env . OPENID _AUDIENCE } ` ,
) ;
}
2025-08-16 17:14:01 +00:00
/** Generate nonce for federated providers that require it */
const shouldGenerateNonce = isEnabled ( process . env . OPENID _GENERATE _NONCE ) ;
if ( shouldGenerateNonce && ! params . has ( 'nonce' ) && this . _sessionKey ) {
const crypto = require ( 'crypto' ) ;
const nonce = crypto . randomBytes ( 16 ) . toString ( 'hex' ) ;
params . set ( 'nonce' , nonce ) ;
logger . debug ( '[openidStrategy] Generated nonce for federated provider:' , nonce ) ;
}
2025-05-25 23:40:37 -04:00
return params ;
}
2023-06-24 21:45:52 -05:00
}
2024-10-27 11:41:48 -04:00
2025-05-22 14:19:24 +02:00
/ * *
* Exchange the access token for a new access token using the on - behalf - of flow if required .
* @ param { Configuration } config
* @ param { string } accessToken access token to be exchanged if necessary
* @ param { string } sub - The subject identifier of the user . usually found as "sub" in the claims of the token
* @ param { boolean } fromCache - Indicates whether to use cached tokens .
* @ returns { Promise < string > } The new access token if exchanged , otherwise the original access token .
* /
const exchangeAccessTokenIfNeeded = async ( config , accessToken , sub , fromCache = false ) => {
const tokensCache = getLogStores ( CacheKeys . OPENID _EXCHANGED _TOKENS ) ;
2025-06-26 19:10:21 -04:00
const onBehalfFlowRequired = isEnabled ( process . env . OPENID _ON _BEHALF _FLOW _FOR _USERINFO _REQUIRED ) ;
2025-05-22 14:19:24 +02:00
if ( onBehalfFlowRequired ) {
if ( fromCache ) {
const cachedToken = await tokensCache . get ( sub ) ;
if ( cachedToken ) {
return cachedToken . access _token ;
}
}
const grantResponse = await client . genericGrantRequest (
config ,
'urn:ietf:params:oauth:grant-type:jwt-bearer' ,
{
2025-06-26 19:10:21 -04:00
scope : process . env . OPENID _ON _BEHALF _FLOW _USERINFO _SCOPE || 'user.read' ,
2025-05-22 14:19:24 +02:00
assertion : accessToken ,
requested _token _use : 'on_behalf_of' ,
} ,
) ;
await tokensCache . set (
sub ,
{
access _token : grantResponse . access _token ,
} ,
grantResponse . expires _in * 1000 ,
) ;
return grantResponse . access _token ;
}
return accessToken ;
} ;
/ * *
* get user info from openid provider
* @ param { Configuration } config
* @ param { string } accessToken access token
* @ param { string } sub - The subject identifier of the user . usually found as "sub" in the claims of the token
* @ returns { Promise < Object | null > }
* /
const getUserInfo = async ( config , accessToken , sub ) => {
try {
const exchangedAccessToken = await exchangeAccessTokenIfNeeded ( config , accessToken , sub ) ;
return await client . fetchUserInfo ( config , exchangedAccessToken , sub ) ;
} catch ( error ) {
2025-09-05 03:12:17 -04:00
logger . error ( '[openidStrategy] getUserInfo: Error fetching user info:' , error ) ;
2025-05-22 14:19:24 +02:00
return null ;
}
} ;
2024-04-19 09:12:55 -04:00
/ * *
* Downloads an image from a URL using an access token .
* @ param { string } url
2025-05-22 14:19:24 +02:00
* @ param { Configuration } config
* @ param { string } accessToken access token
* @ param { string } sub - The subject identifier of the user . usually found as "sub" in the claims of the token
* @ returns { Promise < Buffer | string > } The image buffer or an empty string if the download fails .
2024-04-19 09:12:55 -04:00
* /
2025-05-22 14:19:24 +02:00
const downloadImage = async ( url , config , accessToken , sub ) => {
const exchangedAccessToken = await exchangeAccessTokenIfNeeded ( config , accessToken , sub , true ) ;
2024-04-19 09:12:55 -04:00
if ( ! url ) {
return '' ;
}
2023-06-24 21:45:52 -05:00
try {
2024-07-05 17:23:06 +03:00
const options = {
2024-04-19 09:12:55 -04:00
method : 'GET' ,
2023-06-24 21:45:52 -05:00
headers : {
2025-05-22 14:19:24 +02:00
Authorization : ` Bearer ${ exchangedAccessToken } ` ,
2023-06-24 21:45:52 -05:00
} ,
2024-07-05 17:23:06 +03:00
} ;
if ( process . env . PROXY ) {
options . agent = new HttpsProxyAgent ( process . env . PROXY ) ;
}
const response = await fetch ( url , options ) ;
2023-07-14 09:36:49 -04:00
2024-04-19 09:12:55 -04:00
if ( response . ok ) {
const buffer = await response . buffer ( ) ;
return buffer ;
} else {
throw new Error ( ` ${ response . statusText } (HTTP ${ response . status } ) ` ) ;
}
2023-06-24 21:45:52 -05:00
} catch ( error ) {
2023-12-14 07:49:27 -05:00
logger . error (
` [openidStrategy] downloadImage: Error downloading image at URL " ${ url } ": ${ error } ` ,
) ;
2023-06-24 21:45:52 -05:00
return '' ;
}
} ;
2024-10-27 11:41:48 -04:00
/ * *
* Determines the full name of a user based on OpenID userinfo and environment configuration .
*
* @ param { Object } userinfo - The user information object from OpenID Connect
* @ param { string } [ userinfo . given _name ] - The user ' s first name
* @ param { string } [ userinfo . family _name ] - The user ' s last name
* @ param { string } [ userinfo . username ] - The user ' s username
* @ param { string } [ userinfo . email ] - The user ' s email address
* @ returns { string } The determined full name of the user
* /
function getFullName ( userinfo ) {
if ( process . env . OPENID _NAME _CLAIM ) {
return userinfo [ process . env . OPENID _NAME _CLAIM ] ;
}
if ( userinfo . given _name && userinfo . family _name ) {
return ` ${ userinfo . given _name } ${ userinfo . family _name } ` ;
}
if ( userinfo . given _name ) {
return userinfo . given _name ;
}
if ( userinfo . family _name ) {
return userinfo . family _name ;
}
return userinfo . username || userinfo . email ;
}
2026-02-26 04:31:03 +01:00
/ * *
* Resolves the user identifier from OpenID claims .
* Configurable via OPENID _EMAIL _CLAIM ; defaults to : email - > preferred _username - > upn .
*
* @ param { Object } userinfo - The user information object from OpenID Connect
* @ returns { string | undefined } The resolved identifier string
* /
function getOpenIdEmail ( userinfo ) {
const claimKey = process . env . OPENID _EMAIL _CLAIM ? . trim ( ) ;
if ( claimKey ) {
const value = userinfo [ claimKey ] ;
if ( typeof value === 'string' && value ) {
return value ;
}
if ( value !== undefined && value !== null ) {
logger . warn (
` [openidStrategy] OPENID_EMAIL_CLAIM=" ${ claimKey } " resolved to a non-string value (type: ${ typeof value } ). Falling back to: email -> preferred_username -> upn. ` ,
) ;
} else {
logger . warn (
` [openidStrategy] OPENID_EMAIL_CLAIM=" ${ claimKey } " not present in userinfo. Falling back to: email -> preferred_username -> upn. ` ,
) ;
}
}
const fallback = userinfo . email || userinfo . preferred _username || userinfo . upn ;
return typeof fallback === 'string' ? fallback : undefined ;
}
2024-04-12 12:39:11 -04:00
/ * *
* Converts an input into a string suitable for a username .
* If the input is a string , it will be returned as is .
* If the input is an array , elements will be joined with underscores .
* In case of undefined or other falsy values , a default value will be returned .
*
* @ param { string | string [ ] | undefined } input - The input value to be converted into a username .
* @ param { string } [ defaultValue = '' ] - The default value to return if the input is falsy .
* @ returns { string } The processed input as a string suitable for a username .
* /
function convertToUsername ( input , defaultValue = '' ) {
if ( typeof input === 'string' ) {
return input ;
} else if ( Array . isArray ( input ) ) {
return input . join ( '_' ) ;
}
return defaultValue ;
}
2026-02-12 04:11:05 +01:00
/ * *
* Resolve Azure AD groups when group overage is in effect ( groups moved to _claim _names / _claim _sources ) .
*
* NOTE : Microsoft recommends treating _claim _names / _claim _sources as a signal only and using Microsoft Graph
* to resolve group membership instead of calling the endpoint in _claim _sources directly .
*
* @ param { string } accessToken - Access token with Microsoft Graph permissions
* @ returns { Promise < string [ ] | null > } Resolved group IDs or null on failure
* @ see https : //learn.microsoft.com/en-us/entra/identity-platform/access-token-claims-reference#groups-overage-claim
* @ see https : //learn.microsoft.com/en-us/graph/api/directoryobject-getmemberobjects
* /
async function resolveGroupsFromOverage ( accessToken ) {
try {
if ( ! accessToken ) {
logger . error ( '[openidStrategy] Access token missing; cannot resolve group overage' ) ;
return null ;
}
// Use /me/getMemberObjects so least-privileged delegated permission User.Read is sufficient
// when resolving the signed-in user's group membership.
const url = 'https://graph.microsoft.com/v1.0/me/getMemberObjects' ;
logger . debug (
` [openidStrategy] Detected group overage, resolving groups via Microsoft Graph getMemberObjects: ${ url } ` ,
) ;
const fetchOptions = {
method : 'POST' ,
headers : {
Authorization : ` Bearer ${ accessToken } ` ,
'Content-Type' : 'application/json' ,
} ,
body : JSON . stringify ( { securityEnabledOnly : false } ) ,
} ;
if ( process . env . PROXY ) {
const { ProxyAgent } = undici ;
fetchOptions . dispatcher = new ProxyAgent ( process . env . PROXY ) ;
}
const response = await undici . fetch ( url , fetchOptions ) ;
if ( ! response . ok ) {
logger . error (
` [openidStrategy] Failed to resolve groups via Microsoft Graph getMemberObjects: HTTP ${ response . status } ${ response . statusText } ` ,
) ;
return null ;
}
const data = await response . json ( ) ;
const values = Array . isArray ( data ? . value ) ? data . value : null ;
if ( ! values ) {
logger . error (
'[openidStrategy] Unexpected response format when resolving groups via Microsoft Graph getMemberObjects' ,
) ;
return null ;
}
const groupIds = values . filter ( ( id ) => typeof id === 'string' ) ;
logger . debug (
` [openidStrategy] Successfully resolved ${ groupIds . length } groups via Microsoft Graph getMemberObjects ` ,
) ;
return groupIds ;
} catch ( err ) {
logger . error (
'[openidStrategy] Error resolving groups via Microsoft Graph getMemberObjects:' ,
err ,
) ;
return null ;
}
}
2026-01-11 14:46:23 -05:00
/ * *
* Process OpenID authentication tokenset and userinfo
* This is the core logic extracted from the passport strategy callback
* Can be reused by both the passport strategy and proxy authentication
*
* @ param { Object } tokenset - The OpenID tokenset containing access _token , id _token , etc .
* @ param { boolean } existingUsersOnly - If true , only existing users will be processed
* @ returns { Promise < Object > } The authenticated user object with tokenset
* /
async function processOpenIDAuth ( tokenset , existingUsersOnly = false ) {
const claims = tokenset . claims ? tokenset . claims ( ) : tokenset ;
const userinfo = {
... claims ,
} ;
if ( tokenset . access _token ) {
const providerUserinfo = await getUserInfo ( openidConfig , tokenset . access _token , claims . sub ) ;
Object . assign ( userinfo , providerUserinfo ) ;
}
const appConfig = await getAppConfig ( ) ;
2026-02-26 04:31:03 +01:00
const email = getOpenIdEmail ( userinfo ) ;
2026-01-11 14:46:23 -05:00
if ( ! isEmailDomainAllowed ( email , appConfig ? . registration ? . allowedDomains ) ) {
logger . error (
2026-02-26 04:31:03 +01:00
` [OpenID Strategy] Authentication blocked - email domain not allowed [Identifier: ${ email } ] ` ,
2026-01-11 14:46:23 -05:00
) ;
throw new Error ( 'Email domain not allowed' ) ;
}
const result = await findOpenIDUser ( {
findUser ,
email : email ,
openidId : claims . sub || userinfo . sub ,
idOnTheSource : claims . oid || userinfo . oid ,
strategyName : 'openidStrategy' ,
} ) ;
let user = result . user ;
const error = result . error ;
if ( error ) {
throw new Error ( ErrorTypes . AUTH _FAILED ) ;
}
const fullName = getFullName ( userinfo ) ;
const requiredRole = process . env . OPENID _REQUIRED _ROLE ;
if ( requiredRole ) {
const requiredRoles = requiredRole
. split ( ',' )
. map ( ( role ) => role . trim ( ) )
. filter ( Boolean ) ;
const requiredRoleParameterPath = process . env . OPENID _REQUIRED _ROLE _PARAMETER _PATH ;
const requiredRoleTokenKind = process . env . OPENID _REQUIRED _ROLE _TOKEN _KIND ;
let decodedToken = '' ;
if ( requiredRoleTokenKind === 'access' && tokenset . access _token ) {
decodedToken = jwtDecode ( tokenset . access _token ) ;
} else if ( requiredRoleTokenKind === 'id' && tokenset . id _token ) {
decodedToken = jwtDecode ( tokenset . id _token ) ;
}
let roles = get ( decodedToken , requiredRoleParameterPath ) ;
2026-02-12 04:11:05 +01:00
// Handle Azure AD group overage for ID token groups: when hasgroups or _claim_* indicate overage,
// resolve groups via Microsoft Graph instead of relying on token group values.
if (
! Array . isArray ( roles ) &&
typeof roles !== 'string' &&
requiredRoleTokenKind === 'id' &&
requiredRoleParameterPath === 'groups' &&
decodedToken &&
( decodedToken . hasgroups ||
( decodedToken . _claim _names ? . groups &&
decodedToken . _claim _sources ? . [ decodedToken . _claim _names . groups ] ) )
) {
const overageGroups = await resolveGroupsFromOverage ( tokenset . access _token ) ;
if ( overageGroups ) {
roles = overageGroups ;
}
}
2026-01-11 14:46:23 -05:00
if ( ! roles || ( ! Array . isArray ( roles ) && typeof roles !== 'string' ) ) {
logger . error (
` [openidStrategy] Key ' ${ requiredRoleParameterPath } ' not found in ${ requiredRoleTokenKind } token! ` ,
) ;
const rolesList =
requiredRoles . length === 1
? ` " ${ requiredRoles [ 0 ] } " `
: ` one of: ${ requiredRoles . map ( ( r ) => ` " ${ r } " ` ) . join ( ', ' ) } ` ;
throw new Error ( ` You must have ${ rolesList } role to log in. ` ) ;
}
2026-02-21 18:06:02 -05:00
const roleValues = Array . isArray ( roles ) ? roles : roles . split ( /[\s,]+/ ) . filter ( Boolean ) ;
2026-02-12 04:11:05 +01:00
if ( ! requiredRoles . some ( ( role ) => roleValues . includes ( role ) ) ) {
2026-01-11 14:46:23 -05:00
const rolesList =
requiredRoles . length === 1
? ` " ${ requiredRoles [ 0 ] } " `
: ` one of: ${ requiredRoles . map ( ( r ) => ` " ${ r } " ` ) . join ( ', ' ) } ` ;
throw new Error ( ` You must have ${ rolesList } role to log in. ` ) ;
}
}
let username = '' ;
if ( process . env . OPENID _USERNAME _CLAIM ) {
username = userinfo [ process . env . OPENID _USERNAME _CLAIM ] ;
} else {
username = convertToUsername (
userinfo . preferred _username || userinfo . username || userinfo . email ,
) ;
}
if ( existingUsersOnly && ! user ) {
throw new Error ( 'User does not exist' ) ;
}
if ( ! user ) {
user = {
provider : 'openid' ,
openidId : userinfo . sub ,
username ,
email : email || '' ,
emailVerified : userinfo . email _verified || false ,
name : fullName ,
idOnTheSource : userinfo . oid ,
} ;
const balanceConfig = getBalanceConfig ( appConfig ) ;
user = await createUser ( user , balanceConfig , true , true ) ;
} else {
user . provider = 'openid' ;
user . openidId = userinfo . sub ;
user . username = username ;
user . name = fullName ;
user . idOnTheSource = userinfo . oid ;
if ( email && email !== user . email ) {
user . email = email ;
user . emailVerified = userinfo . email _verified || false ;
}
}
const adminRole = process . env . OPENID _ADMIN _ROLE ;
const adminRoleParameterPath = process . env . OPENID _ADMIN _ROLE _PARAMETER _PATH ;
const adminRoleTokenKind = process . env . OPENID _ADMIN _ROLE _TOKEN _KIND ;
if ( adminRole && adminRoleParameterPath && adminRoleTokenKind ) {
let adminRoleObject ;
switch ( adminRoleTokenKind ) {
case 'access' :
adminRoleObject = jwtDecode ( tokenset . access _token ) ;
break ;
case 'id' :
adminRoleObject = jwtDecode ( tokenset . id _token ) ;
break ;
case 'userinfo' :
adminRoleObject = userinfo ;
break ;
default :
logger . error (
` [openidStrategy] Invalid admin role token kind: ${ adminRoleTokenKind } . Must be one of 'access', 'id', or 'userinfo'. ` ,
) ;
throw new Error ( 'Invalid admin role token kind' ) ;
}
const adminRoles = get ( adminRoleObject , adminRoleParameterPath ) ;
2026-02-21 18:06:02 -05:00
let adminRoleValues = [ ] ;
if ( Array . isArray ( adminRoles ) ) {
adminRoleValues = adminRoles ;
} else if ( typeof adminRoles === 'string' ) {
adminRoleValues = adminRoles . split ( /[\s,]+/ ) . filter ( Boolean ) ;
}
2026-01-11 14:46:23 -05:00
2026-02-21 18:06:02 -05:00
if ( adminRoles && ( adminRoles === true || adminRoleValues . includes ( adminRole ) ) ) {
2026-01-11 14:46:23 -05:00
user . role = SystemRoles . ADMIN ;
logger . info ( ` [openidStrategy] User ${ username } is an admin based on role: ${ adminRole } ` ) ;
} else if ( user . role === SystemRoles . ADMIN ) {
user . role = SystemRoles . USER ;
logger . info (
` [openidStrategy] User ${ username } demoted from admin - role no longer present in token ` ,
) ;
}
}
if ( ! ! userinfo && userinfo . picture && ! user . avatar ? . includes ( 'manual=true' ) ) {
/** @type {string | undefined} */
const imageUrl = userinfo . picture ;
let fileName ;
if ( crypto ) {
fileName = ( await hashToken ( userinfo . sub ) ) + '.png' ;
} else {
fileName = userinfo . sub + '.png' ;
}
const imageBuffer = await downloadImage (
imageUrl ,
openidConfig ,
tokenset . access _token ,
userinfo . sub ,
) ;
if ( imageBuffer ) {
const { saveBuffer } = getStrategyFunctions (
appConfig ? . fileStrategy ? ? process . env . CDN _PROVIDER ,
) ;
const imagePath = await saveBuffer ( {
fileName ,
userId : user . _id . toString ( ) ,
buffer : imageBuffer ,
} ) ;
user . avatar = imagePath ? ? '' ;
}
}
user = await updateUser ( user . _id , user ) ;
logger . info (
` [openidStrategy] login success openidId: ${ user . openidId } | email: ${ user . email } | username: ${ user . username } ` ,
{
user : {
openidId : user . openidId ,
username : user . username ,
email : user . email ,
name : user . name ,
} ,
} ,
) ;
return {
... user ,
tokenset ,
federatedTokens : {
access _token : tokenset . access _token ,
2026-02-13 16:07:39 +00:00
id _token : tokenset . id _token ,
2026-01-11 14:46:23 -05:00
refresh _token : tokenset . refresh _token ,
expires _at : tokenset . expires _at ,
} ,
} ;
}
/ * *
* @ param { boolean | undefined } [ existingUsersOnly ]
* /
function createOpenIDCallback ( existingUsersOnly ) {
return async ( tokenset , done ) => {
try {
const user = await processOpenIDAuth ( tokenset , existingUsersOnly ) ;
done ( null , user ) ;
} catch ( err ) {
if ( err . message === 'Email domain not allowed' ) {
return done ( null , false , { message : err . message } ) ;
}
if ( err . message === ErrorTypes . AUTH _FAILED ) {
return done ( null , false , { message : err . message } ) ;
}
if ( err . message && err . message . includes ( 'role to log in' ) ) {
return done ( null , false , { message : err . message } ) ;
}
logger . error ( '[openidStrategy] login failed' , err ) ;
done ( err ) ;
}
} ;
}
/ * *
* Sets up the OpenID strategy specifically for admin authentication .
* @ param { Configuration } openidConfig
* /
const setupOpenIdAdmin = ( openidConfig ) => {
try {
if ( ! openidConfig ) {
throw new Error ( 'OpenID configuration not initialized' ) ;
}
const openidAdminLogin = new CustomOpenIDStrategy (
{
config : openidConfig ,
scope : process . env . OPENID _SCOPE ,
usePKCE : isEnabled ( process . env . OPENID _USE _PKCE ) ,
clockTolerance : process . env . OPENID _CLOCK _TOLERANCE || 300 ,
callbackURL : process . env . DOMAIN _SERVER + '/api/admin/oauth/openid/callback' ,
} ,
createOpenIDCallback ( true ) ,
) ;
passport . use ( 'openidAdmin' , openidAdminLogin ) ;
} catch ( err ) {
logger . error ( '[openidStrategy] setupOpenIdAdmin' , err ) ;
}
} ;
2025-05-22 14:19:24 +02:00
/ * *
* Sets up the OpenID strategy for authentication .
* This function configures the OpenID client , handles proxy settings ,
* and defines the OpenID strategy for Passport . js .
*
* @ async
* @ function setupOpenId
* @ returns { Promise < Configuration | null > } A promise that resolves when the OpenID strategy is set up and returns the openid client config object .
* @ throws { Error } If an error occurs during the setup process .
* /
2023-07-22 07:29:17 -07:00
async function setupOpenId ( ) {
try {
2025-08-16 17:14:01 +00:00
const shouldGenerateNonce = isEnabled ( process . env . OPENID _GENERATE _NONCE ) ;
2025-05-22 14:19:24 +02:00
/** @type {ClientMetadata} */
2025-01-21 21:49:27 -05:00
const clientMetadata = {
2023-06-24 21:45:52 -05:00
client _id : process . env . OPENID _CLIENT _ID ,
client _secret : process . env . OPENID _CLIENT _SECRET ,
2025-01-21 21:49:27 -05:00
} ;
2025-05-22 14:19:24 +02:00
2025-08-16 17:14:01 +00:00
if ( shouldGenerateNonce ) {
clientMetadata . response _types = [ 'code' ] ;
clientMetadata . grant _types = [ 'authorization_code' ] ;
clientMetadata . token _endpoint _auth _method = 'client_secret_post' ;
}
2025-05-22 14:19:24 +02:00
/** @type {Configuration} */
openidConfig = await client . discovery (
new URL ( process . env . OPENID _ISSUER ) ,
process . env . OPENID _CLIENT _ID ,
clientMetadata ,
2025-06-09 11:27:23 -04:00
undefined ,
{
[ client . customFetch ] : customFetch ,
} ,
2025-05-22 14:19:24 +02:00
) ;
2025-06-09 11:27:23 -04:00
2025-08-16 17:14:01 +00:00
logger . info ( ` [openidStrategy] OpenID authentication configuration ` , {
generateNonce : shouldGenerateNonce ,
reason : shouldGenerateNonce
? 'OPENID_GENERATE_NONCE=true - Will generate nonce and use explicit metadata for federated providers'
: 'OPENID_GENERATE_NONCE=false - Standard flow without explicit nonce or metadata' ,
} ) ;
2025-05-22 14:19:24 +02:00
const openidLogin = new CustomOpenIDStrategy (
2023-06-24 21:45:52 -05:00
{
2025-05-22 14:19:24 +02:00
config : openidConfig ,
scope : process . env . OPENID _SCOPE ,
callbackURL : process . env . DOMAIN _SERVER + process . env . OPENID _CALLBACK _URL ,
2025-08-16 17:14:01 +00:00
clockTolerance : process . env . OPENID _CLOCK _TOLERANCE || 300 ,
2026-01-11 14:46:23 -05:00
usePKCE : isEnabled ( process . env . OPENID _USE _PKCE ) ,
2023-07-14 09:36:49 -04:00
} ,
2026-01-11 14:46:23 -05:00
createOpenIDCallback ( ) ,
2023-06-24 21:45:52 -05:00
) ;
passport . use ( 'openid' , openidLogin ) ;
2026-01-11 14:46:23 -05:00
setupOpenIdAdmin ( openidConfig ) ;
2025-05-22 14:19:24 +02:00
return openidConfig ;
2023-07-22 07:29:17 -07:00
} catch ( err ) {
2023-12-14 07:49:27 -05:00
logger . error ( '[openidStrategy]' , err ) ;
2025-05-22 14:19:24 +02:00
return null ;
2023-07-22 07:29:17 -07:00
}
}
2026-01-11 14:46:23 -05:00
2025-05-22 14:19:24 +02:00
/ * *
* @ function getOpenIdConfig
* @ description Returns the OpenID client instance .
* @ throws { Error } If the OpenID client is not initialized .
* @ returns { Configuration }
* /
function getOpenIdConfig ( ) {
if ( ! openidConfig ) {
throw new Error ( 'OpenID client is not initialized. Please call setupOpenId first.' ) ;
}
return openidConfig ;
}
2023-07-22 07:29:17 -07:00
2025-05-22 14:19:24 +02:00
module . exports = {
setupOpenId ,
getOpenIdConfig ,
2026-02-26 04:31:03 +01:00
getOpenIdEmail ,
2025-05-22 14:19:24 +02:00
} ;