auth.rs 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. use std::str::FromStr;
  2. use axum::extract::{FromRequestParts, State};
  3. use axum::http::request::Parts;
  4. use axum::http::StatusCode;
  5. use axum::response::Response;
  6. use axum::routing::{get, post};
  7. use axum::{Json, Router};
  8. #[cfg(feature = "swagger")]
  9. use cdk::error::ErrorResponse;
  10. use cdk::nuts::{
  11. AuthToken, BlindAuthToken, KeysResponse, KeysetResponse, MintAuthRequest, MintResponse,
  12. };
  13. use serde::{Deserialize, Serialize};
  14. #[cfg(feature = "auth")]
  15. use crate::{get_keyset_pubkeys, into_response, MintState};
  16. const CLEAR_AUTH_KEY: &str = "Clear-auth";
  17. const BLIND_AUTH_KEY: &str = "Blind-auth";
  18. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  19. pub enum AuthHeader {
  20. /// Clear Auth token
  21. Clear(String),
  22. /// Blind Auth token
  23. Blind(BlindAuthToken),
  24. /// No auth
  25. None,
  26. }
  27. impl From<AuthHeader> for Option<AuthToken> {
  28. fn from(value: AuthHeader) -> Option<AuthToken> {
  29. match value {
  30. AuthHeader::Clear(token) => Some(AuthToken::ClearAuth(token)),
  31. AuthHeader::Blind(token) => Some(AuthToken::BlindAuth(token)),
  32. AuthHeader::None => None,
  33. }
  34. }
  35. }
  36. impl<S> FromRequestParts<S> for AuthHeader
  37. where
  38. S: Send + Sync,
  39. {
  40. type Rejection = (StatusCode, String);
  41. async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
  42. // Check for Blind-auth header
  43. if let Some(bat) = parts.headers.get(BLIND_AUTH_KEY) {
  44. let token = bat
  45. .to_str()
  46. .map_err(|_| {
  47. (
  48. StatusCode::BAD_REQUEST,
  49. "Invalid Blind-auth header value".to_string(),
  50. )
  51. })?
  52. .to_string();
  53. let token = BlindAuthToken::from_str(&token).map_err(|_| {
  54. (
  55. StatusCode::BAD_REQUEST,
  56. "Invalid Blind-auth header value".to_string(),
  57. )
  58. })?;
  59. return Ok(AuthHeader::Blind(token));
  60. }
  61. // Check for Clear-auth header
  62. if let Some(cat) = parts.headers.get(CLEAR_AUTH_KEY) {
  63. let token = cat
  64. .to_str()
  65. .map_err(|_| {
  66. (
  67. StatusCode::BAD_REQUEST,
  68. "Invalid Clear-auth header value".to_string(),
  69. )
  70. })?
  71. .to_string();
  72. return Ok(AuthHeader::Clear(token));
  73. }
  74. // No authentication headers found - this is now valid
  75. Ok(AuthHeader::None)
  76. }
  77. }
  78. #[cfg_attr(feature = "swagger", utoipa::path(
  79. get,
  80. context_path = "/v1/auth/blind",
  81. path = "/keysets",
  82. responses(
  83. (status = 200, description = "Successful response", body = KeysetResponse, content_type = "application/json"),
  84. (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
  85. )
  86. ))]
  87. /// Get all active keyset IDs of the mint
  88. ///
  89. /// This endpoint returns a list of keysets that the mint currently supports and will accept tokens from.
  90. #[cfg(feature = "auth")]
  91. pub async fn get_auth_keysets(
  92. State(state): State<MintState>,
  93. ) -> Result<Json<KeysetResponse>, Response> {
  94. Ok(Json(state.mint.auth_keysets()))
  95. }
  96. #[cfg_attr(feature = "swagger", utoipa::path(
  97. get,
  98. context_path = "/v1/auth/blind",
  99. path = "/keys",
  100. responses(
  101. (status = 200, description = "Successful response", body = KeysResponse, content_type = "application/json")
  102. )
  103. ))]
  104. /// Get the public keys of the newest blind auth mint keyset
  105. ///
  106. /// This endpoint returns a dictionary of all supported token values of the mint and their associated public key.
  107. pub async fn get_blind_auth_keys(
  108. State(state): State<MintState>,
  109. ) -> Result<Json<KeysResponse>, Response> {
  110. let pubkeys = state.mint.auth_pubkeys().map_err(|err| {
  111. tracing::error!("Could not get keys: {}", err);
  112. into_response(err)
  113. })?;
  114. Ok(Json(pubkeys))
  115. }
  116. /// Mint tokens by paying a BOLT11 Lightning invoice.
  117. ///
  118. /// Requests the minting of tokens belonging to a paid payment request.
  119. ///
  120. /// Call this endpoint after `POST /v1/mint/quote`.
  121. #[cfg_attr(feature = "swagger", utoipa::path(
  122. post,
  123. context_path = "/v1/auth",
  124. path = "/blind/mint",
  125. request_body(content = MintAuthRequest, description = "Request params", content_type = "application/json"),
  126. responses(
  127. (status = 200, description = "Successful response", body = MintResponse, content_type = "application/json"),
  128. (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
  129. )
  130. ))]
  131. pub async fn post_mint_auth(
  132. auth: AuthHeader,
  133. State(state): State<MintState>,
  134. Json(payload): Json<MintAuthRequest>,
  135. ) -> Result<Json<MintResponse>, Response> {
  136. let auth_token = match auth {
  137. AuthHeader::Clear(cat) => {
  138. if cat.is_empty() {
  139. tracing::debug!("Received blind auth mint request without cat");
  140. return Err(into_response(cdk::Error::ClearAuthRequired));
  141. }
  142. AuthToken::ClearAuth(cat)
  143. }
  144. _ => {
  145. tracing::debug!("Received blind auth mint request without cat");
  146. return Err(into_response(cdk::Error::ClearAuthRequired));
  147. }
  148. };
  149. let res = state
  150. .mint
  151. .mint_blind_auth(auth_token, payload)
  152. .await
  153. .map_err(|err| {
  154. tracing::error!("Could not process blind auth mint: {}", err);
  155. into_response(err)
  156. })?;
  157. Ok(Json(res))
  158. }
  159. pub fn create_auth_router(state: MintState) -> Router<MintState> {
  160. Router::new()
  161. .nest(
  162. "/auth/blind",
  163. Router::new()
  164. .route("/keys", get(get_blind_auth_keys))
  165. .route("/keysets", get(get_auth_keysets))
  166. .route("/keys/{keyset_id}", get(get_keyset_pubkeys))
  167. .route("/mint", post(post_mint_auth)),
  168. )
  169. .with_state(state)
  170. }