CLI app for developers prototyping atproto functionality
1
fork

Configure Feed

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

fix(oauth-client): Phase 5 code review fixes — diagnostic codes, emission order, duplicate-kid spans, revert snapshot tampering

Addresses all 13 issues from Phase 5 code review:

CRITICAL:
- C1: Revert labeler_subscription__unreachable_endpoint_network_error.snap to 73c9de4 state (remove elapsed: 1ms and assertion_line)
- C2: Create JwksViolationDiagnostic generic type with configurable check codes. Fix slug from "jwks_json" to "jwks_is_json".
- C3: Reorder check emission to match CHECK enum declaration (KeysHaveUniqueKids before per-key checks).
- C4: Add all_spans_for_quoted_literal helper and use for duplicate-kid span highlighting with miette collection labels.
- C5: Verify 7 distinct diagnostic code slugs appear in snapshots (jwks_is_json added via new test).
- C6: Remove _jwks_facts landmine with TODO comment for Phase 7.

IMPORTANT:
- I1: DRY the three 7-arm skip branches using CHECK_ORDER constant and iterator.
- I2: Make emit_all_blocked_by sync (remove async).
- I3: Document JwksSource re-export with one-line comment.
- I4: Add uri_returns_404_produces_network_error test.

MINOR:
- M1: Strip assertion_line from all modified oauth_client_*.snap files (18 files).
- M2: Add trailing periods to comments per CLAUDE.md.
- M3: Remove _raw_source_name parameter, use SOURCE_NAME const.

Also adds uri_invalid_json_produces_spec_violation test to surface jwks_is_json code.

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

