Skip to content

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

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

Alwaysgaurav1 wants to merge 13 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:

  • Committers with relevant context: @parshimers
    You can request their reviews formally with /request-review @parshimers.

  • Contributors with relevant context: @bobbai00, @aglinxinyuan, @mengw15
    You can notify them by mentioning @bobbai00, @aglinxinyuan, @mengw15 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.88%. Comparing base (a302e9d) to head (fb66377).
✅ All tests successful. No failed tests found.

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.81%   91.88%   +0.07%     
+ Complexity     5020     5015       -5     
============================================
  Files          1277     1283       +6     
  Lines         56350    56246     -104     
  Branches       7360     7321      -39     
============================================
- Hits          51736    51681      -55     
+ Misses         2758     2734      -24     
+ Partials       1856     1831      -25     
Flag Coverage Δ *Carryforward flag
access-control-service 78.15% <92.85%> (+0.70%) ⬆️
agent-service 99.40% <95.74%> (-0.13%) ⬇️
amber 85.96% <ø> (+0.17%) ⬆️ Carriedforward from 0adb38a
computing-unit-managing-service 64.05% <ø> (ø)
config-service 87.73% <ø> (ø)
file-service 82.31% <ø> (ø)
frontend 96.53% <96.00%> (+<0.01%) ⬆️
notebook-migration-service 83.77% <ø> (ø)
pyamber 98.46% <ø> (-0.04%) ⬇️ Carriedforward from 0adb38a
workflow-compiling-service 78.30% <ø> (ø)

*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 Resolved
Comment thread agent-service/src/config/jwt.ts Resolved
Comment thread agent-service/src/server.ts Resolved
Comment thread agent-service/src/config/jwt.ts Resolved
Comment thread .github/workflows/build.yml Resolved
Comment thread bin/single-node/nginx.conf Resolved
@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.

@chenlica

Copy link
Copy Markdown
Contributor

@dzueck Do you want to take a look at this PR as well?

@chenlica

chenlica commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

@dzueck Please check.

@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.

🔴 13 resolved · 0 open · 6 new (6 new = 4 newly introduced · 2 late catches)

All 13 findings from the last round are fixed in the code. The branch also needs main merged in: the two conflicts are adjacent import lines from #6009, and keeping both sides is enough.

Design & architecture (1)

  • jwt.ts:61: AUTH_JWT_SECRET=random is used as the literal key (advisory, see inline)

Correctness (1)

  • server.ts:175: the guard and the POST handler parse the Authorization header differently, giving a 500 (must-fix, see inline)

Simplifications (2)

  • auth-api.ts:42: validateToken has no production caller left (advisory, see inline)
  • agent.service.ts:255: three agentHeaders() calls now pass empty headers (advisory, see inline)

Conventions (1)

  • agent.service.ts:239: the PR description still describes the first revision (must-fix, see inline)

Polish: 1 quick touch-up (see inline comments).

Verification trace

Checked each of the 13 earlier findings against fb66377 instead of the reply. Ran the agent-service suite (346 pass) and probed the app with signed tokens. Authorization: <jwt> returns 500 on POST /api/agents and 200 on GET. With extractBearerToken in the guard, both return 401. With AUTH_JWT_SECRET=RANDOM, a token signed with the string random is accepted. Merged main in a scratch copy keeping both import lines: typecheck and 357 tests pass. The red macOS frontend job is a 20 s timeout in a spec this PR does not touch.

// 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 }) => {
const token = (request.headers.get("authorization") ?? "").replace(/^Bearer\s+/i, "").trim();

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 guard strips an optional Bearer with a regex. The POST handler reads the token with extractBearerToken(...)!, which needs exactly <scheme> <token>. So Authorization: <jwt> passes here and fails there, and POST /api/agents returns 500 where main returns 401. My earlier note said those handler branches could not fire, which was wrong for this header shape. With the helper used here too, every route returns 401 and the suite passes.

Suggested change
const token = (request.headers.get("authorization") ?? "").replace(/^Bearer\s+/i, "").trim();
const token = extractBearerToken(request.headers.get("authorization") ?? undefined);

if (!secret) {
throw new Error("JWT secret not found: neither AUTH_JWT_SECRET environment variable nor auth.conf was found.");
}
return secret.toLowerCase();

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:

AuthConfig maps the value random to a key generated per JVM (AuthConfig.scala:34). Here it stays the literal key. With AUTH_JWT_SECRET=random, a token signed with the string random passed verifyToken in my probe. The comment below calls the value unsupported, so I'd refuse to start with it.

Suggested change
return secret.toLowerCase();
const normalized = secret.toLowerCase();
if (normalized === "random") {
throw new Error('AUTH_JWT_SECRET="random" is not supported here; all services must share one fixed secret.');
}
return normalized;

}

/** True only when the token is genuinely valid (signature + expiry verified). */
export function validateToken(token: string): boolean {

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:

validateToken lost its only production caller when the check in the POST handler was removed. Only auth-api.spec.ts references it now. I'd delete it together with its describe("validateToken") block. jwt.spec.ts already covers the same cases through verifyToken.

private syncAgentsWithBackend(): void {
this.http
.get<ApiAgentListResponse>(`${this.AGENT_API_BASE}/agents`)
.get<ApiAgentListResponse>(`${this.AGENT_API_BASE}/agents`, this.agentHeaders())

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:

agentHeaders() no longer adds Authorization, so with no arguments it returns an empty header set. This call passes that. So do getAllAgents and fetchModelTypes, and the last one calls /api/models, not agent-service. Reverting the three removes about 30 changed lines. Keep the createAgent change, which sends X-Agent-Workflow-Id.

Suggested change
.get<ApiAgentListResponse>(`${this.AGENT_API_BASE}/agents`, this.agentHeaders())
.get<ApiAgentListResponse>(`${this.AGENT_API_BASE}/agents`)

* Includes X-Agent-Workflow-Id for consistent hash routing in k8s.
*/
private agentHeaders(agentId?: string): { headers: HttpHeaders } {
private agentHeaders(agentId?: string, workflowId?: number): { headers: HttpHeaders } {

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:

The PR description still describes the first revision, and the squash commit copies it. Four statements no longer hold. agentHeaders() does not attach Authorization. The secret has no "built-in defaults" fallback (jwt.ts:59 throws). The matcher is anchored (line 51), not .*/api/agents.*. The frontend spec has no test for the header. Please also add the PATCH handler, the 403 for other roles and the 1008 close code.

}

if (userSession.isEmpty) {
return Response.status(Response.Status.UNAUTHORIZED).build()

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 class comment at lines 319-322 still says "403 on missing/invalid tokens". This branch returns 401 for those. I'd mention both codes there.

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)

4 participants