committed by
GitHub
24 changed files with 312 additions and 15 deletions
@ -0,0 +1 @@ |
|||||
|
DROP TABLE twofactor_incomplete; |
@ -0,0 +1,9 @@ |
|||||
|
CREATE TABLE twofactor_incomplete ( |
||||
|
user_uuid CHAR(36) NOT NULL REFERENCES users(uuid), |
||||
|
device_uuid CHAR(36) NOT NULL, |
||||
|
device_name TEXT NOT NULL, |
||||
|
login_time DATETIME NOT NULL, |
||||
|
ip_address TEXT NOT NULL, |
||||
|
|
||||
|
PRIMARY KEY (user_uuid, device_uuid) |
||||
|
); |
@ -0,0 +1 @@ |
|||||
|
DROP TABLE twofactor_incomplete; |
@ -0,0 +1,9 @@ |
|||||
|
CREATE TABLE twofactor_incomplete ( |
||||
|
user_uuid VARCHAR(40) NOT NULL REFERENCES users(uuid), |
||||
|
device_uuid VARCHAR(40) NOT NULL, |
||||
|
device_name TEXT NOT NULL, |
||||
|
login_time DATETIME NOT NULL, |
||||
|
ip_address TEXT NOT NULL, |
||||
|
|
||||
|
PRIMARY KEY (user_uuid, device_uuid) |
||||
|
); |
@ -0,0 +1 @@ |
|||||
|
DROP TABLE twofactor_incomplete; |
@ -0,0 +1,9 @@ |
|||||
|
CREATE TABLE twofactor_incomplete ( |
||||
|
user_uuid TEXT NOT NULL REFERENCES users(uuid), |
||||
|
device_uuid TEXT NOT NULL, |
||||
|
device_name TEXT NOT NULL, |
||||
|
login_time DATETIME NOT NULL, |
||||
|
ip_address TEXT NOT NULL, |
||||
|
|
||||
|
PRIMARY KEY (user_uuid, device_uuid) |
||||
|
); |
@ -0,0 +1,108 @@ |
|||||
|
use chrono::{NaiveDateTime, Utc}; |
||||
|
|
||||
|
use crate::{api::EmptyResult, auth::ClientIp, db::DbConn, error::MapResult, CONFIG}; |
||||
|
|
||||
|
use super::User; |
||||
|
|
||||
|
db_object! { |
||||
|
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)] |
||||
|
#[table_name = "twofactor_incomplete"] |
||||
|
#[belongs_to(User, foreign_key = "user_uuid")] |
||||
|
#[primary_key(user_uuid, device_uuid)] |
||||
|
pub struct TwoFactorIncomplete { |
||||
|
pub user_uuid: String, |
||||
|
// This device UUID is simply what's claimed by the device. It doesn't
|
||||
|
// necessarily correspond to any UUID in the devices table, since a device
|
||||
|
// must complete 2FA login before being added into the devices table.
|
||||
|
pub device_uuid: String, |
||||
|
pub device_name: String, |
||||
|
pub login_time: NaiveDateTime, |
||||
|
pub ip_address: String, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
impl TwoFactorIncomplete { |
||||
|
pub fn mark_incomplete( |
||||
|
user_uuid: &str, |
||||
|
device_uuid: &str, |
||||
|
device_name: &str, |
||||
|
ip: &ClientIp, |
||||
|
conn: &DbConn, |
||||
|
) -> EmptyResult { |
||||
|
if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() { |
||||
|
return Ok(()); |
||||
|
} |
||||
|
|
||||
|
// Don't update the data for an existing user/device pair, since that
|
||||
|
// would allow an attacker to arbitrarily delay notifications by
|
||||
|
// sending repeated 2FA attempts to reset the timer.
|
||||
|
let existing = Self::find_by_user_and_device(user_uuid, device_uuid, conn); |
||||
|
if existing.is_some() { |
||||
|
return Ok(()); |
||||
|
} |
||||
|
|
||||
|
db_run! { conn: { |
||||
|
diesel::insert_into(twofactor_incomplete::table) |
||||
|
.values(( |
||||
|
twofactor_incomplete::user_uuid.eq(user_uuid), |
||||
|
twofactor_incomplete::device_uuid.eq(device_uuid), |
||||
|
twofactor_incomplete::device_name.eq(device_name), |
||||
|
twofactor_incomplete::login_time.eq(Utc::now().naive_utc()), |
||||
|
twofactor_incomplete::ip_address.eq(ip.ip.to_string()), |
||||
|
)) |
||||
|
.execute(conn) |
||||
|
.map_res("Error adding twofactor_incomplete record") |
||||
|
}} |
||||
|
} |
||||
|
|
||||
|
pub fn mark_complete(user_uuid: &str, device_uuid: &str, conn: &DbConn) -> EmptyResult { |
||||
|
if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() { |
||||
|
return Ok(()); |
||||
|
} |
||||
|
|
||||
|
Self::delete_by_user_and_device(user_uuid, device_uuid, conn) |
||||
|
} |
||||
|
|
||||
|
pub fn find_by_user_and_device(user_uuid: &str, device_uuid: &str, conn: &DbConn) -> Option<Self> { |
||||
|
db_run! { conn: { |
||||
|
twofactor_incomplete::table |
||||
|
.filter(twofactor_incomplete::user_uuid.eq(user_uuid)) |
||||
|
.filter(twofactor_incomplete::device_uuid.eq(device_uuid)) |
||||
|
.first::<TwoFactorIncompleteDb>(conn) |
||||
|
.ok() |
||||
|
.from_db() |
||||
|
}} |
||||
|
} |
||||
|
|
||||
|
pub fn find_logins_before(dt: &NaiveDateTime, conn: &DbConn) -> Vec<Self> { |
||||
|
db_run! {conn: { |
||||
|
twofactor_incomplete::table |
||||
|
.filter(twofactor_incomplete::login_time.lt(dt)) |
||||
|
.load::<TwoFactorIncompleteDb>(conn) |
||||
|
.expect("Error loading twofactor_incomplete") |
||||
|
.from_db() |
||||
|
}} |
||||
|
} |
||||
|
|
||||
|
pub fn delete(self, conn: &DbConn) -> EmptyResult { |
||||
|
Self::delete_by_user_and_device(&self.user_uuid, &self.device_uuid, conn) |
||||
|
} |
||||
|
|
||||
|
pub fn delete_by_user_and_device(user_uuid: &str, device_uuid: &str, conn: &DbConn) -> EmptyResult { |
||||
|
db_run! { conn: { |
||||
|
diesel::delete(twofactor_incomplete::table |
||||
|
.filter(twofactor_incomplete::user_uuid.eq(user_uuid)) |
||||
|
.filter(twofactor_incomplete::device_uuid.eq(device_uuid))) |
||||
|
.execute(conn) |
||||
|
.map_res("Error in twofactor_incomplete::delete_by_user_and_device()") |
||||
|
}} |
||||
|
} |
||||
|
|
||||
|
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult { |
||||
|
db_run! { conn: { |
||||
|
diesel::delete(twofactor_incomplete::table.filter(twofactor_incomplete::user_uuid.eq(user_uuid))) |
||||
|
.execute(conn) |
||||
|
.map_res("Error in twofactor_incomplete::delete_all_by_user()") |
||||
|
}} |
||||
|
} |
||||
|
} |
@ -0,0 +1,10 @@ |
|||||
|
Incomplete Two-Step Login From {{{device}}} |
||||
|
<!----------------> |
||||
|
Someone attempted to log into your account with the correct master password, but did not provide the correct token or action required to complete the two-step login process within {{time_limit}} minutes of the initial login attempt. |
||||
|
|
||||
|
* Date: {{datetime}} |
||||
|
* IP Address: {{ip}} |
||||
|
* Device Type: {{device}} |
||||
|
|
||||
|
If this was not you or someone you authorized, then you should change your master password as soon as possible, as it is likely to be compromised. |
||||
|
{{> email/email_footer_text }} |
@ -0,0 +1,31 @@ |
|||||
|
Incomplete Two-Step Login From {{{device}}} |
||||
|
<!----------------> |
||||
|
{{> email/email_header }} |
||||
|
<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"> |
||||
|
Someone attempted to log into your account with the correct master password, but did not provide the correct token or action required to complete the two-step login process within {{time_limit}} minutes of the initial login attempt. |
||||
|
</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"> |
||||
|
If this was not you or someone you authorized, then you should change your master password as soon as possible, as it is likely to be compromised. |
||||
|
</td> |
||||
|
</tr> |
||||
|
</table> |
||||
|
{{> email/email_footer }} |
Loading…
Reference in new issue