CLI app for developers prototyping atproto functionality
1
fork

Configure Feed

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

Bypass `has_key_for_signing_alg` gate for non-confidential clients

The interactive stage's token-endpoint gate tripped on
`CheckStatus::Skipped`, but the JWKS stage emits `Skipped` for
public, native, and loopback clients because the check is not
applicable: those clients use `token_endpoint_auth_method = none`
and never run `private_key_jwt`. The gate now consults
`metadata_facts.kind` and only fires for `WebConfidential`, so
public-client interactive runs proceed to the live flow instead of
being suppressed wholesale.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+17 -5
+17 -5
src/commands/test/oauth/client/pipeline/interactive.rs
··· 16 16 use crate::common::report::{CheckResult, CheckStatus, Stage, blocked_by}; 17 17 18 18 use super::jwks::JwksFacts; 19 - use super::metadata::MetadataFacts; 19 + use super::metadata::{ClientKind, MetadataFacts}; 20 20 use super::{InteractiveDriveMode, InteractiveOptions, StaticGating}; 21 21 22 22 /// Diagnostic for interactive stage spec violations. ··· 279 279 /// Run the interactive stage. 280 280 pub async fn run( 281 281 static_gating: StaticGating, 282 - _metadata_facts: Option<&MetadataFacts>, 282 + metadata_facts: Option<&MetadataFacts>, 283 283 _jwks_facts: Option<&JwksFacts>, 284 284 interactive_opts: &InteractiveOptions, 285 285 clock: Arc<dyn Clock>, ··· 290 290 // ClientReachedPar, ClientUsedPkceS256, ClientIncludedDpop depend on scope_present + dpop_bound_required. 291 291 // ClientCompletedToken depends on all of the above + has_key_for_signing_alg. 292 292 // ClientRefreshed depends on grant_types including refresh_token. 293 + // 294 + // `has_key_for_signing_alg` is only meaningful for confidential 295 + // clients: it asks whether the JWKS contains a key compatible with 296 + // `token_endpoint_auth_signing_alg` for `private_key_jwt`. Public, 297 + // native, and loopback clients use `token_endpoint_auth_method = 298 + // none` (no client assertion at the token endpoint), so the JWKS 299 + // stage skips every check for those kinds. We must not let that 300 + // intentional skip block the token-endpoint interactive check. 301 + let token_auth_gate_applies = matches!( 302 + metadata_facts.map(|f| f.kind), 303 + Some(ClientKind::WebConfidential), 304 + ); 293 305 294 306 // Compute each check's gate status independently. 295 307 let par_gates_pass = static_gating.scope_present == CheckStatus::Pass 296 308 && static_gating.dpop_bound_required == CheckStatus::Pass; 297 309 298 - let _token_gates_pass = 299 - par_gates_pass && static_gating.has_key_for_signing_alg == CheckStatus::Pass; 310 + let _token_gates_pass = par_gates_pass 311 + && (!token_auth_gate_applies || static_gating.has_key_for_signing_alg == CheckStatus::Pass); 300 312 301 313 // Emit blocked_by results for checks whose gates didn't pass. 302 314 // Each check's result is based on its specific gate, not a cascading if-chain. ··· 365 377 }; 366 378 } 367 379 368 - if static_gating.has_key_for_signing_alg != CheckStatus::Pass { 380 + if token_auth_gate_applies && static_gating.has_key_for_signing_alg != CheckStatus::Pass { 369 381 // PAR gates pass, but token gate (has_key_for_signing_alg) fails. 370 382 // ClientReachedPar, ClientUsedPkceS256, ClientIncludedDpop can pass. 371 383 // ClientCompletedToken is blocked by has_key_for_signing_alg.