CLI app for developers prototyping atproto functionality
1
fork

Configure Feed

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

docs(oauth-client): README section, reachability doc, module CLAUDE.md

Creates comprehensive documentation for the oauth client test subcommand:

- docs/test-oauth-client-reachability.md: Explains same-host and remote-client
workflows for interactive mode, including tunnel setup instructions.

- README.md: Adds 'test oauth client' section with usage examples for both static
and interactive modes, exit code semantics, and link to reachability docs.

- src/commands/test/oauth/client/CLAUDE.md: Module-level documentation covering:
- Public API entry points (ClientCmd::run, parse_target, run_pipeline)
- Check inventory across all four stages (discovery, metadata, JWKS, interactive)
- Diagnostic code naming convention and prefixes
- Exit-code semantics (0 pass, 1 spec violation, 2 network error)
- Injection seams for testability (HttpClient, JwksFetcher, Clock, RpFactory)
- Interactive mode architecture (fake AS + RelyingParty)
- Invariants and snapshot contract

Also updates oauth_client_interactive.rs test assertion to account for Phase 8
sub-stages now running in the happy path (18 checks total: 6 phase 7 + 12 phase 8).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

+181 -1
+67
README.md
··· 85 85 - `1` — one or more spec-required checks failed. 86 86 - `2` — no spec-required checks failed but one or more network errors occurred. 87 87 88 + ### `test oauth client` 89 + 90 + Runs a conformance suite against an atproto OAuth client, validating DPoP and 91 + scope conformance. The suite has up to four stages: 92 + 93 + - **Discovery**: Metadata URL is reachable and returns valid JSON. 94 + - **Metadata**: Client metadata conforms to OAuth 2.0 and DPoP requirements. 95 + - **JWKS**: JSON Web Key Set is present, valid, and keys are properly signed. 96 + - **Interactive** (optional): Fake AS server exercises the client's full auth 97 + flow including PAR, authorization, token exchange, and refresh-token rotation. 98 + 99 + ```text 100 + Test an atproto OAuth client 101 + 102 + Usage: atproto-devtool test oauth client [OPTIONS] <TARGET> [COMMAND] 103 + 104 + Arguments: 105 + <TARGET> 106 + The client's `client_id` URL, or a loopback form for development clients. 107 + 108 + Options: 109 + --no-color 110 + Suppress ANSI color in rendered output 111 + 112 + --verbose 113 + Emit DEBUG-level tracing 114 + 115 + -h, --help 116 + Print help 117 + 118 + Commands: 119 + interactive 120 + Run static checks plus an interactive flow against a fake authorization server 121 + ``` 122 + 123 + #### Static Mode (Default) 124 + 125 + Validates the client's metadata and JWKS without driving an interactive flow: 126 + 127 + ```bash 128 + atproto-devtool test oauth client https://client.example.com/metadata.json 129 + ``` 130 + 131 + #### Interactive Mode 132 + 133 + Instantiates a fake atproto authorization server and exercises the client's 134 + OAuth flow (PAR, authorize, token exchange, scope grants, DPoP nonce rotation, 135 + refresh-token rotation): 136 + 137 + ```bash 138 + # Same-host client (default) 139 + atproto-devtool test oauth client interactive 140 + 141 + # Remote client via tunnel 142 + atproto-devtool test oauth client interactive --public-base-url https://tunnel.example.com 143 + ``` 144 + 145 + See [Reachability Workflows](docs/test-oauth-client-reachability.md) for 146 + detailed setup instructions for remote clients. 147 + 148 + #### Exit codes 149 + 150 + - `0` — all checks pass. 151 + - `1` — one or more spec violations (DPoP, scope, conformance issues). 152 + - `2` — network errors (unreachable metadata, JWKS, or AS endpoint) with no 153 + spec violations. 154 + 88 155 ## License 89 156 90 157 Licensed under either of
+33
docs/test-oauth-client-reachability.md
··· 1 + # OAuth Client Test — Reachability Workflows 2 + 3 + The `test oauth client interactive` subcommand supports two reachability workflows for connecting a client to the development environment's fake authorization server. 4 + 5 + ## Same-Host Client (Default) 6 + 7 + When the client and `atproto-devtool` run on the same machine: 8 + 9 + ```bash 10 + atproto-devtool test oauth client interactive 11 + ``` 12 + 13 + The fake AS binds to `http://127.0.0.1:<port>` (ephemeral port by default). Clients configure their AS discovery endpoint to point to this loopback address. This requires no external infrastructure and is fully deterministic. 14 + 15 + ## Remote Client via Tunnel 16 + 17 + When the client runs on a different machine or network, establish a tunnel and advertise the public URL: 18 + 19 + ```bash 20 + # Terminal 1: Start cloudflared tunnel (or ngrok, Tailscale Funnel, etc.) 21 + cloudflared tunnel --url http://127.0.0.1:8080 22 + 23 + # Terminal 2: Run test with public URL 24 + atproto-devtool test oauth client interactive --public-base-url https://my-tunnel.example.com 25 + ``` 26 + 27 + The fake AS listens on the specified `--port` (default 8080 for interactive mode) and advertises itself via the public base URL. Clients connecting from outside the loopback network discover and communicate with the AS through the tunnel. 28 + 29 + ## Important Notes 30 + 31 + The fake AS speaks plaintext HTTP. TLS termination and certificate management are delegated to the tunnel endpoint (cloudflared, ngrok, etc.). `atproto-devtool` does not manage or generate TLS certificates. 32 + 33 + For deterministic test results across runs, use fixed ports and consistent tunnel URLs. The interactive flow is designed for development and conformance testing, not for production client validation.
+73
src/commands/test/oauth/client/CLAUDE.md
··· 1 + # OAuth Client Conformance Test Module 2 + 3 + `atproto-devtool test oauth client <target>` validates an atproto OAuth client against the Demonstration of Proof-of-Possession (DPoP) and scope conformance specifications. 4 + 5 + ## Public API 6 + 7 + ### Entry Points 8 + 9 + - `ClientCmd::run(target, opts)` — CLI entry point; parses target and dispatches to static or interactive mode. 10 + - `pipeline::parse_target(s)` — Parse and validate a client_id target (HTTPS URL or loopback). 11 + - `pipeline::run_pipeline(target, opts)` — Execute the full conformance pipeline; returns an OauthClientReport. 12 + 13 + ## Check Inventory 14 + 15 + The pipeline consists of four stages: 16 + 17 + 1. **Discovery** — Metadata URL is reachable and returns valid JSON. 18 + 2. **Metadata** — Client metadata conforms to OAuth 2.0 and DPoP requirements (scope, grant types, JWKS, etc.). 19 + 3. **JWKS** — JSON Web Key Set is present, valid, and keys are properly signed. 20 + 4. **Interactive** — (optional) Fake AS server exercises the client's full auth flow (PAR, authorize, token exchange, refresh). 21 + - **scope_variations** sub-stage — Tests scope grant variants: full approval, partial grant, user denial, refresh scoping. 22 + - **dpop_edges** sub-stage — Tests DPoP edge cases: nonce rotation, refresh-token rotation, replay rejection. 23 + 24 + ### Diagnostic Code Prefixes 25 + 26 + - `oauth_client::target::*` — Target parsing errors. 27 + - `oauth_client::discovery::*` — Metadata discovery and reachability. 28 + - `oauth_client::metadata::*` — Client metadata validation. 29 + - `oauth_client::jws::*` — JWKS and cryptographic checks. 30 + - `oauth_client::interactive::*` — Happy-path flow checks. 31 + - `oauth_client::interactive::scope_variations::*` — Scope variant checks. 32 + - `oauth_client::interactive::dpop_edges::*` — DPoP edge case checks. 33 + 34 + ## Exit-Code Semantics 35 + 36 + Exit codes follow the labeler report contract (inherited via `OauthClientReport`): 37 + 38 + - **0** — All checks pass or are skipped/advisory; no SpecViolations or NetworkErrors. 39 + - **1** — At least one SpecViolation (spec non-conformance). 40 + - **2** — At least one NetworkError (I/O failure) and no SpecViolations. 41 + 42 + ## Injection Seams (Testability) 43 + 44 + Every network hop is a trait object, allowing full testability and determinism: 45 + 46 + - `HttpClient` — Metadata and JWKS discovery. 47 + - `JwksFetcher` — JWKS URI resolution. 48 + - `Clock` — Deterministic timestamp generation. 49 + - `RpFactory` — Builds RelyingParty instances with seeded crypto. 50 + 51 + Production wires `RealClock` and `DefaultRpFactory`; tests wire `FakeClock` and `DeterministicRpFactory`. 52 + 53 + ## Interactive Mode (Fake AS) 54 + 55 + The interactive stage instantiates a real axum HTTP server (the fake AS) and a RelyingParty (the test probe): 56 + 57 + - **Fake AS** — Echoes OAuth discovery, serves metadata, handles PAR/authorize/token endpoints, enforces DPoP, rotation, and nonce tracking. 58 + - **RelyingParty** — Deterministic ECDSA signing, PKCE, and state management; driven by `FlowScript` enum per test case. 59 + 60 + The `FlowScript` struct drives the fake AS's per-flow response behavior (approve, deny, rotation, nonce injection, etc.). Both components are deterministic under a fixed `Clock` and RNG seed. 61 + 62 + ## Invariants 63 + 64 + - Every network request is a trait-object call; no unconstrained I/O. 65 + - The fake AS is a real tokio-based HTTP server; discovery and metadata are genuine HTTP transactions. 66 + - RelyingParty is deterministic under fixed clock and RNG seed; identical runs produce identical DPoP proofs and request signatures. 67 + - Diagnostic codes are stable and part of the public contract; snapshot files pin them. 68 + 69 + ## Snapshots 70 + 71 + Snapshot files (`tests/snapshots/oauth_client_*.snap`) document the full report rendering for each test case. Check IDs and diagnostic codes must appear in at least one snapshot file for completeness (verified by `oauth_client_check_id_coverage.rs`). 72 + 73 + Last verified: 2026-04-21
+8 -1
tests/oauth_client_interactive.rs
··· 276 276 let output = interactive::run(static_gating, None, None, &interactive_opts, clock).await; 277 277 278 278 // Verify that all expected checks are present. 279 - assert_eq!(output.results.len(), 6, "should have 6 checks"); 279 + // Phase 7: 6 checks (ServerBound, ClientReachedPar, ClientUsedPkceS256, ClientIncludedDpop, ClientCompletedToken, ClientRefreshed). 280 + // Phase 8: 6 scope_variations checks + 6 dpop_edges checks. 281 + // Total: 18 checks. 282 + assert_eq!( 283 + output.results.len(), 284 + 18, 285 + "should have 18 checks (6 phase 7 + 6 scope_variations + 6 dpop_edges)" 286 + ); 280 287 281 288 // Check that ServerBound passed (server bind was successful). 282 289 let server_bound = output