🪪 feat: Accept Multiple OpenID JWT Audiences (#13404)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions

Adapted from ClickHouse/LibreChat@2e8f9f541d.

Co-authored-by: Alexey Korepanov <alexey.korepanov@clickhouse.com>
This commit is contained in:
Danny Avila 2026-05-29 16:45:41 -04:00 • committed by GitHub
parent a14344ded7
commit a16f08a425
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 51 additions and 1 deletions

View file

@ -15,7 +15,11 @@ const {
const { updateUser, findUser } = require('~/models');
const getOpenIdJwtAudience = () => {
const audiences = [process.env.OPENID_CLIENT_ID, process.env.OPENID_AUDIENCE].filter(Boolean);
const parsedAudience = (process.env.OPENID_AUDIENCE ?? '')
.split(',')
.map((value) => value.trim())
.filter(Boolean);
const audiences = [process.env.OPENID_CLIENT_ID, ...parsedAudience].filter(Boolean);
const uniqueAudiences = [...new Set(audiences)];
return uniqueAudiences.length > 1 ? uniqueAudiences : uniqueAudiences[0];

View file

@ -119,6 +119,52 @@ describe('openIdJwtStrategy – token validation', () => {
});
});
it('uses a single OPENID_AUDIENCE value when no client ID is configured', () => {
withEnv({ OPENID_CLIENT_ID: undefined, OPENID_AUDIENCE: 'librechat' }, () => {
openIdJwtLogin(mockOpenIdConfig);
});
expect(capturedStrategyOptions.audience).toBe('librechat');
});
it('splits comma-separated OPENID_AUDIENCE values into multiple accepted audiences', () => {
withEnv({ OPENID_CLIENT_ID: undefined, OPENID_AUDIENCE: 'librechat,control-plane-web' }, () => {
openIdJwtLogin(mockOpenIdConfig);
});
expect(capturedStrategyOptions.audience).toEqual(['librechat', 'control-plane-web']);
});
it('trims whitespace around comma-separated OPENID_AUDIENCE values', () => {
withEnv(
{ OPENID_CLIENT_ID: undefined, OPENID_AUDIENCE: ' librechat , control-plane-web ' },
() => {
openIdJwtLogin(mockOpenIdConfig);
},
);
expect(capturedStrategyOptions.audience).toEqual(['librechat', 'control-plane-web']);
});
it('falls back to OPENID_CLIENT_ID when OPENID_AUDIENCE is empty', () => {
withEnv({ OPENID_CLIENT_ID: 'client-id-only', OPENID_AUDIENCE: '' }, () => {
openIdJwtLogin(mockOpenIdConfig);
});
expect(capturedStrategyOptions.audience).toBe('client-id-only');
});
it('combines OPENID_CLIENT_ID with comma-separated OPENID_AUDIENCE values and deduplicates', () => {
withEnv(
{ OPENID_CLIENT_ID: 'librechat', OPENID_AUDIENCE: 'librechat,control-plane-web' },
() => {
openIdJwtLogin(mockOpenIdConfig);
},
);
expect(capturedStrategyOptions.audience).toEqual(['librechat', 'control-plane-web']);
});
it('rejects OpenID JWTs whose issuer does not match the configured issuer', async () => {
findOpenIDUser.mockResolvedValue({ user: null, error: null, migration: false });
openIdJwtLogin(mockOpenIdConfig);