diff options
author | Jeremy Lin <[email protected]> | 2022-01-19 02:51:26 -0800 |
---|---|---|
committer | Jeremy Lin <[email protected]> | 2022-01-21 23:10:11 -0800 |
commit | 69ee4a70b453b81b96347fb299bf5c14f1038156 (patch) | |
tree | 71b0a4c2f7234de4904073c265ff1b5e4285d85d /src/api/identity.rs | |
parent | 8ba6e61fd591e7fe038fb13fd24b0e263c87e88a (diff) | |
download | vaultwarden-69ee4a70b453b81b96347fb299bf5c14f1038156.tar.gz vaultwarden-69ee4a70b453b81b96347fb299bf5c14f1038156.zip |
Add support for API keys
This is mainly useful for CLI-based login automation.
Diffstat (limited to 'src/api/identity.rs')
-rw-r--r-- | src/api/identity.rs | 86 |
1 files changed, 83 insertions, 3 deletions
diff --git a/src/api/identity.rs b/src/api/identity.rs index 3cb26ba3..49310930 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -43,6 +43,13 @@ fn login(data: Form<ConnectData>, conn: DbConn, ip: ClientIp) -> JsonResult { _password_login(data, conn, &ip) } + "client_credentials" => { + _check_is_some(&data.client_id, "client_id cannot be blank")?; + _check_is_some(&data.client_secret, "client_secret cannot be blank")?; + _check_is_some(&data.scope, "scope cannot be blank")?; + + _api_key_login(data, conn, &ip) + } t => err!("Invalid type", t), } } @@ -178,6 +185,75 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult Ok(Json(result)) } +fn _api_key_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult { + // Validate scope + let scope = data.scope.as_ref().unwrap(); + if scope != "api" { + err!("Scope not supported") + } + + // Ratelimit the login + crate::ratelimit::check_limit_login(&ip.ip)?; + + // Get the user via the client_id + let client_id = data.client_id.as_ref().unwrap(); + let user_uuid = match client_id.strip_prefix("user.") { + Some(uuid) => uuid, + None => err!("Malformed client_id", format!("IP: {}.", ip.ip)), + }; + let user = match User::find_by_uuid(user_uuid, &conn) { + Some(user) => user, + None => err!("Invalid client_id", format!("IP: {}.", ip.ip)), + }; + + // Check if the user is disabled + if !user.enabled { + err!("This user has been disabled (API key login)", format!("IP: {}. Username: {}.", ip.ip, user.email)) + } + + // Check API key. Note that API key logins bypass 2FA. + let client_secret = data.client_secret.as_ref().unwrap(); + if !user.check_valid_api_key(client_secret) { + err!("Incorrect client_secret", format!("IP: {}. Username: {}.", ip.ip, user.email)) + } + + let (mut device, new_device) = get_device(&data, &conn, &user); + + if CONFIG.mail_enabled() && new_device { + let now = Utc::now().naive_utc(); + if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device.name) { + error!("Error sending new device email: {:#?}", e); + + if CONFIG.require_device_email() { + err!("Could not send login notification email. Please contact your administrator.") + } + } + } + + // Common + let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, &conn); + + let (access_token, expires_in) = device.refresh_tokens(&user, orgs); + device.save(&conn)?; + + info!("User {} logged in successfully via API key. IP: {}", user.email, ip.ip); + + Ok(Json(json!({ + "access_token": access_token, + "expires_in": expires_in, + "token_type": "Bearer", + "refresh_token": device.refresh_token, + "Key": user.akey, + "PrivateKey": user.private_key, + + "Kdf": user.client_kdf_type, + "KdfIterations": user.client_kdf_iter, + "ResetMasterPassword": false, // TODO: Same as above + "scope": "api", + "unofficialServer": true, + }))) +} + /// Retrieves an existing device or creates a new device from ConnectData and the User fn get_device(data: &ConnectData, conn: &DbConn, user: &User) -> (Device, bool) { // On iOS, device_type sends "iOS", on others it sends a number @@ -374,17 +450,20 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api Ok(result) } +// https://github.com/bitwarden/jslib/blob/master/common/src/models/request/tokenRequest.ts // https://github.com/bitwarden/mobile/blob/master/src/Core/Models/Request/TokenRequest.cs #[derive(Debug, Clone, Default)] #[allow(non_snake_case)] struct ConnectData { - grant_type: String, // refresh_token, password + // refresh_token, password, client_credentials (API key) + grant_type: String, // Needed for grant_type="refresh_token" refresh_token: Option<String>, - // Needed for grant_type="password" - client_id: Option<String>, // web, cli, desktop, browser, mobile + // Needed for grant_type = "password" | "client_credentials" + client_id: Option<String>, // web, cli, desktop, browser, mobile + client_secret: Option<String>, // API key login (cli only) password: Option<String>, scope: Option<String>, username: Option<String>, @@ -414,6 +493,7 @@ impl<'f> FromForm<'f> for ConnectData { "granttype" => form.grant_type = value, "refreshtoken" => form.refresh_token = Some(value), "clientid" => form.client_id = Some(value), + "clientsecret" => form.client_secret = Some(value), "password" => form.password = Some(value), "scope" => form.scope = Some(value), "username" => form.username = Some(value), |