|
@ -43,6 +43,13 @@ fn login(data: Form<ConnectData>, conn: DbConn, ip: ClientIp) -> JsonResult { |
|
|
|
|
|
|
|
|
_password_login(data, conn, &ip) |
|
|
_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), |
|
|
t => err!("Invalid type", t), |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
@ -178,6 +185,75 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult |
|
|
Ok(Json(result)) |
|
|
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
|
|
|
/// 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) { |
|
|
fn get_device(data: &ConnectData, conn: &DbConn, user: &User) -> (Device, bool) { |
|
|
// On iOS, device_type sends "iOS", on others it sends a number
|
|
|
// 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) |
|
|
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
|
|
|
// https://github.com/bitwarden/mobile/blob/master/src/Core/Models/Request/TokenRequest.cs
|
|
|
#[derive(Debug, Clone, Default)] |
|
|
#[derive(Debug, Clone, Default)] |
|
|
#[allow(non_snake_case)] |
|
|
#[allow(non_snake_case)] |
|
|
struct ConnectData { |
|
|
struct ConnectData { |
|
|
grant_type: String, // refresh_token, password
|
|
|
// refresh_token, password, client_credentials (API key)
|
|
|
|
|
|
grant_type: String, |
|
|
|
|
|
|
|
|
// Needed for grant_type="refresh_token"
|
|
|
// Needed for grant_type="refresh_token"
|
|
|
refresh_token: Option<String>, |
|
|
refresh_token: Option<String>, |
|
|
|
|
|
|
|
|
// Needed for grant_type="password"
|
|
|
// Needed for grant_type = "password" | "client_credentials"
|
|
|
client_id: Option<String>, // web, cli, desktop, browser, mobile
|
|
|
client_id: Option<String>, // web, cli, desktop, browser, mobile
|
|
|
|
|
|
client_secret: Option<String>, // API key login (cli only)
|
|
|
password: Option<String>, |
|
|
password: Option<String>, |
|
|
scope: Option<String>, |
|
|
scope: Option<String>, |
|
|
username: Option<String>, |
|
|
username: Option<String>, |
|
@ -414,6 +493,7 @@ impl<'f> FromForm<'f> for ConnectData { |
|
|
"granttype" => form.grant_type = value, |
|
|
"granttype" => form.grant_type = value, |
|
|
"refreshtoken" => form.refresh_token = Some(value), |
|
|
"refreshtoken" => form.refresh_token = Some(value), |
|
|
"clientid" => form.client_id = Some(value), |
|
|
"clientid" => form.client_id = Some(value), |
|
|
|
|
|
"clientsecret" => form.client_secret = Some(value), |
|
|
"password" => form.password = Some(value), |
|
|
"password" => form.password = Some(value), |
|
|
"scope" => form.scope = Some(value), |
|
|
"scope" => form.scope = Some(value), |
|
|
"username" => form.username = Some(value), |
|
|
"username" => form.username = Some(value), |
|
|