CLI app for developers prototyping atproto functionality
1
fork

Configure Feed

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

Document oauth_client Check enum variants with spec references

The Check enums are the source-of-truth definitions of what each
check means in each stage, but most oauth_client variants were
undocumented (metadata.rs entirely) or had one-line paraphrases of
the variant name. Replace these with spec-grounded rustdoc: every
variant now cites the relevant RFC section(s) or atproto OAuth
profile URL so a reader can trace the rule from code back to its
specification without guessing.

Docs-only; no behavioural, rendering, or API changes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

+288 -30
+18 -3
src/commands/test/oauth/client/pipeline/discovery.rs
··· 55 55 /// Checks performed by the discovery stage. 56 56 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 57 57 pub enum Check { 58 - /// Whether the client_id is well-formed. 58 + /// `client_id` is well-formed for the atproto OAuth profile: an 59 + /// `https://` URL whose path resolves a client-metadata document 60 + /// (<https://atproto.com/specs/oauth#clients>), or a loopback 61 + /// development URL (`http://localhost`, `http://127.0.0.1`, 62 + /// `http://[::1]`) which gets implicit metadata 63 + /// (<https://atproto.com/specs/oauth#localhost-client-development>). 64 + /// Plain-`http` non-loopback targets and non-HTTP schemes are 65 + /// rejected here. 59 66 ClientIdWellFormed, 60 - /// Whether the metadata document is fetchable (for HTTPS) or implicit (loopback). 67 + /// For HTTPS clients, the metadata document is fetched over 68 + /// HTTPS and returns a 2xx response. For loopback clients the 69 + /// check is skipped because metadata is implicit (the authorization 70 + /// server synthesizes a metadata document from `client_id` rather 71 + /// than retrieving one). Mirrors RFC 7591 §2 72 + /// (<https://datatracker.ietf.org/doc/html/rfc7591#section-2>). 61 73 MetadataDocumentFetchable, 62 - /// Whether the metadata document is valid JSON. 74 + /// The fetched metadata body is valid JSON. Gates 75 + /// `metadata::RawDocumentDeserializes` downstream: if the body 76 + /// doesn't parse as JSON here, the full metadata stage is blocked. 77 + /// Skipped for loopback clients (implicit metadata). 63 78 MetadataIsJson, 64 79 } 65 80
+45 -6
src/commands/test/oauth/client/pipeline/interactive.rs
··· 49 49 } 50 50 51 51 /// Checks performed by the interactive stage. 52 + /// 53 + /// Unlike the static stages, these checks observe what an actual 54 + /// OAuth client *does* when driven against the fake AS. The atproto 55 + /// OAuth profile (<https://atproto.com/specs/oauth>) requires clients 56 + /// to (1) push authorization requests via PAR (RFC 9126 57 + /// <https://datatracker.ietf.org/doc/html/rfc9126>), (2) use PKCE 58 + /// with `S256` (RFC 7636 <https://datatracker.ietf.org/doc/html/rfc7636>), 59 + /// and (3) include DPoP proofs (RFC 9449 60 + /// <https://datatracker.ietf.org/doc/html/rfc9449>); these variants 61 + /// surface whether the client actually did so in the observed flow. 52 62 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 53 63 pub enum Check { 54 - /// Server bound and identity advertised. 64 + /// The in-process fake authorization server successfully bound 65 + /// its TCP port and printed its synthetic identity (handle, DID, 66 + /// base URL). Infrastructure prerequisite for every subsequent 67 + /// interactive check — if it fails, the fake AS never came up, so 68 + /// the client had nothing to talk to. 55 69 ServerBound, 56 - /// Client reached the PAR endpoint. 70 + /// The client sent a request to the fake AS's pushed-authorization 71 + /// endpoint (`/oauth/par`) before any `/oauth/authorize` 72 + /// redirect. Atproto requires PAR per RFC 9126 73 + /// (<https://datatracker.ietf.org/doc/html/rfc9126>) so sensitive 74 + /// request parameters never travel through a browser redirect; 75 + /// a client that jumps straight to `/oauth/authorize` fails this 76 + /// check. 57 77 ClientReachedPar, 58 - /// Client used PKCE S256 method. 78 + /// The PAR request carried `code_challenge_method=S256` (and a 79 + /// `code_challenge`). Atproto mandates the S256 PKCE method per 80 + /// RFC 7636 §4.3 81 + /// (<https://datatracker.ietf.org/doc/html/rfc7636#section-4.3>); 82 + /// the `plain` method is forbidden. 59 83 ClientUsedPkceS256, 60 - /// Client included DPoP proof. 84 + /// The PAR request carried a `DPoP` header containing a 85 + /// well-formed DPoP proof JWT. Atproto's DPoP requirement (RFC 86 + /// 9449 <https://datatracker.ietf.org/doc/html/rfc9449>) binds 87 + /// access tokens to a client-held key and starts at the PAR 88 + /// endpoint, not only at the token endpoint. 61 89 ClientIncludedDpop, 62 - /// Client completed token exchange. 90 + /// The client completed the authorization-code ↔ token exchange 91 + /// against `/oauth/token` and received an access token. Mirrors 92 + /// RFC 6749 §4.1.3 93 + /// (<https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3>); 94 + /// gated on the confidential-client signing key being compatible 95 + /// with `token_endpoint_auth_signing_alg` (see 96 + /// `jwks::Check::HasKeyForSigningAlg`). 63 97 ClientCompletedToken, 64 - /// Client refreshed access token. 98 + /// The client successfully exchanged a refresh token for a new 99 + /// access token via RFC 6749 §6 100 + /// (<https://datatracker.ietf.org/doc/html/rfc6749#section-6>). 101 + /// Gated on the client declaring `refresh_token` in 102 + /// `grant_types`; covered in detail by the `scope_variations` and 103 + /// `dpop_edges` sub-stages. 65 104 ClientRefreshed, 66 105 } 67 106
+45 -6
src/commands/test/oauth/client/pipeline/interactive/dpop_edges.rs
··· 33 33 } 34 34 35 35 /// Checks performed by the DPoP edges sub-stage. 36 + /// 37 + /// These exercise the edge cases of RFC 9449 (DPoP, 38 + /// <https://datatracker.ietf.org/doc/html/rfc9449>) plus the atproto 39 + /// profile's refresh-token rotation requirement. The first three 40 + /// variants verify correct client behaviour under nominal AS 41 + /// responses; the three `*Violation` variants run a deliberately 42 + /// broken relying party against the fake AS and expect the AS to 43 + /// detect and reject the broken behaviour. 36 44 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 37 45 pub enum Check { 38 - /// DPoP nonce rotation on use_dpop_nonce response. 46 + /// The client adopts a new DPoP nonce when the AS returns 47 + /// `use_dpop_nonce` with a `DPoP-Nonce` header. RFC 9449 §8 48 + /// (<https://datatracker.ietf.org/doc/html/rfc9449#section-8>) 49 + /// lets a server demand clients include a server-chosen nonce in 50 + /// subsequent proofs; a compliant client retries with the nonce 51 + /// attached. 39 52 NonceRotation, 40 - /// Refresh token rotation flow. 53 + /// The client successfully rotates refresh tokens: the token 54 + /// endpoint returns a new refresh token on each refresh, and the 55 + /// client uses the newest one on its next request. Atproto 56 + /// requires single-use refresh tokens (per RFC 6819 §5.2.2.3 57 + /// <https://datatracker.ietf.org/doc/html/rfc6819#section-5.2.2.3> 58 + /// and the atproto OAuth profile), so a client that keeps 59 + /// re-using the original refresh token fails the next check 60 + /// (`RefreshTokenReuseViolation`). 41 61 RefreshRotation, 42 - /// Replay rejection for duplicate JTI. 62 + /// The AS rejects a DPoP proof whose `jti` it has already seen. 63 + /// RFC 9449 §11.1 64 + /// (<https://datatracker.ietf.org/doc/html/rfc9449#section-11.1>) 65 + /// requires servers to keep a short-lived `jti` cache to prevent 66 + /// proof replay; this check drives a deliberate replay and 67 + /// expects a rejection. 43 68 ReplayRejection, 44 - /// Violation: JTI reused across requests. 69 + /// Violation check: a client that reuses the same `jti` across 70 + /// multiple DPoP proofs is correctly rejected. RFC 9449 §4.1 71 + /// (<https://datatracker.ietf.org/doc/html/rfc9449#section-4.1>) 72 + /// requires each proof's `jti` to be unique; this variant 73 + /// verifies the AS rejects the offending proof rather than 74 + /// letting it through. 45 75 JtiReuseViolation, 46 - /// Violation: Nonce received but not adopted. 76 + /// Violation check: a client that ignores a server-issued DPoP 77 + /// nonce (continues sending proofs without it) is correctly 78 + /// rejected on its next request. Verifies the nonce-enforcement 79 + /// branch of RFC 9449 §8 80 + /// (<https://datatracker.ietf.org/doc/html/rfc9449#section-8>). 47 81 NonceIgnoredViolation, 48 - /// Violation: Refresh token reused after rotation. 82 + /// Violation check: presenting a refresh token that has already 83 + /// been redeemed is rejected. Atproto's profile requires 84 + /// single-use refresh tokens, and RFC 6819 §5.2.2.3 85 + /// (<https://datatracker.ietf.org/doc/html/rfc6819#section-5.2.2.3>) 86 + /// calls out refresh-token reuse as a replay attack — the AS must 87 + /// invalidate the whole token family on reuse. 49 88 RefreshTokenReuseViolation, 50 89 } 51 90
+46 -6
src/commands/test/oauth/client/pipeline/interactive/scope_variations.rs
··· 32 32 } 33 33 34 34 /// Checks performed by the scope variations sub-stage. 35 + /// 36 + /// The fake AS drives the client through distinct authorization 37 + /// flows with scripted scope outcomes (full approval, partial 38 + /// approval, user denial, downscoped refresh). Each variant records 39 + /// whether the client handled that outcome per RFC 6749 §3.3 40 + /// (<https://datatracker.ietf.org/doc/html/rfc6749#section-3.3>) and 41 + /// the atproto permission grammar 42 + /// (<https://atproto.com/specs/oauth#scopes>). 35 43 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 36 44 pub enum Check { 37 - /// Full grant approval flow. 45 + /// The "user approves all requested scopes" flow completed: PAR, 46 + /// `/oauth/authorize`, and `/oauth/token` all executed, and the 47 + /// returned token carried the full requested scope set. Baseline 48 + /// happy path for RFC 6749 §4.1 49 + /// (<https://datatracker.ietf.org/doc/html/rfc6749#section-4.1>). 38 50 FullGrantApprove, 39 - /// Partial grant approval flow (narrower scope returned). 51 + /// The AS approved only a subset of the requested scopes. Atproto 52 + /// allows the authorization server to narrow scopes at grant time 53 + /// (the user may only consent to a subset of requested 54 + /// permissions); the client must accept the narrower set in the 55 + /// token response rather than treating it as an error. See 56 + /// RFC 6749 §3.3 57 + /// (<https://datatracker.ietf.org/doc/html/rfc6749#section-3.3>) 58 + /// — "the authorization server MAY fully or partially ignore the 59 + /// scope requested by the client". 40 60 PartialGrantApprove, 41 - /// User denial propagated correctly. 61 + /// A user-denied authorization surfaced as an 62 + /// `error=access_denied` response back to the client, per RFC 6749 63 + /// §4.1.2.1 64 + /// (<https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1>). 65 + /// The client must not silently retry or treat the denial as a 66 + /// success. 42 67 UserDenialPropagated, 43 - /// Downscoped refresh flow. 68 + /// When a client requests a refresh-token exchange with a 69 + /// `scope` parameter narrower than the original grant, the AS 70 + /// issues a token with that narrower scope and the client 71 + /// correctly observes it. Per RFC 6749 §6 72 + /// (<https://datatracker.ietf.org/doc/html/rfc6749#section-6>), 73 + /// the `scope` parameter on a refresh request MUST NOT be wider 74 + /// than the original grant. 44 75 DownscopedRefresh, 45 - /// PAR requests include code_challenge (PKCE). 76 + /// A `/oauth/par` request that omits `code_challenge` / 77 + /// `code_challenge_method` is rejected by the AS. Atproto requires 78 + /// PKCE (RFC 7636 79 + /// <https://datatracker.ietf.org/doc/html/rfc7636>) on every 80 + /// authorization request; this check verifies the server-side 81 + /// enforcement rather than client behaviour. 46 82 PkceRequired, 47 - /// PAR requests include DPoP header. 83 + /// A `/oauth/par` request that arrives without a `DPoP` header is 84 + /// rejected by the AS. Atproto binds access tokens to DPoP keys 85 + /// (RFC 9449 <https://datatracker.ietf.org/doc/html/rfc9449>), so 86 + /// a client that attempts PAR without DPoP must be turned away at 87 + /// the authorization server. 48 88 DpopRequired, 49 89 } 50 90
+48 -9
src/commands/test/oauth/client/pipeline/jwks.rs
··· 109 109 } 110 110 111 111 /// A single check performed by the JWKS validation stage. 112 + /// 113 + /// These only run for confidential clients (metadata 114 + /// `token_endpoint_auth_method = "private_key_jwt"`). Public, native, 115 + /// and loopback clients do not publish a JWKS, so every variant here 116 + /// is emitted as `Skipped` for them. Anchored in RFC 7517 117 + /// (<https://datatracker.ietf.org/doc/html/rfc7517>) and the atproto 118 + /// OAuth profile (<https://atproto.com/specs/oauth#clients>). 112 119 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 113 120 pub enum Check { 114 - /// JWKS is present (inline or URI). 121 + /// The client metadata advertises a JWKS: either inline as `jwks` 122 + /// or by reference as `jwks_uri` (exactly one). Gates every 123 + /// downstream JWKS check. Required for confidential clients so the 124 + /// AS can verify `private_key_jwt` assertions (RFC 7523 125 + /// <https://datatracker.ietf.org/doc/html/rfc7523#section-2.2>). 115 126 JwksPresent, 116 - /// JWKS URI is fetchable and returns 2xx. 127 + /// When the client uses `jwks_uri`, the URI is fetchable over 128 + /// HTTPS and returns a 2xx response. Emitted as `Skipped` when the 129 + /// JWKS is inline. Non-2xx or transport failures surface as 130 + /// `NetworkError`, not a spec violation. See RFC 7591 §2.1.2 131 + /// (<https://datatracker.ietf.org/doc/html/rfc7591#section-2.1.2>). 117 132 JwksUriFetchable, 118 - /// JWKS document is valid JSON with a "keys" array. 133 + /// The JWKS body is valid JSON with a top-level `keys` array per 134 + /// RFC 7517 §5 135 + /// (<https://datatracker.ietf.org/doc/html/rfc7517#section-5>). 136 + /// Malformed JSON or a missing / non-array `keys` field fails the 137 + /// check and blocks every per-key check below. 119 138 JwksIsJson, 120 - /// All keys have unique `kid` values. 139 + /// Every key in the JWKS declares a `kid`, and all `kid` values 140 + /// are distinct. RFC 7517 §4.5 141 + /// (<https://datatracker.ietf.org/doc/html/rfc7517#section-4.5>) 142 + /// recommends unique `kid`s within a JWK set so receivers can 143 + /// address a specific key unambiguously; atproto tokens rely on 144 + /// `kid` for key selection, so duplicates are a spec violation 145 + /// here. 121 146 KeysHaveUniqueKids, 122 147 /// JWKS contains at least one key compatible with the client's 123 148 /// declared `token_endpoint_auth_signing_alg`. A key is compatible 124 149 /// if its explicit `alg` equals the declared signing alg, or — if 125 - /// the key omits `alg` (RFC 7517 allows this) — its `crv` 126 - /// structurally matches the declared signing alg (P-256 ↔ ES256, 127 - /// secp256k1 ↔ ES256K). 150 + /// the key omits `alg` (RFC 7517 §4.4 151 + /// <https://datatracker.ietf.org/doc/html/rfc7517#section-4.4> 152 + /// makes it OPTIONAL) — its `crv` structurally matches the 153 + /// declared signing alg (P-256 ↔ ES256, secp256k1 ↔ ES256K). Per 154 + /// the atproto OAuth profile, confidential clients must declare 155 + /// `token_endpoint_auth_signing_alg` so a concrete key can be 156 + /// selected for `private_key_jwt`. 128 157 HasKeyForSigningAlg, 129 - /// All keys have `use == "sig"` (or absent, which defaults to sig). 158 + /// Every key's `use` is `sig` (or absent — RFC 7517 §4.2 159 + /// <https://datatracker.ietf.org/doc/html/rfc7517#section-4.2> 160 + /// treats `use` as OPTIONAL, but atproto keys are used 161 + /// exclusively for signing `private_key_jwt` assertions and DPoP 162 + /// proofs, so `use = "enc"` or any non-`sig` value is a 163 + /// violation). 130 164 KeysUseSigningUse, 131 - /// All algorithms are modern EC (ES256 or ES256K only). 165 + /// Every key that declares `alg` declares a modern EC algorithm: 166 + /// `ES256` (P-256) or `ES256K` (secp256k1). Other algorithms — 167 + /// including RSA-family (`RS*`, `PS*`) and legacy (`RS1`, `HS*`) 168 + /// — are a violation. Atproto mandates ES256 support and the 169 + /// broader ecosystem rejects JWKS keys outside these curves; see 170 + /// <https://atproto.com/specs/oauth#clients>. 132 171 AlgsAreModernEc, 133 172 } 134 173
+86
src/commands/test/oauth/client/pipeline/metadata.rs
··· 309 309 } 310 310 311 311 /// A check performed by the metadata validation stage. 312 + /// 313 + /// Unless stated otherwise, each variant applies to every client kind 314 + /// except loopback — loopback clients use implicit metadata, so every 315 + /// metadata check is emitted as `Skipped` for them 316 + /// (see <https://atproto.com/specs/oauth#clients>). 312 317 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 313 318 pub enum Check { 319 + /// The metadata document is valid JSON and deserializes into the 320 + /// client-metadata shape atproto expects. Gates every downstream 321 + /// metadata check: if the document will not parse there is nothing 322 + /// to validate. Anchored in RFC 7591 §2 323 + /// (<https://datatracker.ietf.org/doc/html/rfc7591#section-2>) 324 + /// plus the atproto OAuth profile 325 + /// (<https://atproto.com/specs/oauth#client-metadata>). 314 326 RawDocumentDeserializes, 327 + /// The document's `client_id` field is a well-formed URL that 328 + /// equals the URL used to fetch the document. The atproto OAuth 329 + /// profile uses `client_id` as both the metadata URL and the 330 + /// client's OAuth identifier; any mismatch breaks trust on 331 + /// discovery. See <https://atproto.com/specs/oauth#clients>. 315 332 ClientIdMatches, 333 + /// The document declares an `application_type`. OpenID Connect 334 + /// Dynamic Client Registration §2 makes this field OPTIONAL, but 335 + /// the atproto OAuth profile requires it to be explicit so that 336 + /// downstream redirect-URI and auth-method rules can be applied 337 + /// unambiguously. See 338 + /// <https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata> 339 + /// and <https://atproto.com/specs/oauth#clients>. 316 340 ApplicationTypePresent, 341 + /// `application_type` is either `"web"` or `"native"` — the only 342 + /// two values the atproto OAuth profile recognises. Loopback 343 + /// development clients don't reach this check (their metadata is 344 + /// implicit). 317 345 ApplicationTypeKnown, 346 + /// `response_types` is exactly `["code"]`. The atproto OAuth 347 + /// profile only supports the authorization-code flow (RFC 6749 348 + /// §4.1, <https://datatracker.ietf.org/doc/html/rfc6749#section-4.1>); 349 + /// implicit, hybrid, and password flows are forbidden. 318 350 ResponseTypesIsCode, 351 + /// `grant_types` includes `authorization_code` and contains only 352 + /// grant types that atproto recognises (`authorization_code`, 353 + /// `refresh_token`). Mirrors RFC 6749 §4.1 plus §6 for refresh 354 + /// tokens (<https://datatracker.ietf.org/doc/html/rfc6749#section-6>). 319 355 GrantTypesIncludesAuthorizationCode, 356 + /// `dpop_bound_access_tokens` is explicitly `true`. The atproto 357 + /// OAuth profile mandates DPoP-bound access tokens per RFC 9449 358 + /// (<https://datatracker.ietf.org/doc/html/rfc9449>); a missing or 359 + /// `false` value disqualifies the client. 320 360 DpopBoundTrue, 361 + /// `redirect_uris` is present and non-empty. RFC 6749 §3.1.2 362 + /// (<https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2>) 363 + /// requires at least one registered redirect URI; atproto imposes 364 + /// the additional shape constraints checked by `RedirectUrisShape`. 321 365 RedirectUrisPresent, 366 + /// Every `redirect_uri` has the shape required for the client 367 + /// kind. Web clients (both confidential and public) must use 368 + /// `https://` URIs whose origin matches `client_id`. Native 369 + /// clients may additionally use a custom scheme whose scheme 370 + /// component is the reverse-domain form of the `client_id` host 371 + /// (e.g. `com.example.app:/callback` for `client_id` 372 + /// `https://app.example.com/...`). See 373 + /// <https://atproto.com/specs/oauth#clients>. 322 374 RedirectUrisShape, 375 + /// `token_endpoint_auth_method` matches the client kind. Atproto 376 + /// requires `private_key_jwt` for confidential clients (so the 377 + /// client is authenticated by a signed assertion per RFC 7523 378 + /// <https://datatracker.ietf.org/doc/html/rfc7523>) and `none` for 379 + /// public / native clients. Any other value — including omitting 380 + /// the field entirely — is a violation. 323 381 TokenEndpointAuthMethodValid, 382 + /// A confidential client supplies exactly one of `jwks` or 383 + /// `jwks_uri`. Confidential clients authenticate to the token 384 + /// endpoint with `private_key_jwt`, so the AS needs their public 385 + /// JWKS to verify the assertion. Providing both fields or neither 386 + /// is a violation. See 387 + /// <https://atproto.com/specs/oauth#clients> and RFC 7591 §2.1.2 388 + /// (<https://datatracker.ietf.org/doc/html/rfc7591#section-2.1.2>). 324 389 ConfidentialRequiresJwks, 390 + /// A public or native client does NOT advertise `jwks` or 391 + /// `jwks_uri`. Public / native clients authenticate with 392 + /// `token_endpoint_auth_method = none`, so publishing a JWKS is 393 + /// meaningless at best and at worst invites misuse. See 394 + /// <https://atproto.com/specs/oauth#clients>. 325 395 PublicForbidsJwks, 396 + /// `scope` is present. RFC 6749 §3.3 397 + /// (<https://datatracker.ietf.org/doc/html/rfc6749#section-3.3>) 398 + /// treats `scope` as OPTIONAL, but the atproto OAuth profile 399 + /// mandates it because the `atproto` base scope is required on 400 + /// every atproto authorization request. 326 401 ScopePresent, 402 + /// `scope` includes the `atproto` base token. Atproto's permission 403 + /// grammar layers resource-specific tokens on top of a mandatory 404 + /// `atproto` baseline scope; omitting it means the client cannot 405 + /// obtain any atproto permissions even if the rest of the flow 406 + /// completes. See <https://atproto.com/specs/oauth#scopes>. 327 407 ScopeIncludesAtproto, 408 + /// `scope` parses against the atproto permission grammar defined 409 + /// at <https://atproto.com/specs/oauth#scopes>. Every token must 410 + /// be either `atproto` or a well-formed permission of the shape 411 + /// `<resource>[:<positional>][?param=val&…]`. A single malformed 412 + /// token fails the check with a span pointing at the offending 413 + /// token. 328 414 ScopeGrammarValid, 329 415 } 330 416