Skip to content

TypeScript: enforce the OAuth address policy via an undici lookup seam, settling the global-fetch conflict (SPEC §16 req 5–6) #815

Description

@jeremy

Extend SPEC §16 requirements 5–6 (dial-time address judgement of the advertised issuer, token_endpoint, and device_authorization_endpoint) to the TypeScript SDK. Appendix F records today's state: scheme gate, bounded timeout, redirect: "manual", bounded body — a private address is still dialed.

Step 0 (upstream, blocking): a TypeScript surfguard classification-core port in the surfguard repo (classification + resolve, IANA generator over script/iana/*.json, mirrored 157-case corpus with byte-equality drift tests). Tracked as basecamp/surfguard#25.

Enforcement design and the open question to settle first:

  • The seam is an undici Agent with a connect.lookup hook (Node ≥18): resolve, judge every resolved address, and hand the connector only the addresses that passed — connect-time judgement with no second resolution, which is the DNS-rebinding requirement (the address judged MUST be the address connected).
  • The conflict to resolve: the SDK's OAuth surface is written against global fetch (WHATWG), which does not accept an undici dispatcher portably. Options to evaluate in the design: (a) accept a Node-only enforcement tier — the policy applies when running on Node/undici and is documented absent elsewhere; (b) use undici's fetch with a per-request dispatcher when undici is importable, falling back to global fetch (documented unenforced) otherwise; (c) an injectable "policed fetch" the SDK builds on Node. The issue is not prejudged; the chosen answer must state the non-Node story explicitly in Appendix F rather than leaving it to inference.
  • Loopback is explicit operator configuration: a policy option on the OAuth entry points (mirroring Go's AllowLoopback), never inferred from the endpoint URL or other request-derived values. The existing isLocalhost HTTPS exemption is a scheme judgement, not address admission.
  • Overrides (SPEC §16 contract): replacement policy, replacement fetch/client, disablement (a custom fetch counts — and stays "yours, enforcement included").
  • Verdicts: invalid_issuer_origin on the discovery hop, api_error elsewhere, non-retryable, device poll terminates on first refusal; unresolvable stays a retryable network fault.

Definition of done: Appendix F rows updated; unit tests assert refusal-before-dial for private/loopback/CGNAT/special-purpose targets on discovery, device, and exchange paths, plus override and custom-fetch contracts, on the supported (Node) tier.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    securitySecurity issue or hardeningspecChanges to the Smithy spec or OpenAPItypescriptPull requests that update TypeScript code

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions