Skip to content

fix: return 403 Forbidden for origin validation failures#2911

Merged
amikofalvy merged 3 commits intomainfrom
worktree-spec+better-401-error-messages
Mar 30, 2026
Merged

fix: return 403 Forbidden for origin validation failures#2911
amikofalvy merged 3 commits intomainfrom
worktree-spec+better-401-error-messages

Conversation

@amikofalvy
Copy link
Copy Markdown
Collaborator

Summary

  • When a web_client app's origin doesn't match allowedDomains, the API now returns a clear 403 Forbidden with "Origin not allowed for this app" instead of a misleading 401 with "Invalid Token" (which was rendered as internal_server_error)
  • Matches the existing pattern in the anonymous session route (auth.ts:173)
  • Throws createApiError() directly from tryAppCredentialAuth instead of returning a failure result, consistent with JWT verification failures at line 638

Before

{ "code": "internal_server_error", "status": 401, "detail": "Error processing request: Invalid Token" }

After

{ "code": "forbidden", "status": 403, "detail": "Origin not allowed for this app" }

Test plan

  • Updated app-credential-auth.test.ts — "should reject when origin is not allowed" now asserts 403 + forbidden code + correct detail message
  • All 14 tests in the test file pass
  • Verify no client-side retry logic depends on the old 401 status for this error path

🤖 Generated with Claude Code

…on failures

When a web_client app's origin doesn't match allowedDomains, the error
response was a misleading 401 "Invalid Token" (rendered as internal_server_error).
Now throws createApiError with code 'forbidden' and message 'Origin not allowed
for this app', matching the existing pattern in the anonymous session route.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agents-api Ready Ready Preview, Comment Mar 30, 2026 8:51pm
agents-docs Ready Ready Preview, Comment Mar 30, 2026 8:51pm
agents-manage-ui Ready Ready Preview, Comment Mar 30, 2026 8:51pm

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 30, 2026

🦋 Changeset detected

Latest commit: 28f89b2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 10 packages
Name Type
@inkeep/agents-api Patch
@inkeep/agents-manage-ui Patch
@inkeep/agents-cli Patch
@inkeep/agents-core Patch
@inkeep/agents-email Patch
@inkeep/agents-mcp Patch
@inkeep/agents-sdk Patch
@inkeep/agents-work-apps Patch
@inkeep/ai-sdk-provider Patch
@inkeep/create-agents Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel vercel Bot temporarily deployed to Preview – agents-docs March 30, 2026 20:44 Inactive
@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog Bot commented Mar 30, 2026

TL;DR — Origin validation failures for web_client apps now return a 403 Forbidden with an actionable error message instead of a misleading 401 Invalid Token that surfaced as an internal_server_error.

Key changes

  • Return 403 Forbidden for origin mismatch in tryAppCredentialAuth — throws createApiError({ code: 'forbidden' }) directly instead of returning a failure result that was later wrapped as a generic 401, aligning with the existing pattern used in the anonymous session route.
  • Update test assertions and mock setup for the new error shape — the "should reject when origin is not allowed" test now asserts status 403, code forbidden, and the correct detail message; the mock uses vi.importActual to pass through the real createApiError.
  • Add changeset — patch bump for @inkeep/agents-api describing the fix.

Summary | 3 files | 3 commits | base: mainworktree-spec+better-401-error-messages


Before: A disallowed origin returned { code: "internal_server_error", status: 401, detail: "Error processing request: Invalid Token" } — unhelpful for debugging and semantically wrong (the token was valid; the origin was not).
After: Returns { code: "forbidden", status: 403, detail: "Origin not allowed for this app" } — correctly identifies the issue as an authorization failure, not an authentication one.

In tryAppCredentialAuth, when validateOrigin fails the function previously returned { authResult: null, failureMessage }. The caller treated this the same as a missing or invalid token, producing a 401. Now it throws via createApiError with a forbidden code, which the framework maps to a 403 response — matching how the anonymous session route already handles this case.

Why change the mock setup in the test?

The test previously mocked the entire @inkeep/agents-core module with stubs. Since the middleware now calls the real createApiError to throw a structured error, the mock needs to pass through the actual implementation via vi.importActual so the error shape is preserved in the test environment.

runAuth.ts · app-credential-auth.test.ts · better-origin-error.md

Pullfrog  | View workflow run | Triggered by Pullfrog | Using Claude Opus𝕏

Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

Clean, well-scoped fix. The change from returning a failure result to throwing createApiError({ code: 'forbidden' }) is correct — the thrown HTTPException propagates cleanly through the catch block at runAuth.ts:998-1001 which re-throws HTTPException instances. This matches the existing pattern in auth.ts:173 and is consistent with how JWT verification failures are already handled at line 638. The test mock update to use vi.importActual for createApiError is the right approach since the real implementation is now exercised in the origin-rejection path. No concerns.

