From 33c8dd4e202c7213d4e5673855fb777521c207e3 Mon Sep 17 00:00:00 2001 From: Alexey Korepanov Date: Fri, 29 May 2026 13:32:56 +0200 Subject: [PATCH] feat: accept multiple JWT audiences via OPENID_AUDIENCE Adapted from ClickHouse/LibreChat@2e8f9f541d3616a89c75c8bc91d50fe1cae0cb16. --- api/strategies/openIdJwtStrategy.js | 6 +++- api/strategies/openIdJwtStrategy.spec.js | 46 ++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/api/strategies/openIdJwtStrategy.js b/api/strategies/openIdJwtStrategy.js index 78145f1109b..ab1bcd1c0a1 100644 --- a/api/strategies/openIdJwtStrategy.js +++ b/api/strategies/openIdJwtStrategy.js @@ -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]; diff --git a/api/strategies/openIdJwtStrategy.spec.js b/api/strategies/openIdJwtStrategy.spec.js index e45499734bc..4a1871110c5 100644 --- a/api/strategies/openIdJwtStrategy.spec.js +++ b/api/strategies/openIdJwtStrategy.spec.js @@ -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);