mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-06-15 23:43:06 +03:00
🥇 fix: Send First OpenID Audience on Authorization Requests (#13694)
This commit is contained in:
@@ -640,7 +640,9 @@ OPENID_NAME_CLAIM=
|
||||
# Set to determine which user info claim to use as the email/identifier for user matching (e.g., "upn" for Entra ID)
|
||||
# When not set, defaults to: email -> preferred_username -> upn
|
||||
OPENID_EMAIL_CLAIM=
|
||||
# Optional audience parameter for OpenID authorization requests
|
||||
# Optional audience parameter for OpenID authorization requests and JWT validation.
|
||||
# If comma-separated values are provided, JWT validation accepts all values and
|
||||
# authorization requests use the first non-empty value.
|
||||
OPENID_AUDIENCE=
|
||||
# Optional audience parameter for OpenID refresh token requests.
|
||||
# Some providers, such as Auth0 custom APIs, require this to preserve
|
||||
|
||||
@@ -107,6 +107,12 @@ This violates RFC 7235 and may cause issues with strict OAuth clients. Removing
|
||||
/** @typedef {Configuration | null} */
|
||||
let openidConfig = null;
|
||||
|
||||
const getOpenIdAuthorizationAudience = () =>
|
||||
(process.env.OPENID_AUDIENCE ?? '')
|
||||
.split(',')
|
||||
.map((value) => value.trim())
|
||||
.find(Boolean);
|
||||
|
||||
/**
|
||||
* Custom OpenID Strategy
|
||||
*
|
||||
@@ -127,10 +133,11 @@ class CustomOpenIDStrategy extends OpenIDStrategy {
|
||||
params.set('state', options.state);
|
||||
}
|
||||
|
||||
if (process.env.OPENID_AUDIENCE) {
|
||||
params.set('audience', process.env.OPENID_AUDIENCE);
|
||||
const authorizationAudience = getOpenIdAuthorizationAudience();
|
||||
if (authorizationAudience) {
|
||||
params.set('audience', authorizationAudience);
|
||||
logger.debug(
|
||||
`[openidStrategy] Adding audience to authorization request: ${process.env.OPENID_AUDIENCE}`,
|
||||
`[openidStrategy] Adding audience to authorization request: ${authorizationAudience}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -140,20 +140,28 @@ jest.mock('openid-client', () => {
|
||||
jest.mock('openid-client/passport', () => {
|
||||
/** Store callbacks by strategy name - 'openid' and 'openidAdmin' */
|
||||
const verifyCallbacks = {};
|
||||
const strategies = {};
|
||||
let lastVerifyCallback;
|
||||
|
||||
const mockStrategy = jest.fn((options, verify) => {
|
||||
const mockStrategy = jest.fn(function (options, verify) {
|
||||
lastVerifyCallback = verify;
|
||||
return { name: 'openid', options, verify };
|
||||
this.name = 'openid';
|
||||
this.options = options;
|
||||
this.verify = verify;
|
||||
});
|
||||
mockStrategy.prototype.authorizationRequestParams = jest.fn(() => new URLSearchParams());
|
||||
|
||||
return {
|
||||
Strategy: mockStrategy,
|
||||
/** Get the last registered callback (for backward compatibility) */
|
||||
__getVerifyCallback: () => lastVerifyCallback,
|
||||
__getStrategyByName: (name) => strategies[name],
|
||||
/** Store callback by name when passport.use is called */
|
||||
__setVerifyCallback: (name, callback) => {
|
||||
verifyCallbacks[name] = callback;
|
||||
__setStrategy: (name, strategy) => {
|
||||
strategies[name] = strategy;
|
||||
if (strategy?.verify) {
|
||||
verifyCallbacks[name] = strategy.verify;
|
||||
}
|
||||
},
|
||||
/** Get callback by strategy name */
|
||||
__getVerifyCallbackByName: (name) => verifyCallbacks[name],
|
||||
@@ -164,9 +172,7 @@ jest.mock('openid-client/passport', () => {
|
||||
jest.mock('passport', () => ({
|
||||
use: jest.fn((name, strategy) => {
|
||||
const passportMock = require('openid-client/passport');
|
||||
if (strategy && strategy.verify) {
|
||||
passportMock.__setVerifyCallback(name, strategy.verify);
|
||||
}
|
||||
passportMock.__setStrategy(name, strategy);
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -232,6 +238,7 @@ describe('setupOpenId', () => {
|
||||
delete process.env.OPENID_USERNAME_CLAIM;
|
||||
delete process.env.OPENID_NAME_CLAIM;
|
||||
delete process.env.OPENID_EMAIL_CLAIM;
|
||||
delete process.env.OPENID_AUDIENCE;
|
||||
delete process.env.OPENID_AVATAR_AUTHORIZED_ORIGINS;
|
||||
delete process.env.PROXY;
|
||||
delete process.env.OPENID_USE_PKCE;
|
||||
@@ -337,6 +344,35 @@ describe('setupOpenId', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('authorizationRequestParams', () => {
|
||||
const getLoginStrategy = () => require('openid-client/passport').__getStrategyByName('openid');
|
||||
|
||||
it('adds a single OpenID audience to authorization requests', () => {
|
||||
process.env.OPENID_AUDIENCE = 'librechat';
|
||||
|
||||
const params = getLoginStrategy().authorizationRequestParams({}, { state: 'login-state' });
|
||||
|
||||
expect(params.get('audience')).toBe('librechat');
|
||||
expect(params.get('state')).toBe('login-state');
|
||||
});
|
||||
|
||||
it('uses the first non-empty audience when OPENID_AUDIENCE accepts multiple JWT audiences', () => {
|
||||
process.env.OPENID_AUDIENCE = ' librechat , control-plane-web ';
|
||||
|
||||
const params = getLoginStrategy().authorizationRequestParams({}, {});
|
||||
|
||||
expect(params.get('audience')).toBe('librechat');
|
||||
});
|
||||
|
||||
it('does not add an authorization audience when OPENID_AUDIENCE is empty', () => {
|
||||
process.env.OPENID_AUDIENCE = ' , ';
|
||||
|
||||
const params = getLoginStrategy().authorizationRequestParams({}, {});
|
||||
|
||||
expect(params.has('audience')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a new user with correct username when preferred_username claim exists', async () => {
|
||||
// Arrange – our userinfo already has preferred_username 'testusername'
|
||||
const userinfo = tokenset.claims();
|
||||
|
||||
Reference in New Issue
Block a user