Heavily customized version of smokesignal - https://whtwnd.com/kayrozen.com/3lpwe4ymowg2t
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(¶ms)
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(¶ms)
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(¶ms)
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}