CLI app for developers prototyping atproto functionality
1
fork

Configure Feed

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

Add client_uri host-match check per atproto OAuth profile

The atproto OAuth profile
(<https://atproto.com/specs/oauth#client-metadata>) requires the
optional `client_uri` field, when declared, to share a hostname with
`client_id`: *"`client_uri` (string, optional): … must have the same
hostname as `client_id`."* The rule exists specifically to deter
metadata uploaded to an attacker-controlled origin from pointing
`client_uri` at an unrelated legitimate-looking domain.

Add the field to `RawMetadataDocument` and introduce
`Check::ClientUriHostMatchesClientId`. Semantics:

- `client_uri` absent → `Skipped` with reason
"`client_uri` not declared" (the field is optional).
- `client_uri` present and its URL's host equals the parsed
`client_id` host → `Pass`.
- `client_uri` present and its host differs → `SpecViolation` with a
diagnostic naming both hosts.
- `client_uri` present but not a valid URL → `SpecViolation`.
- `client_id` itself invalid → `Skipped`/blocked_by
`client_id_matches`.

New `client_uri_host_mismatch/` fixture + integration test covers
the failing path. All 334 tests pass; all 16 real-world atproto
OAuth clients still pass.

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

+234 -33
+1
docs/design-plans/2026-04-16-test-oauth-client.md
··· 90 90 - **test-oauth-client.AC2.7 Failure:** A native client whose `redirect_uri` scheme does not match the reverse-domain of the `client_id` host produces a `SpecViolation`. 91 91 - **test-oauth-client.AC2.7b Failure:** A native client whose custom-scheme `redirect_uri` uses `scheme://host/path` rather than the spec-required `scheme:/path` (single colon, single forward slash, no authority) produces a `SpecViolation` on `redirect_uris_shape`. See <https://atproto.com/specs/oauth#authorization-request-fields>: *"The URI scheme must be followed by a single colon (`:`) then a single forward slash (`/`) and then a URI path component."* 92 92 - **test-oauth-client.AC2.7c Failure:** Any HTTPS `redirect_uri` that explicitly declares the default port (`:443`) produces a `SpecViolation` on `redirect_uris_shape`. The atproto OAuth profile restates the OAuth 2.0 rule: *"The URL may include a port number, but not if it is the default port number."* Applies to web and native HTTPS redirects alike. 93 + - **test-oauth-client.AC2.8a Failure:** A `client_uri` whose hostname differs from `client_id`'s hostname produces a `SpecViolation` on `client_uri_host_matches_client_id`. The spec (<https://atproto.com/specs/oauth#client-metadata>) states: *"`client_uri` … must have the same hostname as `client_id`."* The field is optional overall, so when absent the check emits `Skipped` with reason `"`client_uri` not declared"`. 93 94 - **test-oauth-client.AC2.8 Failure:** A `scope` field that does not parse against the atproto permission grammar (<https://atproto.com/specs/permission#scope-string-syntax>) produces a `SpecViolation` with the offending token highlighted via miette source span. The grammar is `<resource>[:<positional>][?<query>]`; all three bracketed components are optional independently, so `repo` (bare), `repo:app.bsky.feed.post` (positional only), `repo?collection=foo` (query only), and `repo:app.bsky.feed.post?action=create` (both) are all valid. The resource name is **not** constrained to a fixed set — the spec states it will expand over time and defines `transition:generic`, `transition:chat.bsky`, and `transition:email` as migration-period resources, so the grammar check only flags genuinely syntactic failures (empty resource, malformed `key=value` query pairs, invalid percent-encoding). Unknown-but-well-formed resource names are accepted. 94 95 - **test-oauth-client.AC2.9 Skip:** Every metadata-document validation check on a loopback target emits `Skipped` with reason `"metadata is implicit for loopback clients"`. 95 96
+77
src/commands/test/oauth/client/pipeline/metadata.rs
··· 32 32 /// Stored as raw JSON; Phase 5 parses it into JWK format. 33 33 pub jwks: Option<serde_json::Value>, 34 34 pub jwks_uri: Option<String>, 35 + pub client_uri: Option<String>, 35 36 } 36 37 37 38 /// Parse a metadata document from raw bytes. ··· 419 420 /// token fails the check with a span pointing at the offending 420 421 /// token. 421 422 ScopeGrammarValid, 423 + /// `client_uri`, if present, has the same hostname as `client_id`. 424 + /// The atproto OAuth profile 425 + /// (<https://atproto.com/specs/oauth#client-metadata>) states: 426 + /// *"`client_uri` (string, optional): … must have the same 427 + /// hostname as `client_id`."* When absent, the check emits 428 + /// `Skipped`. This deters metadata documents that point 429 + /// `client_uri` at an unrelated domain — one of the spec's 430 + /// stated concerns is that untrusted users may upload client 431 + /// metadata to arbitrary URLs and use these fields for 432 + /// impersonation. 433 + ClientUriHostMatchesClientId, 422 434 } 423 435 424 436 impl Check { ··· 447 459 Check::ScopePresent => "oauth_client::metadata::scope_present", 448 460 Check::ScopeIncludesAtproto => "oauth_client::metadata::scope_includes_atproto", 449 461 Check::ScopeGrammarValid => "oauth_client::metadata::scope_grammar", 462 + Check::ClientUriHostMatchesClientId => { 463 + "oauth_client::metadata::client_uri_host_matches_client_id" 464 + } 450 465 } 451 466 } 452 467 ··· 481 496 Check::ScopePresent => "`scope` field is present", 482 497 Check::ScopeIncludesAtproto => "`scope` includes the `atproto` token", 483 498 Check::ScopeGrammarValid => "`scope` parses against the atproto permission grammar", 499 + Check::ClientUriHostMatchesClientId => "`client_uri` hostname matches `client_id`", 484 500 } 485 501 } 486 502 ··· 530 546 Check::ScopePresent, 531 547 Check::ScopeIncludesAtproto, 532 548 Check::ScopeGrammarValid, 549 + Check::ClientUriHostMatchesClientId, 533 550 ]; 534 551 535 552 /// Output from the metadata validation stage. ··· 1140 1157 } 1141 1158 } 1142 1159 }; 1160 + 1161 + // Check ClientUriHostMatchesClientId. Per the 1162 + // atproto OAuth profile `client_uri` is optional 1163 + // but, if declared, must share `client_id`'s 1164 + // hostname. 1165 + match (doc.client_uri.as_deref(), parsed_client_id.as_ref()) { 1166 + (None, _) => { 1167 + results.push(skipped_with_reason( 1168 + Check::ClientUriHostMatchesClientId.id(), 1169 + Stage::OAUTH_CLIENT_METADATA, 1170 + Check::ClientUriHostMatchesClientId.summary(), 1171 + "`client_uri` not declared", 1172 + )); 1173 + } 1174 + (Some(_), None) => { 1175 + results.push(blocked_by( 1176 + Check::ClientUriHostMatchesClientId.id(), 1177 + Stage::OAUTH_CLIENT_METADATA, 1178 + Check::ClientUriHostMatchesClientId.summary(), 1179 + Check::ClientIdMatches.id(), 1180 + )); 1181 + } 1182 + (Some(client_uri_str), Some(parsed_id)) => { 1183 + match Url::parse(client_uri_str) { 1184 + Err(_) => { 1185 + let diagnostic: Box<dyn miette::Diagnostic + Send + Sync> = 1186 + Box::new(MetadataViolationDiagnostic { 1187 + message: format!( 1188 + "`client_uri` is not a valid URL: `{client_uri_str}`" 1189 + ), 1190 + code: "oauth_client::metadata::client_uri_host_matches_client_id", 1191 + }); 1192 + results.push( 1193 + Check::ClientUriHostMatchesClientId 1194 + .spec_violation(Some(diagnostic)), 1195 + ); 1196 + } 1197 + Ok(parsed_uri) => { 1198 + if parsed_uri.host() == parsed_id.host() { 1199 + results.push(Check::ClientUriHostMatchesClientId.pass()); 1200 + } else { 1201 + let diagnostic: Box<dyn miette::Diagnostic + Send + Sync> = 1202 + Box::new(MetadataViolationDiagnostic { 1203 + message: format!( 1204 + "`client_uri` host (`{}`) does not match \ 1205 + `client_id` host (`{}`)", 1206 + parsed_uri.host_str().unwrap_or("<none>"), 1207 + parsed_id.host_str().unwrap_or("<none>"), 1208 + ), 1209 + code: "oauth_client::metadata::client_uri_host_matches_client_id", 1210 + }); 1211 + results.push( 1212 + Check::ClientUriHostMatchesClientId 1213 + .spec_violation(Some(diagnostic)), 1214 + ); 1215 + } 1216 + } 1217 + } 1218 + } 1219 + } 1143 1220 1144 1221 // Build MetadataFacts if we have enough information. 1145 1222 let facts =
+11
tests/fixtures/oauth_client/metadata/client_uri_host_mismatch/metadata.json
··· 1 + { 2 + "client_id": "https://client.example.com/metadata.json", 3 + "application_type": "web", 4 + "response_types": ["code"], 5 + "grant_types": ["authorization_code"], 6 + "scope": "atproto", 7 + "redirect_uris": ["https://client.example.com/cb"], 8 + "dpop_bound_access_tokens": true, 9 + "token_endpoint_auth_method": "none", 10 + "client_uri": "https://not-the-same.example.org/about" 11 + }
+1
tests/oauth_client_check_id_coverage.rs
··· 137 137 "oauth_client::metadata::redirect_scheme_reverse_domain_mismatch", 138 138 "oauth_client::metadata::token_endpoint_auth_method_valid", 139 139 "oauth_client::metadata::token_endpoint_auth_signing_alg_valid", 140 + "oauth_client::metadata::client_uri_host_matches_client_id", 140 141 // JWKS stage diagnostic codes. 141 142 "oauth_client::jws::jwks_uri_unreachable", 142 143 "oauth_client::jws::jwks_is_json",
+36
tests/oauth_client_metadata.rs
··· 582 582 insta::assert_snapshot!(rendered); 583 583 } 584 584 585 + /// Per <https://atproto.com/specs/oauth#client-metadata>: 586 + /// "`client_uri` (string, optional): … must have the same hostname 587 + /// as `client_id`." A `client_uri` whose host differs from the 588 + /// `client_id` host fails `client_uri_host_matches_client_id`. 589 + #[tokio::test] 590 + async fn client_uri_host_mismatch_is_spec_violation() { 591 + let http = common::FakeHttpClient::new(); 592 + let metadata = 593 + include_bytes!("fixtures/oauth_client/metadata/client_uri_host_mismatch/metadata.json"); 594 + http.add_response( 595 + &Url::parse("https://client.example.com/metadata.json").unwrap(), 596 + 200, 597 + metadata.to_vec(), 598 + ); 599 + 600 + let target = target::parse("https://client.example.com/metadata.json").expect("parse failed"); 601 + let jwks_fetcher = common::FakeJwksFetcher::new(); 602 + let opts = OauthClientOptions { 603 + interactive: None, 604 + http: &http, 605 + jwks: &jwks_fetcher, 606 + verbose: false, 607 + clock: Arc::new(RealClock), 608 + }; 609 + 610 + let report = run_pipeline(target, opts).await; 611 + assert_eq!( 612 + report.exit_code(), 613 + 1, 614 + "client_uri host mismatch must be a SpecViolation" 615 + ); 616 + 617 + let rendered = render_report_to_string(&report); 618 + insta::assert_snapshot!(rendered); 619 + } 620 + 585 621 /// Per <https://atproto.com/specs/oauth#authorization-request-fields>: 586 622 /// "The URL may include a port number, but not if it is the default 587 623 /// port number." An HTTPS `redirect_uri` that explicitly declares
+2 -1
tests/snapshots/oauth_client_discovery__https_404_produces_network_error.snap
··· 30 30 [SKIP] `scope` field is present — blocked by oauth_client::discovery::metadata_document_fetchable 31 31 [SKIP] `scope` includes the `atproto` token — blocked by oauth_client::discovery::metadata_document_fetchable 32 32 [SKIP] `scope` parses against the atproto permission grammar — blocked by oauth_client::discovery::metadata_document_fetchable 33 + [SKIP] `client_uri` hostname matches `client_id` — blocked by oauth_client::discovery::metadata_document_fetchable 33 34 == JWKS == 34 35 [SKIP] JWKS is present — blocked by oauth_client::metadata::raw_document_deserializes 35 36 [SKIP] JWKS URI is fetchable — blocked by oauth_client::metadata::raw_document_deserializes ··· 39 40 [SKIP] Keys use signing use — blocked by oauth_client::metadata::raw_document_deserializes 40 41 [SKIP] Algorithms are modern EC — blocked by oauth_client::metadata::raw_document_deserializes 41 42 42 - Summary: 1 passed, 0 failed (spec), 1 network errors, 0 advisories, 25 skipped. Exit code: 2 43 + Summary: 1 passed, 0 failed (spec), 1 network errors, 0 advisories, 26 skipped. Exit code: 2
+2 -1
tests/snapshots/oauth_client_discovery__https_confidential_happy_discovery.snap
··· 26 26 [OK] `scope` field is present 27 27 [OK] `scope` includes the `atproto` token 28 28 [OK] `scope` parses against the atproto permission grammar 29 + [SKIP] `client_uri` hostname matches `client_id` — `client_uri` not declared 29 30 == JWKS == 30 31 [OK] JWKS is present 31 32 [SKIP] JWKS URI is fetchable — jwks is inline ··· 35 36 [OK] Keys use signing use 36 37 [OK] Algorithms are modern EC 37 38 38 - Summary: 25 passed, 0 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 0 39 + Summary: 25 passed, 0 failed (spec), 0 network errors, 0 advisories, 2 skipped. Exit code: 0
+2 -1
tests/snapshots/oauth_client_discovery__https_not_json_produces_spec_violation.snap
··· 34 34 [SKIP] `scope` field is present — blocked by oauth_client::discovery::metadata_document_fetchable 35 35 [SKIP] `scope` includes the `atproto` token — blocked by oauth_client::discovery::metadata_document_fetchable 36 36 [SKIP] `scope` parses against the atproto permission grammar — blocked by oauth_client::discovery::metadata_document_fetchable 37 + [SKIP] `client_uri` hostname matches `client_id` — blocked by oauth_client::discovery::metadata_document_fetchable 37 38 == JWKS == 38 39 [SKIP] JWKS is present — blocked by oauth_client::metadata::raw_document_deserializes 39 40 [SKIP] JWKS URI is fetchable — blocked by oauth_client::metadata::raw_document_deserializes ··· 43 44 [SKIP] Keys use signing use — blocked by oauth_client::metadata::raw_document_deserializes 44 45 [SKIP] Algorithms are modern EC — blocked by oauth_client::metadata::raw_document_deserializes 45 46 46 - Summary: 3 passed, 1 failed (spec), 0 network errors, 0 advisories, 23 skipped. Exit code: 1 47 + Summary: 3 passed, 1 failed (spec), 0 network errors, 0 advisories, 24 skipped. Exit code: 1
+2 -1
tests/snapshots/oauth_client_discovery__https_not_json_with_content_type_produces_spec_violation_with_ct.snap
··· 38 38 [SKIP] `scope` field is present — blocked by oauth_client::discovery::metadata_document_fetchable 39 39 [SKIP] `scope` includes the `atproto` token — blocked by oauth_client::discovery::metadata_document_fetchable 40 40 [SKIP] `scope` parses against the atproto permission grammar — blocked by oauth_client::discovery::metadata_document_fetchable 41 + [SKIP] `client_uri` hostname matches `client_id` — blocked by oauth_client::discovery::metadata_document_fetchable 41 42 == JWKS == 42 43 [SKIP] JWKS is present — blocked by oauth_client::metadata::raw_document_deserializes 43 44 [SKIP] JWKS URI is fetchable — blocked by oauth_client::metadata::raw_document_deserializes ··· 47 48 [SKIP] Keys use signing use — blocked by oauth_client::metadata::raw_document_deserializes 48 49 [SKIP] Algorithms are modern EC — blocked by oauth_client::metadata::raw_document_deserializes 49 50 50 - Summary: 2 passed, 2 failed (spec), 0 network errors, 0 advisories, 23 skipped. Exit code: 1 51 + Summary: 2 passed, 2 failed (spec), 0 network errors, 0 advisories, 24 skipped. Exit code: 1
+2 -1
tests/snapshots/oauth_client_discovery__https_unreachable_produces_network_error.snap
··· 30 30 [SKIP] `scope` field is present — blocked by oauth_client::discovery::metadata_document_fetchable 31 31 [SKIP] `scope` includes the `atproto` token — blocked by oauth_client::discovery::metadata_document_fetchable 32 32 [SKIP] `scope` parses against the atproto permission grammar — blocked by oauth_client::discovery::metadata_document_fetchable 33 + [SKIP] `client_uri` hostname matches `client_id` — blocked by oauth_client::discovery::metadata_document_fetchable 33 34 == JWKS == 34 35 [SKIP] JWKS is present — blocked by oauth_client::metadata::raw_document_deserializes 35 36 [SKIP] JWKS URI is fetchable — blocked by oauth_client::metadata::raw_document_deserializes ··· 39 40 [SKIP] Keys use signing use — blocked by oauth_client::metadata::raw_document_deserializes 40 41 [SKIP] Algorithms are modern EC — blocked by oauth_client::metadata::raw_document_deserializes 41 42 42 - Summary: 1 passed, 0 failed (spec), 1 network errors, 0 advisories, 25 skipped. Exit code: 2 43 + Summary: 1 passed, 0 failed (spec), 1 network errors, 0 advisories, 26 skipped. Exit code: 2
+2 -1
tests/snapshots/oauth_client_discovery__loopback_root_produces_skip_rows.snap
··· 27 27 [SKIP] `scope` field is present — metadata is implicit for loopback clients 28 28 [SKIP] `scope` includes the `atproto` token — metadata is implicit for loopback clients 29 29 [SKIP] `scope` parses against the atproto permission grammar — metadata is implicit for loopback clients 30 + [SKIP] `client_uri` hostname matches `client_id` — metadata is implicit for loopback clients 30 31 == JWKS == 31 32 [SKIP] JWKS is present — jwks not applicable to loopback clients 32 33 [SKIP] JWKS URI is fetchable — jwks not applicable to loopback clients ··· 36 37 [SKIP] Keys use signing use — jwks not applicable to loopback clients 37 38 [SKIP] Algorithms are modern EC — jwks not applicable to loopback clients 38 39 39 - Summary: 1 passed, 0 failed (spec), 0 network errors, 0 advisories, 26 skipped. Exit code: 0 40 + Summary: 1 passed, 0 failed (spec), 0 network errors, 0 advisories, 27 skipped. Exit code: 0
+2 -1
tests/snapshots/oauth_client_endtoend__full_pipeline_all_pass.snap
··· 26 26 [OK] `scope` field is present 27 27 [OK] `scope` includes the `atproto` token 28 28 [OK] `scope` parses against the atproto permission grammar 29 + [SKIP] `client_uri` hostname matches `client_id` — `client_uri` not declared 29 30 == JWKS == 30 31 [OK] JWKS is present 31 32 [SKIP] JWKS URI is fetchable — jwks is inline ··· 35 36 [OK] Keys use signing use 36 37 [OK] Algorithms are modern EC 37 38 38 - Summary: 25 passed, 0 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 0 39 + Summary: 25 passed, 0 failed (spec), 0 network errors, 0 advisories, 2 skipped. Exit code: 0
+2 -1
tests/snapshots/oauth_client_jwks__discovery_failure_blocks_jwks.snap
··· 30 30 [SKIP] `scope` field is present — blocked by oauth_client::discovery::metadata_document_fetchable 31 31 [SKIP] `scope` includes the `atproto` token — blocked by oauth_client::discovery::metadata_document_fetchable 32 32 [SKIP] `scope` parses against the atproto permission grammar — blocked by oauth_client::discovery::metadata_document_fetchable 33 + [SKIP] `client_uri` hostname matches `client_id` — blocked by oauth_client::discovery::metadata_document_fetchable 33 34 == JWKS == 34 35 [SKIP] JWKS is present — blocked by oauth_client::metadata::raw_document_deserializes 35 36 [SKIP] JWKS URI is fetchable — blocked by oauth_client::metadata::raw_document_deserializes ··· 39 40 [SKIP] Keys use signing use — blocked by oauth_client::metadata::raw_document_deserializes 40 41 [SKIP] Algorithms are modern EC — blocked by oauth_client::metadata::raw_document_deserializes 41 42 42 - Summary: 1 passed, 0 failed (spec), 1 network errors, 0 advisories, 25 skipped. Exit code: 2 43 + Summary: 1 passed, 0 failed (spec), 1 network errors, 0 advisories, 26 skipped. Exit code: 2
+2 -1
tests/snapshots/oauth_client_jwks__duplicate_kids_produces_spec_violation.snap
··· 26 26 [OK] `scope` field is present 27 27 [OK] `scope` includes the `atproto` token 28 28 [OK] `scope` parses against the atproto permission grammar 29 + [SKIP] `client_uri` hostname matches `client_id` — `client_uri` not declared 29 30 == JWKS == 30 31 [OK] JWKS is present 31 32 [SKIP] JWKS URI is fetchable — jwks is inline ··· 44 45 [OK] Keys use signing use 45 46 [OK] Algorithms are modern EC 46 47 47 - Summary: 24 passed, 1 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 1 48 + Summary: 24 passed, 1 failed (spec), 0 network errors, 0 advisories, 2 skipped. Exit code: 1
+2 -1
tests/snapshots/oauth_client_jwks__inline_es256_happy_jwks_passes.snap
··· 26 26 [OK] `scope` field is present 27 27 [OK] `scope` includes the `atproto` token 28 28 [OK] `scope` parses against the atproto permission grammar 29 + [SKIP] `client_uri` hostname matches `client_id` — `client_uri` not declared 29 30 == JWKS == 30 31 [OK] JWKS is present 31 32 [SKIP] JWKS URI is fetchable — jwks is inline ··· 35 36 [OK] Keys use signing use 36 37 [OK] Algorithms are modern EC 37 38 38 - Summary: 25 passed, 0 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 0 39 + Summary: 25 passed, 0 failed (spec), 0 network errors, 0 advisories, 2 skipped. Exit code: 0
+2 -1
tests/snapshots/oauth_client_jwks__loopback_skips_all_jwks.snap
··· 27 27 [SKIP] `scope` field is present — metadata is implicit for loopback clients 28 28 [SKIP] `scope` includes the `atproto` token — metadata is implicit for loopback clients 29 29 [SKIP] `scope` parses against the atproto permission grammar — metadata is implicit for loopback clients 30 + [SKIP] `client_uri` hostname matches `client_id` — metadata is implicit for loopback clients 30 31 == JWKS == 31 32 [SKIP] JWKS is present — jwks not applicable to loopback clients 32 33 [SKIP] JWKS URI is fetchable — jwks not applicable to loopback clients ··· 36 37 [SKIP] Keys use signing use — jwks not applicable to loopback clients 37 38 [SKIP] Algorithms are modern EC — jwks not applicable to loopback clients 38 39 39 - Summary: 1 passed, 0 failed (spec), 0 network errors, 0 advisories, 26 skipped. Exit code: 0 40 + Summary: 1 passed, 0 failed (spec), 0 network errors, 0 advisories, 27 skipped. Exit code: 0
+2 -1
tests/snapshots/oauth_client_jwks__public_client_skips_all_jwks.snap
··· 26 26 [OK] `scope` field is present 27 27 [OK] `scope` includes the `atproto` token 28 28 [OK] `scope` parses against the atproto permission grammar 29 + [SKIP] `client_uri` hostname matches `client_id` — `client_uri` not declared 29 30 == JWKS == 30 31 [SKIP] JWKS is present — jwks not required for public clients 31 32 [SKIP] JWKS URI is fetchable — jwks not required for public clients ··· 35 36 [SKIP] Keys use signing use — jwks not required for public clients 36 37 [SKIP] Algorithms are modern EC — jwks not required for public clients 37 38 38 - Summary: 18 passed, 0 failed (spec), 0 network errors, 0 advisories, 8 skipped. Exit code: 0 39 + Summary: 18 passed, 0 failed (spec), 0 network errors, 0 advisories, 9 skipped. Exit code: 0
+2 -1
tests/snapshots/oauth_client_jwks__signing_alg_has_no_compatible_key_produces_spec_violation.snap
··· 26 26 [OK] `scope` field is present 27 27 [OK] `scope` includes the `atproto` token 28 28 [OK] `scope` parses against the atproto permission grammar 29 + [SKIP] `client_uri` hostname matches `client_id` — `client_uri` not declared 29 30 == JWKS == 30 31 [OK] JWKS is present 31 32 [SKIP] JWKS URI is fetchable — jwks is inline ··· 38 39 [OK] Keys use signing use 39 40 [OK] Algorithms are modern EC 40 41 41 - Summary: 24 passed, 1 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 1 42 + Summary: 24 passed, 1 failed (spec), 0 network errors, 0 advisories, 2 skipped. Exit code: 1
+2 -1
tests/snapshots/oauth_client_jwks__uri_es256_happy_jwks_passes.snap
··· 26 26 [OK] `scope` field is present 27 27 [OK] `scope` includes the `atproto` token 28 28 [OK] `scope` parses against the atproto permission grammar 29 + [SKIP] `client_uri` hostname matches `client_id` — `client_uri` not declared 29 30 == JWKS == 30 31 [OK] JWKS is present 31 32 [OK] JWKS URI is fetchable ··· 35 36 [OK] Keys use signing use 36 37 [OK] Algorithms are modern EC 37 38 38 - Summary: 26 passed, 0 failed (spec), 0 network errors, 0 advisories, 0 skipped. Exit code: 0 39 + Summary: 26 passed, 0 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 0
+2 -1
tests/snapshots/oauth_client_jwks__uri_invalid_json_produces_spec_violation.snap
··· 26 26 [OK] `scope` field is present 27 27 [OK] `scope` includes the `atproto` token 28 28 [OK] `scope` parses against the atproto permission grammar 29 + [SKIP] `client_uri` hostname matches `client_id` — `client_uri` not declared 29 30 == JWKS == 30 31 [OK] JWKS is present 31 32 [OK] JWKS URI is fetchable ··· 38 39 [SKIP] Keys use signing use — blocked by oauth_client::jws::jwks_is_json 39 40 [SKIP] Algorithms are modern EC — blocked by oauth_client::jws::jwks_is_json 40 41 41 - Summary: 21 passed, 1 failed (spec), 0 network errors, 0 advisories, 4 skipped. Exit code: 1 42 + Summary: 21 passed, 1 failed (spec), 0 network errors, 0 advisories, 5 skipped. Exit code: 1
+2 -1
tests/snapshots/oauth_client_jwks__uri_returns_404_produces_network_error.snap
··· 26 26 [OK] `scope` field is present 27 27 [OK] `scope` includes the `atproto` token 28 28 [OK] `scope` parses against the atproto permission grammar 29 + [SKIP] `client_uri` hostname matches `client_id` — `client_uri` not declared 29 30 == JWKS == 30 31 [OK] JWKS is present 31 32 [NET] JWKS URI is fetchable ··· 38 39 [SKIP] Keys use signing use — blocked by oauth_client::jws::jwks_uri_fetchable 39 40 [SKIP] Algorithms are modern EC — blocked by oauth_client::jws::jwks_uri_fetchable 40 41 41 - Summary: 20 passed, 0 failed (spec), 1 network errors, 0 advisories, 5 skipped. Exit code: 2 42 + Summary: 20 passed, 0 failed (spec), 1 network errors, 0 advisories, 6 skipped. Exit code: 2
+2 -1
tests/snapshots/oauth_client_jwks__uri_unreachable_produces_network_error.snap
··· 26 26 [OK] `scope` field is present 27 27 [OK] `scope` includes the `atproto` token 28 28 [OK] `scope` parses against the atproto permission grammar 29 + [SKIP] `client_uri` hostname matches `client_id` — `client_uri` not declared 29 30 == JWKS == 30 31 [OK] JWKS is present 31 32 [NET] JWKS URI is fetchable ··· 38 39 [SKIP] Keys use signing use — blocked by oauth_client::jws::jwks_uri_fetchable 39 40 [SKIP] Algorithms are modern EC — blocked by oauth_client::jws::jwks_uri_fetchable 40 41 41 - Summary: 20 passed, 0 failed (spec), 1 network errors, 0 advisories, 5 skipped. Exit code: 2 42 + Summary: 20 passed, 0 failed (spec), 1 network errors, 0 advisories, 6 skipped. Exit code: 2
+2 -1
tests/snapshots/oauth_client_jwks__weak_alg_rs1_produces_spec_violation.snap
··· 26 26 [OK] `scope` field is present 27 27 [OK] `scope` includes the `atproto` token 28 28 [OK] `scope` parses against the atproto permission grammar 29 + [SKIP] `client_uri` hostname matches `client_id` — `client_uri` not declared 29 30 == JWKS == 30 31 [OK] JWKS is present 31 32 [SKIP] JWKS URI is fetchable — jwks is inline ··· 41 42 42 43 × One or more keys declare non-modern algorithms 43 44 44 - Summary: 23 passed, 2 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 1 45 + Summary: 23 passed, 2 failed (spec), 0 network errors, 0 advisories, 2 skipped. Exit code: 1
+2 -1
tests/snapshots/oauth_client_jwks__wrong_use_produces_spec_violation.snap
··· 26 26 [OK] `scope` field is present 27 27 [OK] `scope` includes the `atproto` token 28 28 [OK] `scope` parses against the atproto permission grammar 29 + [SKIP] `client_uri` hostname matches `client_id` — `client_uri` not declared 29 30 == JWKS == 30 31 [OK] JWKS is present 31 32 [SKIP] JWKS URI is fetchable — jwks is inline ··· 38 39 × One or more keys have `use` other than `sig` 39 40 [OK] Algorithms are modern EC 40 41 41 - Summary: 24 passed, 1 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 1 42 + Summary: 24 passed, 1 failed (spec), 0 network errors, 0 advisories, 2 skipped. Exit code: 1
+2 -1
tests/snapshots/oauth_client_metadata__application_type_omitted_defaults_to_web.snap
··· 26 26 [OK] `scope` field is present 27 27 [OK] `scope` includes the `atproto` token 28 28 [OK] `scope` parses against the atproto permission grammar 29 + [SKIP] `client_uri` hostname matches `client_id` — `client_uri` not declared 29 30 == JWKS == 30 31 [SKIP] JWKS is present — jwks not required for public clients 31 32 [SKIP] JWKS URI is fetchable — jwks not required for public clients ··· 35 36 [SKIP] Keys use signing use — jwks not required for public clients 36 37 [SKIP] Algorithms are modern EC — jwks not required for public clients 37 38 38 - Summary: 17 passed, 0 failed (spec), 0 network errors, 0 advisories, 9 skipped. Exit code: 0 39 + Summary: 17 passed, 0 failed (spec), 0 network errors, 0 advisories, 10 skipped. Exit code: 0
+42
tests/snapshots/oauth_client_metadata__client_uri_host_mismatch_is_spec_violation.snap
··· 1 + --- 2 + source: tests/oauth_client_metadata.rs 3 + expression: rendered 4 + --- 5 + Target: https://client.example.com/metadata.json 6 + elapsed: XXms 7 + 8 + == Discovery == 9 + [OK] Client ID well-formed 10 + [OK] Metadata document fetchable 11 + [OK] Metadata Content-Type is JSON 12 + [OK] Metadata is valid JSON 13 + == Metadata == 14 + [OK] Metadata document deserializes 15 + [OK] Metadata `client_id` matches fetched URL 16 + [OK] `application_type` field is present 17 + [OK] `application_type` is `web` or `native` 18 + [OK] `response_types` includes `code` 19 + [OK] `grant_types` includes `authorization_code` 20 + [OK] `dpop_bound_access_tokens` is `true` 21 + [OK] `redirect_uris` is non-empty 22 + [OK] Every `redirect_uri` has the right shape for the client kind 23 + [OK] `token_endpoint_auth_method` matches client kind 24 + [SKIP] `token_endpoint_auth_signing_alg` is not `none` — `token_endpoint_auth_signing_alg` not declared 25 + [OK] Public/native client does not provide `jwks` or `jwks_uri` 26 + [OK] `scope` field is present 27 + [OK] `scope` includes the `atproto` token 28 + [OK] `scope` parses against the atproto permission grammar 29 + [FAIL] `client_uri` hostname matches `client_id` 30 + oauth_client::metadata::client_uri_host_matches_client_id 31 + 32 + × `client_uri` host (`not-the-same.example.org`) does not match `client_id` host (`client.example.com`) 33 + == JWKS == 34 + [SKIP] JWKS is present — jwks not required for public clients 35 + [SKIP] JWKS URI is fetchable — jwks not required for public clients 36 + [SKIP] JWKS is valid JSON — jwks not required for public clients 37 + [SKIP] Keys have unique kid values — jwks not required for public clients 38 + [SKIP] JWKS contains a key compatible with `token_endpoint_auth_signing_alg` — jwks not required for public clients 39 + [SKIP] Keys use signing use — jwks not required for public clients 40 + [SKIP] Algorithms are modern EC — jwks not required for public clients 41 + 42 + Summary: 18 passed, 1 failed (spec), 0 network errors, 0 advisories, 8 skipped. Exit code: 1
+2 -1
tests/snapshots/oauth_client_metadata__confidential_happy.snap
··· 26 26 [OK] `scope` field is present 27 27 [OK] `scope` includes the `atproto` token 28 28 [OK] `scope` parses against the atproto permission grammar 29 + [SKIP] `client_uri` hostname matches `client_id` — `client_uri` not declared 29 30 == JWKS == 30 31 [OK] JWKS is present 31 32 [SKIP] JWKS URI is fetchable — jwks is inline ··· 35 36 [OK] Keys use signing use 36 37 [OK] Algorithms are modern EC 37 38 38 - Summary: 25 passed, 0 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 0 39 + Summary: 25 passed, 0 failed (spec), 0 network errors, 0 advisories, 2 skipped. Exit code: 0
+2 -1
tests/snapshots/oauth_client_metadata__confidential_missing_jwks.snap
··· 29 29 [OK] `scope` field is present 30 30 [OK] `scope` includes the `atproto` token 31 31 [OK] `scope` parses against the atproto permission grammar 32 + [SKIP] `client_uri` hostname matches `client_id` — `client_uri` not declared 32 33 == JWKS == 33 34 [SKIP] JWKS is present — blocked by oauth_client::metadata::confidential_requires_jwks 34 35 [SKIP] JWKS URI is fetchable — blocked by oauth_client::metadata::confidential_requires_jwks ··· 38 39 [SKIP] Keys use signing use — blocked by oauth_client::metadata::confidential_requires_jwks 39 40 [SKIP] Algorithms are modern EC — blocked by oauth_client::metadata::confidential_requires_jwks 40 41 41 - Summary: 18 passed, 1 failed (spec), 0 network errors, 0 advisories, 7 skipped. Exit code: 1 42 + Summary: 18 passed, 1 failed (spec), 0 network errors, 0 advisories, 8 skipped. Exit code: 1
+2 -1
tests/snapshots/oauth_client_metadata__discovery_failure_blocks_metadata.snap
··· 30 30 [SKIP] `scope` field is present — blocked by oauth_client::discovery::metadata_document_fetchable 31 31 [SKIP] `scope` includes the `atproto` token — blocked by oauth_client::discovery::metadata_document_fetchable 32 32 [SKIP] `scope` parses against the atproto permission grammar — blocked by oauth_client::discovery::metadata_document_fetchable 33 + [SKIP] `client_uri` hostname matches `client_id` — blocked by oauth_client::discovery::metadata_document_fetchable 33 34 == JWKS == 34 35 [SKIP] JWKS is present — blocked by oauth_client::metadata::raw_document_deserializes 35 36 [SKIP] JWKS URI is fetchable — blocked by oauth_client::metadata::raw_document_deserializes ··· 39 40 [SKIP] Keys use signing use — blocked by oauth_client::metadata::raw_document_deserializes 40 41 [SKIP] Algorithms are modern EC — blocked by oauth_client::metadata::raw_document_deserializes 41 42 42 - Summary: 1 passed, 0 failed (spec), 1 network errors, 0 advisories, 25 skipped. Exit code: 2 43 + Summary: 1 passed, 0 failed (spec), 1 network errors, 0 advisories, 26 skipped. Exit code: 2
+2 -1
tests/snapshots/oauth_client_metadata__dpop_bound_false.snap
··· 29 29 [OK] `scope` field is present 30 30 [OK] `scope` includes the `atproto` token 31 31 [OK] `scope` parses against the atproto permission grammar 32 + [SKIP] `client_uri` hostname matches `client_id` — `client_uri` not declared 32 33 == JWKS == 33 34 [OK] JWKS is present 34 35 [SKIP] JWKS URI is fetchable — jwks is inline ··· 38 39 [OK] Keys use signing use 39 40 [OK] Algorithms are modern EC 40 41 41 - Summary: 24 passed, 1 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 1 42 + Summary: 24 passed, 1 failed (spec), 0 network errors, 0 advisories, 2 skipped. Exit code: 1
+2 -1
tests/snapshots/oauth_client_metadata__loopback_skips_all_metadata_checks.snap
··· 27 27 [SKIP] `scope` field is present — metadata is implicit for loopback clients 28 28 [SKIP] `scope` includes the `atproto` token — metadata is implicit for loopback clients 29 29 [SKIP] `scope` parses against the atproto permission grammar — metadata is implicit for loopback clients 30 + [SKIP] `client_uri` hostname matches `client_id` — metadata is implicit for loopback clients 30 31 == JWKS == 31 32 [SKIP] JWKS is present — jwks not applicable to loopback clients 32 33 [SKIP] JWKS URI is fetchable — jwks not applicable to loopback clients ··· 36 37 [SKIP] Keys use signing use — jwks not applicable to loopback clients 37 38 [SKIP] Algorithms are modern EC — jwks not applicable to loopback clients 38 39 39 - Summary: 1 passed, 0 failed (spec), 0 network errors, 0 advisories, 26 skipped. Exit code: 0 40 + Summary: 1 passed, 0 failed (spec), 0 network errors, 0 advisories, 27 skipped. Exit code: 0
+2 -1
tests/snapshots/oauth_client_metadata__metadata_non_200_2xx_is_spec_violation.snap
··· 31 31 [SKIP] `scope` field is present — blocked by oauth_client::discovery::metadata_document_fetchable 32 32 [SKIP] `scope` includes the `atproto` token — blocked by oauth_client::discovery::metadata_document_fetchable 33 33 [SKIP] `scope` parses against the atproto permission grammar — blocked by oauth_client::discovery::metadata_document_fetchable 34 + [SKIP] `client_uri` hostname matches `client_id` — blocked by oauth_client::discovery::metadata_document_fetchable 34 35 == JWKS == 35 36 [SKIP] JWKS is present — blocked by oauth_client::metadata::raw_document_deserializes 36 37 [SKIP] JWKS URI is fetchable — blocked by oauth_client::metadata::raw_document_deserializes ··· 40 41 [SKIP] Keys use signing use — blocked by oauth_client::metadata::raw_document_deserializes 41 42 [SKIP] Algorithms are modern EC — blocked by oauth_client::metadata::raw_document_deserializes 42 43 43 - Summary: 1 passed, 1 failed (spec), 0 network errors, 0 advisories, 25 skipped. Exit code: 1 44 + Summary: 1 passed, 1 failed (spec), 0 network errors, 0 advisories, 26 skipped. Exit code: 1
+2 -1
tests/snapshots/oauth_client_metadata__metadata_non_json_content_type_is_spec_violation.snap
··· 30 30 [OK] `scope` field is present 31 31 [OK] `scope` includes the `atproto` token 32 32 [OK] `scope` parses against the atproto permission grammar 33 + [SKIP] `client_uri` hostname matches `client_id` — `client_uri` not declared 33 34 == JWKS == 34 35 [OK] JWKS is present 35 36 [SKIP] JWKS URI is fetchable — jwks is inline ··· 39 40 [OK] Keys use signing use 40 41 [OK] Algorithms are modern EC 41 42 42 - Summary: 24 passed, 1 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 1 43 + Summary: 24 passed, 1 failed (spec), 0 network errors, 0 advisories, 2 skipped. Exit code: 1
+2 -1
tests/snapshots/oauth_client_metadata__native_happy.snap
··· 26 26 [OK] `scope` field is present 27 27 [OK] `scope` includes the `atproto` token 28 28 [OK] `scope` parses against the atproto permission grammar 29 + [SKIP] `client_uri` hostname matches `client_id` — `client_uri` not declared 29 30 == JWKS == 30 31 [SKIP] JWKS is present — jwks not required for native clients 31 32 [SKIP] JWKS URI is fetchable — jwks not required for native clients ··· 35 36 [SKIP] Keys use signing use — jwks not required for native clients 36 37 [SKIP] Algorithms are modern EC — jwks not required for native clients 37 38 38 - Summary: 18 passed, 0 failed (spec), 0 network errors, 0 advisories, 8 skipped. Exit code: 0 39 + Summary: 18 passed, 0 failed (spec), 0 network errors, 0 advisories, 9 skipped. Exit code: 0
+2 -1
tests/snapshots/oauth_client_metadata__native_redirect_scheme_mismatch.snap
··· 29 29 [OK] `scope` field is present 30 30 [OK] `scope` includes the `atproto` token 31 31 [OK] `scope` parses against the atproto permission grammar 32 + [SKIP] `client_uri` hostname matches `client_id` — `client_uri` not declared 32 33 == JWKS == 33 34 [SKIP] JWKS is present — jwks not required for native clients 34 35 [SKIP] JWKS URI is fetchable — jwks not required for native clients ··· 38 39 [SKIP] Keys use signing use — jwks not required for native clients 39 40 [SKIP] Algorithms are modern EC — jwks not required for native clients 40 41 41 - Summary: 17 passed, 1 failed (spec), 0 network errors, 0 advisories, 8 skipped. Exit code: 1 42 + Summary: 17 passed, 1 failed (spec), 0 network errors, 0 advisories, 9 skipped. Exit code: 1
+2 -1
tests/snapshots/oauth_client_metadata__public_happy.snap
··· 26 26 [OK] `scope` field is present 27 27 [OK] `scope` includes the `atproto` token 28 28 [OK] `scope` parses against the atproto permission grammar 29 + [SKIP] `client_uri` hostname matches `client_id` — `client_uri` not declared 29 30 == JWKS == 30 31 [SKIP] JWKS is present — jwks not required for public clients 31 32 [SKIP] JWKS URI is fetchable — jwks not required for public clients ··· 35 36 [SKIP] Keys use signing use — jwks not required for public clients 36 37 [SKIP] Algorithms are modern EC — jwks not required for public clients 37 38 38 - Summary: 18 passed, 0 failed (spec), 0 network errors, 0 advisories, 8 skipped. Exit code: 0 39 + Summary: 18 passed, 0 failed (spec), 0 network errors, 0 advisories, 9 skipped. Exit code: 0
+2 -1
tests/snapshots/oauth_client_metadata__public_with_token_endpoint_auth.snap
··· 29 29 [OK] `scope` field is present 30 30 [OK] `scope` includes the `atproto` token 31 31 [OK] `scope` parses against the atproto permission grammar 32 + [SKIP] `client_uri` hostname matches `client_id` — `client_uri` not declared 32 33 == JWKS == 33 34 [SKIP] JWKS is present — jwks not required for public clients 34 35 [SKIP] JWKS URI is fetchable — jwks not required for public clients ··· 38 39 [SKIP] Keys use signing use — jwks not required for public clients 39 40 [SKIP] Algorithms are modern EC — jwks not required for public clients 40 41 41 - Summary: 17 passed, 1 failed (spec), 0 network errors, 0 advisories, 8 skipped. Exit code: 1 42 + Summary: 17 passed, 1 failed (spec), 0 network errors, 0 advisories, 9 skipped. Exit code: 1
+2 -1
tests/snapshots/oauth_client_metadata__scope_grammar_invalid.snap
··· 36 36 · ╰── invalid token 37 37 28 │ "token_endpoint_auth_method": "private_key_jwt", 38 38 ╰──── 39 + [SKIP] `client_uri` hostname matches `client_id` — `client_uri` not declared 39 40 == JWKS == 40 41 [OK] JWKS is present 41 42 [SKIP] JWKS URI is fetchable — jwks is inline ··· 45 46 [OK] Keys use signing use 46 47 [OK] Algorithms are modern EC 47 48 48 - Summary: 23 passed, 1 failed (spec), 0 network errors, 0 advisories, 2 skipped. Exit code: 1 49 + Summary: 23 passed, 1 failed (spec), 0 network errors, 0 advisories, 3 skipped. Exit code: 1
+2 -1
tests/snapshots/oauth_client_metadata__signing_alg_none_is_spec_violation.snap
··· 29 29 [OK] `scope` field is present 30 30 [OK] `scope` includes the `atproto` token 31 31 [OK] `scope` parses against the atproto permission grammar 32 + [SKIP] `client_uri` hostname matches `client_id` — `client_uri` not declared 32 33 == JWKS == 33 34 [OK] JWKS is present 34 35 [SKIP] JWKS URI is fetchable — jwks is inline ··· 41 42 [OK] Keys use signing use 42 43 [OK] Algorithms are modern EC 43 44 44 - Summary: 23 passed, 2 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 1 45 + Summary: 23 passed, 2 failed (spec), 0 network errors, 0 advisories, 2 skipped. Exit code: 1