Something went wrong. Try again.
A fork of @slices.network/slices forked from slices.network/slices
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256//! 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<String>,}
/// 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<String, StatusCode> { 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 serverpub async fn verify_oauth_token_cached( token: &str, auth_base_url: &str, cache: Option<Arc<Mutex<SliceCache>>>,) -> Result<UserInfoResponse, StatusCode> { // 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 JWKpub async fn get_atproto_auth_for_user_cached( token: &str, auth_base_url: &str, cache: Option<Arc<Mutex<SliceCache>>>,) -> 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))}