//! Authentication and authorization utilities for OAuth and AT Protocol. //! //! This module provides functions for: //! - Extracting and validating OAuth bearer tokens //! - Verifying tokens with the authorization server //! - Managing AT Protocol DPoP (Demonstrating Proof-of-Possession) authentication //! - Caching authentication state for performance (5-minute TTL) use crate::cache::SliceCache; use atproto_client::client::DPoPAuth; use atproto_identity::key::KeyData; use atproto_oauth::jwk::WrappedJsonWebKey; use axum::http::{HeaderMap, StatusCode}; use serde::{Deserialize, Serialize}; use std::sync::Arc; use tokio::sync::Mutex; /// OAuth userinfo response containing the authenticated user's identity. #[derive(Serialize, Deserialize, Debug)] pub struct UserInfoResponse { /// Subject identifier (user ID) from the OAuth provider pub sub: String, /// Decentralized identifier for the user in AT Protocol pub did: Option, } /// Cached AT Protocol session data to avoid repeated auth server requests. #[derive(Serialize, Deserialize, Debug, Clone)] struct CachedSession { /// Personal Data Server endpoint URL for the user pds_url: String, /// AT Protocol access token for PDS operations atproto_access_token: String, /// DPoP JSON Web Key for proof-of-possession dpop_jwk: serde_json::Value, } /// Extracts the bearer token from the Authorization header. /// /// # Arguments /// * `headers` - HTTP request headers /// /// # Returns /// * `Ok(String)` - The extracted bearer token /// * `Err(StatusCode::UNAUTHORIZED)` - If the header is missing, malformed, or not a Bearer token /// /// # Example /// ```ignore /// let token = extract_bearer_token(&headers)?; /// ``` pub fn extract_bearer_token(headers: &HeaderMap) -> Result { let auth_header = headers .get("authorization") .and_then(|h| h.to_str().ok()) .ok_or(StatusCode::UNAUTHORIZED)?; if !auth_header.starts_with("Bearer ") { return Err(StatusCode::UNAUTHORIZED); } // Safe to unwrap since we just verified the prefix exists let token = auth_header.strip_prefix("Bearer ").unwrap().to_string(); Ok(token) } /// Verifies an OAuth bearer token with the authorization server. /// /// This function first checks the cache for a previously validated token to avoid /// unnecessary network calls. If not found in cache, it validates with the auth server /// and caches the result for 5 minutes. /// /// # Arguments /// * `token` - The OAuth bearer token to verify /// * `auth_base_url` - Base URL of the authorization server /// * `cache` - Optional cache instance (falls back to direct verification if None) /// /// # Returns /// * `Ok(UserInfoResponse)` - User information if the token is valid /// * `Err(StatusCode)` - HTTP status code indicating the failure reason /// - `UNAUTHORIZED` - Invalid or expired token /// - `INTERNAL_SERVER_ERROR` - Network or parsing errors /// /// # Cache Behavior /// - Cache key format: `oauth_userinfo:{token}` /// - TTL: 300 seconds (5 minutes) /// - Cache miss triggers verification with auth server pub async fn verify_oauth_token_cached( token: &str, auth_base_url: &str, cache: Option>>, ) -> Result { // Try cache first if provided to avoid network round-trip if let Some(cache) = &cache { let cached_result = { let mut cache_lock = cache.lock().await; cache_lock.get_cached_oauth_userinfo(token).await }; if let Ok(Some(user_info_value)) = cached_result { let user_info: UserInfoResponse = serde_json::from_value(user_info_value) .map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?; return Ok(user_info); } } // Cache miss - verify token by calling the OAuth userinfo endpoint let client = reqwest::Client::new(); let userinfo_url = format!("{}/oauth/userinfo", auth_base_url); let response = client .get(&userinfo_url) .header("Authorization", format!("Bearer {}", token)) .send() .await .map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?; if !response.status().is_success() { return Err(StatusCode::UNAUTHORIZED); } let user_info: UserInfoResponse = response .json() .await .map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?; // Cache the validated userinfo for 5 minutes to improve performance if let Some(cache) = &cache { let user_info_value = serde_json::to_value(&user_info).map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?; let mut cache_lock = cache.lock().await; let _ = cache_lock .cache_oauth_userinfo(token, &user_info_value, 300) .await; } Ok(user_info) } /// Retrieves AT Protocol DPoP authentication credentials and PDS URL for a user. /// /// DPoP (Demonstrating Proof-of-Possession) is a security mechanism that binds tokens /// to specific cryptographic keys, preventing token theft and replay attacks. /// /// This function first checks the cache for existing credentials, then falls back to /// fetching from the auth server if needed. Results are cached for 5 minutes. /// /// # Arguments /// * `token` - OAuth bearer token identifying the user /// * `auth_base_url` - Base URL of the authorization server /// * `cache` - Optional cache instance (falls back to direct fetch if None) /// /// # Returns /// * `Ok((DPoPAuth, String))` - Tuple of (DPoP authentication object, PDS endpoint URL) /// * `Err(StatusCode)` - HTTP status code indicating the failure reason /// - `UNAUTHORIZED` - Invalid token or session expired /// - `INTERNAL_SERVER_ERROR` - Network, parsing, or key conversion errors /// /// # Cache Behavior /// - Cache key format: `atproto_session:{token}` /// - TTL: 300 seconds (5 minutes) /// - Stores serialized CachedSession with PDS URL, access token, and DPoP JWK pub async fn get_atproto_auth_for_user_cached( token: &str, auth_base_url: &str, cache: Option>>, ) -> Result<(DPoPAuth, String), StatusCode> { // Try cache first if provided to avoid expensive auth server call if let Some(cache) = &cache { let cached_result = { let mut cache_lock = cache.lock().await; cache_lock.get_cached_atproto_session(token).await }; if let Ok(Some(session_value)) = cached_result { let cached_session: CachedSession = serde_json::from_value(session_value) .map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?; // Reconstruct DPoP auth from cached session data let dpop_jwk: WrappedJsonWebKey = serde_json::from_value(cached_session.dpop_jwk) .map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?; let dpop_private_key_data = KeyData::try_from(dpop_jwk).map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?; let dpop_auth = DPoPAuth { dpop_private_key_data, oauth_access_token: cached_session.atproto_access_token, }; return Ok((dpop_auth, cached_session.pds_url)); } } // Cache miss - fetch fresh session data from auth server let client = reqwest::Client::new(); let session_url = format!("{}/api/atprotocol/session", auth_base_url); let session_response = client .get(&session_url) .header("Authorization", format!("Bearer {}", token)) .send() .await .map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?; if !session_response.status().is_success() { return Err(StatusCode::UNAUTHORIZED); } let session_data: serde_json::Value = session_response .json() .await .map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?; // Extract the user's Personal Data Server endpoint URL let pds_url = session_data["pds_endpoint"] .as_str() .ok_or(StatusCode::INTERNAL_SERVER_ERROR)? .to_string(); // Extract the access token used for authenticating with the PDS let atproto_access_token = session_data["access_token"] .as_str() .ok_or(StatusCode::INTERNAL_SERVER_ERROR)? .to_string(); // Extract and convert the DPoP JSON Web Key to internal key representation let dpop_jwk_value = session_data["dpop_jwk"].clone(); let dpop_jwk: WrappedJsonWebKey = serde_json::from_value(dpop_jwk_value.clone()) .map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?; let dpop_private_key_data = KeyData::try_from(dpop_jwk).map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?; let dpop_auth = DPoPAuth { dpop_private_key_data, oauth_access_token: atproto_access_token.clone(), }; // Cache the complete session for 5 minutes to avoid repeated auth server calls if let Some(cache) = &cache { let cached_session = CachedSession { pds_url: pds_url.clone(), atproto_access_token, dpop_jwk: dpop_jwk_value, }; let session_value = serde_json::to_value(&cached_session) .map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?; let mut cache_lock = cache.lock().await; let _ = cache_lock .cache_atproto_session(token, &session_value, 300) .await; } Ok((dpop_auth, pds_url)) }