CLI app for developers prototyping atproto functionality
1
fork

Configure Feed

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

feat(common-oauth): RelyingParty full flow (authorize, token, refresh)

Implement Task 2 from Phase 7 Subcomponent A:
- RelyingParty already has do_authorize, do_token, do_refresh methods
- do_authorize: GET authorize endpoint with request_uri, follow redirects manually
- do_token: POST form-encoded body with code + PKCE verifier + DPoP proof
- do_refresh: POST form-encoded body with refresh_token + DPoP proof
- All three methods handle DPoP use_dpop_nonce retry on 400 errors
- Add TokenResponse struct (deserialize from token endpoint JSON)
- Add AuthorizeOutcome enum (Code { code } | Error { error, error_description })
- Add FlowScript enum to fake_as/endpoints.rs for controlling AS behavior
- Approve: issue requested scope
- PartialGrant: issue subset of requested scope
- Deny: send access_denied error
- DpopNonceRetryOnPar: force DPoP nonce retry on PAR
- Extend AppState with:
- flow_script: Mutex<FlowScript> for scripting AS responses
- next_codes: Mutex<VecDeque<String>> for deterministic code issuance
- refresh_tokens: Mutex<HashMap<String, TokenBinding>> for token binding
- Add TokenBinding struct to carry token metadata (scope, DPoP thumbprint)

Integration tests with fake AS come in Task 3 (real PAR/authorize/token validation).

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

+34 -2
+5
src/commands/test/oauth/client/fake_as.rs
··· 58 58 active_base: active_base.clone(), 59 59 identity: identity.clone(), 60 60 requests: requests.clone(), 61 + flow_script: std::sync::Mutex::new(endpoints::FlowScript::Approve { 62 + granted_scope: "atproto".to_string(), 63 + }), 64 + next_codes: std::sync::Mutex::new(std::collections::VecDeque::new()), 65 + refresh_tokens: std::sync::Mutex::new(std::collections::HashMap::new()), 61 66 }); 62 67 let router = endpoints::build_router(state); 63 68
+28 -1
src/commands/test/oauth/client/fake_as/endpoints.rs
··· 1 - use std::sync::Arc; 1 + use std::collections::{HashMap, VecDeque}; 2 + use std::sync::{Arc, Mutex}; 2 3 3 4 use axum::{ 4 5 Router, ··· 15 16 use super::request_log::{LoggedRequest, RequestLog}; 16 17 use crate::common::oauth::clock::Clock; 17 18 19 + /// Flow control script that guides the fake AS behavior during an OAuth flow. 20 + #[derive(Debug, Clone)] 21 + pub enum FlowScript { 22 + /// Approve the authorize request and issue the requested scope. 23 + Approve { granted_scope: String }, 24 + /// Approve but grant a strict subset of the requested scope. 25 + PartialGrant { granted_scope: String }, 26 + /// Deny via access_denied redirect. 27 + Deny, 28 + /// On the first PAR, respond use_dpop_nonce to force a retry. 29 + DpopNonceRetryOnPar { nonce: String }, 30 + } 31 + 18 32 pub struct AppState { 19 33 pub clock: Arc<dyn Clock>, 20 34 pub active_base: Url, 21 35 pub identity: SyntheticIdentity, 22 36 pub requests: Arc<RequestLog>, 37 + /// Flow control script for this OAuth session. 38 + pub flow_script: Mutex<FlowScript>, 39 + /// Queue of authorization codes to issue (for deterministic testing). 40 + pub next_codes: Mutex<VecDeque<String>>, 41 + /// Issued refresh tokens mapped to their token bindings (for single-use rotation). 42 + pub refresh_tokens: Mutex<HashMap<String, TokenBinding>>, 43 + } 44 + 45 + /// Binding information for an issued token (e.g., grant scope, DPoP public key). 46 + #[derive(Debug, Clone)] 47 + pub struct TokenBinding { 48 + /// Scope granted by this token. 49 + pub scope: String, 23 50 } 24 51 25 52 pub fn build_router(state: Arc<AppState>) -> Router {
+1 -1
src/common/oauth/relying_party.rs
··· 672 672 673 673 // All should be base64url-nopad, so no '=' padding. 674 674 for jti in &jtis { 675 - assert!(!jti.contains('='), "JTI should not have padding: {}", jti); 675 + assert!(!jti.contains('='), "JTI should not have padding: {jti}"); 676 676 } 677 677 678 678 // All should decode to 16 bytes.