Skip to content

feat(did): refuse redirects when resolving did:web by default - #133

Open
EfeDurmaz16 wants to merge 3 commits into
agentcommercekit:mainfrom
EfeDurmaz16:did-resolver-no-redirect
Open

feat(did): refuse redirects when resolving did:web by default#133
EfeDurmaz16 wants to merge 3 commits into
agentcommercekit:mainfrom
EfeDurmaz16:did-resolver-no-redirect

Conversation

@EfeDurmaz16

@EfeDurmaz16 EfeDurmaz16 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

What

getResolver for did:web now sends redirect: "error" when fetching the DID document. A new followRedirects?: boolean option (default false) restores the previous behavior.

Why

allowedHttpHosts is applied to the URL built from the DID:

const url = isHttpAllowed(path, allowedHttpHosts)
  ? `http://${path}`
  : `https://${path}`

The fetch that follows it used the platform default, which follows redirects. So the allowlist only governs the first hop: a did:web resolved over https can answer 302 Location: http://…, and the resolver follows it to a scheme and host the check was there to reject.

flowchart LR
  D["did:web:issuer.example"] --> C{"allowedHttpHosts<br/>allows http for this host?"}
  C -- no --> U["fetch https://issuer.example/.well-known/did.json"]
  U --> H{"host answers 302<br/>Location: http://internal/…"}
  H -->|before: redirect followed| BAD["request lands on the scheme<br/>and host the check rejected"]
  H -->|after: redirect error| OK["resolution fails as notFound"]

  classDef bad stroke-dasharray: 4 3
  class BAD bad
Loading

Because the redirect target is chosen by the DID's own host, this also makes resolution an outbound request an untrusted party controls. That matters for anything that resolves a DID before verifying a signature, which is the normal order: you need the document to check the signature. A server verifying an ACK-ID proof from an arbitrary issuer will fetch whatever that issuer's host points it at.

did:web documents are served directly at /.well-known/did.json (or the configured docPath), so refusing redirects costs legitimate resolution nothing. Deployments that genuinely sit behind a redirect can opt in.

How

  • DidWebResolverOptions gains followRedirects?: boolean, documented with the reason, defaulting to false.
  • fetchDidDocumentAtUrl passes redirect: followRedirects ? "follow" : "error" alongside the existing mode: "cors".
  • Existing tests that assert the fetch options were updated to include the new field; two tests cover the default (refuses, resolves to notFound) and the opt-in (redirect: "follow").

Test plan

pnpm build
pnpm --filter @agentcommercekit/did test   # 72 passed

Found while building an ACK-ID identity-gated x402 demo, where a seller resolves buyer DIDs it has never seen before.

Implemented and tested by gpt 5.6-sol and fable 5, reviewed by me.

Summary by CodeRabbit

  • New Features

    • Added an optional setting to allow HTTP redirects when resolving did:web documents.
    • Redirects are now blocked by default to improve resolution safety.
  • Documentation

    • Documented how to restore redirect-following behavior with followRedirects: true.

allowedHttpHosts is applied to the URL built from the DID, but the document
fetch followed redirects, so the check only governed the first hop: a did:web
served over https could redirect the resolver to plain http, or to a host the
allowlist would have rejected. Because the redirect target is chosen by the
DID's own host, this also turns resolution into an outbound request the
issuer controls.

did:web documents are served directly at a well-known path, so redirects are
refused by default via redirect: "error". followRedirects: true restores the
previous behavior for deployments that need it.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Jul 28, 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: 1 minute

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: d530fc26-14db-4cca-be73-13dcb1dbbeb2

📥 Commits

Reviewing files that changed from the base of the PR and between 8f27a55 and 2ea4a25.

📒 Files selected for processing (3)
  • .changeset/wild-hounds-shave.md
  • packages/did/src/did-resolvers/web-did-resolver.test.ts
  • packages/did/src/did-resolvers/web-did-resolver.ts

Walkthrough

The did:web resolver now rejects HTTP redirects by default through redirect: "error". A followRedirects option restores redirect following when enabled, with updated tests and a minor-release changeset.

Changes

did:web redirect handling

Layer / File(s) Summary
Resolver redirect policy
packages/did/src/did-resolvers/web-did-resolver.ts
Adds the optional followRedirects setting, defaults it to false, and passes the corresponding redirect mode to document fetching.
Redirect validation and release metadata
packages/did/src/did-resolvers/web-did-resolver.test.ts, .changeset/wild-hounds-shave.md
Updates fetch expectations, tests default redirect rejection and opt-in following, and documents the minor release behavior.

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

Possibly related PRs

  • agentcommercekit/ack#129: Modifies the same did:web document-fetching path and its tests with timeout and abort-signal handling.

Suggested reviewers: venables

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: did:web resolution now refuses redirects by default.
✨ 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.

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.

One comment on the error handling, but I think this is the right move!

const res = await fetch(url, { mode: "cors" })
const res = await fetch(url, {
mode: "cors",
redirect: followRedirects ? "follow" : "error",

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.

i think it might be more useful to do manual here. with error this will throw with a TypeError, and then I think the resolver has nothing better ot report than notFound.

with manual, fetch resolves with the redirect repsonse (although in browsers i think it's opaque response), so we could throw a precise error - on node it could include the redirect target from the Location header.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 22f6a5d: switched to redirect: "manual". The resolver now refuses any redirect response with an error naming the Location target on Node; browsers surface an opaque redirect so the same error fires without a target. Tests cover both cases, changeset updated.

Per review: redirect: "error" rejects with a bare TypeError, which the
resolver can only surface as a generic notFound. With redirect: "manual"
the redirect resolves as a response, so the resolver throws a precise
error naming the Location target on Node; browsers surface an opaque
redirect and get the same error without a target.
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.

🚀

) {
const location = res.headers.get("location")
throw new Error(
`DID resolution refused a redirect${location ? ` to ${location}` : ""}`,

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 - might be nice to add an action here, i.e. Set followRedirects: true to allow redirects.

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