CLI app for developers prototyping atproto functionality
1
fork

Configure Feed

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

Tighten static-stage checks against the atproto OAuth spec

Four spec-conformance gaps surfaced during a review pass of the
`test oauth client` implementation against
<https://atproto.com/specs/oauth>, all of which caused either false
positives against spec-conformant clients or false negatives against
non-conformant ones:

1. `response_types` required exact equality with `["code"]`. The spec
(<https://atproto.com/specs/oauth#client-metadata>) only requires
`"code"` to be included; additional values are not forbidden.
Relax the check to membership.

2. `grant_types` rejected any value outside
{`authorization_code`, `refresh_token`}. Spec only requires
`"authorization_code"` be present (and `"refresh_token"` if the
client refreshes). Drop the allow-list.

3. `metadata_document_fetchable` accepted any 2xx status. Spec
requires HTTP 200 exactly ("must be 200 (not another 2xx or a
redirect)"). Non-200 2xx now emits a `SpecViolation` with a
pointed diagnostic; transport failures and non-2xx statuses
remain `NetworkError`.

4. The metadata response's `Content-Type` was never validated. The
atproto OAuth profile requires the JSON content type. Add a new
discovery check `metadata_content_type_is_json` that accepts
`application/json` with optional parameters (e.g.
`charset=utf-8`). A 200 response whose body parses as JSON but is
labelled `text/html` now fails `metadata_content_type_is_json` and
passes `metadata_is_json` — the two checks are deliberately
independent so the specific reason surfaces.

`FakeHttpClient::add_response` now defaults its seeded `Content-Type`
to `application/json` so the dozens of tests that already serve JSON
fixtures don't each need to spell it out; tests that need a specific
(or absent) content type continue to call
`add_response_with_content_type` directly.

All 16 real-world atproto OAuth clients still pass; 325 tests pass.

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

+587 -143
+15 -2
docs/design-plans/2026-04-16-test-oauth-client.md
··· 70 70 - **test-oauth-client.AC1.3 Success:** `http://127.0.0.1[:port][/path]` is treated identically to `localhost` for loopback recognition. 71 71 - **test-oauth-client.AC1.4 Failure:** A target that is neither HTTPS nor a recognized loopback form is rejected before the pipeline starts, with a `parse_target` error surfaced as a clap argument error (non-zero exit, helpful message). 72 72 - **test-oauth-client.AC1.5 Failure:** An HTTPS `client_id` returning a non-2xx status produces a `NetworkError` `CheckResult` with the URL and status in the diagnostic; downstream stages emit `Skipped` rows blocked on `discovery::metadata_document_fetchable`. 73 + - **test-oauth-client.AC1.5b Failure:** An HTTPS `client_id` returning a 2xx status *other than 200* (e.g., 201, 204) produces a `SpecViolation` on `discovery::metadata_document_fetchable`. The atproto OAuth profile (<https://atproto.com/specs/oauth#client-metadata>) requires HTTP 200 exactly ("must be 200 (not another 2xx or a redirect)"). Downstream stages emit `Skipped` rows blocked on this check. 73 74 - **test-oauth-client.AC1.6 Failure:** An HTTPS `client_id` returning a body that is not parseable JSON produces a `SpecViolation` with the content-type and a snippet of the body in the diagnostic. 75 + - **test-oauth-client.AC1.6b Failure:** An HTTPS `client_id` returning a 200 response whose `Content-Type` is not `application/json` (or `application/json; charset=…`) produces a `SpecViolation` on `discovery::metadata_content_type_is_json`. The body is still subject to the JSON-parse check: a body that happens to parse as JSON but is labelled `text/html` still fails `metadata_content_type_is_json` and passes `metadata_is_json`. 74 76 - **test-oauth-client.AC1.7 Edge:** A loopback target with an empty path, an explicit port, or a non-empty path each parse identically and emit the same set of `CheckResult`s. 75 77 76 78 ### test-oauth-client.AC2: Static mode — metadata document validation 77 79 78 - - **test-oauth-client.AC2.1 Success:** A confidential web client document with all required fields (`application_type=web` (optional; defaults to `web` when omitted), `response_types=[code]`, `grant_types=[authorization_code]`, `dpop_bound_access_tokens=true`, `redirect_uris` HTTPS, `token_endpoint_auth_method=private_key_jwt`, `jwks` or `jwks_uri`, valid `scope`) passes every metadata-stage check. 80 + - **test-oauth-client.AC2.1 Success:** A confidential web client document with all required fields (`application_type=web` (optional; defaults to `web` when omitted), `response_types` including `code` (additional values are not forbidden), `grant_types` including `authorization_code` (additional values are not forbidden), `dpop_bound_access_tokens=true`, `redirect_uris` HTTPS, `token_endpoint_auth_method=private_key_jwt`, `jwks` or `jwks_uri`, valid `scope`) passes every metadata-stage check. 79 81 - **test-oauth-client.AC2.2 Success:** A public web client document (no JWKS, `token_endpoint_auth_method=none`) passes. Omitting `application_type` is valid per the atproto OAuth profile, which marks the field OPTIONAL with default `web`; the `application_type_present` check emits `Skipped` (reason: "`application_type` is optional; atproto defaults to `web`") rather than failing. 80 82 - **test-oauth-client.AC2.3 Success:** A native client document with custom-scheme `redirect_uris` whose scheme matches the reverse-domain of the `client_id` host passes. 81 83 - **test-oauth-client.AC2.4 Failure:** `dpop_bound_access_tokens` absent or `false` produces a `SpecViolation` with stable code `oauth_client::metadata::dpop_bound_required`. ··· 743 745 - `RawMetadata::{Document(Bytes), Implicit { client_id: Url }}`. 744 746 - `Check` enum with stable IDs (`oauth_client::discovery::client_id_well_formed`, 745 747 `oauth_client::discovery::metadata_document_fetchable`, 746 - `oauth_client::discovery::metadata_is_json`). 748 + `oauth_client::discovery::metadata_content_type_is_json`, 749 + `oauth_client::discovery::metadata_is_json`). The 750 + `metadata_document_fetchable` check distinguishes transport-layer 751 + failures (network error, non-2xx status — emitted as 752 + `NetworkError`) from atproto-profile violations (2xx-but-not-200 753 + — emitted as `SpecViolation`), per 754 + <https://atproto.com/specs/oauth#client-metadata>. 755 + `metadata_content_type_is_json` verifies the response's 756 + `Content-Type` is `application/json` (with optional parameters 757 + such as `charset=utf-8`), and is independent from 758 + `metadata_is_json` so a mislabelled-but-parseable body surfaces 759 + the content-type violation specifically. 747 760 - `run(target, &OauthClientOptions) -> DiscoveryStageOutput`. 748 761 - Wire `client.rs::run()` to construct a real `HttpClient` (shared 749 762 `reqwest::Client` with rustls + 10s timeout + user-agent), invoke
+159 -36
src/commands/test/oauth/client/pipeline/discovery.rs
··· 64 64 /// Plain-`http` non-loopback targets and non-HTTP schemes are 65 65 /// rejected here. 66 66 ClientIdWellFormed, 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>). 67 + /// For HTTPS clients, the metadata document is fetched over HTTPS 68 + /// and returns HTTP status `200` exactly. The atproto OAuth 69 + /// profile (<https://atproto.com/specs/oauth#client-metadata>) 70 + /// specifies: *"The response HTTP status must be 200 (not another 71 + /// 2xx or a redirect)."* Non-200 2xx statuses are treated as 72 + /// `SpecViolation`; non-2xx statuses and transport errors remain 73 + /// `NetworkError`. For loopback clients the check is skipped 74 + /// because metadata is implicit. 73 75 MetadataDocumentFetchable, 76 + /// The response `Content-Type` is `application/json` (charset 77 + /// parameters allowed). The atproto OAuth profile requires client 78 + /// metadata to be served with the correct JSON content type; a 79 + /// body that happens to parse as JSON but is labelled `text/html` 80 + /// still fails this check. Skipped for loopback clients. 81 + MetadataContentTypeIsJson, 74 82 /// The fetched metadata body is valid JSON. Gates 75 83 /// `metadata::RawDocumentDeserializes` downstream: if the body 76 84 /// doesn't parse as JSON here, the full metadata stage is blocked. ··· 82 90 pub const CHECK_ALL: &[Check] = &[ 83 91 Check::ClientIdWellFormed, 84 92 Check::MetadataDocumentFetchable, 93 + Check::MetadataContentTypeIsJson, 85 94 Check::MetadataIsJson, 86 95 ]; 87 96 ··· 93 102 Check::MetadataDocumentFetchable => { 94 103 "oauth_client::discovery::metadata_document_fetchable" 95 104 } 105 + Check::MetadataContentTypeIsJson => { 106 + "oauth_client::discovery::metadata_content_type_is_json" 107 + } 96 108 Check::MetadataIsJson => "oauth_client::discovery::metadata_is_json", 97 109 } 98 110 } 99 111 112 + fn pass_summary(self) -> &'static str { 113 + match self { 114 + Check::ClientIdWellFormed => "Client ID well-formed", 115 + Check::MetadataDocumentFetchable => "Metadata document fetchable", 116 + Check::MetadataContentTypeIsJson => "Metadata Content-Type is JSON", 117 + Check::MetadataIsJson => "Metadata is valid JSON", 118 + } 119 + } 120 + 100 121 /// Create a passing check result. 101 122 pub fn pass(self) -> CheckResult { 102 123 CheckResult { 103 124 id: self.id(), 104 125 stage: Stage::OAUTH_CLIENT_DISCOVERY, 105 126 status: crate::common::report::CheckStatus::Pass, 106 - summary: Cow::Borrowed(match self { 107 - Check::ClientIdWellFormed => "Client ID well-formed", 108 - Check::MetadataDocumentFetchable => "Metadata document fetchable", 109 - Check::MetadataIsJson => "Metadata is valid JSON", 110 - }), 127 + summary: Cow::Borrowed(self.pass_summary()), 111 128 diagnostic: None, 112 129 skipped_reason: None, 113 130 } ··· 125 142 summary: Cow::Borrowed(match self { 126 143 Check::ClientIdWellFormed => "Client ID validation failed", 127 144 Check::MetadataDocumentFetchable => "Metadata document fetch failed", 145 + Check::MetadataContentTypeIsJson => "Metadata Content-Type is not JSON", 128 146 Check::MetadataIsJson => "Metadata is not valid JSON", 129 147 }), 130 148 diagnostic, ··· 144 162 summary: Cow::Borrowed(match self { 145 163 Check::ClientIdWellFormed => "Client ID network error", 146 164 Check::MetadataDocumentFetchable => "Metadata document unreachable", 165 + Check::MetadataContentTypeIsJson => "Metadata Content-Type unavailable", 147 166 Check::MetadataIsJson => "Metadata fetch failed", 148 167 }), 149 168 diagnostic, ··· 156 175 skipped_with_reason( 157 176 self.id(), 158 177 Stage::OAUTH_CLIENT_DISCOVERY, 159 - match self { 160 - Check::ClientIdWellFormed => "Client ID well-formed", 161 - Check::MetadataDocumentFetchable => "Metadata document fetchable", 162 - Check::MetadataIsJson => "Metadata is valid JSON", 163 - }, 178 + self.pass_summary(), 164 179 reason, 165 180 ) 166 181 } ··· 192 207 let diagnostic: Box<dyn Diagnostic + Send + Sync> = 193 208 Box::new(FetchError::from_identity_error(e, url)); 194 209 results.push(Check::MetadataDocumentFetchable.network_error(Some(diagnostic))); 195 - results.push(blocked_by( 196 - Check::MetadataIsJson.id(), 197 - Stage::OAUTH_CLIENT_DISCOVERY, 198 - "Metadata is valid JSON", 199 - Check::MetadataDocumentFetchable.id(), 200 - )); 210 + for check in [Check::MetadataContentTypeIsJson, Check::MetadataIsJson] { 211 + results.push(blocked_by( 212 + check.id(), 213 + Stage::OAUTH_CLIENT_DISCOVERY, 214 + check.pass_summary(), 215 + Check::MetadataDocumentFetchable.id(), 216 + )); 217 + } 201 218 DiscoveryStageOutput { 202 219 facts: None, 203 220 results, 204 221 } 205 222 } 206 223 Ok((status, _, _)) if !(200..=299).contains(&status) => { 207 - // Non-2xx status. 224 + // Non-2xx status: transport-layer failure. 208 225 let diagnostic: Box<dyn Diagnostic + Send + Sync> = Box::new(HttpStatusError { 209 226 url: url.clone(), 210 227 status, 211 228 }); 212 229 results.push(Check::MetadataDocumentFetchable.network_error(Some(diagnostic))); 213 - results.push(blocked_by( 214 - Check::MetadataIsJson.id(), 215 - Stage::OAUTH_CLIENT_DISCOVERY, 216 - "Metadata is valid JSON", 217 - Check::MetadataDocumentFetchable.id(), 218 - )); 230 + for check in [Check::MetadataContentTypeIsJson, Check::MetadataIsJson] { 231 + results.push(blocked_by( 232 + check.id(), 233 + Stage::OAUTH_CLIENT_DISCOVERY, 234 + check.pass_summary(), 235 + Check::MetadataDocumentFetchable.id(), 236 + )); 237 + } 238 + DiscoveryStageOutput { 239 + facts: None, 240 + results, 241 + } 242 + } 243 + Ok((status, _, _)) if status != 200 => { 244 + // 2xx but not 200: spec violation per 245 + // <https://atproto.com/specs/oauth#client-metadata> 246 + // ("must be 200 (not another 2xx or a redirect)"). 247 + let diagnostic: Box<dyn Diagnostic + Send + Sync> = 248 + Box::new(NonOkStatusError { 249 + url: url.clone(), 250 + status, 251 + }); 252 + results.push(Check::MetadataDocumentFetchable.spec_violation(Some(diagnostic))); 253 + for check in [Check::MetadataContentTypeIsJson, Check::MetadataIsJson] { 254 + results.push(blocked_by( 255 + check.id(), 256 + Stage::OAUTH_CLIENT_DISCOVERY, 257 + check.pass_summary(), 258 + Check::MetadataDocumentFetchable.id(), 259 + )); 260 + } 219 261 DiscoveryStageOutput { 220 262 facts: None, 221 263 results, 222 264 } 223 265 } 224 266 Ok((_status, body, content_type)) => { 225 - // 2xx response received. Check if it's valid JSON. 226 267 results.push(Check::MetadataDocumentFetchable.pass()); 268 + 269 + // Content-Type check: the atproto OAuth profile 270 + // requires the metadata response carry the correct 271 + // JSON content type. Accept `application/json` 272 + // with optional parameters like `charset=utf-8`. 273 + if content_type_is_json(content_type.as_deref()) { 274 + results.push(Check::MetadataContentTypeIsJson.pass()); 275 + } else { 276 + let diagnostic: Box<dyn Diagnostic + Send + Sync> = 277 + Box::new(NonJsonContentTypeError { 278 + url: url.clone(), 279 + content_type: content_type.clone(), 280 + }); 281 + results.push( 282 + Check::MetadataContentTypeIsJson.spec_violation(Some(diagnostic)), 283 + ); 284 + } 227 285 228 286 // Try to parse as JSON. 229 287 match serde_json::from_slice::<serde_json::Value>(&body) { ··· 278 336 results.push(Check::ClientIdWellFormed.pass()); 279 337 280 338 // Metadata is implicit for loopback clients. 281 - results.push( 282 - Check::MetadataDocumentFetchable 283 - .skipped("metadata is implicit for loopback clients"), 284 - ); 285 - results 286 - .push(Check::MetadataIsJson.skipped("metadata is implicit for loopback clients")); 339 + for check in [ 340 + Check::MetadataDocumentFetchable, 341 + Check::MetadataContentTypeIsJson, 342 + Check::MetadataIsJson, 343 + ] { 344 + results.push(check.skipped("metadata is implicit for loopback clients")); 345 + } 287 346 288 347 // Reconstruct the client_id URL. 289 348 let client_id = reconstruct_loopback_url(loopback_target); ··· 401 460 )))) 402 461 } 403 462 } 463 + 464 + /// Error for a 2xx-but-not-200 response (spec violation per the atproto 465 + /// OAuth profile). 466 + #[derive(Debug, miette::Diagnostic)] 467 + #[diagnostic( 468 + code = "oauth_client::discovery::metadata_document_fetchable", 469 + help = "The atproto OAuth profile requires the metadata document to be served with HTTP status 200 exactly. Redirects and other 2xx statuses (201, 204, …) are not allowed." 470 + )] 471 + struct NonOkStatusError { 472 + url: Url, 473 + status: u16, 474 + } 475 + 476 + impl std::fmt::Display for NonOkStatusError { 477 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 478 + write!( 479 + f, 480 + "Metadata document at {} returned HTTP {} (expected exactly 200)", 481 + self.url, self.status 482 + ) 483 + } 484 + } 485 + 486 + impl std::error::Error for NonOkStatusError {} 487 + 488 + /// Error for a metadata response whose `Content-Type` isn't JSON. 489 + #[derive(Debug, miette::Diagnostic)] 490 + #[diagnostic( 491 + code = "oauth_client::discovery::metadata_content_type_is_json", 492 + help = "Serve the client-metadata document with `Content-Type: application/json` (charset parameters such as `; charset=utf-8` are permitted)." 493 + )] 494 + struct NonJsonContentTypeError { 495 + url: Url, 496 + content_type: Option<String>, 497 + } 498 + 499 + impl std::fmt::Display for NonJsonContentTypeError { 500 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 501 + match &self.content_type { 502 + Some(ct) => write!( 503 + f, 504 + "Metadata document at {} returned Content-Type `{}` (expected `application/json`)", 505 + self.url, ct 506 + ), 507 + None => write!( 508 + f, 509 + "Metadata document at {} returned no Content-Type header (expected `application/json`)", 510 + self.url 511 + ), 512 + } 513 + } 514 + } 515 + 516 + impl std::error::Error for NonJsonContentTypeError {} 517 + 518 + /// Returns true if `content_type` is a JSON media type. Accepts 519 + /// `application/json` with optional parameters (`; charset=utf-8`, etc.). 520 + fn content_type_is_json(content_type: Option<&str>) -> bool { 521 + let Some(ct) = content_type else { 522 + return false; 523 + }; 524 + let media_type = ct.split(';').next().unwrap_or("").trim(); 525 + media_type.eq_ignore_ascii_case("application/json") 526 + }
+29 -23
src/commands/test/oauth/client/pipeline/metadata.rs
··· 337 337 /// any other string fails. Loopback clients skip metadata checks 338 338 /// entirely (implicit metadata). 339 339 ApplicationTypeKnown, 340 - /// `response_types` is exactly `["code"]`. The atproto OAuth 341 - /// profile only supports the authorization-code flow (RFC 6749 342 - /// §4.1, <https://datatracker.ietf.org/doc/html/rfc6749#section-4.1>); 343 - /// implicit, hybrid, and password flows are forbidden. 340 + /// `response_types` includes `"code"`. The atproto OAuth profile 341 + /// (<https://atproto.com/specs/oauth#client-metadata>) requires 342 + /// `code` be included; additional values are not forbidden. The 343 + /// only flow actually used at runtime is the authorization code 344 + /// flow (RFC 6749 §4.1, 345 + /// <https://datatracker.ietf.org/doc/html/rfc6749#section-4.1>). 344 346 ResponseTypesIsCode, 345 - /// `grant_types` includes `authorization_code` and contains only 346 - /// grant types that atproto recognises (`authorization_code`, 347 - /// `refresh_token`). Mirrors RFC 6749 §4.1 plus §6 for refresh 348 - /// tokens (<https://datatracker.ietf.org/doc/html/rfc6749#section-6>). 347 + /// `grant_types` includes `authorization_code`. Atproto requires 348 + /// this value be present; `refresh_token` is optional (only 349 + /// required if the client makes token refresh requests, RFC 6749 350 + /// §6, <https://datatracker.ietf.org/doc/html/rfc6749#section-6>). 351 + /// The spec does not forbid additional grant_types values. 349 352 GrantTypesIncludesAuthorizationCode, 350 353 /// `dpop_bound_access_tokens` is explicitly `true`. The atproto 351 354 /// OAuth profile mandates DPoP-bound access tokens per RFC 9449 ··· 441 444 Check::ClientIdMatches => "Metadata `client_id` matches fetched URL", 442 445 Check::ApplicationTypePresent => "`application_type` field is present", 443 446 Check::ApplicationTypeKnown => "`application_type` is `web` or `native`", 444 - Check::ResponseTypesIsCode => "`response_types` is `[\"code\"]`", 447 + Check::ResponseTypesIsCode => "`response_types` includes `code`", 445 448 Check::GrantTypesIncludesAuthorizationCode => { 446 449 "`grant_types` includes `authorization_code`" 447 450 } ··· 716 719 } 717 720 }; 718 721 719 - // Check ResponseTypesIsCode. 722 + // Check ResponseTypesIsCode. The atproto OAuth spec 723 + // (<https://atproto.com/specs/oauth#client-metadata>) 724 + // requires only that `code` be included; it does 725 + // not forbid additional response_types values. 720 726 let response_types_valid = match &doc.response_types { 721 - Some(rt) => rt == &vec!["code".to_string()], 727 + Some(rt) => rt.iter().any(|v| v == "code"), 722 728 None => false, 723 729 }; 724 730 if !response_types_valid { 725 731 let diagnostic: Box<dyn miette::Diagnostic + Send + Sync> = 726 732 Box::new(MetadataViolationDiagnostic { 727 733 message: format!( 728 - "response_types must be exactly [\"code\"], got {doc:?}", 734 + "response_types must include \"code\", got {doc:?}", 729 735 doc = doc.response_types 730 736 ), 731 737 code: "oauth_client::metadata::response_types_is_code", ··· 735 741 results.push(Check::ResponseTypesIsCode.pass()); 736 742 } 737 743 738 - // Check GrantTypesIncludesAuthorizationCode. 744 + // Check GrantTypesIncludesAuthorizationCode. Spec 745 + // requires `authorization_code` to be present; 746 + // `refresh_token` is optional. The spec does not 747 + // forbid additional grant_types values. 739 748 let grant_types_valid = match &doc.grant_types { 740 - Some(gt) => { 741 - gt.contains(&"authorization_code".to_string()) 742 - && gt 743 - .iter() 744 - .all(|g| g == "authorization_code" || g == "refresh_token") 745 - } 749 + Some(gt) => gt.iter().any(|g| g == "authorization_code"), 746 750 None => false, 747 751 }; 748 752 if !grant_types_valid { 749 - let diagnostic: Box<dyn miette::Diagnostic + Send + Sync> = 750 - Box::new(MetadataViolationDiagnostic { 751 - message: "grant_types must include 'authorization_code' and only contain 'authorization_code' or 'refresh_token'".to_string(), 753 + let diagnostic: Box<dyn miette::Diagnostic + Send + Sync> = Box::new( 754 + MetadataViolationDiagnostic { 755 + message: "grant_types must include 'authorization_code'" 756 + .to_string(), 752 757 code: "oauth_client::metadata::grant_types_includes_authorization_code", 753 - }); 758 + }, 759 + ); 754 760 results.push( 755 761 Check::GrantTypesIncludesAuthorizationCode 756 762 .spec_violation(Some(diagnostic)),
+12 -1
tests/common/mod.rs
··· 99 99 } 100 100 101 101 /// Add a response for the given URL. 102 + /// 103 + /// Defaults the response `Content-Type` to `application/json` so the 104 + /// overwhelming majority of tests (which serve JSON metadata) don't 105 + /// each have to spell that out; tests that need a specific 106 + /// (or missing) content type should call 107 + /// `add_response_with_content_type` directly. 102 108 pub fn add_response(&self, url: &Url, status: u16, body: Vec<u8>) { 103 - self.add_response_with_content_type(url, status, body, None); 109 + self.add_response_with_content_type( 110 + url, 111 + status, 112 + body, 113 + Some("application/json".to_string()), 114 + ); 104 115 } 105 116 106 117 /// Add a response for the given URL with an optional Content-Type header.
+10
tests/fixtures/oauth_client/metadata/grant_types_includes_extras/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", "client_credentials"], 6 + "scope": "atproto", 7 + "redirect_uris": ["https://client.example.com/cb"], 8 + "dpop_bound_access_tokens": true, 9 + "token_endpoint_auth_method": "none" 10 + }
+10
tests/fixtures/oauth_client/metadata/response_types_includes_extras/metadata.json
··· 1 + { 2 + "client_id": "https://client.example.com/metadata.json", 3 + "application_type": "web", 4 + "response_types": ["code", "id_token"], 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 + }
+18 -14
tests/oauth_client_check_id_coverage.rs
··· 55 55 // enums (unlike metadata / interactive / scope_variations / dpop_edges), 56 56 // so their Pass-summary strings are hardcoded here. If a Pass summary 57 57 // changes, update this list. 58 - let mut pairs: Vec<(&'static str, &'static str)> = Vec::new(); 59 - 60 - pairs.push(( 61 - discovery::Check::ClientIdWellFormed.id(), 62 - "Client ID well-formed", 63 - )); 64 - pairs.push(( 65 - discovery::Check::MetadataDocumentFetchable.id(), 66 - "Metadata document fetchable", 67 - )); 68 - pairs.push(( 69 - discovery::Check::MetadataIsJson.id(), 70 - "Metadata is valid JSON", 71 - )); 58 + let mut pairs: Vec<(&'static str, &'static str)> = vec![ 59 + ( 60 + discovery::Check::ClientIdWellFormed.id(), 61 + "Client ID well-formed", 62 + ), 63 + ( 64 + discovery::Check::MetadataDocumentFetchable.id(), 65 + "Metadata document fetchable", 66 + ), 67 + ( 68 + discovery::Check::MetadataContentTypeIsJson.id(), 69 + "Metadata Content-Type is JSON", 70 + ), 71 + ( 72 + discovery::Check::MetadataIsJson.id(), 73 + "Metadata is valid JSON", 74 + ), 75 + ]; 72 76 73 77 pairs.extend(metadata::CHECK_ALL.iter().map(|c| (c.id(), c.summary()))); 74 78
+8 -1
tests/oauth_client_discovery.rs
··· 251 251 content_type, 252 252 } => { 253 253 assert!(!bytes.is_empty(), "Expected non-empty metadata bytes"); 254 - assert_eq!(content_type, &None, "Expected no content-type in this test"); 254 + // The FakeHttpClient's `add_response` default now seeds 255 + // `application/json` so the new metadata_content_type_is_json 256 + // discovery check doesn't fail every test that doesn't care. 257 + assert_eq!( 258 + content_type.as_deref(), 259 + Some("application/json"), 260 + "Expected the default JSON content-type to flow through" 261 + ); 255 262 } 256 263 discovery::RawMetadata::Implicit { .. } => { 257 264 panic!("Expected Document variant, got Implicit");
+141
tests/oauth_client_metadata.rs
··· 441 441 let rendered = render_report_to_string(&report); 442 442 insta::assert_snapshot!(rendered); 443 443 } 444 + 445 + /// Per the atproto OAuth profile 446 + /// (<https://atproto.com/specs/oauth#client-metadata>) `response_types` 447 + /// merely requires that `code` be included — extra values must not 448 + /// cause a SpecViolation. 449 + #[tokio::test] 450 + async fn response_types_includes_code_with_extras_passes() { 451 + let http = common::FakeHttpClient::new(); 452 + let metadata = include_bytes!( 453 + "fixtures/oauth_client/metadata/response_types_includes_extras/metadata.json" 454 + ); 455 + http.add_response( 456 + &Url::parse("https://client.example.com/metadata.json").unwrap(), 457 + 200, 458 + metadata.to_vec(), 459 + ); 460 + 461 + let target = target::parse("https://client.example.com/metadata.json").expect("parse failed"); 462 + let jwks_fetcher = common::FakeJwksFetcher::new(); 463 + let opts = OauthClientOptions { 464 + interactive: None, 465 + http: &http, 466 + jwks: &jwks_fetcher, 467 + verbose: false, 468 + clock: Arc::new(RealClock), 469 + }; 470 + 471 + let report = run_pipeline(target, opts).await; 472 + assert_eq!( 473 + report.exit_code(), 474 + 0, 475 + "response_types with code plus extras should pass" 476 + ); 477 + } 478 + 479 + /// Per the atproto OAuth profile `grant_types` merely requires that 480 + /// `authorization_code` be included; the spec does not forbid extra 481 + /// grant types. 482 + #[tokio::test] 483 + async fn grant_types_includes_authorization_code_with_extras_passes() { 484 + let http = common::FakeHttpClient::new(); 485 + let metadata = 486 + include_bytes!("fixtures/oauth_client/metadata/grant_types_includes_extras/metadata.json"); 487 + http.add_response( 488 + &Url::parse("https://client.example.com/metadata.json").unwrap(), 489 + 200, 490 + metadata.to_vec(), 491 + ); 492 + 493 + let target = target::parse("https://client.example.com/metadata.json").expect("parse failed"); 494 + let jwks_fetcher = common::FakeJwksFetcher::new(); 495 + let opts = OauthClientOptions { 496 + interactive: None, 497 + http: &http, 498 + jwks: &jwks_fetcher, 499 + verbose: false, 500 + clock: Arc::new(RealClock), 501 + }; 502 + 503 + let report = run_pipeline(target, opts).await; 504 + assert_eq!( 505 + report.exit_code(), 506 + 0, 507 + "grant_types with authorization_code plus extras should pass" 508 + ); 509 + } 510 + 511 + /// Per the atproto OAuth profile the client-metadata response HTTP 512 + /// status must be 200 exactly (not another 2xx or a redirect). 513 + /// A 201 response with otherwise-valid JSON body fails 514 + /// `metadata_document_fetchable` as a `SpecViolation`. 515 + #[tokio::test] 516 + async fn metadata_non_200_2xx_is_spec_violation() { 517 + let http = common::FakeHttpClient::new(); 518 + // Reuse the otherwise-happy confidential fixture; the failure here 519 + // is entirely driven by the 201 status. 520 + let metadata = 521 + include_bytes!("fixtures/oauth_client/metadata/confidential_happy/metadata.json"); 522 + http.add_response( 523 + &Url::parse("https://client.example.com/metadata.json").unwrap(), 524 + 201, 525 + metadata.to_vec(), 526 + ); 527 + 528 + let target = target::parse("https://client.example.com/metadata.json").expect("parse failed"); 529 + let jwks_fetcher = common::FakeJwksFetcher::new(); 530 + let opts = OauthClientOptions { 531 + interactive: None, 532 + http: &http, 533 + jwks: &jwks_fetcher, 534 + verbose: false, 535 + clock: Arc::new(RealClock), 536 + }; 537 + 538 + let report = run_pipeline(target, opts).await; 539 + assert_eq!( 540 + report.exit_code(), 541 + 1, 542 + "HTTP 201 on the metadata response must be a SpecViolation" 543 + ); 544 + 545 + let rendered = render_report_to_string(&report); 546 + insta::assert_snapshot!(rendered); 547 + } 548 + 549 + /// Per the atproto OAuth profile the client-metadata response must be 550 + /// served with a JSON Content-Type. A `text/html` response — even one 551 + /// whose body parses as JSON — fails 552 + /// `metadata_content_type_is_json`. 553 + #[tokio::test] 554 + async fn metadata_non_json_content_type_is_spec_violation() { 555 + let http = common::FakeHttpClient::new(); 556 + let metadata = 557 + include_bytes!("fixtures/oauth_client/metadata/confidential_happy/metadata.json"); 558 + http.add_response_with_content_type( 559 + &Url::parse("https://client.example.com/metadata.json").unwrap(), 560 + 200, 561 + metadata.to_vec(), 562 + Some("text/html; charset=utf-8".to_string()), 563 + ); 564 + 565 + let target = target::parse("https://client.example.com/metadata.json").expect("parse failed"); 566 + let jwks_fetcher = common::FakeJwksFetcher::new(); 567 + let opts = OauthClientOptions { 568 + interactive: None, 569 + http: &http, 570 + jwks: &jwks_fetcher, 571 + verbose: false, 572 + clock: Arc::new(RealClock), 573 + }; 574 + 575 + let report = run_pipeline(target, opts).await; 576 + assert_eq!( 577 + report.exit_code(), 578 + 1, 579 + "wrong Content-Type must be a SpecViolation" 580 + ); 581 + 582 + let rendered = render_report_to_string(&report); 583 + insta::assert_snapshot!(rendered); 584 + }
+3 -2
tests/snapshots/oauth_client_discovery__https_404_produces_network_error.snap
··· 11 11 oauth_client::discovery::metadata_document_fetchable 12 12 13 13 × Metadata document fetch returned HTTP 404: https://client.example.com/missing.json 14 + [SKIP] Metadata Content-Type is JSON — blocked by oauth_client::discovery::metadata_document_fetchable 14 15 [SKIP] Metadata is valid JSON — blocked by oauth_client::discovery::metadata_document_fetchable 15 16 == Metadata == 16 17 [SKIP] Metadata document deserializes — blocked by oauth_client::discovery::metadata_document_fetchable 17 18 [SKIP] Metadata `client_id` matches fetched URL — blocked by oauth_client::discovery::metadata_document_fetchable 18 19 [SKIP] `application_type` field is present — blocked by oauth_client::discovery::metadata_document_fetchable 19 20 [SKIP] `application_type` is `web` or `native` — blocked by oauth_client::discovery::metadata_document_fetchable 20 - [SKIP] `response_types` is `["code"]` — blocked by oauth_client::discovery::metadata_document_fetchable 21 + [SKIP] `response_types` includes `code` — blocked by oauth_client::discovery::metadata_document_fetchable 21 22 [SKIP] `grant_types` includes `authorization_code` — blocked by oauth_client::discovery::metadata_document_fetchable 22 23 [SKIP] `dpop_bound_access_tokens` is `true` — blocked by oauth_client::discovery::metadata_document_fetchable 23 24 [SKIP] `redirect_uris` is non-empty — blocked by oauth_client::discovery::metadata_document_fetchable ··· 37 38 [SKIP] Keys use signing use — blocked by oauth_client::metadata::raw_document_deserializes 38 39 [SKIP] Algorithms are modern EC — blocked by oauth_client::metadata::raw_document_deserializes 39 40 40 - Summary: 1 passed, 0 failed (spec), 1 network errors, 0 advisories, 23 skipped. Exit code: 2 41 + Summary: 1 passed, 0 failed (spec), 1 network errors, 0 advisories, 24 skipped. Exit code: 2
+3 -2
tests/snapshots/oauth_client_discovery__https_confidential_happy_discovery.snap
··· 8 8 == Discovery == 9 9 [OK] Client ID well-formed 10 10 [OK] Metadata document fetchable 11 + [OK] Metadata Content-Type is JSON 11 12 [OK] Metadata is valid JSON 12 13 == Metadata == 13 14 [OK] Metadata document deserializes 14 15 [OK] Metadata `client_id` matches fetched URL 15 16 [OK] `application_type` field is present 16 17 [OK] `application_type` is `web` or `native` 17 - [OK] `response_types` is `["code"]` 18 + [OK] `response_types` includes `code` 18 19 [OK] `grant_types` includes `authorization_code` 19 20 [OK] `dpop_bound_access_tokens` is `true` 20 21 [OK] `redirect_uris` is non-empty ··· 33 34 [OK] Keys use signing use 34 35 [OK] Algorithms are modern EC 35 36 36 - Summary: 23 passed, 0 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 0 37 + Summary: 24 passed, 0 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 0
+5 -4
tests/snapshots/oauth_client_discovery__https_not_json_produces_spec_violation.snap
··· 8 8 == Discovery == 9 9 [OK] Client ID well-formed 10 10 [OK] Metadata document fetchable 11 + [OK] Metadata Content-Type is JSON 11 12 [FAIL] Metadata is not valid JSON 12 13 oauth_client::discovery::metadata_is_json 13 14 14 - × response body is not valid JSON (content-type: <unknown>) 15 - ╭─[metadata document (content-type: <unknown>):1:2] 15 + × response body is not valid JSON (content-type: application/json) 16 + ╭─[metadata document (content-type: application/json):1:2] 16 17 1 │ not valid json at all 17 18 · ─ 18 19 ╰──── ··· 21 22 [SKIP] Metadata `client_id` matches fetched URL — blocked by oauth_client::discovery::metadata_document_fetchable 22 23 [SKIP] `application_type` field is present — blocked by oauth_client::discovery::metadata_document_fetchable 23 24 [SKIP] `application_type` is `web` or `native` — blocked by oauth_client::discovery::metadata_document_fetchable 24 - [SKIP] `response_types` is `["code"]` — blocked by oauth_client::discovery::metadata_document_fetchable 25 + [SKIP] `response_types` includes `code` — blocked by oauth_client::discovery::metadata_document_fetchable 25 26 [SKIP] `grant_types` includes `authorization_code` — blocked by oauth_client::discovery::metadata_document_fetchable 26 27 [SKIP] `dpop_bound_access_tokens` is `true` — blocked by oauth_client::discovery::metadata_document_fetchable 27 28 [SKIP] `redirect_uris` is non-empty — blocked by oauth_client::discovery::metadata_document_fetchable ··· 41 42 [SKIP] Keys use signing use — blocked by oauth_client::metadata::raw_document_deserializes 42 43 [SKIP] Algorithms are modern EC — blocked by oauth_client::metadata::raw_document_deserializes 43 44 44 - Summary: 2 passed, 1 failed (spec), 0 network errors, 0 advisories, 22 skipped. Exit code: 1 45 + Summary: 3 passed, 1 failed (spec), 0 network errors, 0 advisories, 22 skipped. Exit code: 1
+7 -2
tests/snapshots/oauth_client_discovery__https_not_json_with_content_type_produces_spec_violation_with_ct.snap
··· 8 8 == Discovery == 9 9 [OK] Client ID well-formed 10 10 [OK] Metadata document fetchable 11 + [FAIL] Metadata Content-Type is not JSON 12 + oauth_client::discovery::metadata_content_type_is_json 13 + 14 + × Metadata document at https://client.example.com/metadata.json returned Content-Type `text/html; charset=utf-8` (expected `application/json`) 15 + help: Serve the client-metadata document with `Content-Type: application/json` (charset parameters such as `; charset=utf-8` are permitted). 11 16 [FAIL] Metadata is not valid JSON 12 17 oauth_client::discovery::metadata_is_json 13 18 ··· 21 26 [SKIP] Metadata `client_id` matches fetched URL — blocked by oauth_client::discovery::metadata_document_fetchable 22 27 [SKIP] `application_type` field is present — blocked by oauth_client::discovery::metadata_document_fetchable 23 28 [SKIP] `application_type` is `web` or `native` — blocked by oauth_client::discovery::metadata_document_fetchable 24 - [SKIP] `response_types` is `["code"]` — blocked by oauth_client::discovery::metadata_document_fetchable 29 + [SKIP] `response_types` includes `code` — blocked by oauth_client::discovery::metadata_document_fetchable 25 30 [SKIP] `grant_types` includes `authorization_code` — blocked by oauth_client::discovery::metadata_document_fetchable 26 31 [SKIP] `dpop_bound_access_tokens` is `true` — blocked by oauth_client::discovery::metadata_document_fetchable 27 32 [SKIP] `redirect_uris` is non-empty — blocked by oauth_client::discovery::metadata_document_fetchable ··· 41 46 [SKIP] Keys use signing use — blocked by oauth_client::metadata::raw_document_deserializes 42 47 [SKIP] Algorithms are modern EC — blocked by oauth_client::metadata::raw_document_deserializes 43 48 44 - Summary: 2 passed, 1 failed (spec), 0 network errors, 0 advisories, 22 skipped. Exit code: 1 49 + Summary: 2 passed, 2 failed (spec), 0 network errors, 0 advisories, 22 skipped. Exit code: 1
+3 -2
tests/snapshots/oauth_client_discovery__https_unreachable_produces_network_error.snap
··· 11 11 oauth_client::discovery::metadata_document_fetchable 12 12 13 13 × Failed to fetch metadata from https://client.example.com/metadata.json: connection refused 14 + [SKIP] Metadata Content-Type is JSON — blocked by oauth_client::discovery::metadata_document_fetchable 14 15 [SKIP] Metadata is valid JSON — blocked by oauth_client::discovery::metadata_document_fetchable 15 16 == Metadata == 16 17 [SKIP] Metadata document deserializes — blocked by oauth_client::discovery::metadata_document_fetchable 17 18 [SKIP] Metadata `client_id` matches fetched URL — blocked by oauth_client::discovery::metadata_document_fetchable 18 19 [SKIP] `application_type` field is present — blocked by oauth_client::discovery::metadata_document_fetchable 19 20 [SKIP] `application_type` is `web` or `native` — blocked by oauth_client::discovery::metadata_document_fetchable 20 - [SKIP] `response_types` is `["code"]` — blocked by oauth_client::discovery::metadata_document_fetchable 21 + [SKIP] `response_types` includes `code` — blocked by oauth_client::discovery::metadata_document_fetchable 21 22 [SKIP] `grant_types` includes `authorization_code` — blocked by oauth_client::discovery::metadata_document_fetchable 22 23 [SKIP] `dpop_bound_access_tokens` is `true` — blocked by oauth_client::discovery::metadata_document_fetchable 23 24 [SKIP] `redirect_uris` is non-empty — blocked by oauth_client::discovery::metadata_document_fetchable ··· 37 38 [SKIP] Keys use signing use — blocked by oauth_client::metadata::raw_document_deserializes 38 39 [SKIP] Algorithms are modern EC — blocked by oauth_client::metadata::raw_document_deserializes 39 40 40 - Summary: 1 passed, 0 failed (spec), 1 network errors, 0 advisories, 23 skipped. Exit code: 2 41 + Summary: 1 passed, 0 failed (spec), 1 network errors, 0 advisories, 24 skipped. Exit code: 2
+3 -2
tests/snapshots/oauth_client_discovery__loopback_127_0_0_1.snap
··· 8 8 == Discovery == 9 9 [OK] Client ID well-formed 10 10 [SKIP] Metadata document fetchable — metadata is implicit for loopback clients 11 + [SKIP] Metadata Content-Type is JSON — metadata is implicit for loopback clients 11 12 [SKIP] Metadata is valid JSON — metadata is implicit for loopback clients 12 13 == Metadata == 13 14 [SKIP] Metadata document deserializes — metadata is implicit for loopback clients 14 15 [SKIP] Metadata `client_id` matches fetched URL — metadata is implicit for loopback clients 15 16 [SKIP] `application_type` field is present — metadata is implicit for loopback clients 16 17 [SKIP] `application_type` is `web` or `native` — metadata is implicit for loopback clients 17 - [SKIP] `response_types` is `["code"]` — metadata is implicit for loopback clients 18 + [SKIP] `response_types` includes `code` — metadata is implicit for loopback clients 18 19 [SKIP] `grant_types` includes `authorization_code` — metadata is implicit for loopback clients 19 20 [SKIP] `dpop_bound_access_tokens` is `true` — metadata is implicit for loopback clients 20 21 [SKIP] `redirect_uris` is non-empty — metadata is implicit for loopback clients ··· 34 35 [SKIP] Keys use signing use — jwks not applicable to loopback clients 35 36 [SKIP] Algorithms are modern EC — jwks not applicable to loopback clients 36 37 37 - Summary: 1 passed, 0 failed (spec), 0 network errors, 0 advisories, 24 skipped. Exit code: 0 38 + Summary: 1 passed, 0 failed (spec), 0 network errors, 0 advisories, 25 skipped. Exit code: 0
+3 -2
tests/snapshots/oauth_client_discovery__loopback_root_produces_skip_rows.snap
··· 8 8 == Discovery == 9 9 [OK] Client ID well-formed 10 10 [SKIP] Metadata document fetchable — metadata is implicit for loopback clients 11 + [SKIP] Metadata Content-Type is JSON — metadata is implicit for loopback clients 11 12 [SKIP] Metadata is valid JSON — metadata is implicit for loopback clients 12 13 == Metadata == 13 14 [SKIP] Metadata document deserializes — metadata is implicit for loopback clients 14 15 [SKIP] Metadata `client_id` matches fetched URL — metadata is implicit for loopback clients 15 16 [SKIP] `application_type` field is present — metadata is implicit for loopback clients 16 17 [SKIP] `application_type` is `web` or `native` — metadata is implicit for loopback clients 17 - [SKIP] `response_types` is `["code"]` — metadata is implicit for loopback clients 18 + [SKIP] `response_types` includes `code` — metadata is implicit for loopback clients 18 19 [SKIP] `grant_types` includes `authorization_code` — metadata is implicit for loopback clients 19 20 [SKIP] `dpop_bound_access_tokens` is `true` — metadata is implicit for loopback clients 20 21 [SKIP] `redirect_uris` is non-empty — metadata is implicit for loopback clients ··· 34 35 [SKIP] Keys use signing use — jwks not applicable to loopback clients 35 36 [SKIP] Algorithms are modern EC — jwks not applicable to loopback clients 36 37 37 - Summary: 1 passed, 0 failed (spec), 0 network errors, 0 advisories, 24 skipped. Exit code: 0 38 + Summary: 1 passed, 0 failed (spec), 0 network errors, 0 advisories, 25 skipped. Exit code: 0
+3 -2
tests/snapshots/oauth_client_discovery__loopback_with_port_produces_same_skip_rows.snap
··· 8 8 == Discovery == 9 9 [OK] Client ID well-formed 10 10 [SKIP] Metadata document fetchable — metadata is implicit for loopback clients 11 + [SKIP] Metadata Content-Type is JSON — metadata is implicit for loopback clients 11 12 [SKIP] Metadata is valid JSON — metadata is implicit for loopback clients 12 13 == Metadata == 13 14 [SKIP] Metadata document deserializes — metadata is implicit for loopback clients 14 15 [SKIP] Metadata `client_id` matches fetched URL — metadata is implicit for loopback clients 15 16 [SKIP] `application_type` field is present — metadata is implicit for loopback clients 16 17 [SKIP] `application_type` is `web` or `native` — metadata is implicit for loopback clients 17 - [SKIP] `response_types` is `["code"]` — metadata is implicit for loopback clients 18 + [SKIP] `response_types` includes `code` — metadata is implicit for loopback clients 18 19 [SKIP] `grant_types` includes `authorization_code` — metadata is implicit for loopback clients 19 20 [SKIP] `dpop_bound_access_tokens` is `true` — metadata is implicit for loopback clients 20 21 [SKIP] `redirect_uris` is non-empty — metadata is implicit for loopback clients ··· 34 35 [SKIP] Keys use signing use — jwks not applicable to loopback clients 35 36 [SKIP] Algorithms are modern EC — jwks not applicable to loopback clients 36 37 37 - Summary: 1 passed, 0 failed (spec), 0 network errors, 0 advisories, 24 skipped. Exit code: 0 38 + Summary: 1 passed, 0 failed (spec), 0 network errors, 0 advisories, 25 skipped. Exit code: 0
+3 -2
tests/snapshots/oauth_client_endtoend__full_pipeline_all_pass.snap
··· 8 8 == Discovery == 9 9 [OK] Client ID well-formed 10 10 [OK] Metadata document fetchable 11 + [OK] Metadata Content-Type is JSON 11 12 [OK] Metadata is valid JSON 12 13 == Metadata == 13 14 [OK] Metadata document deserializes 14 15 [OK] Metadata `client_id` matches fetched URL 15 16 [OK] `application_type` field is present 16 17 [OK] `application_type` is `web` or `native` 17 - [OK] `response_types` is `["code"]` 18 + [OK] `response_types` includes `code` 18 19 [OK] `grant_types` includes `authorization_code` 19 20 [OK] `dpop_bound_access_tokens` is `true` 20 21 [OK] `redirect_uris` is non-empty ··· 33 34 [OK] Keys use signing use 34 35 [OK] Algorithms are modern EC 35 36 36 - Summary: 23 passed, 0 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 0 37 + Summary: 24 passed, 0 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 0
+3 -2
tests/snapshots/oauth_client_jwks__discovery_failure_blocks_jwks.snap
··· 11 11 oauth_client::discovery::metadata_document_fetchable 12 12 13 13 × Metadata document fetch returned HTTP 404: https://client.example.com/metadata.json 14 + [SKIP] Metadata Content-Type is JSON — blocked by oauth_client::discovery::metadata_document_fetchable 14 15 [SKIP] Metadata is valid JSON — blocked by oauth_client::discovery::metadata_document_fetchable 15 16 == Metadata == 16 17 [SKIP] Metadata document deserializes — blocked by oauth_client::discovery::metadata_document_fetchable 17 18 [SKIP] Metadata `client_id` matches fetched URL — blocked by oauth_client::discovery::metadata_document_fetchable 18 19 [SKIP] `application_type` field is present — blocked by oauth_client::discovery::metadata_document_fetchable 19 20 [SKIP] `application_type` is `web` or `native` — blocked by oauth_client::discovery::metadata_document_fetchable 20 - [SKIP] `response_types` is `["code"]` — blocked by oauth_client::discovery::metadata_document_fetchable 21 + [SKIP] `response_types` includes `code` — blocked by oauth_client::discovery::metadata_document_fetchable 21 22 [SKIP] `grant_types` includes `authorization_code` — blocked by oauth_client::discovery::metadata_document_fetchable 22 23 [SKIP] `dpop_bound_access_tokens` is `true` — blocked by oauth_client::discovery::metadata_document_fetchable 23 24 [SKIP] `redirect_uris` is non-empty — blocked by oauth_client::discovery::metadata_document_fetchable ··· 37 38 [SKIP] Keys use signing use — blocked by oauth_client::metadata::raw_document_deserializes 38 39 [SKIP] Algorithms are modern EC — blocked by oauth_client::metadata::raw_document_deserializes 39 40 40 - Summary: 1 passed, 0 failed (spec), 1 network errors, 0 advisories, 23 skipped. Exit code: 2 41 + Summary: 1 passed, 0 failed (spec), 1 network errors, 0 advisories, 24 skipped. Exit code: 2
+3 -2
tests/snapshots/oauth_client_jwks__duplicate_kids_produces_spec_violation.snap
··· 8 8 == Discovery == 9 9 [OK] Client ID well-formed 10 10 [OK] Metadata document fetchable 11 + [OK] Metadata Content-Type is JSON 11 12 [OK] Metadata is valid JSON 12 13 == Metadata == 13 14 [OK] Metadata document deserializes 14 15 [OK] Metadata `client_id` matches fetched URL 15 16 [OK] `application_type` field is present 16 17 [OK] `application_type` is `web` or `native` 17 - [OK] `response_types` is `["code"]` 18 + [OK] `response_types` includes `code` 18 19 [OK] `grant_types` includes `authorization_code` 19 20 [OK] `dpop_bound_access_tokens` is `true` 20 21 [OK] `redirect_uris` is non-empty ··· 42 43 [OK] Keys use signing use 43 44 [OK] Algorithms are modern EC 44 45 45 - Summary: 22 passed, 1 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 1 46 + Summary: 23 passed, 1 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 1
+3 -2
tests/snapshots/oauth_client_jwks__inline_es256_happy_jwks_passes.snap
··· 8 8 == Discovery == 9 9 [OK] Client ID well-formed 10 10 [OK] Metadata document fetchable 11 + [OK] Metadata Content-Type is JSON 11 12 [OK] Metadata is valid JSON 12 13 == Metadata == 13 14 [OK] Metadata document deserializes 14 15 [OK] Metadata `client_id` matches fetched URL 15 16 [OK] `application_type` field is present 16 17 [OK] `application_type` is `web` or `native` 17 - [OK] `response_types` is `["code"]` 18 + [OK] `response_types` includes `code` 18 19 [OK] `grant_types` includes `authorization_code` 19 20 [OK] `dpop_bound_access_tokens` is `true` 20 21 [OK] `redirect_uris` is non-empty ··· 33 34 [OK] Keys use signing use 34 35 [OK] Algorithms are modern EC 35 36 36 - Summary: 23 passed, 0 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 0 37 + Summary: 24 passed, 0 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 0
+3 -2
tests/snapshots/oauth_client_jwks__loopback_skips_all_jwks.snap
··· 8 8 == Discovery == 9 9 [OK] Client ID well-formed 10 10 [SKIP] Metadata document fetchable — metadata is implicit for loopback clients 11 + [SKIP] Metadata Content-Type is JSON — metadata is implicit for loopback clients 11 12 [SKIP] Metadata is valid JSON — metadata is implicit for loopback clients 12 13 == Metadata == 13 14 [SKIP] Metadata document deserializes — metadata is implicit for loopback clients 14 15 [SKIP] Metadata `client_id` matches fetched URL — metadata is implicit for loopback clients 15 16 [SKIP] `application_type` field is present — metadata is implicit for loopback clients 16 17 [SKIP] `application_type` is `web` or `native` — metadata is implicit for loopback clients 17 - [SKIP] `response_types` is `["code"]` — metadata is implicit for loopback clients 18 + [SKIP] `response_types` includes `code` — metadata is implicit for loopback clients 18 19 [SKIP] `grant_types` includes `authorization_code` — metadata is implicit for loopback clients 19 20 [SKIP] `dpop_bound_access_tokens` is `true` — metadata is implicit for loopback clients 20 21 [SKIP] `redirect_uris` is non-empty — metadata is implicit for loopback clients ··· 34 35 [SKIP] Keys use signing use — jwks not applicable to loopback clients 35 36 [SKIP] Algorithms are modern EC — jwks not applicable to loopback clients 36 37 37 - Summary: 1 passed, 0 failed (spec), 0 network errors, 0 advisories, 24 skipped. Exit code: 0 38 + Summary: 1 passed, 0 failed (spec), 0 network errors, 0 advisories, 25 skipped. Exit code: 0
+3 -2
tests/snapshots/oauth_client_jwks__public_client_skips_all_jwks.snap
··· 8 8 == Discovery == 9 9 [OK] Client ID well-formed 10 10 [OK] Metadata document fetchable 11 + [OK] Metadata Content-Type is JSON 11 12 [OK] Metadata is valid JSON 12 13 == Metadata == 13 14 [OK] Metadata document deserializes 14 15 [OK] Metadata `client_id` matches fetched URL 15 16 [OK] `application_type` field is present 16 17 [OK] `application_type` is `web` or `native` 17 - [OK] `response_types` is `["code"]` 18 + [OK] `response_types` includes `code` 18 19 [OK] `grant_types` includes `authorization_code` 19 20 [OK] `dpop_bound_access_tokens` is `true` 20 21 [OK] `redirect_uris` is non-empty ··· 33 34 [SKIP] Keys use signing use — jwks not required for public clients 34 35 [SKIP] Algorithms are modern EC — jwks not required for public clients 35 36 36 - Summary: 17 passed, 0 failed (spec), 0 network errors, 0 advisories, 7 skipped. Exit code: 0 37 + Summary: 18 passed, 0 failed (spec), 0 network errors, 0 advisories, 7 skipped. Exit code: 0
+3 -2
tests/snapshots/oauth_client_jwks__signing_alg_has_no_compatible_key_produces_spec_violation.snap
··· 8 8 == Discovery == 9 9 [OK] Client ID well-formed 10 10 [OK] Metadata document fetchable 11 + [OK] Metadata Content-Type is JSON 11 12 [OK] Metadata is valid JSON 12 13 == Metadata == 13 14 [OK] Metadata document deserializes 14 15 [OK] Metadata `client_id` matches fetched URL 15 16 [OK] `application_type` field is present 16 17 [OK] `application_type` is `web` or `native` 17 - [OK] `response_types` is `["code"]` 18 + [OK] `response_types` includes `code` 18 19 [OK] `grant_types` includes `authorization_code` 19 20 [OK] `dpop_bound_access_tokens` is `true` 20 21 [OK] `redirect_uris` is non-empty ··· 36 37 [OK] Keys use signing use 37 38 [OK] Algorithms are modern EC 38 39 39 - Summary: 22 passed, 1 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 1 40 + Summary: 23 passed, 1 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 1
+3 -2
tests/snapshots/oauth_client_jwks__uri_es256_happy_jwks_passes.snap
··· 8 8 == Discovery == 9 9 [OK] Client ID well-formed 10 10 [OK] Metadata document fetchable 11 + [OK] Metadata Content-Type is JSON 11 12 [OK] Metadata is valid JSON 12 13 == Metadata == 13 14 [OK] Metadata document deserializes 14 15 [OK] Metadata `client_id` matches fetched URL 15 16 [OK] `application_type` field is present 16 17 [OK] `application_type` is `web` or `native` 17 - [OK] `response_types` is `["code"]` 18 + [OK] `response_types` includes `code` 18 19 [OK] `grant_types` includes `authorization_code` 19 20 [OK] `dpop_bound_access_tokens` is `true` 20 21 [OK] `redirect_uris` is non-empty ··· 33 34 [OK] Keys use signing use 34 35 [OK] Algorithms are modern EC 35 36 36 - Summary: 24 passed, 0 failed (spec), 0 network errors, 0 advisories, 0 skipped. Exit code: 0 37 + Summary: 25 passed, 0 failed (spec), 0 network errors, 0 advisories, 0 skipped. Exit code: 0
+3 -2
tests/snapshots/oauth_client_jwks__uri_invalid_json_produces_spec_violation.snap
··· 8 8 == Discovery == 9 9 [OK] Client ID well-formed 10 10 [OK] Metadata document fetchable 11 + [OK] Metadata Content-Type is JSON 11 12 [OK] Metadata is valid JSON 12 13 == Metadata == 13 14 [OK] Metadata document deserializes 14 15 [OK] Metadata `client_id` matches fetched URL 15 16 [OK] `application_type` field is present 16 17 [OK] `application_type` is `web` or `native` 17 - [OK] `response_types` is `["code"]` 18 + [OK] `response_types` includes `code` 18 19 [OK] `grant_types` includes `authorization_code` 19 20 [OK] `dpop_bound_access_tokens` is `true` 20 21 [OK] `redirect_uris` is non-empty ··· 36 37 [SKIP] Keys use signing use — blocked by oauth_client::jws::jwks_is_json 37 38 [SKIP] Algorithms are modern EC — blocked by oauth_client::jws::jwks_is_json 38 39 39 - Summary: 19 passed, 1 failed (spec), 0 network errors, 0 advisories, 4 skipped. Exit code: 1 40 + Summary: 20 passed, 1 failed (spec), 0 network errors, 0 advisories, 4 skipped. Exit code: 1
+3 -2
tests/snapshots/oauth_client_jwks__uri_returns_404_produces_network_error.snap
··· 8 8 == Discovery == 9 9 [OK] Client ID well-formed 10 10 [OK] Metadata document fetchable 11 + [OK] Metadata Content-Type is JSON 11 12 [OK] Metadata is valid JSON 12 13 == Metadata == 13 14 [OK] Metadata document deserializes 14 15 [OK] Metadata `client_id` matches fetched URL 15 16 [OK] `application_type` field is present 16 17 [OK] `application_type` is `web` or `native` 17 - [OK] `response_types` is `["code"]` 18 + [OK] `response_types` includes `code` 18 19 [OK] `grant_types` includes `authorization_code` 19 20 [OK] `dpop_bound_access_tokens` is `true` 20 21 [OK] `redirect_uris` is non-empty ··· 36 37 [SKIP] Keys use signing use — blocked by oauth_client::jws::jwks_uri_fetchable 37 38 [SKIP] Algorithms are modern EC — blocked by oauth_client::jws::jwks_uri_fetchable 38 39 39 - Summary: 18 passed, 0 failed (spec), 1 network errors, 0 advisories, 5 skipped. Exit code: 2 40 + Summary: 19 passed, 0 failed (spec), 1 network errors, 0 advisories, 5 skipped. Exit code: 2
+3 -2
tests/snapshots/oauth_client_jwks__uri_unreachable_produces_network_error.snap
··· 8 8 == Discovery == 9 9 [OK] Client ID well-formed 10 10 [OK] Metadata document fetchable 11 + [OK] Metadata Content-Type is JSON 11 12 [OK] Metadata is valid JSON 12 13 == Metadata == 13 14 [OK] Metadata document deserializes 14 15 [OK] Metadata `client_id` matches fetched URL 15 16 [OK] `application_type` field is present 16 17 [OK] `application_type` is `web` or `native` 17 - [OK] `response_types` is `["code"]` 18 + [OK] `response_types` includes `code` 18 19 [OK] `grant_types` includes `authorization_code` 19 20 [OK] `dpop_bound_access_tokens` is `true` 20 21 [OK] `redirect_uris` is non-empty ··· 36 37 [SKIP] Keys use signing use — blocked by oauth_client::jws::jwks_uri_fetchable 37 38 [SKIP] Algorithms are modern EC — blocked by oauth_client::jws::jwks_uri_fetchable 38 39 39 - Summary: 18 passed, 0 failed (spec), 1 network errors, 0 advisories, 5 skipped. Exit code: 2 40 + Summary: 19 passed, 0 failed (spec), 1 network errors, 0 advisories, 5 skipped. Exit code: 2
+3 -2
tests/snapshots/oauth_client_jwks__weak_alg_rs1_produces_spec_violation.snap
··· 8 8 == Discovery == 9 9 [OK] Client ID well-formed 10 10 [OK] Metadata document fetchable 11 + [OK] Metadata Content-Type is JSON 11 12 [OK] Metadata is valid JSON 12 13 == Metadata == 13 14 [OK] Metadata document deserializes 14 15 [OK] Metadata `client_id` matches fetched URL 15 16 [OK] `application_type` field is present 16 17 [OK] `application_type` is `web` or `native` 17 - [OK] `response_types` is `["code"]` 18 + [OK] `response_types` includes `code` 18 19 [OK] `grant_types` includes `authorization_code` 19 20 [OK] `dpop_bound_access_tokens` is `true` 20 21 [OK] `redirect_uris` is non-empty ··· 39 40 40 41 × One or more keys declare non-modern algorithms 41 42 42 - Summary: 21 passed, 2 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 1 43 + Summary: 22 passed, 2 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 1
+3 -2
tests/snapshots/oauth_client_jwks__wrong_use_produces_spec_violation.snap
··· 8 8 == Discovery == 9 9 [OK] Client ID well-formed 10 10 [OK] Metadata document fetchable 11 + [OK] Metadata Content-Type is JSON 11 12 [OK] Metadata is valid JSON 12 13 == Metadata == 13 14 [OK] Metadata document deserializes 14 15 [OK] Metadata `client_id` matches fetched URL 15 16 [OK] `application_type` field is present 16 17 [OK] `application_type` is `web` or `native` 17 - [OK] `response_types` is `["code"]` 18 + [OK] `response_types` includes `code` 18 19 [OK] `grant_types` includes `authorization_code` 19 20 [OK] `dpop_bound_access_tokens` is `true` 20 21 [OK] `redirect_uris` is non-empty ··· 36 37 × One or more keys have `use` other than `sig` 37 38 [OK] Algorithms are modern EC 38 39 39 - Summary: 22 passed, 1 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 1 40 + Summary: 23 passed, 1 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 1
+3 -2
tests/snapshots/oauth_client_metadata__application_type_omitted_defaults_to_web.snap
··· 8 8 == Discovery == 9 9 [OK] Client ID well-formed 10 10 [OK] Metadata document fetchable 11 + [OK] Metadata Content-Type is JSON 11 12 [OK] Metadata is valid JSON 12 13 == Metadata == 13 14 [OK] Metadata document deserializes 14 15 [OK] Metadata `client_id` matches fetched URL 15 16 [SKIP] `application_type` field is present — `application_type` is optional; atproto defaults to `web` 16 17 [OK] `application_type` is `web` or `native` 17 - [OK] `response_types` is `["code"]` 18 + [OK] `response_types` includes `code` 18 19 [OK] `grant_types` includes `authorization_code` 19 20 [OK] `dpop_bound_access_tokens` is `true` 20 21 [OK] `redirect_uris` is non-empty ··· 33 34 [SKIP] Keys use signing use — jwks not required for public clients 34 35 [SKIP] Algorithms are modern EC — jwks not required for public clients 35 36 36 - Summary: 16 passed, 0 failed (spec), 0 network errors, 0 advisories, 8 skipped. Exit code: 0 37 + Summary: 17 passed, 0 failed (spec), 0 network errors, 0 advisories, 8 skipped. Exit code: 0
+3 -2
tests/snapshots/oauth_client_metadata__confidential_happy.snap
··· 8 8 == Discovery == 9 9 [OK] Client ID well-formed 10 10 [OK] Metadata document fetchable 11 + [OK] Metadata Content-Type is JSON 11 12 [OK] Metadata is valid JSON 12 13 == Metadata == 13 14 [OK] Metadata document deserializes 14 15 [OK] Metadata `client_id` matches fetched URL 15 16 [OK] `application_type` field is present 16 17 [OK] `application_type` is `web` or `native` 17 - [OK] `response_types` is `["code"]` 18 + [OK] `response_types` includes `code` 18 19 [OK] `grant_types` includes `authorization_code` 19 20 [OK] `dpop_bound_access_tokens` is `true` 20 21 [OK] `redirect_uris` is non-empty ··· 33 34 [OK] Keys use signing use 34 35 [OK] Algorithms are modern EC 35 36 36 - Summary: 23 passed, 0 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 0 37 + Summary: 24 passed, 0 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 0
+3 -2
tests/snapshots/oauth_client_metadata__confidential_missing_jwks.snap
··· 8 8 == Discovery == 9 9 [OK] Client ID well-formed 10 10 [OK] Metadata document fetchable 11 + [OK] Metadata Content-Type is JSON 11 12 [OK] Metadata is valid JSON 12 13 == Metadata == 13 14 [OK] Metadata document deserializes 14 15 [OK] Metadata `client_id` matches fetched URL 15 16 [OK] `application_type` field is present 16 17 [OK] `application_type` is `web` or `native` 17 - [OK] `response_types` is `["code"]` 18 + [OK] `response_types` includes `code` 18 19 [OK] `grant_types` includes `authorization_code` 19 20 [OK] `dpop_bound_access_tokens` is `true` 20 21 [OK] `redirect_uris` is non-empty ··· 36 37 [SKIP] Keys use signing use — blocked by oauth_client::metadata::confidential_requires_jwks 37 38 [SKIP] Algorithms are modern EC — blocked by oauth_client::metadata::confidential_requires_jwks 38 39 39 - Summary: 16 passed, 1 failed (spec), 0 network errors, 0 advisories, 7 skipped. Exit code: 1 40 + Summary: 17 passed, 1 failed (spec), 0 network errors, 0 advisories, 7 skipped. Exit code: 1
+3 -2
tests/snapshots/oauth_client_metadata__discovery_failure_blocks_metadata.snap
··· 11 11 oauth_client::discovery::metadata_document_fetchable 12 12 13 13 × Metadata document fetch returned HTTP 404: https://client.example.com/metadata.json 14 + [SKIP] Metadata Content-Type is JSON — blocked by oauth_client::discovery::metadata_document_fetchable 14 15 [SKIP] Metadata is valid JSON — blocked by oauth_client::discovery::metadata_document_fetchable 15 16 == Metadata == 16 17 [SKIP] Metadata document deserializes — blocked by oauth_client::discovery::metadata_document_fetchable 17 18 [SKIP] Metadata `client_id` matches fetched URL — blocked by oauth_client::discovery::metadata_document_fetchable 18 19 [SKIP] `application_type` field is present — blocked by oauth_client::discovery::metadata_document_fetchable 19 20 [SKIP] `application_type` is `web` or `native` — blocked by oauth_client::discovery::metadata_document_fetchable 20 - [SKIP] `response_types` is `["code"]` — blocked by oauth_client::discovery::metadata_document_fetchable 21 + [SKIP] `response_types` includes `code` — blocked by oauth_client::discovery::metadata_document_fetchable 21 22 [SKIP] `grant_types` includes `authorization_code` — blocked by oauth_client::discovery::metadata_document_fetchable 22 23 [SKIP] `dpop_bound_access_tokens` is `true` — blocked by oauth_client::discovery::metadata_document_fetchable 23 24 [SKIP] `redirect_uris` is non-empty — blocked by oauth_client::discovery::metadata_document_fetchable ··· 37 38 [SKIP] Keys use signing use — blocked by oauth_client::metadata::raw_document_deserializes 38 39 [SKIP] Algorithms are modern EC — blocked by oauth_client::metadata::raw_document_deserializes 39 40 40 - Summary: 1 passed, 0 failed (spec), 1 network errors, 0 advisories, 23 skipped. Exit code: 2 41 + Summary: 1 passed, 0 failed (spec), 1 network errors, 0 advisories, 24 skipped. Exit code: 2
+3 -2
tests/snapshots/oauth_client_metadata__dpop_bound_false.snap
··· 8 8 == Discovery == 9 9 [OK] Client ID well-formed 10 10 [OK] Metadata document fetchable 11 + [OK] Metadata Content-Type is JSON 11 12 [OK] Metadata is valid JSON 12 13 == Metadata == 13 14 [OK] Metadata document deserializes 14 15 [OK] Metadata `client_id` matches fetched URL 15 16 [OK] `application_type` field is present 16 17 [OK] `application_type` is `web` or `native` 17 - [OK] `response_types` is `["code"]` 18 + [OK] `response_types` includes `code` 18 19 [OK] `grant_types` includes `authorization_code` 19 20 [FAIL] `dpop_bound_access_tokens` is `true` 20 21 oauth_client::metadata::dpop_bound_required ··· 36 37 [OK] Keys use signing use 37 38 [OK] Algorithms are modern EC 38 39 39 - Summary: 22 passed, 1 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 1 40 + Summary: 23 passed, 1 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 1
+3 -2
tests/snapshots/oauth_client_metadata__loopback_skips_all_metadata_checks.snap
··· 8 8 == Discovery == 9 9 [OK] Client ID well-formed 10 10 [SKIP] Metadata document fetchable — metadata is implicit for loopback clients 11 + [SKIP] Metadata Content-Type is JSON — metadata is implicit for loopback clients 11 12 [SKIP] Metadata is valid JSON — metadata is implicit for loopback clients 12 13 == Metadata == 13 14 [SKIP] Metadata document deserializes — metadata is implicit for loopback clients 14 15 [SKIP] Metadata `client_id` matches fetched URL — metadata is implicit for loopback clients 15 16 [SKIP] `application_type` field is present — metadata is implicit for loopback clients 16 17 [SKIP] `application_type` is `web` or `native` — metadata is implicit for loopback clients 17 - [SKIP] `response_types` is `["code"]` — metadata is implicit for loopback clients 18 + [SKIP] `response_types` includes `code` — metadata is implicit for loopback clients 18 19 [SKIP] `grant_types` includes `authorization_code` — metadata is implicit for loopback clients 19 20 [SKIP] `dpop_bound_access_tokens` is `true` — metadata is implicit for loopback clients 20 21 [SKIP] `redirect_uris` is non-empty — metadata is implicit for loopback clients ··· 34 35 [SKIP] Keys use signing use — jwks not applicable to loopback clients 35 36 [SKIP] Algorithms are modern EC — jwks not applicable to loopback clients 36 37 37 - Summary: 1 passed, 0 failed (spec), 0 network errors, 0 advisories, 24 skipped. Exit code: 0 38 + Summary: 1 passed, 0 failed (spec), 0 network errors, 0 advisories, 25 skipped. Exit code: 0
+42
tests/snapshots/oauth_client_metadata__metadata_non_200_2xx_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 + [FAIL] Metadata document fetch failed 11 + oauth_client::discovery::metadata_document_fetchable 12 + 13 + × Metadata document at https://client.example.com/metadata.json returned HTTP 201 (expected exactly 200) 14 + help: The atproto OAuth profile requires the metadata document to be served with HTTP status 200 exactly. Redirects and other 2xx statuses (201, 204, …) are not allowed. 15 + [SKIP] Metadata Content-Type is JSON — blocked by oauth_client::discovery::metadata_document_fetchable 16 + [SKIP] Metadata is valid JSON — blocked by oauth_client::discovery::metadata_document_fetchable 17 + == Metadata == 18 + [SKIP] Metadata document deserializes — blocked by oauth_client::discovery::metadata_document_fetchable 19 + [SKIP] Metadata `client_id` matches fetched URL — blocked by oauth_client::discovery::metadata_document_fetchable 20 + [SKIP] `application_type` field is present — blocked by oauth_client::discovery::metadata_document_fetchable 21 + [SKIP] `application_type` is `web` or `native` — blocked by oauth_client::discovery::metadata_document_fetchable 22 + [SKIP] `response_types` includes `code` — blocked by oauth_client::discovery::metadata_document_fetchable 23 + [SKIP] `grant_types` includes `authorization_code` — blocked by oauth_client::discovery::metadata_document_fetchable 24 + [SKIP] `dpop_bound_access_tokens` is `true` — blocked by oauth_client::discovery::metadata_document_fetchable 25 + [SKIP] `redirect_uris` is non-empty — blocked by oauth_client::discovery::metadata_document_fetchable 26 + [SKIP] Every `redirect_uri` has the right shape for the client kind — blocked by oauth_client::discovery::metadata_document_fetchable 27 + [SKIP] `token_endpoint_auth_method` matches client kind — blocked by oauth_client::discovery::metadata_document_fetchable 28 + [SKIP] Confidential client provides exactly one of `jwks`/`jwks_uri` — blocked by oauth_client::discovery::metadata_document_fetchable 29 + [SKIP] Public/native client does not provide `jwks` or `jwks_uri` — blocked by oauth_client::discovery::metadata_document_fetchable 30 + [SKIP] `scope` field is present — blocked by oauth_client::discovery::metadata_document_fetchable 31 + [SKIP] `scope` includes the `atproto` token — blocked by oauth_client::discovery::metadata_document_fetchable 32 + [SKIP] `scope` parses against the atproto permission grammar — blocked by oauth_client::discovery::metadata_document_fetchable 33 + == JWKS == 34 + [SKIP] JWKS is present — blocked by oauth_client::metadata::raw_document_deserializes 35 + [SKIP] JWKS URI is fetchable — blocked by oauth_client::metadata::raw_document_deserializes 36 + [SKIP] JWKS is valid JSON — blocked by oauth_client::metadata::raw_document_deserializes 37 + [SKIP] Keys have unique kid values — blocked by oauth_client::metadata::raw_document_deserializes 38 + [SKIP] JWKS contains a key compatible with `token_endpoint_auth_signing_alg` — blocked by oauth_client::metadata::raw_document_deserializes 39 + [SKIP] Keys use signing use — blocked by oauth_client::metadata::raw_document_deserializes 40 + [SKIP] Algorithms are modern EC — blocked by oauth_client::metadata::raw_document_deserializes 41 + 42 + Summary: 1 passed, 1 failed (spec), 0 network errors, 0 advisories, 24 skipped. Exit code: 1
+41
tests/snapshots/oauth_client_metadata__metadata_non_json_content_type_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 + [FAIL] Metadata Content-Type is not JSON 12 + oauth_client::discovery::metadata_content_type_is_json 13 + 14 + × Metadata document at https://client.example.com/metadata.json returned Content-Type `text/html; charset=utf-8` (expected `application/json`) 15 + help: Serve the client-metadata document with `Content-Type: application/json` (charset parameters such as `; charset=utf-8` are permitted). 16 + [OK] Metadata is valid JSON 17 + == Metadata == 18 + [OK] Metadata document deserializes 19 + [OK] Metadata `client_id` matches fetched URL 20 + [OK] `application_type` field is present 21 + [OK] `application_type` is `web` or `native` 22 + [OK] `response_types` includes `code` 23 + [OK] `grant_types` includes `authorization_code` 24 + [OK] `dpop_bound_access_tokens` is `true` 25 + [OK] `redirect_uris` is non-empty 26 + [OK] Every `redirect_uri` has the right shape for the client kind 27 + [OK] `token_endpoint_auth_method` matches client kind 28 + [OK] Confidential client provides exactly one of `jwks`/`jwks_uri` 29 + [OK] `scope` field is present 30 + [OK] `scope` includes the `atproto` token 31 + [OK] `scope` parses against the atproto permission grammar 32 + == JWKS == 33 + [OK] JWKS is present 34 + [SKIP] JWKS URI is fetchable — jwks is inline 35 + [OK] JWKS is valid JSON 36 + [OK] Keys have unique kid values 37 + [OK] JWKS contains a key compatible with `token_endpoint_auth_signing_alg` 38 + [OK] Keys use signing use 39 + [OK] Algorithms are modern EC 40 + 41 + Summary: 23 passed, 1 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 1
+3 -2
tests/snapshots/oauth_client_metadata__native_happy.snap
··· 8 8 == Discovery == 9 9 [OK] Client ID well-formed 10 10 [OK] Metadata document fetchable 11 + [OK] Metadata Content-Type is JSON 11 12 [OK] Metadata is valid JSON 12 13 == Metadata == 13 14 [OK] Metadata document deserializes 14 15 [OK] Metadata `client_id` matches fetched URL 15 16 [OK] `application_type` field is present 16 17 [OK] `application_type` is `web` or `native` 17 - [OK] `response_types` is `["code"]` 18 + [OK] `response_types` includes `code` 18 19 [OK] `grant_types` includes `authorization_code` 19 20 [OK] `dpop_bound_access_tokens` is `true` 20 21 [OK] `redirect_uris` is non-empty ··· 33 34 [SKIP] Keys use signing use — jwks not required for native clients 34 35 [SKIP] Algorithms are modern EC — jwks not required for native clients 35 36 36 - Summary: 17 passed, 0 failed (spec), 0 network errors, 0 advisories, 7 skipped. Exit code: 0 37 + Summary: 18 passed, 0 failed (spec), 0 network errors, 0 advisories, 7 skipped. Exit code: 0
+3 -2
tests/snapshots/oauth_client_metadata__native_redirect_scheme_mismatch.snap
··· 8 8 == Discovery == 9 9 [OK] Client ID well-formed 10 10 [OK] Metadata document fetchable 11 + [OK] Metadata Content-Type is JSON 11 12 [OK] Metadata is valid JSON 12 13 == Metadata == 13 14 [OK] Metadata document deserializes 14 15 [OK] Metadata `client_id` matches fetched URL 15 16 [OK] `application_type` field is present 16 17 [OK] `application_type` is `web` or `native` 17 - [OK] `response_types` is `["code"]` 18 + [OK] `response_types` includes `code` 18 19 [OK] `grant_types` includes `authorization_code` 19 20 [OK] `dpop_bound_access_tokens` is `true` 20 21 [OK] `redirect_uris` is non-empty ··· 36 37 [SKIP] Keys use signing use — jwks not required for native clients 37 38 [SKIP] Algorithms are modern EC — jwks not required for native clients 38 39 39 - Summary: 16 passed, 1 failed (spec), 0 network errors, 0 advisories, 7 skipped. Exit code: 1 40 + Summary: 17 passed, 1 failed (spec), 0 network errors, 0 advisories, 7 skipped. Exit code: 1
+3 -2
tests/snapshots/oauth_client_metadata__public_happy.snap
··· 8 8 == Discovery == 9 9 [OK] Client ID well-formed 10 10 [OK] Metadata document fetchable 11 + [OK] Metadata Content-Type is JSON 11 12 [OK] Metadata is valid JSON 12 13 == Metadata == 13 14 [OK] Metadata document deserializes 14 15 [OK] Metadata `client_id` matches fetched URL 15 16 [OK] `application_type` field is present 16 17 [OK] `application_type` is `web` or `native` 17 - [OK] `response_types` is `["code"]` 18 + [OK] `response_types` includes `code` 18 19 [OK] `grant_types` includes `authorization_code` 19 20 [OK] `dpop_bound_access_tokens` is `true` 20 21 [OK] `redirect_uris` is non-empty ··· 33 34 [SKIP] Keys use signing use — jwks not required for public clients 34 35 [SKIP] Algorithms are modern EC — jwks not required for public clients 35 36 36 - Summary: 17 passed, 0 failed (spec), 0 network errors, 0 advisories, 7 skipped. Exit code: 0 37 + Summary: 18 passed, 0 failed (spec), 0 network errors, 0 advisories, 7 skipped. Exit code: 0
+3 -2
tests/snapshots/oauth_client_metadata__public_with_token_endpoint_auth.snap
··· 8 8 == Discovery == 9 9 [OK] Client ID well-formed 10 10 [OK] Metadata document fetchable 11 + [OK] Metadata Content-Type is JSON 11 12 [OK] Metadata is valid JSON 12 13 == Metadata == 13 14 [OK] Metadata document deserializes 14 15 [OK] Metadata `client_id` matches fetched URL 15 16 [OK] `application_type` field is present 16 17 [OK] `application_type` is `web` or `native` 17 - [OK] `response_types` is `["code"]` 18 + [OK] `response_types` includes `code` 18 19 [OK] `grant_types` includes `authorization_code` 19 20 [OK] `dpop_bound_access_tokens` is `true` 20 21 [OK] `redirect_uris` is non-empty ··· 36 37 [SKIP] Keys use signing use — jwks not required for public clients 37 38 [SKIP] Algorithms are modern EC — jwks not required for public clients 38 39 39 - Summary: 16 passed, 1 failed (spec), 0 network errors, 0 advisories, 7 skipped. Exit code: 1 40 + Summary: 17 passed, 1 failed (spec), 0 network errors, 0 advisories, 7 skipped. Exit code: 1
+3 -2
tests/snapshots/oauth_client_metadata__scope_grammar_invalid.snap
··· 8 8 == Discovery == 9 9 [OK] Client ID well-formed 10 10 [OK] Metadata document fetchable 11 + [OK] Metadata Content-Type is JSON 11 12 [OK] Metadata is valid JSON 12 13 == Metadata == 13 14 [OK] Metadata document deserializes 14 15 [OK] Metadata `client_id` matches fetched URL 15 16 [OK] `application_type` field is present 16 17 [OK] `application_type` is `web` or `native` 17 - [OK] `response_types` is `["code"]` 18 + [OK] `response_types` includes `code` 18 19 [OK] `grant_types` includes `authorization_code` 19 20 [OK] `dpop_bound_access_tokens` is `true` 20 21 [OK] `redirect_uris` is non-empty ··· 43 44 [OK] Keys use signing use 44 45 [OK] Algorithms are modern EC 45 46 46 - Summary: 21 passed, 1 failed (spec), 0 network errors, 0 advisories, 2 skipped. Exit code: 1 47 + Summary: 22 passed, 1 failed (spec), 0 network errors, 0 advisories, 2 skipped. Exit code: 1