diff options
author | Jesse Luehrs <doy@tozt.net> | 2021-10-27 05:38:03 -0400 |
---|---|---|
committer | Jesse Luehrs <doy@tozt.net> | 2021-10-27 05:44:08 -0400 |
commit | 9acabc3a7497e3eb3536992212d34f77a649ae16 (patch) | |
tree | e1433fe36c6768704ea63443910731c4ad7fbcc1 /src/api.rs | |
parent | eb5e4ea7bcc3008a8d06da6884ee9713f8df0034 (diff) | |
download | rbw-9acabc3a7497e3eb3536992212d34f77a649ae16.tar.gz rbw-9acabc3a7497e3eb3536992212d34f77a649ae16.zip |
start working on apikey authentication
not entirely working yet, because api keys aren't allowed to use the
offline_access scope which provides access to the oauth refresh token.
we will probably need to move to using stable device identifiers (should
be able to generate one when the config is first created) and move this
logic to a separate "rbw register" command which just does an apikey
login but doesn't try to store any data, and then use the normal login
flow after that
Diffstat (limited to 'src/api.rs')
-rw-r--r-- | src/api.rs | 69 |
1 files changed, 54 insertions, 15 deletions
@@ -148,9 +148,10 @@ struct PreloginRes { struct ConnectPasswordReq { grant_type: String, username: String, - password: String, + password: Option<String>, scope: String, client_id: String, + client_secret: Option<String>, #[serde(rename = "deviceType")] device_type: u32, #[serde(rename = "deviceIdentifier")] @@ -178,7 +179,7 @@ struct ConnectPasswordRes { #[derive(serde::Deserialize, Debug)] struct ConnectErrorRes { error: String, - error_description: String, + error_description: Option<String>, #[serde(rename = "ErrorModel")] error_model: Option<ConnectErrorResErrorModel>, #[serde(rename = "TwoFactorProviders")] @@ -578,16 +579,38 @@ impl Client { pub async fn login( &self, email: &str, - master_password_hash: &crate::locked::PasswordHash, + creds: &crate::locked::HashedLoginCredentials, two_factor_token: Option<&str>, two_factor_provider: Option<TwoFactorProviderType>, ) -> Result<(String, String, String)> { + let (grant_type, scope, password, client_id, client_secret) = + match creds { + crate::locked::HashedLoginCredentials::Password { + password_hash, + } => ( + "password", + "api offline_access", + Some(base64::encode(password_hash.hash())), + &b"desktop"[..], + None, + ), + crate::locked::HashedLoginCredentials::ApiKey { apikey } => ( + "client_credentials", + "api", + None, + apikey.client_id(), + Some(apikey.client_secret()), + ), + }; let connect_req = ConnectPasswordReq { - grant_type: "password".to_string(), + grant_type: grant_type.to_string(), username: email.to_string(), - password: base64::encode(master_password_hash.hash()), - scope: "api offline_access".to_string(), - client_id: "desktop".to_string(), + password, + scope: scope.to_string(), + // XXX unwraps here are not necessarily safe + client_id: String::from_utf8(client_id.to_vec()).unwrap(), + client_secret: client_secret + .map(|secret| String::from_utf8(secret.to_vec()).unwrap()), device_type: 8, device_identifier: uuid::Uuid::new_v4() .to_hyphenated() @@ -599,13 +622,24 @@ impl Client { two_factor_provider: two_factor_provider.map(|ty| ty as u32), }; let client = reqwest::Client::new(); + let mut headers = reqwest::header::HeaderMap::new(); + if connect_req.client_secret.is_none() + && connect_req.password.is_some() + { + headers.insert( + "auth-email", + // unwrap is safe because url safe base64 strings are always + // valid header values + reqwest::header::HeaderValue::from_str( + &base64::encode_config(email, base64::URL_SAFE_NO_PAD), + ) + .unwrap(), + ); + } let res = client .post(&self.identity_url("/connect/token")) .form(&connect_req) - .header( - "auth-email", - base64::encode_config(email, base64::URL_SAFE_NO_PAD), - ) + .headers(headers) .send() .await .map_err(|source| Error::Reqwest { source })?; @@ -1050,15 +1084,17 @@ impl Client { } fn classify_login_error(error_res: &ConnectErrorRes, code: u16) -> Error { + let error_desc = error_res.error_description.clone(); + let error_desc = error_desc.as_deref(); match error_res.error.as_str() { - "invalid_grant" => match error_res.error_description.as_str() { - "invalid_username_or_password" => { + "invalid_grant" => match error_desc { + Some("invalid_username_or_password") => { if let Some(error_model) = error_res.error_model.as_ref() { let message = error_model.message.as_str().to_string(); return Error::IncorrectPassword { message }; } } - "Two factor required." => { + Some("Two factor required.") => { if let Some(providers) = error_res.two_factor_providers.as_ref() { @@ -1069,10 +1105,13 @@ fn classify_login_error(error_res: &ConnectErrorRes, code: u16) -> Error { } _ => {} }, + "invalid_client" => { + return Error::IncorrectApiKey; + } "" => { // bitwarden_rs returns an empty error and error_description for // this case, for some reason - if error_res.error_description.is_empty() { + if error_desc.is_none() || error_desc == Some("") { if let Some(error_model) = error_res.error_model.as_ref() { let message = error_model.message.as_str().to_string(); match message.as_str() { |