diff --git a/Cargo.lock b/Cargo.lock index 4026376..6937cf2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -500,8 +500,10 @@ dependencies = [ "serde_json", "thiserror 2.0.18", "tokio", + "tracing", "url", "web-sys", + "zeroize", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0341718..89e98a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ atproto-lexicon = { version = "0.15.0-alpha.2", path = "crates/atproto-lexicon" atproto-oauth = { version = "0.15.0-alpha.2", path = "crates/atproto-oauth" } atproto-oauth-aip = { version = "0.15.0-alpha.2", path = "crates/atproto-oauth-aip" } atproto-oauth-axum = { version = "0.15.0-alpha.2", path = "crates/atproto-oauth-axum" } +atproto-oauth-dioxus = { version = "0.15.0-alpha.2", path = "crates/atproto-oauth-dioxus" } atproto-pds = { version = "0.15.0-alpha.2", path = "crates/atproto-pds" } atproto-record = { version = "0.15.0-alpha.2", path = "crates/atproto-record" } atproto-repo = { version = "0.15.0-alpha.2", path = "crates/atproto-repo" } diff --git a/crates/atproto-oauth-dioxus/Cargo.toml b/crates/atproto-oauth-dioxus/Cargo.toml index 1cf9c16..c1c4efa 100644 --- a/crates/atproto-oauth-dioxus/Cargo.toml +++ b/crates/atproto-oauth-dioxus/Cargo.toml @@ -17,13 +17,16 @@ categories.workspace = true [dependencies] dioxus = { version = "0.7.1", features = ["router", "fullstack"] } -serde = { workspace = true, features = ["derive"] } +serde.workspace = true serde_json.workspace = true thiserror.workspace = true +tracing.workspace = true url = "2.5" web-sys = { version = "0.3", features = ["Window", "Location", "Storage"] } +zeroize = { workspace = true, optional = true } + atproto-identity = { workspace = true, optional = true } atproto-oauth = { workspace = true, optional = true } reqwest = { workspace = true, optional = true } @@ -52,6 +55,11 @@ hickory-dns = [ "atproto-identity?/hickory-dns", "atproto-oauth?/hickory-dns", ] +zeroize = [ + "dep:zeroize", + "atproto-identity?/zeroize", + "atproto-oauth?/zeroize", +] [lints] workspace = true diff --git a/crates/atproto-oauth-dioxus/src/server.rs b/crates/atproto-oauth-dioxus/src/server.rs index afc2431..dd9c5a9 100644 --- a/crates/atproto-oauth-dioxus/src/server.rs +++ b/crates/atproto-oauth-dioxus/src/server.rs @@ -9,6 +9,9 @@ use atproto_oauth::workflow::{ }; use p256::SecretKey; +#[cfg(feature = "zeroize")] +use zeroize::{Zeroize, ZeroizeOnDrop}; + use crate::errors::DioxusOAuthError; use crate::types::SessionData; @@ -41,6 +44,7 @@ struct StoredOAuthState { /// can retrieve this session to make DPoP-authenticated API calls to the /// user's PDS on their behalf. #[derive(Clone)] +#[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))] #[allow(dead_code)] pub struct ActiveSession { /// The user's decentralized identifier (DID). @@ -73,13 +77,16 @@ fn get_or_generate_signing_key() -> Result { let trimmed = seed_hex.trim(); if !trimmed.is_empty() { let seed = hex::decode(trimmed) + .inspect_err(|e| tracing::error!(error = ?e, "Invalid OAUTH_KEY_SEED hex")) .map_err(|e| DioxusOAuthError::InvalidKeySeed(format!("Invalid hex: {}", e)))?; let seed: [u8; 32] = seed.try_into().map_err(|_| { + tracing::error!("OAUTH_KEY_SEED must be exactly 32 bytes (64 hex chars)"); DioxusOAuthError::InvalidKeySeed( "OAUTH_KEY_SEED must be exactly 32 bytes (64 hex chars)".to_string(), ) })?; let sk = SecretKey::from_slice(&seed).map_err(|_| { + tracing::error!("OAUTH_KEY_SEED is not a valid P-256 private key"); DioxusOAuthError::InvalidKeySeed( "OAUTH_KEY_SEED is not a valid P-256 private key".to_string(), ) @@ -88,17 +95,22 @@ fn get_or_generate_signing_key() -> Result { } } generate_key(KeyType::P256Private) + .inspect_err(|e| tracing::error!(error = ?e, "Failed to generate OAuth signing key")) .map_err(|e| DioxusOAuthError::KeyInitializationFailed(e.to_string())) } /// Derives the public key JWKS for the signing key. pub fn signing_key_jwks() -> Result { let public_key = to_public(&SIGNING_KEY) + .inspect_err(|e| tracing::error!(error = ?e, "Failed to derive public key from signing key")) .map_err(|e| DioxusOAuthError::PublicKeyDerivationFailed(e.to_string()))?; let jwk = atproto_oauth::jwk::generate(&public_key) + .inspect_err(|e| tracing::error!(error = ?e, "Failed to generate JWK")) .map_err(|e| DioxusOAuthError::JwkGenerationFailed(e.to_string()))?; let jwks = atproto_oauth::jwk::WrappedJsonWebKeySet { keys: vec![jwk] }; - serde_json::to_value(jwks).map_err(|e| DioxusOAuthError::JwkGenerationFailed(e.to_string())) + serde_json::to_value(jwks) + .inspect_err(|e| tracing::error!(error = ?e, "Failed to serialize JWKS")) + .map_err(|e| DioxusOAuthError::JwkGenerationFailed(e.to_string())) } fn generate_random_hex(len: usize) -> String { @@ -140,18 +152,21 @@ pub async fn init_oauth(handle: String) -> Result { let doc = identity_resolver .resolve(&handle) .await + .inspect_err(|e| tracing::error!(error = ?e, "Failed to resolve handle: {}", &handle)) .map_err(|e| DioxusOAuthError::HandleResolutionFailed(e.to_string()))?; let pds_url = doc .pds_endpoints() .first() .ok_or_else(|| { + tracing::error!("No PDS endpoints in DID document for handle: {}", &handle); DioxusOAuthError::PdsResolutionFailed("No PDS endpoints in DID document".to_string()) })? .to_string(); let (_protected, auth_server) = pds_resources(&http_client, &pds_url) .await + .inspect_err(|e| tracing::error!(error = ?e, "Failed to discover PDS resources at: {}", &pds_url)) .map_err(|e| DioxusOAuthError::PdsResourceDiscoveryFailed(e.to_string()))?; let base = base_url(); @@ -173,6 +188,7 @@ pub async fn init_oauth(handle: String) -> Result { let signing_key = get_signing_key().clone(); let dpop_key = generate_key(KeyType::P256Private) + .inspect_err(|e| tracing::error!(error = ?e, "Failed to generate DPoP key")) .map_err(|e| DioxusOAuthError::KeyInitializationFailed(e.to_string()))?; let oauth_client = OAuthClient { @@ -197,6 +213,7 @@ pub async fn init_oauth(handle: String) -> Result { &oauth_request_state, ) .await + .inspect_err(|e| tracing::error!(error = ?e, "OAuth authorization initiation failed for handle: {}", &handle)) .map_err(|e| DioxusOAuthError::OAuthInitFailed(e.to_string()))?; let oauth_request = OAuthRequest { @@ -245,7 +262,10 @@ pub async fn complete_oauth(code: String, state: String) -> Result Result Result