From bc6a53b847a398191f221d8d98829d50b2505e53 Mon Sep 17 00:00:00 2001 From: vpl <vpl@vpl.me> Date: Mon, 22 Jul 2019 08:26:24 +0200 Subject: [PATCH 1/3] Add new device email when user logs in --- src/api/identity.rs | 18 ++- src/config.rs | 1 + src/mail.rs | 27 +++- .../templates/email/new_device_logged_in.hbs | 14 ++ .../email/new_device_logged_in.html.hbs | 144 ++++++++++++++++++ 5 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 src/static/templates/email/new_device_logged_in.hbs create mode 100644 src/static/templates/email/new_device_logged_in.html.hbs diff --git a/src/api/identity.rs b/src/api/identity.rs index 3602dc97..74e9fa37 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -15,6 +15,8 @@ use crate::api::{ApiResult, EmptyResult, JsonResult}; use crate::auth::ClientIp; +use crate::mail; + use crate::CONFIG; pub fn routes() -> Vec<Route> { @@ -104,20 +106,34 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult let device_id = data.device_identifier.clone().expect("No device id provided"); let device_name = data.device_name.clone().expect("No device name provided"); + let mut send_email = false; // Find device or create new let mut device = match Device::find_by_uuid(&device_id, &conn) { Some(device) => { // Check if owned device, and recreate if not if device.user_uuid != user.uuid { info!("Device exists but is owned by another user. The old device will be discarded"); + send_email = true; Device::new(device_id, user.uuid.clone(), device_name, device_type) } else { device } } - None => Device::new(device_id, user.uuid.clone(), device_name, device_type), + None => { + send_email = true; + Device::new(device_id, user.uuid.clone(), device_name, device_type) + } }; + if CONFIG.mail_enabled() && send_email { + mail::send_new_device_logged_in( + &username, + &ip.ip.to_string(), + &device.updated_at, + &device.name, + )?; + } + let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, &conn)?; // Common diff --git a/src/config.rs b/src/config.rs index 91feb1c6..85d97511 100644 --- a/src/config.rs +++ b/src/config.rs @@ -526,6 +526,7 @@ fn load_templates(path: &str) -> Handlebars { // First register default templates here reg!("email/invite_accepted", ".html"); reg!("email/invite_confirmed", ".html"); + reg!("email/new_device_logged_in", ".html"); reg!("email/pw_hint_none", ".html"); reg!("email/pw_hint_some", ".html"); reg!("email/send_org_invite", ".html"); diff --git a/src/mail.rs b/src/mail.rs index 2b814b54..576cb6b0 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -3,13 +3,14 @@ use lettre::smtp::ConnectionReuseParameters; use lettre::{ClientSecurity, ClientTlsParameters, SmtpClient, SmtpTransport, Transport}; use lettre_email::{EmailBuilder, MimeMultipartType, PartBuilder}; use native_tls::{Protocol, TlsConnector}; -use quoted_printable::encode_to_str; use percent_encoding::{percent_encode, DEFAULT_ENCODE_SET}; +use quoted_printable::encode_to_str; use crate::api::EmptyResult; use crate::auth::{encode_jwt, generate_invite_claims}; use crate::error::Error; use crate::CONFIG; +use chrono::NaiveDateTime; fn mailer() -> SmtpTransport { let host = CONFIG.smtp_host().unwrap(); @@ -136,6 +137,30 @@ pub fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult { send_email(&address, &subject, &body_html, &body_text) } +pub fn send_new_device_logged_in( + address: &str, + ip: &str, + dt: &NaiveDateTime, + device: &str, +) -> EmptyResult { + use crate::util::upcase_first; + let device = upcase_first(device); + + let datetime = dt.format("%A, %B %_d, %Y at %H:%M").to_string(); + + let (subject, body_html, body_text) = get_text( + "email/new_device_logged_in", + json!({ + "url": CONFIG.domain(), + "ip": ip, + "device": device, + "datetime": datetime, + }), + )?; + + send_email(&address, &subject, &body_html, &body_text) +} + fn send_email(address: &str, subject: &str, body_html: &str, body_text: &str) -> EmptyResult { let html = PartBuilder::new() .body(encode_to_str(body_html)) diff --git a/src/static/templates/email/new_device_logged_in.hbs b/src/static/templates/email/new_device_logged_in.hbs new file mode 100644 index 00000000..0ef4bb26 --- /dev/null +++ b/src/static/templates/email/new_device_logged_in.hbs @@ -0,0 +1,14 @@ +New Device Logged In From {{device}} +<!----------------> +<html> +<p> + Your account was just logged into from a new device. + + Date: {{datetime}} + IP Address: {{ip}} + Device Type: {{device}} + + You can deauthorize all devices that have access to your account from the + <a href="{{url}}">web vault</a> under Settings > My Account > Deauthorize Sessions. +</p> +</html> diff --git a/src/static/templates/email/new_device_logged_in.html.hbs b/src/static/templates/email/new_device_logged_in.html.hbs new file mode 100644 index 00000000..0bf1e7b8 --- /dev/null +++ b/src/static/templates/email/new_device_logged_in.html.hbs @@ -0,0 +1,144 @@ +New Device Logged In From {{device}} +<!----------------> +<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;"> + <head> + <meta name="viewport" content="width=device-width" /> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> + <title>Bitwarden_rs</title> + </head> + <body style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 25px; width: 100% !important;" bgcolor="#f6f6f6"> + <style type="text/css"> + body { + margin: 0; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + box-sizing: border-box; + font-size: 16px; + color: #333; + line-height: 25px; + -webkit-font-smoothing: antialiased; + -webkit-text-size-adjust: none; + } + body * { + margin: 0; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + box-sizing: border-box; + font-size: 16px; + color: #333; + line-height: 25px; + -webkit-font-smoothing: antialiased; + -webkit-text-size-adjust: none; + } + img { + max-width: 100%; + border: none; + } + body { + -webkit-font-smoothing: antialiased; + -webkit-text-size-adjust: none; + width: 100% !important; + height: 100%; + line-height: 25px; + } + body { + background-color: #f6f6f6; + } + @media only screen and (max-width: 600px) { + body { + padding: 0 !important; + } + .container { + padding: 0 !important; + width: 100% !important; + } + .container-table { + padding: 0 !important; + width: 100% !important; + } + .content { + padding: 0 0 10px 0 !important; + } + .content-wrap { + padding: 10px !important; + } + .invoice { + width: 100% !important; + } + .main { + border-right: none !important; + border-left: none !important; + border-radius: 0 !important; + } + .logo { + padding-top: 10px !important; + } + .footer { + margin-top: 10px !important; + } + .indented { + padding-left: 10px; + } + } + </style> + <table class="body-wrap" cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; width: 100%;" bgcolor="#f6f6f6"> + <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;"> + <td valign="middle" class="aligncenter middle logo" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; padding: 20px 0 10px;" align="center"> + <img src="{{url}}/bwrs_images/logo-gray.png" alt="" width="250" height="39" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" /> + </td> + </tr> + <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;"> + <td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top"> + <table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;"> + <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;"> + <td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top"> + <table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white"> + <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> + <td class="content-wrap" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 20px; -webkit-text-size-adjust: none;" valign="top"> + <table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> + <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> + <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top"> + Your account was just logged into from a new device. + </td> + </tr> + <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> + <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top"> + <b>Date</b>: {{datetime}} + </td> + </tr> + <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> + <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top"> + <b>IP Address:</b> {{ip}} + </td> + </tr> + <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> + <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top"> + <b>Device Type:</b> {{device}} + </td> + </tr> + <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> + <td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top"> + You can deauthorize all devices that have access to your account from the <a href="{{url}}">web vault</a> under Settings > My Account > Deauthorize Sessions. + </td> + </tr> + </table> + </td> + </tr> + </table> + <table class="footer" cellpadding="0" cellspacing="0" width="100%" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; width: 100%;"> + <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;"> + <td class="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 15px 0 0 0;" valign="top"> + <table cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto;"> + <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;"> + <td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;"><img src="{{url}}/bwrs_images/mail-github.png" alt="GitHub" width="30" height="30" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" /></a></td> + </tr> + </table> + </td> + </tr> + </table> + </td> + </tr> + </table> + </td> + </tr> + </table> + </body> +</html> From 60e39a9dd167959fdc6e474c4497853462d847f2 Mon Sep 17 00:00:00 2001 From: vpl <vpl@vpl.me> Date: Mon, 22 Jul 2019 08:24:19 +0200 Subject: [PATCH 2/3] Move retrieve/new device from connData to separate function --- src/api/identity.rs | 65 +++++++++++++++++++++++---------------------- src/mail.rs | 7 +---- 2 files changed, 34 insertions(+), 38 deletions(-) diff --git a/src/api/identity.rs b/src/api/identity.rs index 74e9fa37..b293b7e6 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -101,38 +101,7 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult ) } - // On iOS, device_type sends "iOS", on others it sends a number - let device_type = util::try_parse_string(data.device_type.as_ref()).unwrap_or(0); - let device_id = data.device_identifier.clone().expect("No device id provided"); - let device_name = data.device_name.clone().expect("No device name provided"); - - let mut send_email = false; - // Find device or create new - let mut device = match Device::find_by_uuid(&device_id, &conn) { - Some(device) => { - // Check if owned device, and recreate if not - if device.user_uuid != user.uuid { - info!("Device exists but is owned by another user. The old device will be discarded"); - send_email = true; - Device::new(device_id, user.uuid.clone(), device_name, device_type) - } else { - device - } - } - None => { - send_email = true; - Device::new(device_id, user.uuid.clone(), device_name, device_type) - } - }; - - if CONFIG.mail_enabled() && send_email { - mail::send_new_device_logged_in( - &username, - &ip.ip.to_string(), - &device.updated_at, - &device.name, - )?; - } + let mut device = get_device(&data, &conn, &user, &ip)?; let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, &conn)?; @@ -161,6 +130,38 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult Ok(Json(result)) } +fn get_device(data: &ConnectData, conn: &DbConn, user: &User, ip: &ClientIp) -> Result<Device, crate::error::Error> { + // On iOS, device_type sends "iOS", on others it sends a number + let device_type = util::try_parse_string(data.device_type.as_ref()).unwrap_or(0); + let device_id = data.device_identifier.clone().expect("No device id provided"); + let device_name = data.device_name.clone().expect("No device name provided"); + + let mut new_device = false; + // Find device or create new + let device = match Device::find_by_uuid(&device_id, &conn) { + Some(device) => { + // Check if owned device, and recreate if not + if device.user_uuid != user.uuid { + info!("Device exists but is owned by another user. The old device will be discarded"); + new_device = true; + Device::new(device_id, user.uuid.clone(), device_name, device_type) + } else { + device + } + } + None => { + new_device = true; + Device::new(device_id, user.uuid.clone(), device_name, device_type) + } + }; + + if CONFIG.mail_enabled() && new_device { + mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &device.updated_at, &device.name)? + } + + Ok(device) +} + fn twofactor_auth( user_uuid: &str, data: &ConnectData, diff --git a/src/mail.rs b/src/mail.rs index 576cb6b0..506c85eb 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -137,12 +137,7 @@ pub fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult { send_email(&address, &subject, &body_html, &body_text) } -pub fn send_new_device_logged_in( - address: &str, - ip: &str, - dt: &NaiveDateTime, - device: &str, -) -> EmptyResult { +pub fn send_new_device_logged_in(address: &str, ip: &str, dt: &NaiveDateTime, device: &str) -> EmptyResult { use crate::util::upcase_first; let device = upcase_first(device); From df71f57d869d981d56697d255ad3e87630d3629e Mon Sep 17 00:00:00 2001 From: vpl <vpl@vpl.me> Date: Thu, 25 Jul 2019 20:47:58 +0200 Subject: [PATCH 3/3] Move send device email to end of password login Send new device email after two factor authentication. --- src/api/identity.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/api/identity.rs b/src/api/identity.rs index b293b7e6..0fbe77ed 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -101,10 +101,14 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult ) } - let mut device = get_device(&data, &conn, &user, &ip)?; + let (mut device, new_device) = get_device(&data, &conn, &user); let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, &conn)?; + if CONFIG.mail_enabled() && new_device { + mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &device.updated_at, &device.name)? + } + // Common let user = User::find_by_uuid(&device.user_uuid, &conn).unwrap(); let orgs = UserOrganization::find_by_user(&user.uuid, &conn); @@ -119,7 +123,6 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult "refresh_token": device.refresh_token, "Key": user.akey, "PrivateKey": user.private_key, - //"TwoFactorToken": "11122233333444555666777888999" }); if let Some(token) = twofactor_token { @@ -130,7 +133,8 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult Ok(Json(result)) } -fn get_device(data: &ConnectData, conn: &DbConn, user: &User, ip: &ClientIp) -> Result<Device, crate::error::Error> { +/// 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 let device_type = util::try_parse_string(data.device_type.as_ref()).unwrap_or(0); let device_id = data.device_identifier.clone().expect("No device id provided"); @@ -155,11 +159,7 @@ fn get_device(data: &ConnectData, conn: &DbConn, user: &User, ip: &ClientIp) -> } }; - if CONFIG.mail_enabled() && new_device { - mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &device.updated_at, &device.name)? - } - - Ok(device) + (device, new_device) } fn twofactor_auth(