Skip to content

Python: enforce the OAuth address policy via a pinning httpx transport (SPEC §16 req 5–6) #816

Description

@jeremy

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

Step 0 (upstream, blocking): a Python 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 a custom httpx transport over a resolving httpcore backend: resolve the hostname, judge every resolved address, and connect to exactly one judged address (pin), keeping the hostname for SNI/Host — connect-time judgement with no second resolution, which is the DNS-rebinding requirement (the address judged MUST be the address connected).
  • The design question: httpx/httpcore has no first-class dial hook; the options are a custom httpcore ConnectionPool with an overridden origin resolver, or constructing requests against the pinned IP with an explicit Host header + SNI override (ssl.SSLContext wrapping). The chosen mechanism must hold TLS verification against the original hostname. Prototype before speccing the final shape.
  • The OAuth module currently uses module-level httpx.post/httpx.stream calls (exchange) and client construction inside the device/discovery paths — enforcement lands by routing all of these through one policed client/transport factory, which is also where the override surface attaches.
  • 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 is_localhost HTTPS exemption is a scheme judgement, not address admission.
  • Overrides (SPEC §16 contract): replacement policy, replacement client/transport, disablement (a caller-supplied client counts — "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 injected-client contracts.

Metadata

Metadata

Assignees

No one assigned

    Labels

    pythonPull requests that update the Python SDKsecuritySecurity issue or hardeningspecChanges to the Smithy spec or OpenAPI

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions