Skip to content

feat(jwt)!: require aud when an audience is expected - #130

Open
EfeDurmaz16 wants to merge 6 commits into
agentcommercekit:mainfrom
EfeDurmaz16:jwt-require-audience
Open

feat(jwt)!: require aud when an audience is expected#130
EfeDurmaz16 wants to merge 6 commits into
agentcommercekit:mainfrom
EfeDurmaz16:jwt-require-audience

Conversation

@EfeDurmaz16

@EfeDurmaz16 EfeDurmaz16 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

What

verifyJwt now fails a token whose aud claim is missing or empty whenever
the caller supplies an audience. No flag: supplying audience is the
signal, matching jose and PyJWT semantics (this PR originally added an
opt-in requireAudience boolean; reshaped per review).

Why

did-jwt's verifyJWT only runs its audience match when the token carries
an aud claim, so a token that omits aud verifies even when the caller
supplies an audience, allowing cross-service replay. jose throws
ERR_JWT_CLAIM_VALIDATION_FAILED (reason: "missing") and PyJWT raises
MissingRequiredClaimError in the same situation; did-jwt is the outlier.
A quick code search also found an external consumer passing audience to
verifyJwt whose doc comment assumes aud is enforced, so the strict
default silently fixes real usage.

A2A call sites

The two ack-id A2A verifiers turn out to differ:

  • Handshake keeps audience: did: handshake JWTs have embedded
    aud: params.recipient since the original implementation
    (sign-message.ts), so the strict rule just works there.
  • Signed messages drop audience: did: createSignedA2AMessage has no
    recipient parameter, its JWTs never carry aud, and the option never
    provided a check on that path. No behavior change.

Possible follow-up (separate PR if wanted): give createSignedA2AMessage a
recipient parameter and verify aud strictly on signed messages too. The
identity-a2a README already documents aud on signed messages as MITM
protection, but the code never implemented it. It changes that function's
signature; a code search finds no external consumers of the a2a module.

Change

  • VerifyJwtOptions.requireAudience removed; presence check keys on
    options.audience
  • hasAudience helper unchanged (non-empty string or array with a
    non-empty entry)
  • verifyA2ASignedMessage no longer passes audience
  • changeset rewritten: @agentcommercekit/jwt minor (behavior change
    documented), @agentcommercekit/ack-id patch

Tests

packages/jwt: expected-audience-with-matching-aud passes; missing and
empty-array aud reject; no-audience-expected accepts aud-less tokens.
packages/ack-id: handshake still asserts audience; signed-message
asserts no audience is passed. pnpm run check (build + lint + format +
all tests) green: jwt 22, ack-id 39.

AI assistance disclosure

Per the repository AI policy: implemented and tested by gpt 5.6-sol and fable 5, then reviewed and understood by me. I can explain the implementation and its trade-offs without AI aid. Tests, typecheck, and formatting pass locally against the package suite.

Summary by CodeRabbit

  • Bug Fixes
    • Updated JWT verification so that when an expected audience is provided, verification fails if the token’s aud claim is missing or empty.
    • Adjusted signed A2A message verification so underlying JWT checks no longer expect an aud claim (handshake verification still validates aud).
  • Tests
    • Strengthened audience-related test cases, including additional “aud missing/empty” scenarios, and fixed test isolation by clearing mock call history.
    • Updated A2A signed-message verification assertions to match the new contract.
  • Documentation
    • Added a changeset describing the audience verification behavior update.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@EfeDurmaz16, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 5 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 204457cf-5726-42ce-a8c2-92fc79ec7185

📥 Commits

Reviewing files that changed from the base of the PR and between 8a1ef67 and 8ece5e9.

📒 Files selected for processing (2)
  • packages/ack-id/src/a2a/verify.ts
  • packages/jwt/src/verify.test.ts

Walkthrough

verifyJwt now requires a non-empty aud claim when an audience option is supplied. Signed A2A message verification no longer supplies an audience expectation, while handshake audience verification remains unchanged. Tests cover these behaviors.

Changes

JWT audience verification

Layer / File(s) Summary
Audience presence enforcement
packages/jwt/src/verify.ts
Adds validation that supplied audience expectations require a non-empty string or string-array aud claim.
Signed A2A message verification
packages/ack-id/src/a2a/verify.ts, packages/ack-id/src/a2a/verify.test.ts
Removes audience forwarding for signed messages and updates the signature-verification assertion to require only the issuer.
Audience behavior coverage and release metadata
packages/jwt/src/verify.test.ts, .changeset/jwt-require-audience.md
Tests forwarding, missing and empty claims, optional audience handling, and records the behavior changes.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: venables

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main behavioral change: requiring a non-empty aud claim when an audience is provided.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/jwt/src/verify.test.ts (1)

160-193: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for an empty-string audience.

The contract includes aud: "", but these tests cover only aud: [] and a missing claim. Add a case that verifies requireAudience: true rejects the empty string branch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/jwt/src/verify.test.ts` around lines 160 - 193, Add a test alongside
the existing empty-array case in the verifyJwt tests, using a mock verified
payload with aud set to an empty string and requireAudience enabled. Assert that
verifyJwt rejects with the existing “JWT audience is required but missing”
error.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/jwt/src/verify.test.ts`:
- Around line 149-156: Update the verifyJWT invocation assertions in this test
to inspect this test’s call rather than a prior suite call: clear the mock
history in beforeEach or reference the latest mock call before asserting
requireAudience is absent. Preserve the existing audience assertion.

---

Nitpick comments:
In `@packages/jwt/src/verify.test.ts`:
- Around line 160-193: Add a test alongside the existing empty-array case in the
verifyJwt tests, using a mock verified payload with aud set to an empty string
and requireAudience enabled. Assert that verifyJwt rejects with the existing
“JWT audience is required but missing” error.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d341f587-2a22-4760-bf48-e95f555af5fe

📥 Commits

Reviewing files that changed from the base of the PR and between 43a066c and b6875fa.

📒 Files selected for processing (3)
  • .changeset/jwt-require-audience.md
  • packages/jwt/src/verify.test.ts
  • packages/jwt/src/verify.ts

Comment thread packages/jwt/src/verify.test.ts Outdated
@EfeDurmaz16
EfeDurmaz16 force-pushed the jwt-require-audience branch from b6875fa to ea70b1f Compare July 22, 2026 00:32
@EfeDurmaz16

Copy link
Copy Markdown
Contributor Author

@venables a review when convenient would be great. Companion to #129: an opt-in requireAudience on verifyJwt (the motivating gap is live at ack-id/a2a/verify.ts, noted in the description). Happy to take this issue-first or wire the ack-id call sites if you prefer; CodeRabbit's note is addressed. CI needs a maintainer 'approve and run'.

@domleboss97 domleboss97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Good catch here! I think, however, we shouldn't have a standalone requireAudience flag - whether or not audience was passed is sufficient signal. Couple reasons for this:

  1. it's how the JOSE and PyJWT libraries behave
  2. the requireAudience flag doesn't make sense if passed alone. Passing requireAudience: true with no audience fails everything.

I'd say let's do it so that, when audience is supplied, missing/empty aud fails verification.

I think the above would break verifyA2ASignedMessage, but I think we could do one of two things there: (1) just drop the audience: did, because the i think the messages we're producing don't even have aud on them so it'd be fine or (2) drop audience but, after verifyJwt returns, do the audience validation manually ourselves.

Per review: drop the standalone requireAudience flag. Supplying audience to
verifyJwt now fails a token whose aud claim is missing or empty, matching
jose and PyJWT semantics; did-jwt alone only matches aud when present.

A2A call sites differ and are handled accordingly:
- handshake verification keeps audience: did, since handshake JWTs have
  embedded aud (aud: params.recipient) since the original implementation
- signed-message verification drops audience: did, since signed messages
  carry no aud (createSignedA2AMessage has no recipient parameter) and the
  option never provided a check there

Changeset rewritten for the new semantics; tests updated on both packages.
@EfeDurmaz16 EfeDurmaz16 changed the title feat(jwt): add optional requireAudience to verifyJwt feat(jwt)!: require aud when an audience is expected Jul 30, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/jwt/src/verify.test.ts (1)

157-190: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Cover empty-string audience claims.

This only exercises aud: []; add cases for aud: "" and aud: [""] to lock down both hasAudience branches described by the release note.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/jwt/src/verify.test.ts` around lines 157 - 190, Extend the audience
validation tests near the existing empty-array case to cover JWT payloads with
aud set to an empty string and an array containing an empty string. Mock each
verified result through verifyJWT and assert verifyJwt rejects with “JWT
audience is required but missing,” covering both hasAudience branches while
preserving the existing test setup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/jwt/src/verify.test.ts`:
- Around line 157-190: Extend the audience validation tests near the existing
empty-array case to cover JWT payloads with aud set to an empty string and an
array containing an empty string. Mock each verified result through verifyJWT
and assert verifyJwt rejects with “JWT audience is required but missing,”
covering both hasAudience branches while preserving the existing test setup.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bd7026e2-78a2-43e1-a8c4-2db9a334ea10

📥 Commits

Reviewing files that changed from the base of the PR and between b6875fa and a5a6d0d.

📒 Files selected for processing (5)
  • .changeset/jwt-require-audience.md
  • packages/ack-id/src/a2a/verify.test.ts
  • packages/ack-id/src/a2a/verify.ts
  • packages/jwt/src/verify.test.ts
  • packages/jwt/src/verify.ts

@EfeDurmaz16

Copy link
Copy Markdown
Contributor Author

@domleboss97

Agreed, done in a5a6d0d: the flag is gone, supplying audience now fails on a missing or empty aud, same as JOSE and PyJWT.

One thing I found while applying it to the A2A sites: the two paths differ.

  • Handshake JWTs have always embedded aud (createA2AHandshakePayload sets aud: params.recipient), so I kept audience: did there and it just works under the strict rule.
  • Signed messages never carry aud, createSignedA2AMessage has no recipient parameter at all

so I went with your (1) and dropped audience from that call. No behavior change, did-jwt was skipping the check there anyway.

Worth a follow-up maybe: the identity-a2a README documents aud on signed messages as MITM protection, but the code never implemented it, and jti is generated but never checked.

Happy to do a small PR adding a recipient param to createSignedA2AMessage and verifying aud strictly there. It changes that function's signature, though code search finds no external consumers of the a2a module.

Lock down both hasAudience branches: aud as "", [] and [""] all reject
when an audience is expected.
@EfeDurmaz16

Copy link
Copy Markdown
Contributor Author

🧹 Nitpick comments (1)

packages/jwt/src/verify.test.ts (1)> 157-190: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Cover empty-string audience claims.
This only exercises aud: []; add cases for aud: "" and aud: [""] to lock down both hasAudience branches described by the release note.

🤖 Prompt for AI Agents

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/jwt/src/verify.test.ts` around lines 157 - 190, Extend the audience
validation tests near the existing empty-array case to cover JWT payloads with
aud set to an empty string and an array containing an empty string. Mock each
verified result through verifyJWT and assert verifyJwt rejects with “JWT
audience is required but missing,” covering both hasAudience branches while
preserving the existing test setup.

🤖 Prompt for all review comments with AI agents

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/jwt/src/verify.test.ts`:
- Around line 157-190: Extend the audience validation tests near the existing
empty-array case to cover JWT payloads with aud set to an empty string and an
array containing an empty string. Mock each verified result through verifyJWT
and assert verifyJwt rejects with “JWT audience is required but missing,”
covering both hasAudience branches while preserving the existing test setup.

ℹ️ Review info

Added in 8a1ef67: the empty-aud test is now an it.each over [], "" and [""], so both hasAudience branches are covered.

domleboss97
domleboss97 previously approved these changes Jul 30, 2026

@domleboss97 domleboss97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

very nice

Comment thread packages/jwt/src/verify.test.ts Outdated
},
)

it("throws when an audience is expected and the aud claim is missing", async () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit - i think this could just fold into the above if the it.each(...) included the missing case

…dMessage

The option stays in the type for callers; signed messages carry no aud
claim to verify it against today.
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.

2 participants