Pullfrog  | View workflow run | Using Claude Opus𝕏

Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

New commit is indentation-only (fixing the mock return object alignment). No functional changes. Approval stands.

Pullfrog  | View workflow run | Using Claude Opus𝕏

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@github-actions github-actions Bot deleted a comment from claude Bot Mar 30, 2026
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 30, 2026

Claude encountered an error —— View job

Invalid branch name: "worktree-spec+better-401-error-messages". Branch names must start with an alphanumeric character and contain only alphanumeric characters, forward slashes, hyphens, underscores, or periods.

I'll analyze this and get back to you.

@amikofalvy amikofalvy added this pull request to the merge queue Mar 30, 2026
Merged via the queue into main with commit 4141e26 Mar 30, 2026
16 of 18 checks passed
@amikofalvy amikofalvy deleted the worktree-spec+better-401-error-messages branch March 30, 2026 21:08
@inkeep
Copy link
Copy Markdown
Contributor

inkeep Bot commented Mar 30, 2026

No documentation updates needed for this fix. The existing App Credentials docs describe the domain allowlist feature without specifying error codes, and this improvement aligns the API behavior with standard HTTP semantics (403 for origin restrictions).

@itoqa
Copy link
Copy Markdown

itoqa Bot commented Mar 30, 2026

Ito Test Report ✅

14 test cases ran. 14 passed.

All 14 origin-validation and app-credential tests passed (14/14) in local verification, confirming that allowed-origin anonymous session minting works and legitimate wildcard subdomains (for example, *.example.com) are correctly accepted on protected Run endpoints. The run also verified strong deny behavior and stability: disallowed or missing Origin headers consistently returned explicit 403 forbidden with the expected origin-denied contract across chat and conversation routes (including burst and mobile attempts), while spoofed/lookalike origins, cross-origin token replay, cross-app token/app-id mismatch, and disallowed deep-link conversation reads were blocked with no data leakage, port matching remained strict, and requests without x-inkeep-app-id stayed on the non-app auth path.

✅ Passed (14)
Category Summary Screenshot
Adversarial Spoofed lookalike origin was denied with 403 forbidden and no data exposure. ADV-1
Adversarial Same token succeeded on allowed origin and was denied on disallowed origin replay. ADV-2
Adversarial Cross-app token/app-id mismatch was denied on re-check and did not grant app B access. ADV-3
Adversarial Repeated disallowed-origin deep-link conversation reads returned 403 with no leakage. ADV-4
Adversarial Under mobile viewport emulation, rapid disallowed-origin /run/api/chat attempts all returned consistent explicit 403 forbidden responses with identical origin-denied detail and no mobile-specific bypass. ADV-5
Edge When Origin header was omitted, endpoint still returned explicit 403 forbidden with origin-not-allowed detail and no 401 regression. EDGE-1
Edge Wildcard allowedDomains accepted https://docs.example.com on /run/api/chat with HTTP 200 after fixture seeding. EDGE-2
Edge Port-sensitive origin handling is correct: matching host+port is accepted while mismatched port is rejected with 403 forbidden. EDGE-3
Edge Requests without x-inkeep-app-id stayed on the non-app auth path and did not misclassify as origin-forbidden. EDGE-4
Edge Two immediate 10-request parallel bursts to /run/v1/chat/completions from disallowed origin consistently returned explicit 403 forbidden responses with identical code/detail and no 401/500/timeout. EDGE-5
Happy-path Allowed-origin anonymous session request returned 200 with token/expiresAt and valid anon JWT claims. ROUTE-1
Happy-path Disallowed origin request returned deterministic HTTP 403 with forbidden code and exact origin-denied detail. ROUTE-2
Happy-path Disallowed origin on /run/api/chat produced the same explicit 403 forbidden contract and detail string as /run/v1/chat/completions. ROUTE-3
Happy-path Conversation list endpoint rejected disallowed origin with 403 forbidden and did not expose conversation list data. ROUTE-4

Commit: 28f89b2

View Full Run


Tell us how we did: Give Ito Feedback

tim-inkeep pushed a commit that referenced this pull request Mar 31, 2026
* fix: return 403 Forbidden with actionable message for origin validation failures

When a web_client app's origin doesn't match allowedDomains, the error
response was a misleading 401 "Invalid Token" (rendered as internal_server_error).
Now throws createApiError with code 'forbidden' and message 'Origin not allowed
for this app', matching the existing pattern in the anonymous session route.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* style: auto-format with biome

* add changeset for origin validation error fix

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant