cat_device_login.rs 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. use std::path::Path;
  2. use std::str::FromStr;
  3. use std::time::Duration;
  4. use anyhow::{anyhow, Result};
  5. use cdk::mint_url::MintUrl;
  6. use cdk::nuts::{CurrencyUnit, MintInfo};
  7. use cdk::wallet::types::WalletKey;
  8. use cdk::wallet::MultiMintWallet;
  9. use cdk::OidcClient;
  10. use clap::Args;
  11. use serde::{Deserialize, Serialize};
  12. use tokio::time::sleep;
  13. use crate::token_storage;
  14. #[derive(Args, Serialize, Deserialize)]
  15. pub struct CatDeviceLoginSubCommand {
  16. /// Mint url
  17. mint_url: MintUrl,
  18. /// Currency unit e.g. sat
  19. #[arg(default_value = "sat")]
  20. #[arg(short, long)]
  21. unit: String,
  22. /// Client ID for OIDC authentication
  23. #[arg(default_value = "cashu-client")]
  24. #[arg(long)]
  25. client_id: String,
  26. }
  27. pub async fn cat_device_login(
  28. multi_mint_wallet: &MultiMintWallet,
  29. sub_command_args: &CatDeviceLoginSubCommand,
  30. work_dir: &Path,
  31. ) -> Result<()> {
  32. let mint_url = sub_command_args.mint_url.clone();
  33. let unit = CurrencyUnit::from_str(&sub_command_args.unit)?;
  34. let wallet = match multi_mint_wallet
  35. .get_wallet(&WalletKey::new(mint_url.clone(), unit.clone()))
  36. .await
  37. {
  38. Some(wallet) => wallet.clone(),
  39. None => {
  40. multi_mint_wallet
  41. .create_and_add_wallet(&mint_url.to_string(), unit, None)
  42. .await?
  43. }
  44. };
  45. let mint_info = wallet
  46. .get_mint_info()
  47. .await?
  48. .ok_or(anyhow!("Mint info not found"))?;
  49. let (access_token, refresh_token) =
  50. get_device_code_token(&mint_info, &sub_command_args.client_id).await;
  51. // Save tokens to file in work directory
  52. if let Err(e) =
  53. token_storage::save_tokens(work_dir, &mint_url, &access_token, &refresh_token).await
  54. {
  55. println!("Warning: Failed to save tokens to file: {e}");
  56. } else {
  57. println!("Tokens saved to work directory");
  58. }
  59. // Print a cute ASCII cat
  60. println!("\nAuthentication successful! 🎉\n");
  61. println!("\nYour tokens:");
  62. println!("access_token: {access_token}");
  63. println!("refresh_token: {refresh_token}");
  64. Ok(())
  65. }
  66. async fn get_device_code_token(mint_info: &MintInfo, client_id: &str) -> (String, String) {
  67. let openid_discovery = mint_info
  68. .nuts
  69. .nut21
  70. .clone()
  71. .expect("Nut21 defined")
  72. .openid_discovery;
  73. let oidc_client = OidcClient::new(openid_discovery);
  74. // Get the OIDC configuration
  75. let oidc_config = oidc_client
  76. .get_oidc_config()
  77. .await
  78. .expect("Failed to get OIDC config");
  79. // Get the device authorization endpoint
  80. let device_auth_url = oidc_config.device_authorization_endpoint;
  81. // Make the device code request
  82. let client = reqwest::Client::new();
  83. let device_code_response = client
  84. .post(device_auth_url)
  85. .form(&[("client_id", client_id)])
  86. .send()
  87. .await
  88. .expect("Failed to send device code request");
  89. let device_code_data: serde_json::Value = device_code_response
  90. .json()
  91. .await
  92. .expect("Failed to parse device code response");
  93. let device_code = device_code_data["device_code"]
  94. .as_str()
  95. .expect("No device code in response");
  96. let user_code = device_code_data["user_code"]
  97. .as_str()
  98. .expect("No user code in response");
  99. let verification_uri = device_code_data["verification_uri"]
  100. .as_str()
  101. .expect("No verification URI in response");
  102. let verification_uri_complete = device_code_data["verification_uri_complete"]
  103. .as_str()
  104. .unwrap_or(verification_uri);
  105. let interval = device_code_data["interval"].as_u64().unwrap_or(5);
  106. println!("\nTo login, visit: {verification_uri}");
  107. println!("And enter code: {user_code}\n");
  108. if verification_uri_complete != verification_uri {
  109. println!("Or visit this URL directly: {verification_uri_complete}\n");
  110. }
  111. // Poll for the token
  112. let token_url = oidc_config.token_endpoint;
  113. loop {
  114. sleep(Duration::from_secs(interval)).await;
  115. let token_response = client
  116. .post(&token_url)
  117. .form(&[
  118. ("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
  119. ("device_code", device_code),
  120. ("client_id", client_id),
  121. ])
  122. .send()
  123. .await
  124. .expect("Failed to send token request");
  125. if token_response.status().is_success() {
  126. let token_data: serde_json::Value = token_response
  127. .json()
  128. .await
  129. .expect("Failed to parse token response");
  130. let access_token = token_data["access_token"]
  131. .as_str()
  132. .expect("No access token in response")
  133. .to_string();
  134. let refresh_token = token_data["refresh_token"]
  135. .as_str()
  136. .expect("No refresh token in response")
  137. .to_string();
  138. return (access_token, refresh_token);
  139. } else {
  140. let error_data: serde_json::Value = token_response
  141. .json()
  142. .await
  143. .expect("Failed to parse error response");
  144. let error = error_data["error"].as_str().unwrap_or("unknown_error");
  145. // If the user hasn't completed the flow yet, continue polling
  146. if error == "authorization_pending" || error == "slow_down" {
  147. if error == "slow_down" {
  148. // If we're polling too fast, slow down
  149. sleep(Duration::from_secs(interval + 5)).await;
  150. }
  151. println!("Waiting for user to complete authentication...");
  152. continue;
  153. } else {
  154. // For other errors, exit with an error message
  155. panic!("Authentication failed: {error}");
  156. }
  157. }
  158. }
  159. }