···11+# OAuth Integration Plan for pds-git-remote
22+33+## Background
44+55+pds-git-remote currently authenticates via AT Protocol's `com.atproto.server.createSession` endpoint (handle + app password). Credentials are stored as plaintext JSON in `~/.config/pds-git-remote/auth.json`. There is no token refresh logic — the stored `refresh_jwt` is never used. This works for development but has clear production shortcomings: plaintext secrets on disk, no session renewal, and requiring users to manage app passwords.
66+77+This document explores two distinct integration angles inspired by git-credential-oauth, evaluates their complexity and tradeoffs, and recommends a sequencing strategy.
88+99+---
1010+1111+## What git-credential-oauth Does
1212+1313+[git-credential-oauth](https://github.com/hickford/git-credential-oauth) is a ~500-line Go program that acts as a Git credential helper. When Git needs HTTPS credentials, it invokes helpers through a simple stdin/stdout text protocol with three operations: `get`, `store`, and `erase`. Helpers are chained — Git tries them in order until one returns a username and password.
1414+1515+git-credential-oauth only implements `get`. When called, it:
1616+1717+1. Matches the requested host against preconfigured OAuth providers (GitHub, GitLab, Bitbucket, etc.)
1818+2. Starts a temporary localhost HTTP server
1919+3. Opens the user's browser to the provider's OAuth authorization page
2020+4. Captures the redirect with the authorization code
2121+5. Exchanges the code for an access token
2222+6. Returns `username=oauth2` and `password=<token>` to Git on stdout
2323+2424+It is designed to sit *after* a storage helper in the chain (e.g., `git-credential-cache` or OS keychain), so tokens are cached and the browser flow only triggers on cache miss.
2525+2626+Key design properties:
2727+- **Stateless generator** — delegates all persistence to a paired storage helper
2828+- **Public OAuth client** — no embedded client secret; uses PKCE for security
2929+- **Supports device flow** — for headless/SSH environments (GitHub, GitLab only)
3030+3131+---
3232+3333+## Integration Angle A: Git Credential Helper Protocol for Token Storage
3434+3535+### Concept
3636+3737+Instead of storing AT Protocol credentials in a custom `auth.json` file, store them through the standard Git credential helper infrastructure. This means pds-git-remote would call `git credential fill` / `git credential approve` / `git credential reject` to retrieve and persist tokens, leveraging whatever credential backend the user has configured (OS keychain, `git-credential-cache`, `pass`, etc.).
3838+3939+### What This Would Look Like
4040+4141+**On login (`git-remote-pds auth login`):**
4242+1. Authenticate with PDS via `createSession` (as today)
4343+2. Instead of writing `auth.json`, call `git credential approve` with:
4444+ ```
4545+ protocol=pds
4646+ host=<pds-hostname>
4747+ username=<handle>
4848+ password=<access_jwt>
4949+ ```
5050+5151+**On push/fetch (when auth is needed):**
5252+1. Call `git credential fill` with `protocol=pds` and `host=<pds-hostname>`
5353+2. If a stored credential is returned, use it
5454+3. If the token is expired/rejected by the PDS, call `git credential reject` to clear it, then re-authenticate
5555+5656+**On logout:**
5757+1. Call `git credential reject` to remove stored credentials
5858+5959+### What It Would Require
6060+6161+- **Shell out to `git credential`** — spawn `git credential fill`, `approve`, and `reject` subprocesses with the text protocol on stdin/stdout. This is straightforward; the protocol is line-based key=value pairs.
6262+- **Remove or deprecate `auth.json`** — provide a migration path (read old file on first run, store via credential helper, delete old file).
6363+- **Handle the `protocol` field** — Git credential helpers key on `protocol` + `host`. We'd use a synthetic protocol like `pds` or reuse `https` with the PDS hostname. Using `https` is more compatible with existing helpers but could collide with other credentials for the same host.
6464+- **Token refresh integration** — when `git credential fill` returns an expired JWT, we need to refresh it (using the stored refresh token) and update the credential store. This is the trickiest part: refresh tokens are a second credential that also needs secure storage.
6565+6666+### Complexity
6767+6868+**Low-medium.** The git credential protocol is simple text over stdin/stdout. The main complexity is:
6969+- Deciding how to store refresh tokens alongside access tokens (the credential helper protocol has one `password` field)
7070+- Handling token expiry gracefully (detect 401, refresh, retry, update stored credential)
7171+- Testing across different credential helper backends
7272+7373+### Tradeoffs
7474+7575+| Pro | Con |
7676+|-----|-----|
7777+| Credentials stored securely via OS keychain | Adds dependency on `git` binary being available for credential ops |
7878+| Familiar model for Git users | Credential helper protocol has one `password` field — awkward for access + refresh token pair |
7979+| No custom storage code to maintain | Users must have a storage helper configured (not always the case) |
8080+| Works with any existing credential backend | Synthetic `protocol=pds` may confuse some helpers |
8181+8282+---
8383+8484+## Integration Angle B: AT Protocol OAuth Browser Flow
8585+8686+### Concept
8787+8888+Replace password-based `createSession` authentication with AT Protocol's native OAuth flow. Instead of users providing their handle and app password, pds-git-remote would open a browser, the user would authorize the application on their PDS, and pds-git-remote would receive scoped OAuth tokens.
8989+9090+### How AT Protocol OAuth Works
9191+9292+AT Protocol OAuth is a security-hardened profile of OAuth 2.1 with several mandatory extensions:
9393+9494+1. **Discovery** — Resolve the user's handle to a DID, then to a PDS endpoint. Fetch `/.well-known/oauth-protected-resource` and `/.well-known/oauth-authorization-server` to discover OAuth endpoints.
9595+2. **Pushed Authorization Request (PAR)** — POST authorization parameters to the PAR endpoint (mandatory; URL query parameters are not allowed). Includes PKCE challenge and DPoP proof.
9696+3. **Browser authorization** — Redirect user to the authorization endpoint with the `request_uri` from PAR.
9797+4. **Callback** — Capture the authorization code via localhost redirect.
9898+5. **Token exchange** — POST to the token endpoint with the authorization code, PKCE verifier, and DPoP proof.
9999+6. **Authenticated requests** — Every API call requires an `Authorization: DPoP <token>` header plus a fresh DPoP proof JWT with an `ath` (access token hash) field.
100100+101101+Key differences from standard OAuth:
102102+- **No pre-registration** — the `client_id` is a URL pointing to a publicly hosted JSON client metadata document.
103103+- **Mandatory DPoP** — all tokens are bound to a client-held ES256 keypair; proof JWTs are required on every request.
104104+- **Mandatory PAR** — authorization parameters go via POST, not URL query strings.
105105+- **Mandatory PKCE** with S256 method.
106106+- **Single-use refresh tokens** — each refresh returns a new refresh token.
107107+- **Short-lived access tokens** — 5–30 minutes; refresh tokens last up to 2 weeks (public client) or 3 months (confidential client).
108108+109109+### What It Would Require
110110+111111+1. **Client metadata document** — Host a JSON file at a public HTTPS URL (e.g., `https://pds-git-remote.example.com/oauth-client-metadata.json`) describing the application. For native/CLI apps, a `http://127.0.0.1` redirect URI is standard. Alternatively, use the `did:web:` client ID scheme if available.
112112+113113+2. **DPoP implementation** — Generate an ES256 keypair per session. Sign a DPoP proof JWT for every token request and every authenticated API call. Handle server-issued nonce rotation. This is the most complex piece.
114114+115115+3. **PAR + PKCE + authorization code flow** — Standard OAuth plumbing but with the PAR indirection layer. Generate code verifier, POST to PAR endpoint, open browser, capture callback, exchange code for tokens.
116116+117117+4. **Token management** — Store the DPoP private key, access token, and refresh token securely. Implement single-use refresh token rotation. Handle the `DPoP-Nonce` header on every response.
118118+119119+5. **Modify `PdsClient`** — Replace `Authorization: Bearer <token>` with `Authorization: DPoP <token>` plus DPoP proof headers. The client needs the ES256 private key available for every request.
120120+121121+6. **Identity verification** — After token exchange, verify the returned `sub` DID matches expectations by independently resolving it.
122122+123123+7. **Localhost HTTP server** — For the browser redirect callback (similar to what git-credential-oauth does).
124124+125125+### Available Rust Libraries
126126+127127+- **`atproto-oauth`** (v0.14.0) — Community crate providing DPoP, PKCE, JWT, JWK, and AT Protocol OAuth workflow modules. Actively maintained.
128128+- **`atproto-oauth-aip`** (v0.14.0) — Companion crate with `oauth_init()` and `oauth_complete()` helpers.
129129+- **`p256`**, **`ecdsa`**, **`sha2`** — Low-level crypto crates if building from scratch.
130130+131131+### Complexity
132132+133133+**High.** This is substantially more work than Angle A:
134134+- DPoP proof generation on every request is a significant change to `PdsClient`
135135+- PAR adds an extra round-trip and error-handling layer (nonce retry)
136136+- Single-use refresh token rotation requires careful concurrency handling
137137+- Client metadata hosting is an operational requirement
138138+- The community Rust crates help but are not official and may have gaps
139139+- Testing requires a PDS with OAuth support configured
140140+141141+### Tradeoffs
142142+143143+| Pro | Con |
144144+|-----|-----|
145145+| No app passwords needed — browser-based consent | Significant implementation complexity (DPoP, PAR, nonce rotation) |
146146+| Scoped permissions (only grant what's needed) | Requires hosting a client metadata document at a public URL |
147147+| Aligned with AT Protocol's direction — password auth may be deprecated | Short token lifetimes (5–30 min) mean frequent refresh cycles |
148148+| Better security (token binding, proof-of-possession) | Community Rust libraries exist but are not officially supported |
149149+| Familiar "Sign in with..." UX for users | Every HTTP request to PDS must include a fresh DPoP proof JWT |
150150+151151+---
152152+153153+## Comparison Summary
154154+155155+| Dimension | A: Credential Helper | B: AT Protocol OAuth |
156156+|-----------|----------------------|----------------------|
157157+| **Auth mechanism** | Still password/createSession | OAuth 2.1 browser flow |
158158+| **Security improvement** | Credentials in OS keychain instead of plaintext | Token binding, proof-of-possession, scoped access |
159159+| **User experience** | Same login flow, better storage | Browser-based consent, no passwords to manage |
160160+| **Implementation effort** | ~1–2 weeks | ~4–8 weeks |
161161+| **New dependencies** | None (shells out to git) | `p256`, `ecdsa`, DPoP/JWT libraries or `atproto-oauth` crate |
162162+| **Operational requirements** | None | Host client metadata JSON at a public URL |
163163+| **Risk** | Low — well-understood protocol | Medium — AT Protocol OAuth is newer, Rust ecosystem is community-maintained |
164164+| **Blocks on** | Nothing | AT Protocol OAuth scopes maturation (currently using `transition:generic`) |
165165+166166+---
167167+168168+## Recommendation: Sequencing
169169+170170+**Phase 1: Credential Helper Integration (Angle A)**
171171+172172+Do this first. It's the highest-value, lowest-risk improvement:
173173+- Eliminates plaintext credential storage immediately
174174+- Adds token refresh logic (needed regardless of auth method)
175175+- Small, well-scoped change (~200–400 lines)
176176+- No external dependencies or infrastructure
177177+- Users with OS keychains get secure storage automatically
178178+- Can be done incrementally: add credential helper support alongside `auth.json`, then deprecate the file
179179+180180+**Phase 2: AT Protocol OAuth (Angle B)**
181181+182182+Do this second, once Phase 1 is stable:
183183+- Phase 1's token refresh logic carries over
184184+- Phase 1's credential helper integration can store OAuth tokens too (DPoP keys need separate secure storage)
185185+- By then, AT Protocol's auth scopes system should be more mature (replacing `transition:generic`)
186186+- The `atproto-oauth` Rust crate will have more production mileage
187187+- Can be introduced as an alternative auth method alongside password-based login, with OAuth becoming the default over time
188188+189189+**Phase 1.5 (Optional): Device Flow for Headless Environments**
190190+191191+Between phases, consider adding AT Protocol's device authorization flow if/when PDS implementations support it. This would cover SSH sessions and CI environments where a browser isn't available. This is not currently specified in AT Protocol OAuth but may emerge as the protocol matures.
+3-2
todo.txt
···1122-- finalize lexicon
22+- finalize lexicon
33- more thorough tests
44- inspect pds via pds browser
55- add an e2e test that tests with larger files
66-- add an e2e test that tests what happens with a conflict66+- add an e2e test that tests what happens with a conflict
77+- come up with a better handling of oauth and tokens