CLI app for developers prototyping atproto functionality
1
fork

Configure Feed

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

feat(oauth-client): orchestrate scope-variations and dpop-edges sub-stages from interactive stage

Extends interactive.rs run() to wire scope_variations and dpop_edges sub-stages
after the Phase 7 happy-path flow. Both sub-stages run against the same fake AS
server, allowing flows to share the server lifetime.

Adds gate tables for both sub-stages:
- scope_variations depends on scope_present, grant_types_includes_authorization_code,
and dpop_bound_required checks
- dpop_edges depends on the above plus response_types_is_code

When gates fail, emits blocked_by results for all 6 checks in each sub-stage,
referencing the first failing gate check ID. When gates pass, runs the sub-stage
and extends results with its check outcomes.

Updates StaticGating struct in pipeline.rs to include response_types_is_code field
and wires it into the static gating scan logic.

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

+95
+7
src/commands/test/oauth/client/pipeline.rs
··· 301 301 /// Status of the refresh_token grant type check. 302 302 /// Phase 7 hard-codes ClientRefreshed.skipped; refresh token support is wired in Phase 8. 303 303 pub grant_types_includes_refresh_token: CheckStatus, 304 + /// Status of the response_types check. 305 + /// Used by dpop_edges sub-stage gating in Phase 8. 306 + pub response_types_is_code: CheckStatus, 304 307 } 305 308 306 309 /// Options passed to the pipeline runner. ··· 420 423 keys_have_alg: CheckStatus::Pass, 421 424 grant_types_includes_authorization_code: CheckStatus::Pass, 422 425 grant_types_includes_refresh_token: CheckStatus::Pass, 426 + response_types_is_code: CheckStatus::Pass, 423 427 }; 424 428 425 429 // Scan the report for key static check results. ··· 436 440 } 437 441 "oauth_client::metadata::grant_types_includes_authorization_code" => { 438 442 static_gating.grant_types_includes_authorization_code = check.status; 443 + } 444 + "oauth_client::metadata::response_types_is_code" => { 445 + static_gating.response_types_is_code = check.status; 439 446 } 440 447 _ => {} 441 448 }
+86
src/commands/test/oauth/client/pipeline/interactive.rs
··· 444 444 445 445 // Phase 7 doesn't test refresh; skip with reason. 446 446 results.push(Check::ClientRefreshed.skipped("covered in Phase 8 flow variants")); 447 + 448 + // Phase 8: Gate and run scope_variations sub-stage. 449 + let scope_gates_pass = static_gating.scope_present == CheckStatus::Pass 450 + && static_gating.grant_types_includes_authorization_code == CheckStatus::Pass 451 + && static_gating.dpop_bound_required == CheckStatus::Pass; 452 + 453 + if !scope_gates_pass { 454 + // Determine which gate failed. 455 + let blocking_check = if static_gating.scope_present != CheckStatus::Pass { 456 + "oauth_client::metadata::scope_present" 457 + } else if static_gating.grant_types_includes_authorization_code != CheckStatus::Pass 458 + { 459 + "oauth_client::metadata::grant_types_includes_authorization_code" 460 + } else { 461 + "oauth_client::metadata::dpop_bound_required" 462 + }; 463 + 464 + // Emit blocked_by for all 6 scope_variations checks. 465 + let scope_check_ids = [ 466 + "oauth_client::interactive::scope_variations::full_grant_approve", 467 + "oauth_client::interactive::scope_variations::partial_grant_approve", 468 + "oauth_client::interactive::scope_variations::user_denial_propagated", 469 + "oauth_client::interactive::scope_variations::downscoped_refresh", 470 + "oauth_client::interactive::scope_variations::pkce_required", 471 + "oauth_client::interactive::scope_variations::dpop_required", 472 + ]; 473 + for check_id in scope_check_ids { 474 + results.push(blocked_by( 475 + check_id, 476 + Stage::INTERACTIVE, 477 + "Scope variation flows", 478 + blocking_check, 479 + )); 480 + } 481 + } else { 482 + // Scope gates pass; run scope_variations sub-stage. 483 + let scope_results = 484 + scope_variations::run(&server, rp_factory.as_ref(), clock.clone()).await; 485 + results.extend(scope_results); 486 + } 487 + 488 + // Phase 8: Gate and run dpop_edges sub-stage. 489 + let dpop_gates_pass = 490 + scope_gates_pass && static_gating.response_types_is_code == CheckStatus::Pass; 491 + 492 + if !dpop_gates_pass { 493 + // Determine which gate failed. 494 + let blocking_check = if !scope_gates_pass { 495 + // One of the scope gates failed; reference the first one that did. 496 + if static_gating.scope_present != CheckStatus::Pass { 497 + "oauth_client::metadata::scope_present" 498 + } else if static_gating.grant_types_includes_authorization_code 499 + != CheckStatus::Pass 500 + { 501 + "oauth_client::metadata::grant_types_includes_authorization_code" 502 + } else { 503 + "oauth_client::metadata::dpop_bound_required" 504 + } 505 + } else { 506 + // scope gates pass but response_types_is_code fails. 507 + "oauth_client::metadata::response_types_is_code" 508 + }; 509 + 510 + // Emit blocked_by for all 6 dpop_edges checks. 511 + let dpop_check_ids = [ 512 + "oauth_client::interactive::dpop_edges::nonce_rotation", 513 + "oauth_client::interactive::dpop_edges::refresh_rotation", 514 + "oauth_client::interactive::dpop_edges::replay_rejection", 515 + "oauth_client::interactive::dpop_edges::jti_reused", 516 + "oauth_client::interactive::dpop_edges::nonce_ignored", 517 + "oauth_client::interactive::dpop_edges::refresh_token_reused", 518 + ]; 519 + for check_id in dpop_check_ids { 520 + results.push(blocked_by( 521 + check_id, 522 + Stage::INTERACTIVE, 523 + "DPoP edge case flows", 524 + blocking_check, 525 + )); 526 + } 527 + } else { 528 + // DPoP gates pass; run dpop_edges sub-stage. 529 + let dpop_results = 530 + dpop_edges::run(&server, rp_factory.as_ref(), clock.clone()).await; 531 + results.extend(dpop_results); 532 + } 447 533 } 448 534 } 449 535
+2
tests/oauth_client_interactive.rs
··· 260 260 keys_have_alg: CheckStatus::Pass, 261 261 grant_types_includes_authorization_code: CheckStatus::Pass, 262 262 grant_types_includes_refresh_token: CheckStatus::Pass, 263 + response_types_is_code: CheckStatus::Pass, 263 264 }; 264 265 265 266 // Set up interactive options with in-process RP driver. ··· 364 365 keys_have_alg: CheckStatus::Pass, 365 366 grant_types_includes_authorization_code: CheckStatus::Pass, 366 367 grant_types_includes_refresh_token: CheckStatus::Pass, 368 + response_types_is_code: CheckStatus::Pass, 367 369 }; 368 370 369 371 // Create interactive output directly (without full pipeline).