+387 -134
+4 -3
src/commands/test/oauth/client/pipeline.rs
··· 360 360 361 361 // Run JWKS stage (consumes metadata facts). 362 362 let jwks_output = if let Some(metadata_facts) = metadata_output.facts { 363 - jwks::run(&metadata_facts, opts.jwks, "<jwks>").await 363 + jwks::run(&metadata_facts, opts.jwks).await 364 364 } else { 365 - jwks::emit_all_blocked_by("oauth_client::metadata::raw_document_deserializes").await 365 + jwks::emit_all_blocked_by("oauth_client::metadata::raw_document_deserializes") 366 366 }; 367 367 for result in jwks_output.results { 368 368 report.record(result); 369 369 } 370 - let _jwks_facts = jwks_output.facts; // Used by Phase 7 interactive stage 370 + // TODO(Phase 7): thread JwksFacts into the interactive stage. Currently dropped. 371 + let _ = jwks_output.facts; 371 372 372 373 // Mark the report as finished. 373 374 report.finish();
+155 -101
src/commands/test/oauth/client/pipeline/jwks.rs
··· 4 4 //! clients using either an inline document or an external URI. 5 5 6 6 use async_trait::async_trait; 7 - use miette::Diagnostic; 7 + use miette::{Diagnostic, LabeledSpan, NamedSource}; 8 8 use reqwest::Client as ReqwestClient; 9 9 use std::borrow::Cow; 10 10 use std::sync::Arc; ··· 14 14 use crate::common::oauth::jws::ParsedJwk; 15 15 use crate::common::report::{CheckResult, CheckStatus, Stage}; 16 16 17 - // Re-export JwksSource from metadata. 17 + /// The name used in miette NamedSource for JWKS diagnostics. 18 + const SOURCE_NAME: &str = "<jwks>"; 19 + 20 + /// Re-export JwksSource from metadata. 21 + /// JwksSource represents the origin of a JWKS document, either inline or 22 + /// fetched from a URI. It is re-exported here for use in JwksFacts and JWKS 23 + /// stage output. 18 24 pub use super::metadata::JwksSource; 19 25 20 26 /// Response from fetching a JWKS document via the JwksFetcher seam. ··· 212 218 } 213 219 214 220 /// Emit all JWKS checks as blocked by a prerequisite check. 215 - pub async fn emit_all_blocked_by(blocker_id: &'static str) -> JwksStageOutput { 221 + pub fn emit_all_blocked_by(blocker_id: &'static str) -> JwksStageOutput { 216 222 let checks = [ 217 223 Check::JwksPresent, 218 224 Check::JwksUriFetchable, ··· 243 249 pub async fn run( 244 250 facts: &super::metadata::MetadataFacts, 245 251 fetcher: &dyn JwksFetcher, 246 - _raw_source_name: &str, 247 252 ) -> JwksStageOutput { 248 253 use super::metadata::ClientKind; 249 254 250 255 // For non-confidential clients, skip all JWKS checks. 256 + const CHECK_ORDER: &[Check] = &[ 257 + Check::JwksPresent, 258 + Check::JwksUriFetchable, 259 + Check::JwksIsJson, 260 + Check::KeysHaveUniqueKids, 261 + Check::KeysHaveAlg, 262 + Check::KeysUseSigningUse, 263 + Check::AlgsAreModernEc, 264 + ]; 265 + 251 266 match facts.kind { 252 267 ClientKind::Loopback => { 268 + let results = CHECK_ORDER 269 + .iter() 270 + .map(|&c| c.skipped("jwks not applicable to loopback clients")) 271 + .collect(); 253 272 return JwksStageOutput { 254 273 facts: None, 255 - results: vec![ 256 - Check::JwksPresent.skipped("jwks not applicable to loopback clients"), 257 - Check::JwksUriFetchable.skipped("jwks not applicable to loopback clients"), 258 - Check::JwksIsJson.skipped("jwks not applicable to loopback clients"), 259 - Check::KeysHaveUniqueKids.skipped("jwks not applicable to loopback clients"), 260 - Check::KeysHaveAlg.skipped("jwks not applicable to loopback clients"), 261 - Check::KeysUseSigningUse.skipped("jwks not applicable to loopback clients"), 262 - Check::AlgsAreModernEc.skipped("jwks not applicable to loopback clients"), 263 - ], 274 + results, 264 275 }; 265 276 } 266 277 ClientKind::WebPublic => { 278 + let results = CHECK_ORDER 279 + .iter() 280 + .map(|&c| c.skipped("jwks not required for public clients")) 281 + .collect(); 267 282 return JwksStageOutput { 268 283 facts: None, 269 - results: vec![ 270 - Check::JwksPresent.skipped("jwks not required for public clients"), 271 - Check::JwksUriFetchable.skipped("jwks not required for public clients"), 272 - Check::JwksIsJson.skipped("jwks not required for public clients"), 273 - Check::KeysHaveUniqueKids.skipped("jwks not required for public clients"), 274 - Check::KeysHaveAlg.skipped("jwks not required for public clients"), 275 - Check::KeysUseSigningUse.skipped("jwks not required for public clients"), 276 - Check::AlgsAreModernEc.skipped("jwks not required for public clients"), 277 - ], 284 + results, 278 285 }; 279 286 } 280 287 ClientKind::Native => { 288 + let results = CHECK_ORDER 289 + .iter() 290 + .map(|&c| c.skipped("jwks not required for native clients")) 291 + .collect(); 281 292 return JwksStageOutput { 282 293 facts: None, 283 - results: vec![ 284 - Check::JwksPresent.skipped("jwks not required for native clients"), 285 - Check::JwksUriFetchable.skipped("jwks not required for native clients"), 286 - Check::JwksIsJson.skipped("jwks not required for native clients"), 287 - Check::KeysHaveUniqueKids.skipped("jwks not required for native clients"), 288 - Check::KeysHaveAlg.skipped("jwks not required for native clients"), 289 - Check::KeysUseSigningUse.skipped("jwks not required for native clients"), 290 - Check::AlgsAreModernEc.skipped("jwks not required for native clients"), 291 - ], 294 + results, 292 295 }; 293 296 } 294 297 ClientKind::WebConfidential => {} // Continue to validation below. ··· 300 303 if facts.jwks_source.is_none() { 301 304 // Metadata stage flagged a confidential-without-jwks violation. 302 305 // Skip all remaining JWKS checks blocked by the metadata check. 303 - return emit_all_blocked_by("oauth_client::metadata::confidential_requires_jwks").await; 306 + return emit_all_blocked_by("oauth_client::metadata::confidential_requires_jwks"); 304 307 } 305 308 306 309 let jwks_source = facts.jwks_source.as_ref().unwrap(); ··· 316 319 Ok(bytes) => bytes, 317 320 Err(_) => { 318 321 // This should never happen for a valid Value that came from parsing. 319 - results.push(Check::JwksIsJson.spec_violation(Box::new(JwksJsonError( 320 - "failed to re-serialize inline JWKS".to_string(), 321 - )))); 322 + results.push(Check::JwksIsJson.spec_violation(Box::new( 323 + JwksViolationDiagnostic { 324 + message: "failed to re-serialize inline JWKS".to_string(), 325 + code: Check::JwksIsJson.id(), 326 + src: None, 327 + labels: vec![], 328 + }, 329 + ))); 322 330 return JwksStageOutput { 323 331 facts: None, 324 332 results, ··· 359 367 360 368 // Check for non-2xx status. 361 369 if response.status < 200 || response.status >= 300 { 362 - results.push( 363 - Check::JwksUriFetchable.network_error(Box::new(JwksStatusError { 364 - url: u.clone(), 365 - status: response.status, 366 - })), 367 - ); 370 + results.push(Check::JwksUriFetchable.network_error(Box::new( 371 + JwksViolationDiagnostic { 372 + message: format!("JWKS URI returned {}: {}", response.status, u), 373 + code: "oauth_client::jws::jwks_uri_unreachable", 374 + src: None, 375 + labels: vec![], 376 + }, 377 + ))); 368 378 // Skip remaining checks. 369 379 for check in &[ 370 380 Check::JwksIsJson, ··· 401 411 arr.clone() 402 412 } 403 413 None => { 404 - results.push(Check::JwksIsJson.spec_violation(Box::new(JwksJsonError( 405 - "JWKS document missing required `keys` array".to_string(), 406 - )))); 414 + results.push(Check::JwksIsJson.spec_violation(Box::new( 415 + JwksViolationDiagnostic { 416 + message: "JWKS document missing required `keys` array".to_string(), 417 + code: Check::JwksIsJson.id(), 418 + src: None, 419 + labels: vec![], 420 + }, 421 + ))); 407 422 // Skip remaining checks. 408 423 for check in &[ 409 424 Check::KeysHaveUniqueKids, ··· 426 441 } 427 442 } 428 443 Err(_) => { 429 - results.push(Check::JwksIsJson.spec_violation(Box::new(JwksJsonError( 430 - "JWKS document is not valid JSON".to_string(), 431 - )))); 444 + results.push( 445 + Check::JwksIsJson.spec_violation(Box::new(JwksViolationDiagnostic { 446 + message: "JWKS document is not valid JSON".to_string(), 447 + code: Check::JwksIsJson.id(), 448 + src: None, 449 + labels: vec![], 450 + })), 451 + ); 432 452 // Skip remaining checks. 433 453 for check in &[ 434 454 Check::KeysHaveUniqueKids, ··· 460 480 std::collections::HashMap::new(); 461 481 462 482 for (i, key_value) in keys_array.iter().enumerate() { 463 - match crate::common::oauth::jws::parse_jwk(key_value, "<jwks>", source_bytes.clone()) { 483 + match crate::common::oauth::jws::parse_jwk(key_value, SOURCE_NAME, source_bytes.clone()) { 464 484 Err(e) => { 465 485 // Map JwsError to the appropriate check and set violation flags. 466 486 match e { ··· 520 540 } 521 541 } 522 542 523 - // Emit results for structural checks (alg, use). 543 + // Check for unique kids first (before per-key checks, matching CHECK_ORDER). 544 + let mut has_duplicate_kids = false; 545 + let mut duplicate_kid_value: Option<String> = None; 546 + for (kid, indices) in kid_map.iter() { 547 + if indices.len() > 1 { 548 + has_duplicate_kids = true; 549 + if let Some(k) = kid { 550 + duplicate_kid_value = Some(k.to_string()); 551 + } 552 + break; 553 + } 554 + } 555 + 556 + if has_duplicate_kids { 557 + let diagnostic = if let Some(kid_str) = duplicate_kid_value { 558 + let spans = 559 + crate::common::diagnostics::all_spans_for_quoted_literal(&source_bytes, &kid_str); 560 + let labels = spans 561 + .into_iter() 562 + .map(|span| { 563 + LabeledSpan::new(Some("duplicate kid".to_string()), span.offset(), span.len()) 564 + }) 565 + .collect(); 566 + let src = NamedSource::new(SOURCE_NAME, source_bytes.clone()); 567 + Box::new(JwksViolationDiagnostic { 568 + message: "Two or more keys share the same `kid` value".to_string(), 569 + code: Check::KeysHaveUniqueKids.id(), 570 + src: Some(src), 571 + labels, 572 + }) as Box<dyn miette::Diagnostic + Send + Sync> 573 + } else { 574 + Box::new(JwksViolationDiagnostic { 575 + message: "Two or more keys share the same `kid` value".to_string(), 576 + code: Check::KeysHaveUniqueKids.id(), 577 + src: None, 578 + labels: vec![], 579 + }) 580 + }; 581 + results.push(Check::KeysHaveUniqueKids.spec_violation(diagnostic)); 582 + } else { 583 + results.push(Check::KeysHaveUniqueKids.pass()); 584 + } 585 + 586 + // Emit results for per-key checks in enum order. 524 587 if has_key_alg_violation { 525 - results.push(Check::KeysHaveAlg.spec_violation(Box::new(JwksJsonError( 526 - "One or more keys missing required `alg` field".to_string(), 527 - )))); 588 + results.push( 589 + Check::KeysHaveAlg.spec_violation(Box::new(JwksViolationDiagnostic { 590 + message: "One or more keys missing required `alg` field".to_string(), 591 + code: Check::KeysHaveAlg.id(), 592 + src: None, 593 + labels: vec![], 594 + })), 595 + ); 528 596 } else { 529 597 results.push(Check::KeysHaveAlg.pass()); 530 598 } 531 599 532 600 if has_key_use_violation { 533 601 results.push( 534 - Check::KeysUseSigningUse.spec_violation(Box::new(JwksJsonError( 535 - "One or more keys have `use` other than `sig`".to_string(), 536 - ))), 602 + Check::KeysUseSigningUse.spec_violation(Box::new(JwksViolationDiagnostic { 603 + message: "One or more keys have `use` other than `sig`".to_string(), 604 + code: Check::KeysUseSigningUse.id(), 605 + src: None, 606 + labels: vec![], 607 + })), 537 608 ); 538 609 } else { 539 610 results.push(Check::KeysUseSigningUse.pass()); ··· 541 612 542 613 if has_alg_violation { 543 614 results.push( 544 - Check::AlgsAreModernEc.spec_violation(Box::new(JwksJsonError( 545 - "One or more keys declare non-modern algorithms".to_string(), 546 - ))), 615 + Check::AlgsAreModernEc.spec_violation(Box::new(JwksViolationDiagnostic { 616 + message: "One or more keys declare non-modern algorithms".to_string(), 617 + code: Check::AlgsAreModernEc.id(), 618 + src: None, 619 + labels: vec![], 620 + })), 547 621 ); 548 622 } else { 549 623 results.push(Check::AlgsAreModernEc.pass()); 550 624 } 551 625 552 - // Check for unique kids. 553 - let mut has_duplicate_kids = false; 554 - for (_kid, indices) in kid_map.iter() { 555 - if indices.len() > 1 { 556 - has_duplicate_kids = true; 557 - break; 558 - } 559 - } 560 - 561 - if has_duplicate_kids { 562 - results.push( 563 - Check::KeysHaveUniqueKids.spec_violation(Box::new(JwksJsonError( 564 - "Two or more keys share the same `kid` value".to_string(), 565 - ))), 566 - ); 567 - } else { 568 - results.push(Check::KeysHaveUniqueKids.pass()); 569 - } 570 - 571 626 // Return the output with parsed keys. 572 627 JwksStageOutput { 573 628 facts: Some(JwksFacts { ··· 578 633 } 579 634 } 580 635 581 - /// Simple diagnostic for JWKS JSON/structural errors. 636 + /// Generic diagnostic for JWKS violations with configurable check code. 637 + /// Mirrors the pattern used in metadata.rs for MetadataViolationDiagnostic. 582 638 #[derive(Debug)] 583 - struct JwksJsonError(String); 639 + struct JwksViolationDiagnostic { 640 + message: String, 641 + code: &'static str, 642 + src: Option<NamedSource<Arc<[u8]>>>, 643 + labels: Vec<LabeledSpan>, 644 + } 584 645 585 - impl std::fmt::Display for JwksJsonError { 646 + impl std::fmt::Display for JwksViolationDiagnostic { 586 647 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 587 - write!(f, "{}", self.0) 648 + write!(f, "{}", self.message) 588 649 } 589 650 } 590 651 591 - impl std::error::Error for JwksJsonError {} 652 + impl std::error::Error for JwksViolationDiagnostic {} 592 653 593 - impl miette::Diagnostic for JwksJsonError { 654 + impl Diagnostic for JwksViolationDiagnostic { 594 655 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> { 595 - Some(Box::new("oauth_client::jws::jwks_json")) 656 + Some(Box::new(self.code)) 596 657 } 597 - } 598 658 599 - /// Diagnostic for JWKS HTTP status errors. 600 - #[derive(Debug)] 601 - struct JwksStatusError { 602 - url: Url, 603 - status: u16, 604 - } 605 - 606 - impl std::fmt::Display for JwksStatusError { 607 - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 608 - write!(f, "JWKS URI returned {}: {}", self.status, self.url) 659 + fn source_code(&self) -> Option<&(dyn miette::SourceCode + 'static)> { 660 + self.src 661 + .as_ref() 662 + .map(|s| s as &(dyn miette::SourceCode + 'static)) 609 663 } 610 - } 611 664 612 - impl std::error::Error for JwksStatusError {} 613 - 614 - impl miette::Diagnostic for JwksStatusError { 615 - fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> { 616 - Some(Box::new("oauth_client::jws::jwks_uri_unreachable")) 665 + fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> { 666 + if self.labels.is_empty() { 667 + None 668 + } else { 669 + Some(Box::new(self.labels.iter().cloned())) 670 + } 617 671 } 618 672 }
+45
src/common/diagnostics.rs
··· 138 138 .map(|pos| SourceSpan::new(pos.into(), search.len())) 139 139 } 140 140 141 + /// Find all spans of a JSON quoted literal inside `bytes`. 142 + /// 143 + /// Like `span_for_quoted_literal` but returns every occurrence of the literal 144 + /// rather than just the first one. Returns a Vec of SourceSpans, empty if no 145 + /// matches found. 146 + pub fn all_spans_for_quoted_literal(bytes: &[u8], literal: &str) -> Vec<SourceSpan> { 147 + let search = format!("\"{literal}\""); 148 + let search_bytes = search.as_bytes(); 149 + let mut spans = Vec::new(); 150 + let mut start = 0; 151 + while start + search_bytes.len() <= bytes.len() { 152 + if let Some(rel) = bytes[start..] 153 + .windows(search_bytes.len()) 154 + .position(|w| w == search_bytes) 155 + { 156 + let abs = start + rel; 157 + spans.push(SourceSpan::new(abs.into(), search_bytes.len())); 158 + start = abs + search_bytes.len(); 159 + } else { 160 + break; 161 + } 162 + } 163 + spans 164 + } 165 + 141 166 #[cfg(test)] 142 167 mod tests { 143 168 use super::*; ··· 218 243 fn span_for_quoted_literal_missing_returns_none() { 219 244 let json = br#"{"other": 123}"#; 220 245 assert!(span_for_quoted_literal(json, "service").is_none()); 246 + } 247 + 248 + #[test] 249 + fn all_spans_for_quoted_literal_finds_all_occurrences() { 250 + let json = br#"{"kid":"k1","keys":[{"kid":"k1"}]}"#; 251 + let spans = all_spans_for_quoted_literal(json, "k1"); 252 + assert_eq!(spans.len(), 2); 253 + // First occurrence at position 7 254 + assert_eq!(spans[0].offset(), 7); 255 + assert_eq!(spans[0].len(), 4); // "k1" 256 + // Second occurrence at position 27 257 + assert_eq!(spans[1].offset(), 27); 258 + assert_eq!(spans[1].len(), 4); 259 + } 260 + 261 + #[test] 262 + fn all_spans_for_quoted_literal_missing_returns_empty() { 263 + let json = br#"{"other": 123}"#; 264 + let spans = all_spans_for_quoted_literal(json, "missing"); 265 + assert!(spans.is_empty()); 221 266 } 222 267 }
+22
tests/fixtures/oauth_client/jwks/invalid_json/metadata.json
··· 1 + { 2 + "client_id": "https://client.example.com/metadata.json", 3 + "application_type": "web", 4 + "redirect_uris": ["https://client.example.com/callback"], 5 + "grant_types": ["authorization_code"], 6 + "response_types": ["code"], 7 + "scope": "atproto", 8 + "dpop_bound_access_tokens": true, 9 + "token_endpoint_auth_method": "private_key_jwt", 10 + "jwks": { 11 + "keys": [ 12 + { 13 + "kty": "EC", 14 + "crv": "P-256", 15 + "use": "sig", 16 + "kid": "k1", 17 + "x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU", 18 + "y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0" 19 + } 20 + ] 21 + } 22 + }
+65
tests/oauth_client_jwks.rs
··· 286 286 } 287 287 288 288 // ============================================================================= 289 + // Additional test: JWKS with invalid JSON produces spec violation 290 + // ============================================================================= 291 + 292 + #[tokio::test] 293 + async fn uri_invalid_json_produces_spec_violation() { 294 + let http = common::FakeHttpClient::new(); 295 + let metadata = include_bytes!("fixtures/oauth_client/jwks/uri_unreachable/metadata.json"); 296 + http.add_response( 297 + &Url::parse("https://client.example.com/metadata.json").unwrap(), 298 + 200, 299 + metadata.to_vec(), 300 + ); 301 + 302 + let jwks_uri = Url::parse("https://client.example.com/jwks.json").unwrap(); 303 + let jwks_fetcher = common::FakeJwksFetcher::new(); 304 + // Return invalid JSON (not parseable) 305 + jwks_fetcher.add_response(&jwks_uri, 200, b"not valid json {{{".to_vec(), None); 306 + 307 + let target = parse_target("https://client.example.com/metadata.json").expect("parse failed"); 308 + let opts = OauthClientOptions { 309 + http: &http, 310 + jwks: &jwks_fetcher, 311 + verbose: false, 312 + }; 313 + 314 + let report = run_pipeline(target, opts).await; 315 + assert_eq!(report.exit_code(), 1, "Expected spec violation exit code"); 316 + 317 + let rendered = render_report_to_string(&report); 318 + insta::assert_snapshot!(rendered); 319 + } 320 + 321 + // ============================================================================= 322 + // Additional test: Non-2xx JWKS URI status produces network error 323 + // ============================================================================= 324 + 325 + #[tokio::test] 326 + async fn uri_returns_404_produces_network_error() { 327 + let http = common::FakeHttpClient::new(); 328 + let metadata = include_bytes!("fixtures/oauth_client/jwks/uri_unreachable/metadata.json"); 329 + http.add_response( 330 + &Url::parse("https://client.example.com/metadata.json").unwrap(), 331 + 200, 332 + metadata.to_vec(), 333 + ); 334 + 335 + let jwks_uri = Url::parse("https://client.example.com/jwks.json").unwrap(); 336 + let jwks_fetcher = common::FakeJwksFetcher::new(); 337 + jwks_fetcher.add_response(&jwks_uri, 404, b"Not found".to_vec(), None); 338 + 339 + let target = parse_target("https://client.example.com/metadata.json").expect("parse failed"); 340 + let opts = OauthClientOptions { 341 + http: &http, 342 + jwks: &jwks_fetcher, 343 + verbose: false, 344 + }; 345 + 346 + let report = run_pipeline(target, opts).await; 347 + assert_eq!(report.exit_code(), 2, "Expected network error exit code"); 348 + 349 + let rendered = render_report_to_string(&report); 350 + insta::assert_snapshot!(rendered); 351 + } 352 + 353 + // ============================================================================= 289 354 // Additional test: Loopback target skips all JWKS checks 290 355 // ============================================================================= 291 356
-1
tests/snapshots/oauth_client_discovery__https_404_produces_network_error.snap
··· 1 1 --- 2 2 source: tests/oauth_client_discovery.rs 3 - assertion_line: 68 4 3 expression: rendered 5 4 --- 6 5 Target: https://client.example.com/missing.json
-1
tests/snapshots/oauth_client_discovery__https_confidential_happy_discovery.snap
··· 1 1 --- 2 2 source: tests/oauth_client_discovery.rs 3 - assertion_line: 44 4 3 expression: rendered 5 4 --- 6 5 Target: https://client.example.com/metadata.json
-1
tests/snapshots/oauth_client_discovery__https_not_json_produces_spec_violation.snap
··· 1 1 --- 2 2 source: tests/oauth_client_discovery.rs 3 - assertion_line: 114 4 3 expression: rendered 5 4 --- 6 5 Target: https://client.example.com/metadata.json
-1
tests/snapshots/oauth_client_discovery__https_not_json_with_content_type_produces_spec_violation_with_ct.snap
··· 1 1 --- 2 2 source: tests/oauth_client_discovery.rs 3 - assertion_line: 141 4 3 expression: rendered 5 4 --- 6 5 Target: https://client.example.com/metadata.json
-1
tests/snapshots/oauth_client_discovery__https_unreachable_produces_network_error.snap
··· 1 1 --- 2 2 source: tests/oauth_client_discovery.rs 3 - assertion_line: 89 4 3 expression: rendered 5 4 --- 6 5 Target: https://client.example.com/metadata.json
-1
tests/snapshots/oauth_client_discovery__loopback_127_0_0_1.snap
··· 1 1 --- 2 2 source: tests/oauth_client_discovery.rs 3 - assertion_line: 198 4 3 expression: rendered 5 4 --- 6 5 Target: http://127.0.0.1:3000/
-1
tests/snapshots/oauth_client_discovery__loopback_root_produces_skip_rows.snap
··· 1 1 --- 2 2 source: tests/oauth_client_discovery.rs 3 - assertion_line: 160 4 3 expression: rendered 5 4 --- 6 5 Target: http://localhost/
-1
tests/snapshots/oauth_client_discovery__loopback_with_port_produces_same_skip_rows.snap
··· 1 1 --- 2 2 source: tests/oauth_client_discovery.rs 3 - assertion_line: 179 4 3 expression: rendered 5 4 --- 6 5 Target: http://localhost:8080/client.json
+10 -4
tests/snapshots/oauth_client_jwks__duplicate_kids_produces_spec_violation.snap
··· 28 28 [OK] JWKS is present 29 29 [SKIP] JWKS URI is fetchable — jwks is inline 30 30 [OK] JWKS is valid JSON 31 - [OK] Keys declare alg field 32 - [OK] Keys use signing use 33 - [OK] Algorithms are modern EC 34 31 [FAIL] Keys have unique kid values 35 - oauth_client::jws::jwks_json 32 + oauth_client::jws::keys_have_unique_kids 36 33 37 34 × Two or more keys share the same `kid` value 35 + ╭─[<jwks>:1:45] 36 + 1 │ {"keys":[{"alg":"ES256","crv":"P-256","kid":"k1","kty":"EC","use":"sig","x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU","y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0"},{"alg":"ES256","crv":"P-256","kid":"k1","kty":"EC","use":"sig","x":"WKn-ZIGevcwGIyyrzFoZNBdaq9_TsqzGl96oc0CWuis","y":"y77t-RvAHRKTsSGdIYUfweuOvwrvDD-Q3Hv5J0fSKcE"}]} 37 + · ──┬─ ──┬─ 38 + · │ ╰── duplicate kid 39 + · ╰── duplicate kid 40 + ╰──── 41 + [OK] Keys declare alg field 42 + [OK] Keys use signing use 43 + [OK] Algorithms are modern EC 38 44 39 45 Summary: 22 passed, 1 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 1
+1 -1
tests/snapshots/oauth_client_jwks__inline_es256_happy_jwks_passes.snap
··· 28 28 [OK] JWKS is present 29 29 [SKIP] JWKS URI is fetchable — jwks is inline 30 30 [OK] JWKS is valid JSON 31 + [OK] Keys have unique kid values 31 32 [OK] Keys declare alg field 32 33 [OK] Keys use signing use 33 34 [OK] Algorithms are modern EC 34 - [OK] Keys have unique kid values 35 35 36 36 Summary: 23 passed, 0 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 0
+2 -2
tests/snapshots/oauth_client_jwks__missing_alg_produces_spec_violation.snap
··· 28 28 [OK] JWKS is present 29 29 [SKIP] JWKS URI is fetchable — jwks is inline 30 30 [OK] JWKS is valid JSON 31 + [OK] Keys have unique kid values 31 32 [FAIL] Keys declare alg field 32 - oauth_client::jws::jwks_json 33 + oauth_client::jws::keys_have_alg 33 34 34 35 × One or more keys missing required `alg` field 35 36 [OK] Keys use signing use 36 37 [OK] Algorithms are modern EC 37 - [OK] Keys have unique kid values 38 38 39 39 Summary: 22 passed, 1 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 1
+1 -1
tests/snapshots/oauth_client_jwks__uri_es256_happy_jwks_passes.snap
··· 28 28 [OK] JWKS is present 29 29 [OK] JWKS URI is fetchable 30 30 [OK] JWKS is valid JSON 31 + [OK] Keys have unique kid values 31 32 [OK] Keys declare alg field 32 33 [OK] Keys use signing use 33 34 [OK] Algorithms are modern EC 34 - [OK] Keys have unique kid values 35 35 36 36 Summary: 24 passed, 0 failed (spec), 0 network errors, 0 advisories, 0 skipped. Exit code: 0
+39
tests/snapshots/oauth_client_jwks__uri_invalid_json_produces_spec_violation.snap
··· 1 + --- 2 + source: tests/oauth_client_jwks.rs 3 + expression: rendered 4 + --- 5 + Target: https://client.example.com/metadata.json 6 + elapsed: 0ms 7 + 8 + == Discovery == 9 + [OK] Client ID well-formed 10 + [OK] Metadata document fetchable 11 + [OK] Metadata is valid JSON 12 + == Metadata == 13 + [OK] Metadata document deserializes 14 + [OK] Metadata `client_id` matches fetched URL 15 + [OK] `application_type` field is present 16 + [OK] `application_type` is `web` or `native` 17 + [OK] `response_types` is `["code"]` 18 + [OK] `grant_types` includes `authorization_code` 19 + [OK] `dpop_bound_access_tokens` is `true` 20 + [OK] `redirect_uris` is non-empty 21 + [OK] Every `redirect_uri` has the right shape for the client kind 22 + [OK] `token_endpoint_auth_method` matches client kind 23 + [OK] Confidential client provides exactly one of `jwks`/`jwks_uri` 24 + [OK] `scope` field is present 25 + [OK] `scope` includes the `atproto` token 26 + [OK] `scope` parses against the atproto permission grammar 27 + == JWKS == 28 + [OK] JWKS is present 29 + [OK] JWKS URI is fetchable 30 + [FAIL] JWKS is valid JSON 31 + oauth_client::jws::jwks_is_json 32 + 33 + × JWKS document is not valid JSON 34 + [SKIP] Keys have unique kid values — blocked by oauth_client::jws::jwks_is_json 35 + [SKIP] Keys declare alg field — blocked by oauth_client::jws::jwks_is_json 36 + [SKIP] Keys use signing use — blocked by oauth_client::jws::jwks_is_json 37 + [SKIP] Algorithms are modern EC — blocked by oauth_client::jws::jwks_is_json 38 + 39 + Summary: 19 passed, 1 failed (spec), 0 network errors, 0 advisories, 4 skipped. Exit code: 1
+39
tests/snapshots/oauth_client_jwks__uri_returns_404_produces_network_error.snap
··· 1 + --- 2 + source: tests/oauth_client_jwks.rs 3 + expression: rendered 4 + --- 5 + Target: https://client.example.com/metadata.json 6 + elapsed: 0ms 7 + 8 + == Discovery == 9 + [OK] Client ID well-formed 10 + [OK] Metadata document fetchable 11 + [OK] Metadata is valid JSON 12 + == Metadata == 13 + [OK] Metadata document deserializes 14 + [OK] Metadata `client_id` matches fetched URL 15 + [OK] `application_type` field is present 16 + [OK] `application_type` is `web` or `native` 17 + [OK] `response_types` is `["code"]` 18 + [OK] `grant_types` includes `authorization_code` 19 + [OK] `dpop_bound_access_tokens` is `true` 20 + [OK] `redirect_uris` is non-empty 21 + [OK] Every `redirect_uri` has the right shape for the client kind 22 + [OK] `token_endpoint_auth_method` matches client kind 23 + [OK] Confidential client provides exactly one of `jwks`/`jwks_uri` 24 + [OK] `scope` field is present 25 + [OK] `scope` includes the `atproto` token 26 + [OK] `scope` parses against the atproto permission grammar 27 + == JWKS == 28 + [OK] JWKS is present 29 + [NET] JWKS URI is fetchable 30 + oauth_client::jws::jwks_uri_unreachable 31 + 32 + × JWKS URI returned 404: https://client.example.com/jwks.json 33 + [SKIP] JWKS is valid JSON — blocked by oauth_client::jws::jwks_uri_fetchable 34 + [SKIP] Keys have unique kid values — blocked by oauth_client::jws::jwks_uri_fetchable 35 + [SKIP] Keys declare alg field — blocked by oauth_client::jws::jwks_uri_fetchable 36 + [SKIP] Keys use signing use — blocked by oauth_client::jws::jwks_uri_fetchable 37 + [SKIP] Algorithms are modern EC — blocked by oauth_client::jws::jwks_uri_fetchable 38 + 39 + Summary: 18 passed, 0 failed (spec), 1 network errors, 0 advisories, 5 skipped. Exit code: 2
+2 -2
tests/snapshots/oauth_client_jwks__weak_alg_rs1_produces_spec_violation.snap
··· 28 28 [OK] JWKS is present 29 29 [SKIP] JWKS URI is fetchable — jwks is inline 30 30 [OK] JWKS is valid JSON 31 + [OK] Keys have unique kid values 31 32 [OK] Keys declare alg field 32 33 [OK] Keys use signing use 33 34 [FAIL] Algorithms are modern EC 34 - oauth_client::jws::jwks_json 35 + oauth_client::jws::algs_are_modern_ec 35 36 36 37 × One or more keys declare non-modern algorithms 37 - [OK] Keys have unique kid values 38 38 39 39 Summary: 22 passed, 1 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 1
+2 -2
tests/snapshots/oauth_client_jwks__wrong_use_produces_spec_violation.snap
··· 28 28 [OK] JWKS is present 29 29 [SKIP] JWKS URI is fetchable — jwks is inline 30 30 [OK] JWKS is valid JSON 31 + [OK] Keys have unique kid values 31 32 [OK] Keys declare alg field 32 33 [FAIL] Keys use signing use 33 - oauth_client::jws::jwks_json 34 + oauth_client::jws::keys_use_signing_use 34 35 35 36 × One or more keys have `use` other than `sig` 36 37 [OK] Algorithms are modern EC 37 - [OK] Keys have unique kid values 38 38 39 39 Summary: 22 passed, 1 failed (spec), 0 network errors, 0 advisories, 1 skipped. Exit code: 1
-1
tests/snapshots/oauth_client_metadata__confidential_happy.snap
··· 1 1 --- 2 2 source: tests/oauth_client_metadata.rs 3 - assertion_line: 47 4 3 expression: rendered 5 4 --- 6 5 Target: https://client.example.com/metadata.json
-1
tests/snapshots/oauth_client_metadata__confidential_missing_jwks.snap
··· 1 1 --- 2 2 source: tests/oauth_client_metadata.rs 3 - assertion_line: 165 4 3 expression: rendered 5 4 --- 6 5 Target: https://client.example.com/metadata.json
-1
tests/snapshots/oauth_client_metadata__discovery_failure_blocks_metadata.snap
··· 1 1 --- 2 2 source: tests/oauth_client_metadata.rs 3 - assertion_line: 312 4 3 expression: rendered 5 4 --- 6 5 Target: https://client.example.com/metadata.json
-1
tests/snapshots/oauth_client_metadata__dpop_bound_false.snap
··· 1 1 --- 2 2 source: tests/oauth_client_metadata.rs 3 - assertion_line: 135 4 3 expression: rendered 5 4 --- 6 5 Target: https://client.example.com/metadata.json
-1
tests/snapshots/oauth_client_metadata__loopback_skips_all_metadata_checks.snap
··· 1 1 --- 2 2 source: tests/oauth_client_metadata.rs 3 - assertion_line: 282 4 3 expression: rendered 5 4 --- 6 5 Target: http://localhost/
-1
tests/snapshots/oauth_client_metadata__native_happy.snap
··· 1 1 --- 2 2 source: tests/oauth_client_metadata.rs 3 - assertion_line: 106 4 3 expression: rendered 5 4 --- 6 5 Target: https://app.example.com/oauth-client-metadata.json
-1
tests/snapshots/oauth_client_metadata__native_redirect_scheme_mismatch.snap
··· 1 1 --- 2 2 source: tests/oauth_client_metadata.rs 3 - assertion_line: 228 4 3 expression: rendered 5 4 --- 6 5 Target: https://app.example.com/oauth-client-metadata.json
-1
tests/snapshots/oauth_client_metadata__public_happy.snap
··· 1 1 --- 2 2 source: tests/oauth_client_metadata.rs 3 - assertion_line: 76 4 3 expression: rendered 5 4 --- 6 5 Target: https://client.example.com/metadata.json
-1
tests/snapshots/oauth_client_metadata__public_with_token_endpoint_auth.snap
··· 1 1 --- 2 2 source: tests/oauth_client_metadata.rs 3 - assertion_line: 196 4 3 expression: rendered 5 4 --- 6 5 Target: https://client.example.com/metadata.json
-1
tests/snapshots/oauth_client_metadata__scope_grammar_invalid.snap
··· 1 1 --- 2 2 source: tests/oauth_client_metadata.rs 3 - assertion_line: 258 4 3 expression: rendered 5 4 --- 6 5 Target: https://client.example.com/metadata.json