auth.rs 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. //! Authentication providers for NpubCash API
  2. //!
  3. //! Implements NIP-98 and JWT authentication
  4. use std::sync::Arc;
  5. use std::time::{Duration, SystemTime};
  6. use base64::Engine;
  7. use cdk_http_client::RequestBuilderExt;
  8. use nostr_sdk::{EventBuilder, Keys, Kind, Tag};
  9. use tokio::sync::RwLock;
  10. use crate::types::Nip98Response;
  11. use crate::{Error, Result};
  12. #[derive(Debug)]
  13. struct CachedToken {
  14. token: String,
  15. expires_at: SystemTime,
  16. }
  17. /// JWT authentication provider using NIP-98
  18. #[derive(Debug)]
  19. pub struct JwtAuthProvider {
  20. base_url: String,
  21. keys: Keys,
  22. http_client: cdk_common::HttpClient,
  23. cached_token: Arc<RwLock<Option<CachedToken>>>,
  24. }
  25. impl JwtAuthProvider {
  26. /// Create a new JWT authentication provider
  27. ///
  28. /// # Arguments
  29. ///
  30. /// * `base_url` - Base URL of the NpubCash service
  31. /// * `keys` - Nostr keys for signing NIP-98 tokens
  32. pub fn new(base_url: String, keys: Keys) -> Self {
  33. Self {
  34. base_url,
  35. keys,
  36. http_client: cdk_common::HttpClient::new(),
  37. cached_token: Arc::new(RwLock::new(None)),
  38. }
  39. }
  40. /// Ensure we have a valid cached JWT token, fetching a new one if needed
  41. ///
  42. /// This method checks the cache first and returns the cached token if it's still valid.
  43. /// If the cache is empty or expired, it fetches a new JWT token from the API.
  44. ///
  45. /// # Errors
  46. ///
  47. /// Returns an error if token generation or API request fails
  48. async fn ensure_cached_token(&self) -> Result<String> {
  49. // Check if we have a valid cached token
  50. if let Some(token) = self.get_valid_cached_token().await {
  51. return Ok(token);
  52. }
  53. // Fetch a new JWT token from the API
  54. let token = self.fetch_fresh_jwt_token().await?;
  55. // Cache the new token
  56. self.cache_token(&token).await;
  57. Ok(token)
  58. }
  59. /// Get a valid token from cache, if one exists and hasn't expired
  60. async fn get_valid_cached_token(&self) -> Option<String> {
  61. let cache = self.cached_token.read().await;
  62. cache.as_ref().and_then(|cached| {
  63. if cached.expires_at > SystemTime::now() {
  64. Some(cached.token.clone())
  65. } else {
  66. None
  67. }
  68. })
  69. }
  70. /// Fetch a fresh JWT token from the NpubCash API using NIP-98 authentication
  71. async fn fetch_fresh_jwt_token(&self) -> Result<String> {
  72. let auth_url = format!("{}/api/v2/auth/nip98", self.base_url);
  73. // Create NIP-98 token for authentication
  74. let nostr_token = self.create_nip98_token_with_logging(&auth_url)?;
  75. // Send authentication request
  76. let response = self.send_auth_request(&auth_url, &nostr_token).await?;
  77. // Parse and validate response
  78. self.parse_jwt_response(response).await
  79. }
  80. /// Create a NIP-98 token with debug logging
  81. fn create_nip98_token_with_logging(&self, auth_url: &str) -> Result<String> {
  82. tracing::debug!("Creating NIP-98 token for URL: {}", auth_url);
  83. let nostr_token = self.create_nip98_token(auth_url, "GET")?;
  84. tracing::debug!(
  85. "NIP-98 token created (first 50 chars): {}",
  86. &nostr_token[..50.min(nostr_token.len())]
  87. );
  88. Ok(nostr_token)
  89. }
  90. /// Send the authentication request to the API
  91. async fn send_auth_request(
  92. &self,
  93. auth_url: &str,
  94. nostr_token: &str,
  95. ) -> Result<cdk_common::RawResponse> {
  96. tracing::debug!("Sending request to: {}", auth_url);
  97. tracing::debug!(
  98. "Authorization header: Nostr {}",
  99. &nostr_token[..50.min(nostr_token.len())]
  100. );
  101. let response = self
  102. .http_client
  103. .get(auth_url)
  104. .header("Authorization", format!("Nostr {nostr_token}"))
  105. .header("Content-Type", "application/json")
  106. .header("Accept", "application/json")
  107. .header("User-Agent", "cdk-npubcash/0.13.0")
  108. .send()
  109. .await?;
  110. tracing::debug!("Response status: {}", response.status());
  111. Ok(response)
  112. }
  113. /// Parse the JWT response from the API
  114. async fn parse_jwt_response(&self, response: cdk_common::RawResponse) -> Result<String> {
  115. let status = response.status();
  116. if !response.is_success() {
  117. let error_text = response.text().await.unwrap_or_default();
  118. tracing::error!("Auth failed - Status: {}, Body: {}", status, error_text);
  119. return Err(Error::Auth(format!(
  120. "Failed to get JWT: {status} - {error_text}"
  121. )));
  122. }
  123. let nip98_response: Nip98Response = response.json().await?;
  124. Ok(nip98_response.data.token)
  125. }
  126. /// Cache the JWT token with a 5-minute expiration
  127. async fn cache_token(&self, token: &str) {
  128. let expires_at = SystemTime::now() + Duration::from_secs(5 * 60);
  129. let mut cache = self.cached_token.write().await;
  130. *cache = Some(CachedToken {
  131. token: token.to_string(),
  132. expires_at,
  133. });
  134. }
  135. fn create_nip98_token(&self, url: &str, method: &str) -> Result<String> {
  136. let u_tag = Tag::custom(
  137. nostr_sdk::TagKind::Custom(std::borrow::Cow::Borrowed("u")),
  138. vec![url],
  139. );
  140. let method_tag = Tag::custom(
  141. nostr_sdk::TagKind::Custom(std::borrow::Cow::Borrowed("method")),
  142. vec![method],
  143. );
  144. let event = EventBuilder::new(Kind::Custom(27235), "")
  145. .tags(vec![u_tag, method_tag])
  146. .sign_with_keys(&self.keys)
  147. .map_err(|e| Error::Nostr(e.to_string()))?;
  148. let json = serde_json::to_string(&event)?;
  149. tracing::debug!("NIP-98 event JSON: {}", json);
  150. let encoded = base64::engine::general_purpose::STANDARD.encode(json);
  151. tracing::debug!("Base64 encoded token length: {}", encoded.len());
  152. Ok(encoded)
  153. }
  154. /// Get a Bearer token for authenticated requests
  155. ///
  156. /// # Arguments
  157. ///
  158. /// * `_url` - The URL being accessed (unused, kept for future extensibility)
  159. /// * `_method` - The HTTP method being used (unused, kept for future extensibility)
  160. ///
  161. /// # Errors
  162. ///
  163. /// Returns an error if token generation or fetching fails
  164. pub async fn get_auth_token(&self, _url: &str, _method: &str) -> Result<String> {
  165. let token = self.ensure_cached_token().await?;
  166. Ok(format!("Bearer {token}"))
  167. }
  168. /// Get a NIP-98 auth header for direct authentication
  169. ///
  170. /// This creates a fresh NIP-98 signed event for the specific URL and method,
  171. /// returning the full Authorization header value (e.g., "Nostr <base64_event>").
  172. ///
  173. /// # Arguments
  174. ///
  175. /// * `url` - The URL being accessed
  176. /// * `method` - The HTTP method being used (GET, POST, PATCH, etc.)
  177. ///
  178. /// # Errors
  179. ///
  180. /// Returns an error if token generation fails
  181. pub fn get_nip98_auth_header(&self, url: &str, method: &str) -> Result<String> {
  182. let token = self.create_nip98_token(url, method)?;
  183. Ok(format!("Nostr {token}"))
  184. }
  185. }