⚘ use your pds as a git remote if you want to ⚘
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

update todo

notplants 5cb2f96e 18ae7100

+194 -2
+191
oauth-plan.md
··· 1 + # OAuth Integration Plan for pds-git-remote 2 + 3 + ## Background 4 + 5 + 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. 6 + 7 + This document explores two distinct integration angles inspired by git-credential-oauth, evaluates their complexity and tradeoffs, and recommends a sequencing strategy. 8 + 9 + --- 10 + 11 + ## What git-credential-oauth Does 12 + 13 + [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. 14 + 15 + git-credential-oauth only implements `get`. When called, it: 16 + 17 + 1. Matches the requested host against preconfigured OAuth providers (GitHub, GitLab, Bitbucket, etc.) 18 + 2. Starts a temporary localhost HTTP server 19 + 3. Opens the user's browser to the provider's OAuth authorization page 20 + 4. Captures the redirect with the authorization code 21 + 5. Exchanges the code for an access token 22 + 6. Returns `username=oauth2` and `password=<token>` to Git on stdout 23 + 24 + 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. 25 + 26 + Key design properties: 27 + - **Stateless generator** — delegates all persistence to a paired storage helper 28 + - **Public OAuth client** — no embedded client secret; uses PKCE for security 29 + - **Supports device flow** — for headless/SSH environments (GitHub, GitLab only) 30 + 31 + --- 32 + 33 + ## Integration Angle A: Git Credential Helper Protocol for Token Storage 34 + 35 + ### Concept 36 + 37 + 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.). 38 + 39 + ### What This Would Look Like 40 + 41 + **On login (`git-remote-pds auth login`):** 42 + 1. Authenticate with PDS via `createSession` (as today) 43 + 2. Instead of writing `auth.json`, call `git credential approve` with: 44 + ``` 45 + protocol=pds 46 + host=<pds-hostname> 47 + username=<handle> 48 + password=<access_jwt> 49 + ``` 50 + 51 + **On push/fetch (when auth is needed):** 52 + 1. Call `git credential fill` with `protocol=pds` and `host=<pds-hostname>` 53 + 2. If a stored credential is returned, use it 54 + 3. If the token is expired/rejected by the PDS, call `git credential reject` to clear it, then re-authenticate 55 + 56 + **On logout:** 57 + 1. Call `git credential reject` to remove stored credentials 58 + 59 + ### What It Would Require 60 + 61 + - **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. 62 + - **Remove or deprecate `auth.json`** — provide a migration path (read old file on first run, store via credential helper, delete old file). 63 + - **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. 64 + - **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. 65 + 66 + ### Complexity 67 + 68 + **Low-medium.** The git credential protocol is simple text over stdin/stdout. The main complexity is: 69 + - Deciding how to store refresh tokens alongside access tokens (the credential helper protocol has one `password` field) 70 + - Handling token expiry gracefully (detect 401, refresh, retry, update stored credential) 71 + - Testing across different credential helper backends 72 + 73 + ### Tradeoffs 74 + 75 + | Pro | Con | 76 + |-----|-----| 77 + | Credentials stored securely via OS keychain | Adds dependency on `git` binary being available for credential ops | 78 + | Familiar model for Git users | Credential helper protocol has one `password` field — awkward for access + refresh token pair | 79 + | No custom storage code to maintain | Users must have a storage helper configured (not always the case) | 80 + | Works with any existing credential backend | Synthetic `protocol=pds` may confuse some helpers | 81 + 82 + --- 83 + 84 + ## Integration Angle B: AT Protocol OAuth Browser Flow 85 + 86 + ### Concept 87 + 88 + 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. 89 + 90 + ### How AT Protocol OAuth Works 91 + 92 + AT Protocol OAuth is a security-hardened profile of OAuth 2.1 with several mandatory extensions: 93 + 94 + 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. 95 + 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. 96 + 3. **Browser authorization** — Redirect user to the authorization endpoint with the `request_uri` from PAR. 97 + 4. **Callback** — Capture the authorization code via localhost redirect. 98 + 5. **Token exchange** — POST to the token endpoint with the authorization code, PKCE verifier, and DPoP proof. 99 + 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. 100 + 101 + Key differences from standard OAuth: 102 + - **No pre-registration** — the `client_id` is a URL pointing to a publicly hosted JSON client metadata document. 103 + - **Mandatory DPoP** — all tokens are bound to a client-held ES256 keypair; proof JWTs are required on every request. 104 + - **Mandatory PAR** — authorization parameters go via POST, not URL query strings. 105 + - **Mandatory PKCE** with S256 method. 106 + - **Single-use refresh tokens** — each refresh returns a new refresh token. 107 + - **Short-lived access tokens** — 5–30 minutes; refresh tokens last up to 2 weeks (public client) or 3 months (confidential client). 108 + 109 + ### What It Would Require 110 + 111 + 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. 112 + 113 + 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. 114 + 115 + 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. 116 + 117 + 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. 118 + 119 + 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. 120 + 121 + 6. **Identity verification** — After token exchange, verify the returned `sub` DID matches expectations by independently resolving it. 122 + 123 + 7. **Localhost HTTP server** — For the browser redirect callback (similar to what git-credential-oauth does). 124 + 125 + ### Available Rust Libraries 126 + 127 + - **`atproto-oauth`** (v0.14.0) — Community crate providing DPoP, PKCE, JWT, JWK, and AT Protocol OAuth workflow modules. Actively maintained. 128 + - **`atproto-oauth-aip`** (v0.14.0) — Companion crate with `oauth_init()` and `oauth_complete()` helpers. 129 + - **`p256`**, **`ecdsa`**, **`sha2`** — Low-level crypto crates if building from scratch. 130 + 131 + ### Complexity 132 + 133 + **High.** This is substantially more work than Angle A: 134 + - DPoP proof generation on every request is a significant change to `PdsClient` 135 + - PAR adds an extra round-trip and error-handling layer (nonce retry) 136 + - Single-use refresh token rotation requires careful concurrency handling 137 + - Client metadata hosting is an operational requirement 138 + - The community Rust crates help but are not official and may have gaps 139 + - Testing requires a PDS with OAuth support configured 140 + 141 + ### Tradeoffs 142 + 143 + | Pro | Con | 144 + |-----|-----| 145 + | No app passwords needed — browser-based consent | Significant implementation complexity (DPoP, PAR, nonce rotation) | 146 + | Scoped permissions (only grant what's needed) | Requires hosting a client metadata document at a public URL | 147 + | Aligned with AT Protocol's direction — password auth may be deprecated | Short token lifetimes (5–30 min) mean frequent refresh cycles | 148 + | Better security (token binding, proof-of-possession) | Community Rust libraries exist but are not officially supported | 149 + | Familiar "Sign in with..." UX for users | Every HTTP request to PDS must include a fresh DPoP proof JWT | 150 + 151 + --- 152 + 153 + ## Comparison Summary 154 + 155 + | Dimension | A: Credential Helper | B: AT Protocol OAuth | 156 + |-----------|----------------------|----------------------| 157 + | **Auth mechanism** | Still password/createSession | OAuth 2.1 browser flow | 158 + | **Security improvement** | Credentials in OS keychain instead of plaintext | Token binding, proof-of-possession, scoped access | 159 + | **User experience** | Same login flow, better storage | Browser-based consent, no passwords to manage | 160 + | **Implementation effort** | ~1–2 weeks | ~4–8 weeks | 161 + | **New dependencies** | None (shells out to git) | `p256`, `ecdsa`, DPoP/JWT libraries or `atproto-oauth` crate | 162 + | **Operational requirements** | None | Host client metadata JSON at a public URL | 163 + | **Risk** | Low — well-understood protocol | Medium — AT Protocol OAuth is newer, Rust ecosystem is community-maintained | 164 + | **Blocks on** | Nothing | AT Protocol OAuth scopes maturation (currently using `transition:generic`) | 165 + 166 + --- 167 + 168 + ## Recommendation: Sequencing 169 + 170 + **Phase 1: Credential Helper Integration (Angle A)** 171 + 172 + Do this first. It's the highest-value, lowest-risk improvement: 173 + - Eliminates plaintext credential storage immediately 174 + - Adds token refresh logic (needed regardless of auth method) 175 + - Small, well-scoped change (~200–400 lines) 176 + - No external dependencies or infrastructure 177 + - Users with OS keychains get secure storage automatically 178 + - Can be done incrementally: add credential helper support alongside `auth.json`, then deprecate the file 179 + 180 + **Phase 2: AT Protocol OAuth (Angle B)** 181 + 182 + Do this second, once Phase 1 is stable: 183 + - Phase 1's token refresh logic carries over 184 + - Phase 1's credential helper integration can store OAuth tokens too (DPoP keys need separate secure storage) 185 + - By then, AT Protocol's auth scopes system should be more mature (replacing `transition:generic`) 186 + - The `atproto-oauth` Rust crate will have more production mileage 187 + - Can be introduced as an alternative auth method alongside password-based login, with OAuth becoming the default over time 188 + 189 + **Phase 1.5 (Optional): Device Flow for Headless Environments** 190 + 191 + 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
··· 1 1 2 - - finalize lexicon 2 + - finalize lexicon 3 3 - more thorough tests 4 4 - inspect pds via pds browser 5 5 - add an e2e test that tests with larger files 6 - - add an e2e test that tests what happens with a conflict 6 + - add an e2e test that tests what happens with a conflict 7 + - come up with a better handling of oauth and tokens