Skip to content

feat(agent-service): authenticate requests via JWT and gateway ext_authz - #9043

Open
Alwaysgaurav1 wants to merge 10 commits into
apache:mainfrom
Alwaysgaurav1:feat/5561-agent-service-auth
Open

Alwaysgaurav1 wants to merge 10 commits into
apache:mainfrom
Alwaysgaurav1:feat/5561-agent-service-auth

Conversation

@Alwaysgaurav1

Copy link
Copy Markdown
Contributor

What changes were proposed in this PR?

This PR implements Phase 1 authorization for requests reaching the agent-service, providing defense-in-depth across the API gateways, the access-control-service, and the agent-service itself:

  1. agent-service Direct JWT Verification (config/jwt.ts):

    • Replaced non-cryptographic JWT decoding with real cryptographic HS256 signature verification using node:crypto (createHmac, timingSafeEqual).
    • Resolves the shared secret from AUTH_JWT_SECRET (falling back to candidate auth.conf locations and built-in defaults) with a 30-second clock skew tolerance.
    • Added an .onBeforeHandle guard across agentsRouter in server.ts that rejects requests missing a valid Bearer token with 401 Unauthorized (while preserving unauthenticated probes on /api/healthcheck).
    • Enforced authentication on the /api/agents/:id/react WebSocket via the access-token query parameter during the open(ws) handshake.
  2. access-control-service Ext-Authz Route (AccessControlResource.scala):

    • Added a route matcher for .*/api/agents.* to AccessControlResource.authorize().
    • Validates the token using parseToken() and requires a REGULAR or ADMIN user role.
    • Forwards trusted user identity headers (x-user-id, x-user-name, x-user-email) on HTTP 200, returning HTTP 401 for missing/malformed tokens and HTTP 403 for inactive roles.
  3. Gateway & Kubernetes Deployment:

    • bin/single-node/nginx.conf: Configured upstream access_control_authz, an internal /_agent_auth subrequest location, and auth_request /_agent_auth; on /api/agents.
    • bin/k8s/templates/base/gateway/gateway-agent-security-policy.yaml: Added an Envoy Gateway SecurityPolicy for agent-service-route pointing extAuth to access-control-service.
    • bin/k8s/templates/base/agent-service/agent-service-deployment.yaml: Injected .Values.texeraEnvVars (providing AUTH_JWT_SECRET).
    • bin/dockerfiles/agent-service.dockerfile: Bundled auth.conf into the container image.
  4. Frontend (agent.service.ts):

    • Updated agentHeaders() to attach Authorization: Bearer <token> from AuthService.getAccessToken().
    • Appended ?access-token=${encodeURIComponent(token)} to the WebSocket connection URL in startStatePolling().
    • Ensured all agent HTTP requests (syncAgentsWithBackend, createAgent, getAllAgents, fetchModelTypes) pass agentHeaders().

Any related issues, documentation, discussions?

Resolves #5561

How was this PR tested?

  • agent-service Unit & Integration Tests:

    • agent-service/src/config/jwt.spec.ts: Added 9 unit tests testing HS256 signature verification, string coercion, secret mismatches, tampering, expiration within/outside clock skew, non-HS256 algorithms, and malformed tokens.
    • agent-service/src/server.spec.ts: Added unit tests verifying 401 rejection for unauthenticated, invalid, and expired tokens, 200 pass-through for valid tokens, and unauthenticated bypass for /api/healthcheck.
    • agent-service/src/server.ws.spec.ts: Added unit tests verifying WebSocket disconnection with WsServerErrorEvent("Unauthorized") when access-token is missing or invalid.
    • Ran Bun test suite: 327 tests pass, 0 fail. TypeScript check (tsc --noEmit) passes with 0 errors.
  • access-control-service Unit Tests:

    • AgentAccessAuthSpec.scala: Added tests covering 200 OK for REGULAR and ADMIN roles, forwarding of x-user-* headers, 401 on missing/malformed tokens, and 403 on INACTIVE role.
  • Frontend Unit Tests:

    • agent.service.spec.ts: Added tests verifying that Authorization is attached when an access token is present and omitted when unauthenticated, and that ?access-token= is correctly appended to the WebSocket URL.

Was this PR authored or co-authored using generative AI tooling?

Generated-by: Antigravity 2.0 (Gemini 2.5 Pro)

…t_authz (apache#5561)

Authorize requests reaching the agent service using JWT signature
verification and gateway external authorization, providing defense in depth:

1. agent-service:
   - Added real HS256 signature verification in config/jwt.ts using
     crypto.createHmac and timingSafeEqual.
   - Resolves shared secret from AUTH_JWT_SECRET env or auth.conf with
     30-second clock skew tolerance.
   - Added .onBeforeHandle guard on agentsRouter rejecting unauthenticated,
     expired, or invalid tokens with 401.
   - Added access-token query param check in WebSocket open() handler.

2. access-control-service:
   - Added /api/agents routing branch in AccessControlResource.
   - Verifies JWT validity and enforces REGULAR or ADMIN role.
   - Forwards trusted identity headers (x-user-id, x-user-name, x-user-email).

3. Gateways & Deployment:
   - bin/single-node/nginx.conf: Added auth_request /_agent_auth subrequest
     on /api/agents routing to access-control-service.
   - bin/k8s/templates/base/gateway/gateway-agent-security-policy.yaml: Added Envoy
     SecurityPolicy for agent-service-route.
   - bin/k8s/templates/base/agent-service/agent-service-deployment.yaml: Passed
     texeraEnvVars into agent-service container.
   - bin/dockerfiles/agent-service.dockerfile: Bundled auth.conf for container runtime.

4. Frontend:
   - Attaches Authorization Bearer token via agentHeaders() to REST requests.
   - Appends ?access-token= query param to WebSocket connection in startStatePolling().
   - Added unit tests in agent.service.spec.ts.
@github-actions github-actions Bot added feature frontend Changes related to the frontend GUI infra platform Non-amber Scala service paths agent-service labels Oct 20, 2026
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Automated Reviewer Suggestions

Based on the git blame history of the changed files, we recommend the following reviewers:

  • Contributors with relevant context: @bobbai00, @aglinxinyuan, @zyratlo
    You can notify them by mentioning @bobbai00, @aglinxinyuan, @zyratlo in a comment.

@codecov-commenter

codecov-commenter commented Oct 20, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.23810% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.87%. Comparing base (d108cc1) to head (5b899d9).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
agent-service/src/config/jwt.ts 93.33% 4 Missing ⚠️
...exera/service/resource/AccessControlResource.scala 92.85% 1 Missing and 1 partial ⚠️
...d/src/app/workspace/service/agent/agent.service.ts 96.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff            @@
##               main    #9043   +/-   ##
=========================================
  Coverage     91.87%   91.87%           
- Complexity     5048     5049    +1     
=========================================
  Files          1265     1266    +1     
  Lines         54915    54974   +59     
  Branches       6940     6943    +3     
=========================================
+ Hits          50452    50508   +56     
- Misses         2769     2772    +3     
  Partials       1694     1694           
Flag Coverage Δ *Carryforward flag
access-control-service 78.08% <92.85%> (+0.70%) ⬆️
agent-service 99.05% <95.74%> (-0.12%) ⬇️
amber 86.06% <ø> (-0.01%) ⬇️ Carriedforward from b3ef171
computing-unit-managing-service 60.48% <ø> (ø)
config-service 87.37% <ø> (ø)
file-service 82.00% <ø> (ø)
frontend 96.50% <96.00%> (+<0.01%) ⬆️
notebook-migration-service 83.73% <ø> (ø)
pyamber 98.57% <ø> (+0.03%) ⬆️ Carriedforward from b3ef171
workflow-compiling-service 72.43% <ø> (ø)

*This pull request uses carry forward flags. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions github-actions Bot added the ci changes related to CI label Oct 20, 2026
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ Benchmark changes need a look

🟢 2 better · 🔴 5 worse · ⚪ 8 noise (<±5%) · 0 without baseline

Compared against main 2407d79 benchmarked on this same runner, so the delta is largely free of cross-runner hardware noise. The "7d avg" column still reflects the gh-pages dashboard. Treat <±5% as noise unless repeated.

Dashboard · Run

config throughput MB/s latency max Δ latest / 7d
🔴 bs=10 sw=10 sl=64 358 0.219 26,789/41,434/41,434 us 🔴 -11.2% / 🔴 +193.4%
🟢 bs=100 sw=10 sl=64 802 0.489 123,477/139,574/139,574 us 🟢 -25.7% / 🔴 +50.9%
🔴 bs=1000 sw=10 sl=64 931 0.568 1,063,249/1,147,336/1,147,336 us 🔴 +5.1% / 🔴 +35.4%
Baseline details

Latest main 2407d79 from same runner

config metric PR latest main 7d avg Δ latest Δ 7d
bs=10 sw=10 sl=64 throughput 358 tuples/sec 403 tuples/sec 968.33 tuples/sec -11.2% -63.0%
bs=10 sw=10 sl=64 MB/s 0.219 MB/s 0.246 MB/s 0.591 MB/s -11.0% -62.9%
bs=10 sw=10 sl=64 p50 26,789 us 25,259 us 10,646 us +6.1% +151.6%
bs=10 sw=10 sl=64 p95 41,434 us 41,144 us 14,120 us +0.7% +193.4%
bs=10 sw=10 sl=64 p99 41,434 us 41,144 us 16,498 us +0.7% +151.2%
bs=100 sw=10 sl=64 throughput 802 tuples/sec 800 tuples/sec 1,252 tuples/sec +0.3% -35.9%
bs=100 sw=10 sl=64 MB/s 0.489 MB/s 0.488 MB/s 0.764 MB/s +0.2% -36.0%
bs=100 sw=10 sl=64 p50 123,477 us 119,696 us 83,736 us +3.2% +47.5%
bs=100 sw=10 sl=64 p95 139,574 us 187,894 us 92,464 us -25.7% +50.9%
bs=100 sw=10 sl=64 p99 139,574 us 187,894 us 108,561 us -25.7% +28.6%
bs=1000 sw=10 sl=64 throughput 931 tuples/sec 945 tuples/sec 1,321 tuples/sec -1.5% -29.5%
bs=1000 sw=10 sl=64 MB/s 0.568 MB/s 0.577 MB/s 0.806 MB/s -1.6% -29.5%
bs=1000 sw=10 sl=64 p50 1,063,249 us 1,059,342 us 811,260 us +0.4% +31.1%
bs=1000 sw=10 sl=64 p95 1,147,336 us 1,091,499 us 847,636 us +5.1% +35.4%
bs=1000 sw=10 sl=64 p99 1,147,336 us 1,091,499 us 879,277 us +5.1% +30.5%
Raw CSV
config_idx,batch_size,schema_width,string_len,num_batches,total_ms,total_tuples,total_bytes,tuples_per_sec,mb_per_sec,lat_p50_us,lat_p95_us,lat_p99_us
0,10,10,64,20,558.50,200,128000,358,0.219,26788.63,41434.40,41434.40
1,100,10,64,20,2494.46,2000,1280000,802,0.489,123476.51,139573.57,139573.57
2,1000,10,64,20,21472.56,20000,12800000,931,0.568,1063248.68,1147336.01,1147336.01

@bobbai00
bobbai00 self-requested a review October 21, 2026 08:40

@bobbai00 bobbai00 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 4 must-fix · 7 advisory · 2 polish: the auth design matches #5561 and works end to end on single-node; CI, k8s PATCH and the path regex need fixes.

Design & architecture (2)

  • server.ts:177: in-service guard accepts INACTIVE users (advisory, see inline)
  • jwt.ts:36: hard-coded fallback copy of the JWT secret (advisory, see inline)

Correctness (4)

  • AccessControlResource.scala:89: no PATCH handler, so settings saves get 405 behind Envoy (must-fix, see inline)
  • AccessControlResource.scala:51: unanchored regex authorizes unrelated dynamic-route paths (must-fix, see inline)
  • AgentAccessAuthSpec.scala:46: spec does not compile, failing CI (must-fix, see inline)
  • server.ts:466: rejected socket closes with the normal close code (advisory, see inline)

Simplifications (3)

  • agent.service.ts:244: duplicates the JwtModule interceptor header (advisory, see inline)
  • agent-service-deployment.yaml:62: injects every texeraEnvVars secret into agent-service (advisory, see inline)
  • server.ts:175: POST /agents token checks are now unreachable (advisory, see inline)

Conventions (2)

  • jwt.ts:89: title scope is not a module name, and the title is over length (must-fix, see inline)
  • build.yml:249: unrelated sbt bump duplicates #9064 (advisory, see inline)

Polish: 2 quick touch-ups (see inline comments).

Verification trace

Ran this branch end to end on single-node (PR builds of access-control-service, agent-service and frontend, the PR's nginx.conf, a real login token). /api/agents returned 401 without a token, 403 for an INACTIVE user and 200 for REGULAR. The WebSocket upgrade was refused without a token, and an agent created from the UI built and executed a TextInput to Filter workflow. Behind a standalone Envoy ext_authz pointed at this branch's access-control-service, GET/POST/DELETE returned 200 and PATCH /api/agents/:id/settings returned 405. Unit suites: agent-service 327 pass, frontend agent.service.spec 89 pass. The access-control-service tests fail to compile, and pass 30/30 after the two-line fix noted inline.

Comment thread agent-service/src/server.ts Outdated
// separately in its open() handler via the access-token query param.
.onBeforeHandle(({ request, set }) => {
const token = (request.headers.get("authorization") ?? "").replace(/^Bearer\s+/i, "").trim();
if (!verifyToken(token)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Advisory:

This guard accepts any role, while the gateway branch requires REGULAR or ADMIN (AccessControlResource.scala:174). An INACTIVE user's unexpired token passes here: a direct GET /api/agents returned 200, and the same request through nginx returned 403. The comment above names direct access as the reason this layer exists, so have verifyToken (or this guard) reject other roles too.

Comment thread agent-service/src/config/jwt.ts Outdated

// auth.conf default, used as the last-resort fallback when neither the env
// override nor the file is available (matches AuthConfig's literal default).
const AUTH_CONF_DEFAULT_SECRET = "8a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Advisory:

This literal is a third copy of the auth.conf secret. When neither AUTH_JWT_SECRET nor the file is found, the module logs a warning and silently uses it (line 57). If the auth.conf default changes, an agent-service started elsewhere 401s every request. The image already bundles auth.conf, so throw at startup instead.

case wsapiWorkflowWebsocket() | apiExecutionsStats() | apiExecutionsResultExport() |
pveRoute() =>
checkComputingUnitAccess(uriInfo, headers, bodyOpt)
case apiAgents() =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Must fix:

This resource only has GET/POST/PUT/DELETE handlers (lines 328-360), and Envoy's ext_authz forwards the client's method. So every settings Save (PATCH /api/agents/:id/settings, agent.service.ts:1157) gets a 405 on k8s; I reproduced it with Envoy in front of this branch. Add an @PATCH handler mirroring authorizePut, plus a PATCH test (nginx is fine: its subrequests are GET).

private val apiExecutionsResultExport: Regex = """.*/api/executions/result/export.*""".r
private val pveRoute: Regex = """^/?(?:auth/)?(?:api/|wsapi/)?pve(?:/.*)?$""".r
// Agent service: authenticate any /api/agents request (Phase 1 — see #5561).
private val apiAgents: Regex = """.*/api/agents.*""".r

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Must fix:

authorize() also backs the dynamic-routes policy (/wsapi, /api/pve), so auth/wsapi/x/api/agents now returns 200 for any REGULAR user instead of 403. It does so without the Host rewrite checkComputingUnitAccess sets (line 264). I confirmed the 403 to 200 flip, not where the dynamic backend then routes it. Anchor it like pveRoute and add a negative /wsapi test.

Suggested change
private val apiAgents: Regex = """.*/api/agents.*""".r
private val apiAgents: Regex = """^/?(?:auth/)?api/agents(?:/.*)?$""".r

u.setUid(7)
u.setName("agent-user")
u.setEmail("agent-user@example.com")
u.setGoogleId(null)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Must fix:

This spec does not compile. User has no setGoogleId since #7055, and JwtAuth.jwtClaims has no expireInDays parameter (line 48). That is why build / platform (access-control-service) and build / amber are red, so the testing section's claim does not hold yet. Deleting this line and calling JwtAuth.jwtClaims(u) makes all 30 resource tests pass locally.

// (defense in depth — the gateway ext_authz also checks it, but this also
// covers direct access, e.g. bare-metal dev). The WebSocket route is guarded
// separately in its open() handler via the access-token query param.
.onBeforeHandle(({ request, set }) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Advisory:

With this guard in place, the POST handler's missing-token and validateToken branches (lines 199-205) and the two token entries in ERROR_STATUS (lines 151-152) can no longer fire. The updated specs already expect "Unauthorized" for those cases, and the handler now verifies the same token up to three times. Keep extractUserFromToken there and drop the rest.

* null if the token is missing, malformed, mis-signed, expired, or uses a
* different algorithm. Mirrors org.apache.texera.auth.JwtParser.parseToken.
*/
export function verifyToken(token: string | undefined | null): JwtUser | null {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Must fix:

This is what the PR does, but the title scopes it as agent, which is not a module name. AGENTS.md asks for module scopes, and main uses agent-service. The title is also 84 characters (max 75), and (#5561) repeats the Closes link. Suggested: feat(agent-service): authenticate requests via JWT and gateway ext_authz.

PGPASSWORD: postgres
- name: Setup sbt launcher
uses: sbt/setup-sbt@8feba82adc7f01ddcf8165b86f778bdb5b82cebc # v1.5.7
uses: sbt/setup-sbt@ecad189dd245444b9ae0a0cd5922fd938b88906f # v1.5.12

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Advisory:

This sbt/setup-sbt bump (commit a0791b6, four workflow files) is unrelated to agent auth and duplicates open #9064. Once #9064 merges, rebase it out so this PR's squash commit carries only the auth change.

Comment thread bin/single-node/nginx.conf Outdated
Comment on lines +105 to +108
# LLM traffic (chat + model list) is handled by the agent-service, which
# talks to LiteLLM directly; /api/agents below already routes there.
# Each request is first authenticated by the access-control-service
# (ext_authz) via the auth_request subrequest below.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Polish:

The first two lines contradict the blocks just above: /api/models and /api/chat/ go to access-control-service, not agent-service.

Suggested change
# LLM traffic (chat + model list) is handled by the agent-service, which
# talks to LiteLLM directly; /api/agents below already routes there.
# Each request is first authenticated by the access-control-service
# (ext_authz) via the auth_request subrequest below.
# Each /api/agents request is first authenticated by the
# access-control-service (ext_authz) via the auth_request subrequest below.

Comment on lines +153 to +154
// ownership is deferred (see #5302 / #5561). On success, forward the user
// identity headers so the agent service can trust them.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Polish:

agent-service never reads x-user-id, x-user-name or x-user-email, so "can trust them" overstates what happens today.

Suggested change
// ownership is deferred (see #5302 / #5561). On success, forward the user
// identity headers so the agent service can trust them.
// ownership is deferred (see #5302 / #5561). On success, return the user
// identity headers for future per-agent authorization in agent-service.

@Alwaysgaurav1 Alwaysgaurav1 changed the title feat(agent): authenticate agent-service requests via JWT + gateway ext_authz (#5561) feat(agent-service): authenticate requests via JWT and gateway ext_authz Oct 22, 2026
@Alwaysgaurav1

Copy link
Copy Markdown
Contributor Author

@bobbai00

Thanks for the thorough review and verification trace! I have addressed all items in commit 7eadccc:

🔴 Must-Fix

  • Compilation in AgentAccessAuthSpec.scala: Removed u.setGoogleId(null) (removed since refactor(common/auth): move login credentials to auth_provider #7055) and called JwtAuth.jwtClaims(u) without the invalid parameter.
  • Added @PATCH to AccessControlResource.scala: Added the @PATCH annotation/handler mirroring authorizePut so settings saves behind Envoy ext_authz no longer 405. Added a corresponding unit test in AgentAccessAuthSpec.scala.
  • Anchored apiAgents Regex: Anchored to ^/?(?:auth/)?api/agents(?:/.*)?$ so dynamic routes like auth/wsapi/x/api/agents are not inadvertently matched. Added a negative test for /wsapi.
  • PR Title: Updated title to feat(agent-service): authenticate requests via JWT and gateway ext_authz to follow module scoping and length conventions.

🟡 Advisory & Simplifications

  • In-Service Role Guard: .onBeforeHandle in server.ts now enforces REGULAR or ADMIN roles, rejecting INACTIVE users with 403 to match gateway behavior (with tests added for 403 INACTIVE and 200 ADMIN).
  • Fail-Fast Secret Resolution: Dropped the hardcoded fallback secret in jwt.ts; the service now throws on startup if neither AUTH_JWT_SECRET nor auth.conf is found.
  • WebSocket Rejection Code: Rejected sockets in open(ws) now close with code 1008 (Policy Violation) so the frontend detects denied access rather than normal completion.
  • Frontend Authorization Header: Dropped redundant manual Authorization header addition in agentHeaders() (letting Angular's JwtModule handle it automatically), while keeping X-Agent-Workflow-Id and the WebSocket ?access-token= query param.
  • K8s Secret Scoping: Filtered texeraEnvVars in agent-service-deployment.yaml to inject only AUTH_JWT_SECRET.
  • Dead Code Cleanup: Removed redundant token validation logic and unused ERROR_STATUS entries in server.ts.
  • Workflow / sbt Bump: Noted; will rebase out the setup-sbt workflow commits once fix(deps, ci): bump sbt/setup-sbt to v1.5.12 to restore CI #9064 merges.

🟢 Polish

  • Corrected the routing comment in bin/single-node/nginx.conf.
  • Clarified the header forwarding comment in AccessControlResource.scala.

Verification:

  • Formatted all touched files with Prettier.
  • All 330 unit & WebSocket integration tests in agent-service pass (bun test).

Please take another look when you get a chance!

@github-actions github-actions Bot removed the ci changes related to CI label Oct 25, 2026
@Alwaysgaurav1

Copy link
Copy Markdown
Contributor Author

@bobbai00 could you plss review it again.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-service feature frontend Changes related to the frontend GUI infra platform Non-amber Scala service paths

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Authorize agent-service requests in the access-control-service (JWT authn + per-agent authz)

3 participants