From 52ccb1379b17ab4a96e964c9b11009653c09ddce Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 11 May 2026 08:38:13 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=AA=AA=20refactor:=20Require=20Remote=20O?= =?UTF-8?q?IDC=20Audience=20for=20Agents=20API=20OAuth=20(#13066)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/middleware/remoteAgentAuth.spec.ts | 17 ++++++++++++++- .../api/src/middleware/remoteAgentAuth.ts | 20 ++++++++++++++---- .../specs/config-schemas.spec.ts | 21 +++++++++++++++++++ packages/data-provider/src/config.ts | 9 +++++++- 4 files changed, 61 insertions(+), 6 deletions(-) diff --git a/packages/api/src/middleware/remoteAgentAuth.spec.ts b/packages/api/src/middleware/remoteAgentAuth.spec.ts index c0ae2ca090..983f7bf706 100644 --- a/packages/api/src/middleware/remoteAgentAuth.spec.ts +++ b/packages/api/src/middleware/remoteAgentAuth.spec.ts @@ -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}` }); diff --git a/packages/api/src/middleware/remoteAgentAuth.ts b/packages/api/src/middleware/remoteAgentAuth.ts index 0471352744..e51c687c18 100644 --- a/packages/api/src/middleware/remoteAgentAuth.ts +++ b/packages/api/src/middleware/remoteAgentAuth.ts @@ -25,7 +25,7 @@ type OidcConfig = NonNullable< >; type AgentAuthConfig = NonNullable['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) { diff --git a/packages/data-provider/specs/config-schemas.spec.ts b/packages/data-provider/specs/config-schemas.spec.ts index 14182d6f18..b27f804726 100644 --- a/packages/data-provider/specs/config-schemas.spec.ts +++ b/packages/data-provider/specs/config-schemas.spec.ts @@ -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', }, }, diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 43563650b8..38615d5d46 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -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({