mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-05-13 07:46:47 +00:00
🪪 refactor: Require Remote OIDC Audience for Agents API OAuth (#13066)
This commit is contained in:
parent
7129b1b1e4
commit
52ccb1379b
4 changed files with 61 additions and 6 deletions
|
|
@ -135,6 +135,7 @@ function makeConfig(
|
|||
oidc: {
|
||||
enabled: true,
|
||||
issuer: BASE_ISSUER,
|
||||
audience: 'remote-agent-api',
|
||||
jwksUri: BASE_JWKS_URI,
|
||||
...oidcOverrides,
|
||||
},
|
||||
|
|
@ -384,6 +385,17 @@ describe('createRemoteAgentAuth', () => {
|
|||
expect(json).toHaveBeenCalledWith({ error: 'Internal server error' });
|
||||
expect(mockNext).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns 500 when OIDC is enabled without an audience', async () => {
|
||||
const deps = makeDeps(makeConfig({ audience: undefined }, { enabled: false }));
|
||||
const { res, status, json } = makeRes();
|
||||
|
||||
await createRemoteAgentAuth(deps)(makeReq() as Request, res, mockNext);
|
||||
|
||||
expect(status).toHaveBeenCalledWith(500);
|
||||
expect(json).toHaveBeenCalledWith({ error: 'Internal server error' });
|
||||
expect(mockNext).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when OIDC verification succeeds', () => {
|
||||
|
|
@ -396,6 +408,9 @@ describe('createRemoteAgentAuth', () => {
|
|||
await createRemoteAgentAuth(deps)(req as Request, res, mockNext);
|
||||
|
||||
expect(req.user).toMatchObject({ id: 'uid123', email: 'agent@test.com' });
|
||||
expect((jwt.verify as jest.Mock).mock.calls[0][2]).toEqual(
|
||||
expect.objectContaining({ audience: 'remote-agent-api' }),
|
||||
);
|
||||
expect(mockNext).toHaveBeenCalledWith();
|
||||
expect(deps.apiKeyMiddleware).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
@ -407,7 +422,7 @@ describe('createRemoteAgentAuth', () => {
|
|||
deps.getAppConfig.mockImplementation(async (options) =>
|
||||
options?.tenantId === 'tenant-strict'
|
||||
? makeConfig({ audience: 'tenant-audience', scope: 'remote_agent' }, { enabled: false })
|
||||
: makeConfig({ audience: undefined, scope: undefined }, { enabled: true }),
|
||||
: makeConfig({ scope: undefined }, { enabled: true }),
|
||||
);
|
||||
const req = makeReq({ authorization: `Bearer ${FAKE_TOKEN}` });
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ type OidcConfig = NonNullable<
|
|||
>;
|
||||
|
||||
type AgentAuthConfig = NonNullable<NonNullable<TAgentsEndpoint['remoteApi']>['auth']>;
|
||||
type EnabledOidcConfig = OidcConfig & { issuer: string };
|
||||
type EnabledOidcConfig = OidcConfig & { audience: string; issuer: string };
|
||||
type JwksCacheOptions = {
|
||||
enabled: boolean;
|
||||
maxAge: number;
|
||||
|
|
@ -233,8 +233,8 @@ function getVerifyOptions(oidcConfig: EnabledOidcConfig): VerifyOptions {
|
|||
|
||||
return {
|
||||
algorithms: JWT_ALGORITHMS,
|
||||
audience: oidcConfig.audience,
|
||||
issuer,
|
||||
...(oidcConfig.audience ? { audience: oidcConfig.audience } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -268,7 +268,14 @@ function getEnabledOidcConfig(
|
|||
): EnabledOidcConfig | undefined {
|
||||
if (authConfig?.oidc?.enabled !== true) return undefined;
|
||||
if (!authConfig.oidc.issuer) throw new Error('OIDC issuer is required when OIDC auth is enabled');
|
||||
return { ...authConfig.oidc, issuer: authConfig.oidc.issuer };
|
||||
if (!authConfig.oidc.audience) {
|
||||
throw new Error('OIDC audience is required when OIDC auth is enabled');
|
||||
}
|
||||
return {
|
||||
...authConfig.oidc,
|
||||
audience: authConfig.oidc.audience,
|
||||
issuer: authConfig.oidc.issuer,
|
||||
};
|
||||
}
|
||||
|
||||
function isApiKeyEnabled(config: AppConfig): boolean {
|
||||
|
|
@ -491,9 +498,14 @@ export function createRemoteAgentAuth({
|
|||
res.status(500).json({ error: 'Internal server error' });
|
||||
return;
|
||||
}
|
||||
if (!authConfig.oidc.audience) {
|
||||
logger.error('[remoteAgentAuth] OIDC audience is required when OIDC auth is enabled');
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
return;
|
||||
}
|
||||
|
||||
const oidcConfig = getEnabledOidcConfig(authConfig);
|
||||
if (!oidcConfig) throw new Error('OIDC issuer is required when OIDC auth is enabled');
|
||||
if (!oidcConfig) throw new Error('OIDC configuration is required when OIDC auth is enabled');
|
||||
|
||||
const token = extractBearer(req.headers.authorization);
|
||||
if (token == null) {
|
||||
|
|
|
|||
|
|
@ -374,6 +374,7 @@ describe('agentsEndpointSchema', () => {
|
|||
auth: {
|
||||
oidc: {
|
||||
enabled: true,
|
||||
audience: 'remote-agent-api',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -384,6 +385,7 @@ describe('agentsEndpointSchema', () => {
|
|||
oidc: {
|
||||
enabled: true,
|
||||
issuer: 'my-realm',
|
||||
audience: 'remote-agent-api',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -393,6 +395,21 @@ describe('agentsEndpointSchema', () => {
|
|||
expect(invalidIssuer.success).toBe(false);
|
||||
});
|
||||
|
||||
it('requires an audience when remote OIDC auth is enabled', () => {
|
||||
const result = agentsEndpointSchema.safeParse({
|
||||
remoteApi: {
|
||||
auth: {
|
||||
oidc: {
|
||||
enabled: true,
|
||||
issuer: 'https://auth.example.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('requires HTTPS remote OIDC issuer and JWKS URLs outside localhost', () => {
|
||||
const insecureIssuer = agentsEndpointSchema.safeParse({
|
||||
remoteApi: {
|
||||
|
|
@ -400,6 +417,7 @@ describe('agentsEndpointSchema', () => {
|
|||
oidc: {
|
||||
enabled: true,
|
||||
issuer: 'http://auth.example.com',
|
||||
audience: 'remote-agent-api',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -410,6 +428,7 @@ describe('agentsEndpointSchema', () => {
|
|||
oidc: {
|
||||
enabled: true,
|
||||
issuer: 'https://auth.example.com',
|
||||
audience: 'remote-agent-api',
|
||||
jwksUri: 'http://auth.example.com/jwks',
|
||||
},
|
||||
},
|
||||
|
|
@ -427,6 +446,7 @@ describe('agentsEndpointSchema', () => {
|
|||
oidc: {
|
||||
enabled: true,
|
||||
issuer: 'http://localhost:8080/realms/test',
|
||||
audience: 'remote-agent-api',
|
||||
jwksUri: 'http://127.0.0.1:8080/realms/test/protocol/openid-connect/certs',
|
||||
},
|
||||
},
|
||||
|
|
@ -443,6 +463,7 @@ describe('agentsEndpointSchema', () => {
|
|||
oidc: {
|
||||
enabled: true,
|
||||
issuer: 'https://auth.example.com',
|
||||
audience: 'remote-agent-api',
|
||||
scope: 'remote_agent,admin',
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -493,7 +493,7 @@ const remoteApiOidcSchema = z
|
|||
.object({
|
||||
enabled: z.boolean().default(false),
|
||||
issuer: remoteApiOidcUrlSchema.optional(),
|
||||
audience: z.string().optional(),
|
||||
audience: z.string().min(1).optional(),
|
||||
jwksUri: remoteApiOidcUrlSchema.optional(),
|
||||
scope: remoteApiOidcScopeSchema.optional(),
|
||||
})
|
||||
|
|
@ -505,6 +505,13 @@ const remoteApiOidcSchema = z
|
|||
message: 'issuer is required when OIDC auth is enabled',
|
||||
});
|
||||
}
|
||||
if (oidc.enabled === true && !oidc.audience) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['audience'],
|
||||
message: 'audience is required when OIDC auth is enabled',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const remoteApiAuthSchema = z.object({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue