🥇 fix: Send First OpenID Audience on Authorization Requests (#13694)

This commit is contained in:
Danny Avila
2026-06-11 13:21:54 -04:00
committed by GitHub
parent 788cc5ac07
commit 731a7c57c1
3 changed files with 56 additions and 11 deletions

View File

@@ -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

View File

@@ -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}`,
);
}

View File

@@ -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();