Heavily customized version of smokesignal - https://whtwnd.com/kayrozen.com/3lpwe4ymowg2t
at main 794 lines 29 kB view raw
1//! # OAuth Module 2//! 3//! OAuth 2.0 and OpenID Connect implementation for secure authentication with AT Protocol services. 4//! 5//! This module provides a comprehensive OAuth implementation that handles the complete authentication 6//! flow for AT Protocol-based applications. It supports OAuth 2.0 with PKCE (Proof Key for Code Exchange), 7//! DPoP (Demonstration of Proof-of-Possession), and OpenID Connect for secure, modern authentication. 8//! 9//! ## Architecture 10//! 11//! The OAuth implementation follows the AT Protocol OAuth specifications and includes: 12//! 13//! ### Core Components 14//! - **Authorization Flow** - Complete OAuth 2.0 authorization code flow with PKCE 15//! - **Token Management** - Access token, refresh token, and ID token handling 16//! - **DPoP Support** - Demonstration of Proof-of-Possession for enhanced security 17//! - **Session Management** - Secure session storage and lifecycle management 18//! 19//! ### Security Features 20//! - **PKCE** - Proof Key for Code Exchange to prevent authorization code interception 21//! - **DPoP** - Cryptographic proof of possession for tokens 22//! - **State Parameter** - CSRF protection for authorization requests 23//! - **Secure Storage** - Encrypted token storage with Redis/Valkey 24//! 25//! ## OAuth Flow 26//! 27//! The module implements the standard OAuth 2.0 authorization code flow: 28//! 29//! 1. **Authorization Request** - Redirect user to authorization server 30//! 2. **Authorization Grant** - User grants permission, receives authorization code 31//! 3. **Token Exchange** - Exchange authorization code for access token 32//! 4. **Token Usage** - Use access token to access protected resources 33//! 5. **Token Refresh** - Refresh access token when expired 34//! 35//! ## AT Protocol Integration 36//! 37//! The OAuth implementation is specifically designed for AT Protocol services: 38//! - **PDS Discovery** - Automatic discovery of Personal Data Server OAuth endpoints 39//! - **Handle Resolution** - Integration with AT Protocol handle resolution 40//! - **DID Authentication** - Support for DID-based identity verification 41//! - **Resource Server** - Communication with AT Protocol resource servers 42//! 43//! ## Features 44//! 45//! ### Authorization Server Discovery 46//! Automatic discovery of OAuth configuration from AT Protocol services: 47//! - Authorization endpoint discovery 48//! - Token endpoint configuration 49//! - Supported grant types and scopes 50//! - JWKS (JSON Web Key Set) endpoint location 51//! 52//! ### Token Security 53//! - **Access Token** - Short-lived tokens for API access 54//! - **Refresh Token** - Long-lived tokens for access token renewal 55//! - **ID Token** - OpenID Connect identity tokens with user information 56//! - **DPoP Proof** - Cryptographic binding of tokens to client 57//! 58//! ### Session Management 59//! - **Secure Storage** - Encrypted session data in Redis/Valkey 60//! - **Session Expiry** - Automatic cleanup of expired sessions 61//! - **Concurrent Sessions** - Support for multiple device sessions 62//! - **Session Invalidation** - Secure logout and session termination 63//! 64//! ## Example Usage 65//! 66//! ```rust,no_run 67//! use smokesignal::oauth::{pds_resources, OAuthRequest}; 68//! use smokesignal::storage::oauth::model::OAuthRequestState; 69//! 70//! async fn authenticate_user() -> anyhow::Result<()> { 71//! let http_client = reqwest::Client::new(); 72//! let pds_url = "https://bsky.social"; 73//! 74//! // Discover OAuth configuration 75//! let (protected_resource, auth_server) = pds_resources(&http_client, pds_url).await?; 76//! 77//! // Create authorization request 78//! let oauth_request = OAuthRequest::new( 79//! &auth_server, 80//! "https://example.com/callback", 81//! vec!["atproto".to_string()], 82//! )?; 83//! 84//! // Generate authorization URL 85//! let auth_url = oauth_request.authorization_url()?; 86//! println!("Visit: {}", auth_url); 87//! 88//! // After user authorization, exchange code for tokens 89//! // let tokens = oauth_request.exchange_code(authorization_code).await?; 90//! 91//! Ok(()) 92//! } 93//! ``` 94//! 95//! ## Security Considerations 96//! 97//! When implementing OAuth authentication: 98//! - Always use HTTPS for authorization and token endpoints 99//! - Implement proper CSRF protection with state parameters 100//! - Store tokens securely with appropriate encryption 101//! - Use short-lived access tokens with refresh token rotation 102//! - Implement proper session timeout and cleanup 103//! - Validate all tokens and signatures before use 104 105use dpop::DpopRetry; 106use p256::SecretKey; 107use rand::distributions::{Alphanumeric, DistString}; 108use reqwest_chain::ChainMiddleware; 109use reqwest_middleware::ClientBuilder; 110use std::time::Duration; 111 112use crate::oauth_client_errors::OAuthClientError; 113use crate::oauth_errors::{AuthServerValidationError, ResourceValidationError}; 114use model::{AuthorizationServer, OAuthProtectedResource, ParResponse, TokenResponse}; 115 116use crate::{ 117 jose::{ 118 jwt::{Claims, Header, JoseClaims}, 119 mint_token, 120 }, 121 storage::{ 122 handle::model::Handle, 123 oauth::model::{OAuthRequest, OAuthRequestState}, 124 }, 125}; 126 127const HTTP_CLIENT_TIMEOUT_SECS: u64 = 8; 128 129pub async fn pds_resources( 130 http_client: &reqwest::Client, 131 pds: &str, 132) -> Result<(OAuthProtectedResource, AuthorizationServer), OAuthClientError> { 133 let protected_resource = oauth_protected_resource(http_client, pds).await?; 134 135 let first_authorization_server = protected_resource 136 .authorization_servers 137 .first() 138 .ok_or(OAuthClientError::InvalidOAuthProtectedResource)?; 139 140 let authorization_server = 141 oauth_authorization_server(http_client, first_authorization_server).await?; 142 Ok((protected_resource, authorization_server)) 143} 144 145pub async fn oauth_protected_resource( 146 http_client: &reqwest::Client, 147 pds: &str, 148) -> Result<OAuthProtectedResource, OAuthClientError> { 149 let destination = format!("{}/.well-known/oauth-protected-resource", pds); 150 151 let resource: OAuthProtectedResource = http_client 152 .get(destination) 153 .timeout(Duration::from_secs(HTTP_CLIENT_TIMEOUT_SECS)) 154 .send() 155 .await 156 .map_err(OAuthClientError::OAuthProtectedResourceRequestFailed)? 157 .json() 158 .await 159 .map_err(OAuthClientError::MalformedOAuthProtectedResourceResponse)?; 160 161 if resource.resource != pds { 162 return Err(OAuthClientError::InvalidOAuthProtectedResourceResponse( 163 ResourceValidationError::ResourceMustMatchPds.into(), 164 )); 165 } 166 167 if resource.authorization_servers.is_empty() { 168 return Err(OAuthClientError::InvalidOAuthProtectedResourceResponse( 169 ResourceValidationError::AuthorizationServersMustNotBeEmpty.into(), 170 )); 171 } 172 173 Ok(resource) 174} 175 176#[tracing::instrument(skip(http_client), err)] 177pub async fn oauth_authorization_server( 178 http_client: &reqwest::Client, 179 pds: &str, 180) -> Result<AuthorizationServer, OAuthClientError> { 181 let destination = format!("{}/.well-known/oauth-authorization-server", pds); 182 183 let resource: AuthorizationServer = http_client 184 .get(destination) 185 .timeout(Duration::from_secs(HTTP_CLIENT_TIMEOUT_SECS)) 186 .send() 187 .await 188 .map_err(OAuthClientError::AuthorizationServerRequestFailed)? 189 .json() 190 .await 191 .map_err(OAuthClientError::MalformedAuthorizationServerResponse)?; 192 193 // All of this is going to change. 194 195 if resource.issuer != pds { 196 return Err(OAuthClientError::InvalidAuthorizationServerResponse( 197 AuthServerValidationError::IssuerMustMatchPds.into(), 198 )); 199 } 200 201 resource 202 .response_types_supported 203 .iter() 204 .find(|&x| x == "code") 205 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse( 206 AuthServerValidationError::ResponseTypesSupportMustIncludeCode.into(), 207 ))?; 208 209 resource 210 .grant_types_supported 211 .iter() 212 .find(|&x| x == "authorization_code") 213 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse( 214 AuthServerValidationError::GrantTypesSupportMustIncludeAuthorizationCode.into(), 215 ))?; 216 resource 217 .grant_types_supported 218 .iter() 219 .find(|&x| x == "refresh_token") 220 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse( 221 AuthServerValidationError::GrantTypesSupportMustIncludeRefreshToken.into(), 222 ))?; 223 resource 224 .code_challenge_methods_supported 225 .iter() 226 .find(|&x| x == "S256") 227 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse( 228 AuthServerValidationError::CodeChallengeMethodsSupportedMustIncludeS256.into(), 229 ))?; 230 resource 231 .token_endpoint_auth_methods_supported 232 .iter() 233 .find(|&x| x == "none") 234 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse( 235 AuthServerValidationError::TokenEndpointAuthMethodsSupportedMustIncludeNone.into(), 236 ))?; 237 resource 238 .token_endpoint_auth_methods_supported 239 .iter() 240 .find(|&x| x == "private_key_jwt") 241 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse( 242 AuthServerValidationError::TokenEndpointAuthMethodsSupportedMustIncludePrivateKeyJwt 243 .into(), 244 ))?; 245 resource 246 .token_endpoint_auth_signing_alg_values_supported 247 .iter() 248 .find(|&x| x == "ES256") 249 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse( 250 AuthServerValidationError::TokenEndpointAuthSigningAlgValuesMustIncludeES256.into(), 251 ))?; 252 resource 253 .scopes_supported 254 .iter() 255 .find(|&x| x == "atproto") 256 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse( 257 AuthServerValidationError::ScopesSupportedMustIncludeAtProto.into(), 258 ))?; 259 resource 260 .scopes_supported 261 .iter() 262 .find(|&x| x == "transition:generic") 263 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse( 264 AuthServerValidationError::ScopesSupportedMustIncludeTransitionGeneric.into(), 265 ))?; 266 resource 267 .dpop_signing_alg_values_supported 268 .iter() 269 .find(|&x| x == "ES256") 270 .ok_or(OAuthClientError::InvalidAuthorizationServerResponse( 271 AuthServerValidationError::DpopSigningAlgValuesSupportedMustIncludeES256.into(), 272 ))?; 273 274 if !(resource.authorization_response_iss_parameter_supported 275 && resource.require_pushed_authorization_requests 276 && resource.client_id_metadata_document_supported) 277 { 278 return Err(OAuthClientError::InvalidAuthorizationServerResponse( 279 AuthServerValidationError::RequiredServerFeaturesMustBeSupported.into(), 280 )); 281 } 282 283 Ok(resource) 284} 285 286pub async fn oauth_init( 287 http_client: &reqwest::Client, 288 external_url_base: &str, 289 (secret_key_id, secret_key): (&str, SecretKey), 290 dpop_secret_key: &SecretKey, 291 handle: &str, 292 authorization_server: &AuthorizationServer, 293 oauth_request_state: &OAuthRequestState, 294) -> Result<ParResponse, OAuthClientError> { 295 let par_url = authorization_server 296 .pushed_authorization_request_endpoint 297 .clone(); 298 299 let redirect_uri = format!("https://{}/oauth/callback", external_url_base); 300 let client_id = format!("https://{}/oauth/client-metadata.json", external_url_base); 301 302 let scope = "atproto transition:generic".to_string(); 303 304 let client_assertion_header = Header { 305 algorithm: Some("ES256".to_string()), 306 key_id: Some(secret_key_id.to_string()), 307 ..Default::default() 308 }; 309 310 let client_assertion_jti = Alphanumeric.sample_string(&mut rand::thread_rng(), 30); 311 let client_assertion_claims = Claims::new(JoseClaims { 312 issuer: Some(client_id.clone()), 313 subject: Some(client_id.clone()), 314 audience: Some(authorization_server.issuer.clone()), 315 json_web_token_id: Some(client_assertion_jti), 316 issued_at: Some(chrono::Utc::now().timestamp() as u64), 317 ..Default::default() 318 }); 319 tracing::info!("client_assertion_claims: {:?}", client_assertion_claims); 320 321 let client_assertion_token = mint_token( 322 &secret_key, 323 &client_assertion_header, 324 &client_assertion_claims, 325 ) 326 .map_err(|jose_err| OAuthClientError::MintTokenFailed(jose_err.into()))?; 327 328 let now = chrono::Utc::now(); 329 let public_key = dpop_secret_key.public_key(); 330 331 let dpop_proof_header = Header { 332 type_: Some("dpop+jwt".to_string()), 333 algorithm: Some("ES256".to_string()), 334 json_web_key: Some(public_key.to_jwk()), 335 ..Default::default() 336 }; 337 let dpop_proof_jti = Alphanumeric.sample_string(&mut rand::thread_rng(), 30); 338 339 let dpop_proof_claim = Claims::new(JoseClaims { 340 json_web_token_id: Some(dpop_proof_jti), 341 http_method: Some("POST".to_string()), 342 http_uri: Some(par_url.clone()), 343 issued_at: Some(now.timestamp() as u64), 344 expiration: Some((now + chrono::Duration::seconds(30)).timestamp() as u64), 345 ..Default::default() 346 }); 347 let dpop_proof_token = mint_token(dpop_secret_key, &dpop_proof_header, &dpop_proof_claim) 348 .map_err(|jose_err| OAuthClientError::MintTokenFailed(jose_err.into()))?; 349 350 let dpop_retry = DpopRetry::new( 351 dpop_proof_header.clone(), 352 dpop_proof_claim.clone(), 353 dpop_secret_key.clone(), 354 ); 355 356 let dpop_retry_client = ClientBuilder::new(http_client.clone()) 357 .with(ChainMiddleware::new(dpop_retry.clone())) 358 .build(); 359 360 let params = [ 361 ("response_type", "code"), 362 ("code_challenge", &oauth_request_state.code_challenge), 363 ("code_challenge_method", "S256"), 364 ("client_id", client_id.as_str()), 365 ("state", oauth_request_state.state.as_str()), 366 ("redirect_uri", redirect_uri.as_str()), 367 ("scope", scope.as_str()), 368 ("login_hint", handle), 369 ( 370 "client_assertion_type", 371 "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", 372 ), 373 ("client_assertion", client_assertion_token.as_str()), 374 ]; 375 376 tracing::warn!("params: {:?}", params); 377 378 dpop_retry_client 379 .post(par_url) 380 .header("DPoP", dpop_proof_token.as_str()) 381 .form(&params) 382 .timeout(Duration::from_secs(HTTP_CLIENT_TIMEOUT_SECS)) 383 .send() 384 .await 385 .map_err(OAuthClientError::PARMiddlewareRequestFailed)? 386 .json() 387 .await 388 .map_err(OAuthClientError::MalformedPARResponse) 389} 390 391pub async fn oauth_complete( 392 http_client: &reqwest::Client, 393 external_url_base: &str, 394 (secret_key_id, secret_key): (&str, SecretKey), 395 callback_code: &str, 396 oauth_request: &OAuthRequest, 397 handle: &Handle, 398 dpop_secret_key: &SecretKey, 399) -> Result<TokenResponse, OAuthClientError> { 400 let (_, authorization_server) = pds_resources(http_client, &handle.pds).await?; 401 402 let client_assertion_header = Header { 403 algorithm: Some("ES256".to_string()), 404 key_id: Some(secret_key_id.to_string()), 405 ..Default::default() 406 }; 407 408 let client_id = format!("https://{}/oauth/client-metadata.json", external_url_base); 409 let redirect_uri = format!("https://{}/oauth/callback", external_url_base); 410 411 let client_assertion_jti = Alphanumeric.sample_string(&mut rand::thread_rng(), 30); 412 let client_assertion_claims = Claims::new(JoseClaims { 413 issuer: Some(client_id.clone()), 414 subject: Some(client_id.clone()), 415 audience: Some(authorization_server.issuer.clone()), 416 json_web_token_id: Some(client_assertion_jti), 417 issued_at: Some(chrono::Utc::now().timestamp() as u64), 418 ..Default::default() 419 }); 420 421 let client_assertion_token = mint_token( 422 &secret_key, 423 &client_assertion_header, 424 &client_assertion_claims, 425 ) 426 .map_err(|jose_err| OAuthClientError::MintTokenFailed(jose_err.into()))?; 427 428 let params = [ 429 ("client_id", client_id.as_str()), 430 ("redirect_uri", redirect_uri.as_str()), 431 ("grant_type", "authorization_code"), 432 ("code", callback_code), 433 ("code_verifier", &oauth_request.pkce_verifier), 434 ( 435 "client_assertion_type", 436 "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", 437 ), 438 ("client_assertion", client_assertion_token.as_str()), 439 ]; 440 441 let public_key = dpop_secret_key.public_key(); 442 443 let token_endpoint = authorization_server.token_endpoint.clone(); 444 445 let now = chrono::Utc::now(); 446 447 let dpop_proof_header = Header { 448 type_: Some("dpop+jwt".to_string()), 449 algorithm: Some("ES256".to_string()), 450 json_web_key: Some(public_key.to_jwk()), 451 ..Default::default() 452 }; 453 let dpop_proof_jti = Alphanumeric.sample_string(&mut rand::thread_rng(), 30); 454 let dpop_proof_claim = Claims::new(JoseClaims { 455 json_web_token_id: Some(dpop_proof_jti), 456 http_method: Some("POST".to_string()), 457 http_uri: Some(authorization_server.token_endpoint.clone()), 458 issued_at: Some(now.timestamp() as u64), 459 expiration: Some((now + chrono::Duration::seconds(30)).timestamp() as u64), 460 ..Default::default() 461 }); 462 let dpop_proof_token = mint_token(dpop_secret_key, &dpop_proof_header, &dpop_proof_claim) 463 .map_err(|jose_err| OAuthClientError::MintTokenFailed(jose_err.into()))?; 464 465 let dpop_retry = DpopRetry::new( 466 dpop_proof_header.clone(), 467 dpop_proof_claim.clone(), 468 dpop_secret_key.clone(), 469 ); 470 471 let dpop_retry_client = ClientBuilder::new(http_client.clone()) 472 .with(ChainMiddleware::new(dpop_retry.clone())) 473 .build(); 474 475 dpop_retry_client 476 .post(token_endpoint) 477 .header("DPoP", dpop_proof_token.as_str()) 478 .form(&params) 479 .timeout(Duration::from_secs(HTTP_CLIENT_TIMEOUT_SECS)) 480 .send() 481 .await 482 .map_err(OAuthClientError::TokenMiddlewareRequestFailed)? 483 .json() 484 .await 485 .map_err(OAuthClientError::MalformedTokenResponse) 486} 487 488pub async fn client_oauth_refresh( 489 http_client: &reqwest::Client, 490 external_url_base: &str, 491 (secret_key_id, secret_key): (&str, SecretKey), 492 refresh_token: &str, 493 handle: &Handle, 494 dpop_secret_key: &SecretKey, 495) -> Result<TokenResponse, OAuthClientError> { 496 let (_, authorization_server) = pds_resources(http_client, &handle.pds).await?; 497 498 let client_assertion_header = Header { 499 algorithm: Some("ES256".to_string()), 500 key_id: Some(secret_key_id.to_string()), 501 ..Default::default() 502 }; 503 504 let client_id = format!("https://{}/oauth/client-metadata.json", external_url_base); 505 let redirect_uri = format!("https://{}/oauth/callback", external_url_base); 506 507 let client_assertion_jti = Alphanumeric.sample_string(&mut rand::thread_rng(), 30); 508 let client_assertion_claims = Claims::new(JoseClaims { 509 issuer: Some(client_id.clone()), 510 subject: Some(client_id.clone()), 511 audience: Some(authorization_server.issuer.clone()), 512 json_web_token_id: Some(client_assertion_jti), 513 issued_at: Some(chrono::Utc::now().timestamp() as u64), 514 ..Default::default() 515 }); 516 517 let client_assertion_token = mint_token( 518 &secret_key, 519 &client_assertion_header, 520 &client_assertion_claims, 521 ) 522 .map_err(|jose_err| OAuthClientError::MintTokenFailed(jose_err.into()))?; 523 524 let params = [ 525 ("client_id", client_id.as_str()), 526 ("redirect_uri", redirect_uri.as_str()), 527 ("grant_type", "refresh_token"), 528 ("refresh_token", refresh_token), 529 ( 530 "client_assertion_type", 531 "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", 532 ), 533 ("client_assertion", client_assertion_token.as_str()), 534 ]; 535 536 tracing::info!("params: {:?}", params); 537 538 let public_key = dpop_secret_key.public_key(); 539 540 let token_endpoint = authorization_server.token_endpoint.clone(); 541 542 let now = chrono::Utc::now(); 543 544 let dpop_proof_header = Header { 545 type_: Some("dpop+jwt".to_string()), 546 algorithm: Some("ES256".to_string()), 547 json_web_key: Some(public_key.to_jwk()), 548 ..Default::default() 549 }; 550 let dpop_proof_jti = Alphanumeric.sample_string(&mut rand::thread_rng(), 30); 551 let dpop_proof_claim = Claims::new(JoseClaims { 552 json_web_token_id: Some(dpop_proof_jti), 553 http_method: Some("POST".to_string()), 554 http_uri: Some(authorization_server.token_endpoint.clone()), 555 issued_at: Some(now.timestamp() as u64), 556 expiration: Some((now + chrono::Duration::seconds(30)).timestamp() as u64), 557 ..Default::default() 558 }); 559 let dpop_proof_token = mint_token(dpop_secret_key, &dpop_proof_header, &dpop_proof_claim) 560 .map_err(|jose_err| OAuthClientError::MintTokenFailed(jose_err.into()))?; 561 562 let dpop_retry = DpopRetry::new( 563 dpop_proof_header.clone(), 564 dpop_proof_claim.clone(), 565 dpop_secret_key.clone(), 566 ); 567 568 let dpop_retry_client = ClientBuilder::new(http_client.clone()) 569 .with(ChainMiddleware::new(dpop_retry.clone())) 570 .build(); 571 572 dpop_retry_client 573 .post(token_endpoint) 574 .header("DPoP", dpop_proof_token.as_str()) 575 .form(&params) 576 .timeout(Duration::from_secs(HTTP_CLIENT_TIMEOUT_SECS)) 577 .send() 578 .await 579 .map_err(OAuthClientError::TokenMiddlewareRequestFailed)? 580 .json() 581 .await 582 .map_err(OAuthClientError::MalformedTokenResponse) 583} 584 585pub mod dpop { 586 use p256::SecretKey; 587 use reqwest::header::HeaderValue; 588 use reqwest_chain::Chainer; 589 use serde::Deserialize; 590 591 use crate::{ 592 jose::{ 593 jwt::{Claims, Header}, 594 mint_token, 595 }, 596 jose_errors::JoseError, 597 }; 598 599 #[derive(Clone, Debug, Deserialize)] 600 pub struct SimpleError { 601 pub error: Option<String>, 602 pub error_description: Option<String>, 603 pub message: Option<String>, 604 } 605 606 impl std::fmt::Display for SimpleError { 607 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 608 if let Some(value) = &self.error { 609 write!(f, "{}", value) 610 } else if let Some(value) = &self.message { 611 write!(f, "{}", value) 612 } else if let Some(value) = &self.error_description { 613 write!(f, "{}", value) 614 } else { 615 write!(f, "unknown") 616 } 617 } 618 } 619 620 #[derive(Clone)] 621 pub struct DpopRetry { 622 pub header: Header, 623 pub claims: Claims, 624 pub secret: SecretKey, 625 } 626 627 impl DpopRetry { 628 pub fn new(header: Header, claims: Claims, secret: SecretKey) -> Self { 629 DpopRetry { 630 header, 631 claims, 632 secret, 633 } 634 } 635 } 636 637 #[async_trait::async_trait] 638 impl Chainer for DpopRetry { 639 type State = (); 640 641 async fn chain( 642 &self, 643 result: Result<reqwest::Response, reqwest_middleware::Error>, 644 _state: &mut Self::State, 645 request: &mut reqwest::Request, 646 ) -> Result<Option<reqwest::Response>, reqwest_middleware::Error> { 647 let response = result?; 648 649 let status_code = response.status(); 650 651 if status_code != 400 && status_code != 401 { 652 return Ok(Some(response)); 653 }; 654 655 let headers = response.headers().clone(); 656 657 let simple_error = response.json::<SimpleError>().await; 658 if simple_error.is_err() { 659 return Err(reqwest_middleware::Error::Middleware( 660 JoseError::UnableToParseSimpleError.into(), 661 )); 662 } 663 664 let simple_error = simple_error.unwrap(); 665 666 tracing::error!("dpop error: {:?}", simple_error); 667 668 let is_use_dpop_nonce_error = simple_error 669 .clone() 670 .error 671 .is_some_and(|error_value| error_value == "use_dpop_nonce"); 672 673 if !is_use_dpop_nonce_error { 674 return Err(reqwest_middleware::Error::Middleware( 675 JoseError::UnexpectedError(simple_error.to_string()).into(), 676 )); 677 } 678 679 let dpop_header = headers.get("DPoP-Nonce"); 680 681 if dpop_header.is_none() { 682 return Err(reqwest_middleware::Error::Middleware( 683 JoseError::MissingDpopHeader.into(), 684 )); 685 } 686 687 let new_dpop_header = dpop_header.unwrap().to_str().map_err(|dpop_header_err| { 688 reqwest_middleware::Error::Middleware( 689 JoseError::UnableToParseDpopHeader(dpop_header_err.to_string()).into(), 690 ) 691 })?; 692 693 let dpop_proof_header = self.header.clone(); 694 let mut dpop_proof_claim = self.claims.clone(); 695 dpop_proof_claim 696 .private 697 .insert("nonce".to_string(), new_dpop_header.to_string().into()); 698 699 let dpop_proof_token = mint_token(&self.secret, &dpop_proof_header, &dpop_proof_claim) 700 .map_err(|dpop_proof_token_err| { 701 reqwest_middleware::Error::Middleware( 702 JoseError::UnableToMintDpopProofToken(dpop_proof_token_err.to_string()) 703 .into(), 704 ) 705 })?; 706 707 request.headers_mut().insert( 708 "DPoP", 709 HeaderValue::from_str(&dpop_proof_token).expect("invalid header value"), 710 ); 711 Ok(None) 712 } 713 } 714} 715 716pub mod model { 717 use serde::Deserialize; 718 719 #[derive(Clone, Deserialize)] 720 pub struct OAuthProtectedResource { 721 pub resource: String, 722 pub authorization_servers: Vec<String>, 723 pub scopes_supported: Vec<String>, 724 pub bearer_methods_supported: Vec<String>, 725 } 726 727 #[derive(Clone, Deserialize, Default, Debug)] 728 pub struct AuthorizationServer { 729 pub introspection_endpoint: String, 730 pub authorization_endpoint: String, 731 pub authorization_response_iss_parameter_supported: bool, 732 pub client_id_metadata_document_supported: bool, 733 pub code_challenge_methods_supported: Vec<String>, 734 pub dpop_signing_alg_values_supported: Vec<String>, 735 pub grant_types_supported: Vec<String>, 736 pub issuer: String, 737 pub pushed_authorization_request_endpoint: String, 738 pub request_parameter_supported: bool, 739 pub require_pushed_authorization_requests: bool, 740 pub response_types_supported: Vec<String>, 741 pub scopes_supported: Vec<String>, 742 pub token_endpoint_auth_methods_supported: Vec<String>, 743 pub token_endpoint_auth_signing_alg_values_supported: Vec<String>, 744 pub token_endpoint: String, 745 746 // Additional fields that may be present in the server response 747 #[serde(default)] 748 pub jwks_uri: Option<String>, 749 #[serde(default)] 750 pub revocation_endpoint: Option<String>, 751 #[serde(default)] 752 pub protected_resources: Option<Vec<String>>, 753 #[serde(default)] 754 pub request_object_signing_alg_values_supported: Option<Vec<String>>, 755 #[serde(default)] 756 pub request_object_encryption_alg_values_supported: Option<Vec<String>>, 757 #[serde(default)] 758 pub request_object_encryption_enc_values_supported: Option<Vec<String>>, 759 #[serde(default)] 760 pub subject_types_supported: Option<Vec<String>>, 761 #[serde(default)] 762 pub response_modes_supported: Option<Vec<String>>, 763 #[serde(default)] 764 pub ui_locales_supported: Option<Vec<String>>, 765 #[serde(default)] 766 pub display_values_supported: Option<Vec<String>>, 767 #[serde(default)] 768 pub request_uri_parameter_supported: Option<bool>, 769 #[serde(default)] 770 pub require_request_uri_registration: Option<bool>, 771 } 772 773 #[derive(Clone, Deserialize)] 774 pub struct ParResponse { 775 pub request_uri: String, 776 pub expires_in: u64, 777 } 778 779 #[derive(Clone, Deserialize)] 780 pub struct TokenResponse { 781 pub access_token: String, 782 pub token_type: String, 783 pub refresh_token: String, 784 pub scope: String, 785 pub expires_in: u32, 786 pub sub: String, 787 } 788} 789 790// This errors module is now deprecated. 791// Use crate::oauth_client_errors::OAuthClientError instead. 792pub mod errors { 793 pub use crate::oauth_client_errors::OAuthClientError; 794}