Skip to content

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

Open
EfeDurmaz16 wants to merge 4 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 4 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 followRedirects: true setting for resolving did:web documents.
    • Redirects are blocked by default during resolution.
    • When a redirect is blocked, the resolver reports the redirect target when available.
    • Redirect responses without a usable target now return a clear resolution error.
  • 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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2689f691-aa41-4efd-89f0-b025bb7b154e

📥 Commits

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

📒 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
🚧 Files skipped from review as they are similar to previous changes (1)
  • .changeset/wild-hounds-shave.md

Walkthrough

The did:web resolver now rejects HTTP redirects by default through manual redirect handling. The followRedirects option restores redirect following when enabled. Tests and a minor-release changeset document the behavior.

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 selects manual or automatic redirect handling for 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 redirect rejection and opt-in following, and documents the 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: refusing redirects by default during did:web resolution.
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.

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.

domleboss97
domleboss97 previously approved these changes Jul 30, 2026
@venables

venables commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

@EfeDurmaz16 mind resolving the conflicts?

Resolves the conflicts between the redirect refusal in this branch and the
did:web fetch timeout added upstream in agentcommercekit#129. Both options now apply to the
same request: the resolver sends `redirect: "manual"` (unless
`followRedirects` is set) and an `AbortSignal` built from `timeout`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ScNeLEEDvtbyDg8MQwRob3
@EfeDurmaz16

Copy link
Copy Markdown
Contributor Author

@venables resolved!

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.

4 participants