commit 9bcaedd00e7f52bd798944896801322b3920f803
parent 9f177f8367f1ee83cf99fc3c3ead7295e6037510
Author: Antoine A <>
Date: Wed, 8 Jan 2025 22:48:25 +0100
magnet-bank: more setup work
Diffstat:
11 files changed, 425 insertions(+), 68 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -1,3 +1,4 @@
.env
target
-dev.conf
-\ No newline at end of file
+dev.conf
+keys.json
+\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
@@ -245,6 +245,12 @@ dependencies = [
]
[[package]]
+name = "base16ct"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
+
+[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -547,6 +553,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
+name = "crypto-bigint"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
+dependencies = [
+ "generic-array",
+ "rand_core",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -688,6 +706,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
+name = "ecdsa"
+version = "0.16.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
+dependencies = [
+ "der",
+ "digest",
+ "elliptic-curve",
+ "rfc6979",
+ "signature",
+ "spki",
+]
+
+[[package]]
name = "ed25519"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -718,6 +750,26 @@ dependencies = [
]
[[package]]
+name = "elliptic-curve"
+version = "0.13.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
+dependencies = [
+ "base16ct",
+ "crypto-bigint",
+ "digest",
+ "ff",
+ "generic-array",
+ "group",
+ "pem-rfc7468",
+ "pkcs8",
+ "rand_core",
+ "sec1",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -771,6 +823,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
+name = "ff"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449"
+dependencies = [
+ "rand_core",
+ "subtle",
+]
+
+[[package]]
name = "fiat-crypto"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -903,6 +965,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
+ "zeroize",
]
[[package]]
@@ -929,6 +992,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
+name = "group"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
+dependencies = [
+ "ff",
+ "rand_core",
+ "subtle",
+]
+
+[[package]]
name = "h2"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1506,18 +1580,21 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
name = "magnet-bank"
version = "0.1.0"
dependencies = [
+ "anyhow",
"base64",
"clap",
"form_urlencoded",
- "getrandom",
"hmac",
"jiff",
+ "p256",
"percent-encoding",
+ "rand_core",
"reqwest",
"serde",
"serde_json",
"serde_urlencoded",
"sha1",
+ "spki",
"taler-common",
"thiserror 2.0.9",
"tokio",
@@ -1744,6 +1821,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
+name = "p256"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
+dependencies = [
+ "ecdsa",
+ "elliptic-curve",
+ "primeorder",
+ "sha2",
+]
+
+[[package]]
name = "parking"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1895,6 +1984,15 @@ dependencies = [
]
[[package]]
+name = "primeorder"
+version = "0.13.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
+dependencies = [
+ "elliptic-curve",
+]
+
+[[package]]
name = "proc-macro2"
version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2070,6 +2168,16 @@ dependencies = [
]
[[package]]
+name = "rfc6979"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
+dependencies = [
+ "hmac",
+ "subtle",
+]
+
+[[package]]
name = "ring"
version = "0.17.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2225,6 +2333,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
+name = "sec1"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
+dependencies = [
+ "base16ct",
+ "der",
+ "generic-array",
+ "pkcs8",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/common/taler-common/src/json_file.rs b/common/taler-common/src/json_file.rs
@@ -0,0 +1,33 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+
+use std::path::Path;
+
+use serde::{de::DeserializeOwned, Serialize};
+
+pub fn persist<T: Serialize>(path: impl AsRef<Path>, value: &T) -> std::io::Result<()> {
+ let path = path.as_ref();
+ let mut tmp = tempfile::NamedTempFile::new_in(path.parent().unwrap())?;
+ serde_json::to_writer(&mut tmp, value)?;
+ tmp.persist(path)?;
+ Ok(())
+}
+
+pub fn load<T: DeserializeOwned>(path: impl AsRef<Path>) -> std::io::Result<T> {
+ let mut file = std::fs::File::open(path)?;
+ let content = serde_json::from_reader(&mut file)?;
+ Ok(content)
+}
diff --git a/common/taler-common/src/lib.rs b/common/taler-common/src/lib.rs
@@ -19,4 +19,5 @@ pub mod api_params;
pub mod api_wire;
pub mod config;
pub mod error_code;
+pub mod json_file;
pub mod types;
diff --git a/wire-gateway/magnet-bank/Cargo.toml b/wire-gateway/magnet-bank/Cargo.toml
@@ -4,14 +4,17 @@ version = "0.1.0"
edition = "2021"
[dependencies]
-reqwest = "0.12"
+rand_core = { version = "*" }
+reqwest = { version = "0.12", features = ["json"] }
hmac = "0.12"
sha1 = "0.10"
-getrandom = "0.2"
+p256 = { version = "0.13.2", features = ["alloc", "ecdsa"] }
+spki = "0.7.3"
base64 = "0.22"
form_urlencoded = "1.2"
percent-encoding = "2.3"
serde_urlencoded = "0.7.1"
+anyhow = "1.0"
taler-common = { path = "../../common/taler-common" }
serde_json = { workspace = true, features = ["raw_value"] }
jiff = { workspace = true, features = ["serde"] }
diff --git a/wire-gateway/magnet-bank/src/config.rs b/wire-gateway/magnet-bank/src/config.rs
@@ -22,6 +22,7 @@ use crate::magnet::Token;
pub struct MagnetConfig {
pub api_url: Url,
pub consumer: Token,
+ pub keys_path: String,
}
impl MagnetConfig {
@@ -33,6 +34,7 @@ impl MagnetConfig {
key: sect.str("CONSUMER_KEY").require()?,
secret: sect.str("CONSUMER_SECRET").require()?,
},
+ keys_path: sect.path("KEYS_FILE").require()?,
})
}
}
diff --git a/wire-gateway/magnet-bank/src/keys.rs b/wire-gateway/magnet-bank/src/keys.rs
@@ -0,0 +1,119 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+
+use std::io::{BufRead, ErrorKind};
+
+use p256::ecdsa::SigningKey;
+use taler_common::{json_file, types::base32::Base32};
+use tracing::info;
+
+use crate::{
+ config::MagnetConfig,
+ magnet::{AuthClient, Token, TokenAuth},
+};
+
+#[derive(Default, Debug, serde::Deserialize, serde::Serialize)]
+pub struct MagnetKeys {
+ access_token: Option<Token>,
+ signing_key: Option<Base32<32>>,
+}
+
+pub async fn setup(cfg: MagnetConfig) -> Result<(), anyhow::Error> {
+ let mut keys = match json_file::load(&cfg.keys_path) {
+ Ok(existing) => existing,
+ Err(e) => match e.kind() {
+ ErrorKind::NotFound => MagnetKeys::default(),
+ _ => Err(e)?,
+ },
+ };
+
+ let client = AuthClient::new(
+ reqwest::Client::new(),
+ cfg.api_url.clone(),
+ cfg.consumer.clone(),
+ );
+
+ info!("Setup OAuth access token");
+ if keys.access_token.is_none() {
+ let token_request = client.token_request().await?;
+
+ // TODO how to do it in a generic way ?
+ // TODO Ask MagnetBank if they could support out-of-band configuration
+ println!(
+ "Login at {}?oauth_token={}\nEnter the result url>",
+ client.join("/NetBankOAuth/authtoken.xhtml"),
+ token_request.key
+ );
+ // TODO better prompting
+ let prompt = std::io::stdin()
+ .lock()
+ .lines()
+ .next()
+ .expect("Missing auth URL line")
+ .expect("Reading auth URL prompt");
+ let auth_url = reqwest::Url::parse(&prompt).expect("Auth URL malformed");
+ let token_auth: TokenAuth =
+ serde_urlencoded::from_str(auth_url.query().unwrap_or_default())
+ .expect("Auth URL malformed");
+ assert_eq!(token_request.key, token_auth.oauth_token);
+
+ let access_token = client.token_access(&token_request, &token_auth).await?;
+ keys.access_token = Some(access_token);
+ json_file::persist(&cfg.keys_path, &keys)?;
+ }
+
+ let client = client.upgrade(keys.access_token.clone().unwrap());
+
+ info!("Setup Strong Customer Authentication");
+ // TODO find a proper way to check if SCA is required without trigerring SCA.GLOBAL_FEATURE_NOT_ENABLED
+ let perform_sca = false;
+ if perform_sca {
+ let request = client.request_sms_code().await?;
+ println!(
+ "A SCA code have been sent through {} to {}\nEnter the code>",
+ request.channel,
+ request.sent_to.join(", ")
+ );
+ // TODO better prompting
+ let prompt = std::io::stdin()
+ .lock()
+ .lines()
+ .next()
+ .expect("Missing SCA code line")
+ .expect("Reading SCA code prompt");
+ client.perform_sca(&prompt).await?;
+ }
+
+ info!("Setup public key");
+ // TODO find a proper way to check if a public key have been setup
+ let perform_public_key = false;
+ if perform_public_key {
+ // TODO use the beter from/to_array API in the next version of the crypto lib
+ let signing_key = match keys.signing_key {
+ Some(bytes) => SigningKey::from_slice(bytes.as_ref())?,
+ None => {
+ let rand = SigningKey::random(&mut rand_core::OsRng);
+ let array: [u8; 32] = rand.to_bytes().as_slice().try_into().unwrap();
+ keys.signing_key = Some(Base32::from(array));
+ json_file::persist(&cfg.keys_path, &keys)?;
+ rand
+ }
+ };
+ client.setup_public_key(&signing_key).await?;
+ }
+
+ Ok(())
+}
diff --git a/wire-gateway/magnet-bank/src/magnet.rs b/wire-gateway/magnet-bank/src/magnet.rs
@@ -14,14 +14,18 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+use base64::{prelude::BASE64_STANDARD, Engine};
use error::ApiResult;
+use p256::{ecdsa::SigningKey, PublicKey};
+use serde_json::{json, Value};
+use spki::EncodePublicKey;
use crate::magnet::{error::MagnetBuilder, oauth::OAuthBuilder};
pub mod error;
mod oauth;
-#[derive(serde::Deserialize, Debug)]
+#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
pub struct Token {
#[serde(rename = "oauth_token")]
pub key: String,
@@ -35,13 +39,62 @@ pub struct TokenAuth {
pub oauth_verifier: String,
}
+#[derive(serde::Deserialize, Debug)]
+pub struct Consumer {
+ #[serde(rename = "consumerKey")]
+ pub key: String,
+ #[serde(rename = "megnevezes")]
+ pub name: String,
+ #[serde(rename = "callbackUri")]
+ pub callback_uri: String,
+ #[serde(rename = "elettartam")]
+ pub lifetime: u64,
+}
+
+#[derive(serde::Deserialize, Debug)]
+pub struct TokenInfo {
+ #[serde(rename = "keszult")]
+ pub created: jiff::Timestamp,
+ #[serde(rename = "lejarat")]
+ pub expiration: jiff::Timestamp,
+ #[serde(rename = "kliensinfo")]
+ pub client_info: Option<String>,
+ pub consumer: Consumer,
+ #[serde(rename = "hitelesitett")]
+ pub authenticated: bool,
+}
+
+#[derive(serde::Deserialize, Debug)]
+pub struct SmsCodeSubmission {
+ #[serde(rename = "csatorna")]
+ pub channel: String,
+ #[serde(rename = "hovaMentKi")]
+ pub sent_to: Vec<String>,
+}
+
+#[derive(serde::Deserialize, Debug)]
+pub struct ScaResult {
+ #[serde(rename = "csatorna")]
+ pub channel: String,
+ #[serde(rename = "hovaMentKi")]
+ pub sent_to: Vec<String>,
+}
+
pub struct AuthClient {
- pub client: reqwest::Client,
- pub api_url: reqwest::Url,
- pub consumer: Token,
+ client: reqwest::Client,
+ api_url: reqwest::Url,
+ consumer: Token,
}
impl AuthClient {
+ pub fn new(client: reqwest::Client, api_url: reqwest::Url, consumer: Token) -> Self {
+ Self {
+ client,
+ api_url,
+ consumer,
+ }
+ }
+
pub fn join(&self, path: &str) -> reqwest::Url {
self.api_url.join(path).unwrap()
}
@@ -72,4 +125,70 @@ impl AuthClient {
.magnet_call_encoded()
.await
}
+
+ pub fn upgrade(self, access: Token) -> ApiClient {
+ ApiClient {
+ client: self.client,
+ api_url: self.api_url,
+ consumer: self.consumer,
+ access,
+ }
+ }
+}
+
+pub struct ApiClient {
+ client: reqwest::Client,
+ api_url: reqwest::Url,
+ consumer: Token,
+ access: Token,
+}
+
+impl ApiClient {
+ pub fn join(&self, path: &str) -> reqwest::Url {
+ self.api_url.join(path).unwrap()
+ }
+
+ pub async fn token_info(&self) -> ApiResult<TokenInfo> {
+ self.client
+ .get(self.join("/RESTApi/resources/v2/token"))
+ .oauth(&self.consumer, Some(&self.access), None)
+ .await
+ .magnet_json()
+ .await
+ }
+
+ pub async fn request_sms_code(&self) -> ApiResult<SmsCodeSubmission> {
+ self.client
+ .get(self.join("/RESTApi/resources/v2/kodszo/sms/token"))
+ .oauth(&self.consumer, Some(&self.access), None)
+ .await
+ .magnet_json()
+ .await
+ }
+
+ pub async fn perform_sca(&self, code: &str) -> ApiResult<()> {
+ self.client
+ .put(self.join("/RESTApi/resources/v2/token/SCA"))
+ .json(&json!({
+ "kodszo": code
+ }))
+ .oauth(&self.consumer, Some(&self.access), None)
+ .await
+ .magnet_json()
+ .await
+ }
+
+ pub async fn setup_public_key(&self, key: &SigningKey) -> ApiResult<Value> {
+ let public_key = PublicKey::from_secret_scalar(key.as_nonzero_scalar());
+ let der = public_key.to_public_key_der().unwrap().to_vec();
+ self.client
+ .post(self.join("/RESTApi/resources/v2/token/public-key"))
+ .json(&json!({
+ "keyData": BASE64_STANDARD.encode(der)
+ }))
+ .oauth(&self.consumer, Some(&self.access), None)
+ .await
+ .magnet_json()
+ .await
+ }
}
diff --git a/wire-gateway/magnet-bank/src/magnet/error.rs b/wire-gateway/magnet-bank/src/magnet/error.rs
@@ -42,15 +42,15 @@ pub struct MagnetError {
pub enum ApiError {
#[error("transport: {0}")]
Transport(#[from] reqwest::Error),
- #[error("magnet: {0}")]
+ #[error("magnet {0}")]
Magnet(#[from] MagnetError),
#[error("JSON body: {0}")]
Json(#[from] serde_json::Error),
#[error("form body: {0}")]
Form(#[from] serde_urlencoded::de::Error),
- #[error("status: {0}")]
+ #[error("status {0}")]
Status(StatusCode),
- #[error("status: {0} '{1}'")]
+ #[error("status {0} '{1}'")]
StatusCause(StatusCode, String),
}
diff --git a/wire-gateway/magnet-bank/src/magnet/oauth.rs b/wire-gateway/magnet-bank/src/magnet/oauth.rs
@@ -21,6 +21,7 @@ use hmac::{Hmac, Mac};
use percent_encoding::NON_ALPHANUMERIC;
use reqwest::header::HeaderValue;
use sha1::Sha1;
+use rand_core::RngCore;
use super::Token;
@@ -30,7 +31,7 @@ type HmacSha1 = Hmac<Sha1>;
fn oauth_nonce() -> String {
// Generate 8 secure random bytes
let mut buf = [0u8; 8];
- getrandom::getrandom(&mut buf).expect("OS rand 8 bytes");
+ rand_core::OsRng.fill_bytes(&mut buf);
// Encode as base64 string
BASE64_STANDARD.encode(buf)
}
diff --git a/wire-gateway/magnet-bank/src/main.rs b/wire-gateway/magnet-bank/src/main.rs
@@ -14,19 +14,16 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-use std::{fmt::Display, future::Future, io::BufRead, path::PathBuf};
+use std::{future::Future, path::PathBuf};
use clap::Parser;
use config::MagnetConfig;
-use magnet::{error::ApiError, AuthClient, TokenAuth};
-use taler_common::config::{
- parser::{ConfigSource, ParserErr},
- Config, ValueError,
-};
+use taler_common::config::{parser::ConfigSource, Config};
use tracing::{error, Level};
-use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _, EnvFilter};
+use tracing_subscriber::{util::SubscriberInitExt as _, FmtSubscriber};
mod config;
+mod keys;
mod magnet;
#[derive(clap::Parser, Debug)]
@@ -52,14 +49,13 @@ enum Command {
Setup,
}
-fn setup<E: Display>(level: Option<tracing::Level>, app: impl Future<Output = Result<(), E>>) {
+fn setup(level: Option<tracing::Level>, app: impl Future<Output = Result<(), anyhow::Error>>) {
// Setup logger
let level = level.unwrap_or(Level::INFO);
- let filter: EnvFilter = format!("magnet-bank={level}").into();
- tracing_subscriber::registry()
- .with(tracing_subscriber::fmt::layer())
- .with(filter)
- .init();
+ FmtSubscriber::builder()
+ .with_max_level(level)
+ .finish()
+ .set_default();
// Setup async runtime
let runtime = tokio::runtime::Builder::new_multi_thread()
@@ -75,53 +71,13 @@ fn setup<E: Display>(level: Option<tracing::Level>, app: impl Future<Output = Re
}
}
-#[derive(Debug, thiserror::Error)]
-enum MagnetError {
- #[error("{0}")]
- CfgParsing(#[from] ParserErr),
- #[error("{0}")]
- Config(#[from] ValueError),
- #[error("{0}")]
- Api(#[from] ApiError),
-}
-
-async fn app(args: Args) -> Result<(), MagnetError> {
+async fn app(args: Args) -> Result<(), anyhow::Error> {
let source = ConfigSource::new("magnet-bank", "magnet-bank", "magnet-bank");
let cfg = Config::from_file(source, args.config)?;
let cfg = MagnetConfig::parse(&cfg)?;
match args.cmd {
- Command::Setup => {
- println!("Setup");
- let client = AuthClient {
- client: reqwest::Client::new(),
- api_url: cfg.api_url,
- consumer: cfg.consumer,
- };
- let token_request = client.token_request().await?;
-
- // TODO how to do it in a generic way ?
- // TODO Ask MagnetBank if they could support out-of-band configuration
- println!(
- "Login at {}?oauth_token={}\nEnter the result url>",
- client.join("/NetBankOAuth/authtoken.xhtml"),
- token_request.key
- );
- let prompt = std::io::stdin()
- .lock()
- .lines()
- .next()
- .expect("Missing auth URL line")
- .expect("Reading auth URL prompt");
- let auth_url = reqwest::Url::parse(&prompt).expect("Auth URL malformed");
- let token_auth: TokenAuth =
- serde_urlencoded::from_str(auth_url.query().unwrap_or_default())
- .expect("Auth URL malformed");
- assert_eq!(token_request.key, token_auth.oauth_token);
-
- let auth_token = client.token_access(&token_request, &token_auth).await?;
- dbg!(auth_token);
- }
+ Command::Setup => keys::setup(cfg).await?,
}
Ok(())
}