aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2020-04-12 03:44:48 -0400
committerJesse Luehrs <doy@tozt.net>2020-04-12 03:44:48 -0400
commit201d2d40cac8760fb69b9282ba2a4f9f7ec82d47 (patch)
tree0c1cfd3986e28e7be2b73cdcba11e9ebbbb7c2fa
parentbcae263e4e888274f399a75ec5b171ee25edf894 (diff)
downloadrbw-201d2d40cac8760fb69b9282ba2a4f9f7ec82d47.tar.gz
rbw-201d2d40cac8760fb69b9282ba2a4f9f7ec82d47.zip
implement refresh token handling
-rw-r--r--src/actions.rs7
-rw-r--r--src/api.rs60
-rw-r--r--src/bin/rbw-agent/actions.rs27
-rw-r--r--src/error.rs6
-rw-r--r--src/lib.rs2
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<String> {
+ 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<String> {
+ 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;