From 201d2d40cac8760fb69b9282ba2a4f9f7ec82d47 Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Sun, 12 Apr 2020 03:44:48 -0400 Subject: implement refresh token handling --- src/actions.rs | 7 ++++++ src/api.rs | 60 +++++++++++++++++++++++++++++++++++++++----- src/bin/rbw-agent/actions.rs | 27 +++++++++++++++++--- src/error.rs | 6 +++++ src/lib.rs | 2 +- 5 files changed, 91 insertions(+), 11 deletions(-) diff --git a/src/actions.rs b/src/actions.rs index 7d8569c..c942f32 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -51,3 +51,10 @@ pub async fn sync( crate::api::Client::new(&config.base_url(), &config.identity_url()); client.sync(access_token).await } + +pub async fn exchange_refresh_token(refresh_token: &str) -> Result { + let config = crate::config::Config::load_async().await?; + let client = + crate::api::Client::new(&config.base_url(), &config.identity_url()); + client.exchange_refresh_token(refresh_token).await +} diff --git a/src/api.rs b/src/api.rs index ce8ae28..b578348 100644 --- a/src/api.rs +++ b/src/api.rs @@ -14,7 +14,7 @@ struct PreloginRes { } #[derive(serde::Serialize, Debug)] -struct ConnectReq { +struct ConnectPasswordReq { grant_type: String, username: String, password: String, @@ -30,8 +30,25 @@ struct ConnectReq { device_push_token: String, } +#[derive(serde::Serialize, Debug)] +struct ConnectRefreshTokenReq { + grant_type: String, + client_id: String, + refresh_token: String, +} + +#[derive(serde::Deserialize, Debug)] +struct ConnectPasswordRes { + access_token: String, + expires_in: u32, + token_type: String, + refresh_token: String, + #[serde(rename = "Key")] + key: String, +} + #[derive(serde::Deserialize, Debug)] -struct ConnectRes { +struct ConnectRefreshTokenRes { access_token: String, expires_in: u32, token_type: String, @@ -105,7 +122,7 @@ impl Client { email: &str, master_password_hash: &crate::locked::PasswordHash, ) -> Result<(String, String, String)> { - let connect_req = ConnectReq { + let connect_req = ConnectPasswordReq { grant_type: "password".to_string(), username: email.to_string(), password: base64::encode(master_password_hash.hash()), @@ -125,7 +142,7 @@ impl Client { .send() .await .context(crate::error::Reqwest)?; - let connect_res: ConnectRes = + let connect_res: ConnectPasswordRes = res.json().await.context(crate::error::Reqwest)?; Ok(( connect_res.access_token, @@ -145,9 +162,40 @@ impl Client { .send() .await .context(crate::error::Reqwest)?; - let sync_res: SyncRes = + match res.status() { + reqwest::StatusCode::OK => { + let sync_res: SyncRes = + res.json().await.context(crate::error::Reqwest)?; + Ok((sync_res.profile.key, sync_res.ciphers)) + } + reqwest::StatusCode::UNAUTHORIZED => { + Err(Error::RequestUnauthorized) + } + _ => Err(Error::RequestFailed { + status: res.status().as_u16(), + }), + } + } + + pub async fn exchange_refresh_token( + &self, + refresh_token: &str, + ) -> Result { + let connect_req = ConnectRefreshTokenReq { + grant_type: "refresh_token".to_string(), + client_id: "desktop".to_string(), + refresh_token: refresh_token.to_string(), + }; + let client = reqwest::Client::new(); + let res = client + .post(&self.identity_url("/connect/token")) + .form(&connect_req) + .send() + .await + .context(crate::error::Reqwest)?; + let connect_res: ConnectRefreshTokenRes = res.json().await.context(crate::error::Reqwest)?; - Ok((sync_res.profile.key, sync_res.ciphers)) + Ok(connect_res.access_token) } fn api_url(&self, path: &str) -> String { diff --git a/src/bin/rbw-agent/actions.rs b/src/bin/rbw-agent/actions.rs index 81e2597..4662164 100644 --- a/src/bin/rbw-agent/actions.rs +++ b/src/bin/rbw-agent/actions.rs @@ -133,13 +133,32 @@ pub async fn sync(sock: &mut crate::sock::Sock) -> anyhow::Result<()> { .context("failed to load local database")?; let access_token = if let Some(access_token) = &db.access_token { - access_token + access_token.clone() } else { return Err(anyhow::anyhow!("failed to find access token in db")); }; - let (protected_key, ciphers) = rbw::actions::sync(&access_token) - .await - .context("failed to sync database from server")?; + let res = rbw::actions::sync(&access_token).await; + let res = if let Err(e) = &res { + if let rbw::error::Error::RequestUnauthorized = e { + if let Some(refresh_token) = &db.refresh_token { + let access_token = + rbw::actions::exchange_refresh_token(refresh_token) + .await?; + db.access_token = Some(access_token.clone()); + rbw::actions::sync(&access_token).await + } else { + return Err(anyhow::anyhow!( + "failed to find refresh token in db" + )); + } + } else { + res + } + } else { + res + }; + let (protected_key, ciphers) = + res.context("failed to sync database from server")?; db.protected_key = Some(protected_key); db.ciphers = ciphers; db.save_async(&email) diff --git a/src/error.rs b/src/error.rs index d3e168b..6a5244f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -66,6 +66,12 @@ pub enum Error { #[snafu(display("failed to remove db: {}", source))] RemoveDb { source: std::io::Error }, + #[snafu(display("api request returned error: {}", status))] + RequestFailed { status: u16 }, + + #[snafu(display("api request unauthorized"))] + RequestUnauthorized, + #[snafu(display("error making api request: {}", source))] Reqwest { source: reqwest::Error }, diff --git a/src/lib.rs b/src/lib.rs index f1460c5..6ae5f7e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,7 @@ pub mod cipherstring; pub mod config; pub mod db; pub mod dirs; -mod error; +pub mod error; pub mod identity; pub mod locked; pub mod pinentry; -- cgit v1.2.3-54-g00ecf