basecamp / basecamp/basecamp-sdk

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

Open
#816 0 comments 0 reactions 0 assignees View on GitHub
python security spec
Dominant language
Go
Stars
49
Forks
12
Avg merge
20h 47m
Merged PRs (30d)
89

Description

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.

Contributor guide

Open the contributing guide

Research direction

Start with the Python OAuth module's discovery, device, and exchange paths, including its module-level httpx calls and client construction. Review SPEC §16 requirements 5–6 and Appendix F, then prototype the transport seam after the blocking surfguard classification-core port is available. Done means updating Appendix F and adding refusal-before-dial and override or injected-client tests for all three paths.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, authentication, security
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.