From 7a3eaa944edb6d7a8023a5c05c0c059449ba4d92 Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Sat, 12 Nov 2022 10:24:00 +0100 Subject: [PATCH 1/6] Start to create 2FA exemption after successful 2FA login --- src/actors/users_actor.rs | 14 ++++++++++++++ src/constants.rs | 4 ++++ src/controllers/admin_controller.rs | 5 +++++ src/controllers/login_api.rs | 11 +++++++++++ src/controllers/login_controller.rs | 9 ++++++++- src/data/user.rs | 29 +++++++++++++++++++++++++++++ templates/settings/edit_user.html | 23 +++++++++++++++++------ 7 files changed, 88 insertions(+), 7 deletions(-) diff --git a/src/actors/users_actor.rs b/src/actors/users_actor.rs index ed9feae..dd11869 100644 --- a/src/actors/users_actor.rs +++ b/src/actors/users_actor.rs @@ -1,4 +1,5 @@ use actix::{Actor, Context, Handler, Message, MessageResult}; +use std::net::IpAddr; use crate::data::entity_manager::EntityManager; use crate::data::user::{User, UserID}; @@ -50,6 +51,10 @@ pub struct ChangePasswordRequest { #[derive(Debug)] pub struct ChangePasswordResult(pub bool); +#[derive(Message)] +#[rtype(result = "bool")] +pub struct AddSuccessful2FALogin(pub UserID, pub IpAddr); + #[derive(Debug)] pub struct UpdateUserResult(pub bool); @@ -111,6 +116,15 @@ impl Handler for UsersActor { } } +impl Handler for UsersActor { + type Result = ::Result; + + fn handle(&mut self, msg: AddSuccessful2FALogin, _ctx: &mut Self::Context) -> Self::Result { + self.manager + .save_new_successful_2fa_authentication(&msg.0, msg.1) + } +} + impl Handler for UsersActor { type Result = MessageResult; diff --git a/src/constants.rs b/src/constants.rs index 688753d..6603348 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -19,6 +19,10 @@ pub const MAX_INACTIVITY_DURATION: u64 = 60 * 30; /// Maximum session duration (6 hours) pub const MAX_SESSION_DURATION: u64 = 3600 * 6; +/// When the user successfully authenticate using 2FA, period of time during which the user is +/// exempted from this IP address to use 2FA +pub const SECOND_FACTOR_EXEMPTION_AFTER_SUCCESSFUL_LOGIN: u64 = 7 * 24 * 3600; + /// Minimum password length pub const MIN_PASS_LEN: usize = 4; diff --git a/src/controllers/admin_controller.rs b/src/controllers/admin_controller.rs index 37ff7fa..090b7df 100644 --- a/src/controllers/admin_controller.rs +++ b/src/controllers/admin_controller.rs @@ -55,6 +55,7 @@ pub struct UpdateUserQuery { email: String, gen_new_password: Option, enabled: Option, + two_factor_exemption_after_successful_login: Option, admin: Option, grant_type: String, granted_clients: String, @@ -84,6 +85,10 @@ pub async fn users_route( user.last_name = update.0.last_name; user.email = update.0.email; user.enabled = update.0.enabled.is_some(); + user.two_factor_exemption_after_successful_login = update + .0 + .two_factor_exemption_after_successful_login + .is_some(); user.admin = update.0.admin.is_some(); let factors_to_keep = update.0.two_factor.split(';').collect::>(); diff --git a/src/controllers/login_api.rs b/src/controllers/login_api.rs index eb9d8ee..b1e0bfa 100644 --- a/src/controllers/login_api.rs +++ b/src/controllers/login_api.rs @@ -1,3 +1,7 @@ +use crate::actors::users_actor; +use crate::actors::users_actor::UsersActor; +use crate::data::remote_ip::RemoteIP; +use actix::Addr; use actix_identity::Identity; use actix_web::{web, HttpRequest, HttpResponse, Responder}; use webauthn_rs::prelude::PublicKeyCredential; @@ -16,6 +20,8 @@ pub async fn auth_webauthn( req: web::Json, manager: WebAuthManagerReq, http_req: HttpRequest, + remote_ip: RemoteIP, + users: web::Data>, ) -> impl Responder { if !SessionIdentity(Some(&id)).need_2fa_auth() { return HttpResponse::Unauthorized().json("No 2FA required!"); @@ -25,6 +31,11 @@ pub async fn auth_webauthn( match manager.finish_authentication(&user_id, &req.opaque_state, &req.credential) { Ok(_) => { + users + .send(users_actor::AddSuccessful2FALogin(user_id, remote_ip.0)) + .await + .unwrap(); + SessionIdentity(Some(&id)).set_status(&http_req, SessionStatus::SignedIn); HttpResponse::Ok().body("You are authenticated!") } diff --git a/src/controllers/login_controller.rs b/src/controllers/login_controller.rs index a879ec6..4517aed 100644 --- a/src/controllers/login_controller.rs +++ b/src/controllers/login_controller.rs @@ -139,7 +139,8 @@ pub async fn login_route( LoginResult::Success(user) => { let status = if user.need_reset_password { SessionStatus::NeedNewPassword - } else if user.has_two_factor() { + } else if user.has_two_factor() && !user.can_bypass_two_factors_for_ip(remote_ip.0) + { SessionStatus::Need2FA } else { SessionStatus::SignedIn @@ -326,6 +327,7 @@ pub async fn login_with_otp( form: Option>, users: web::Data>, http_req: HttpRequest, + remote_ip: RemoteIP, ) -> impl Responder { let mut danger = None; @@ -354,6 +356,11 @@ pub async fn login_with_otp( { danger = Some("Specified code is invalid!".to_string()); } else { + users + .send(users_actor::AddSuccessful2FALogin(user.uid, remote_ip.0)) + .await + .unwrap(); + SessionIdentity(id.as_ref()).set_status(&http_req, SessionStatus::SignedIn); return redirect_user(query.redirect.get()); } diff --git a/src/data/user.rs b/src/data/user.rs index 908324f..c14266a 100644 --- a/src/data/user.rs +++ b/src/data/user.rs @@ -1,9 +1,13 @@ +use crate::constants::SECOND_FACTOR_EXEMPTION_AFTER_SUCCESSFUL_LOGIN; use crate::data::client::ClientID; use crate::data::entity_manager::EntityManager; use crate::data::login_redirect::LoginRedirect; use crate::data::totp_key::TotpKey; use crate::data::webauthn_manager::WebauthnPubKey; use crate::utils::err::Res; +use crate::utils::time::time; +use std::collections::HashMap; +use std::net::IpAddr; #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub struct UserID(pub String); @@ -76,6 +80,15 @@ pub struct User { #[serde(default)] pub two_factor: Vec, + /// Exempt the user from validating a second factor after a previous successful authentication + /// for a defined amount of time + #[serde(default)] + pub two_factor_exemption_after_successful_login: bool, + + /// IP addresses of last successful logins + #[serde(default)] + pub last_successful_2fa: HashMap, + /// None = all services /// Some([]) = no service pub authorized_clients: Option>, @@ -101,6 +114,13 @@ impl User { !self.two_factor.is_empty() } + pub fn can_bypass_two_factors_for_ip(&self, ip: IpAddr) -> bool { + self.two_factor_exemption_after_successful_login + && self.last_successful_2fa.get(&ip).unwrap_or(&0) + + SECOND_FACTOR_EXEMPTION_AFTER_SUCCESSFUL_LOGIN + > time() + } + pub fn add_factor(&mut self, factor: TwoFactor) { self.two_factor.push(factor); } @@ -178,6 +198,8 @@ impl Default for User { enabled: true, admin: false, two_factor: vec![], + two_factor_exemption_after_successful_login: false, + last_successful_2fa: Default::default(), authorized_clients: Some(Vec::new()), } } @@ -249,4 +271,11 @@ impl EntityManager { user }) } + + pub fn save_new_successful_2fa_authentication(&mut self, id: &UserID, ip: IpAddr) -> bool { + self.update_user(id, |mut user| { + user.last_successful_2fa.insert(ip, time()); + user + }) + } } diff --git a/templates/settings/edit_user.html b/templates/settings/edit_user.html index 91fcd9e..1cfadcd 100644 --- a/templates/settings/edit_user.html +++ b/templates/settings/edit_user.html @@ -12,7 +12,7 @@
-
This username is valid
This username is already taken.
@@ -51,17 +51,27 @@
- +
+ +
+ + +
+
- + @@ -80,7 +90,7 @@ - {{ f.name }} (Factor icon + {{ f.name }} (Factor icon {{ f.type_str() }})
@@ -190,6 +200,7 @@ form.submit(); }); + {% endblock content %} \ No newline at end of file From 7e1cbb184d0125d3cbbf101b430c0ab11c08416f Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Sat, 12 Nov 2022 11:16:55 +0100 Subject: [PATCH 2/6] Can clear 2FA login history from edit_user page --- Cargo.lock | 161 ++++++++++++++++++++++++++-- Cargo.toml | 1 + src/controllers/admin_controller.rs | 6 ++ src/data/user.rs | 32 +++++- src/utils/time.rs | 14 +++ templates/settings/edit_user.html | 22 ++++ 6 files changed, 226 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6ba27b7..0680d62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -222,7 +222,7 @@ dependencies = [ "serde_urlencoded", "smallvec", "socket2", - "time", + "time 0.3.17", "url", ] @@ -325,6 +325,15 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.66" @@ -392,7 +401,7 @@ dependencies = [ "num-traits", "rusticata-macros", "thiserror", - "time", + "time 0.3.17", ] [[package]] @@ -495,6 +504,7 @@ dependencies = [ "base64", "bcrypt", "bincode", + "chrono", "clap", "digest", "env_logger", @@ -644,6 +654,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "time 0.1.44", + "wasm-bindgen", + "winapi", +] + [[package]] name = "cipher" version = "0.4.3" @@ -699,10 +724,20 @@ checksum = "454038500439e141804c655b4cd1bc6a70bcb95cd2bc9463af5661b6956f0e46" dependencies = [ "libc", "once_cell", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -752,10 +787,16 @@ dependencies = [ "rand", "sha2", "subtle", - "time", + "time 0.3.17", "version_check", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + [[package]] name = "cpufeatures" version = "0.2.5" @@ -861,6 +902,50 @@ dependencies = [ "cipher", ] +[[package]] +name = "cxx" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97abf9f0eca9e52b7f81b945524e76710e6cb2366aead23b7d4fbf72e281f888" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc32cc5fea1d894b77d269ddb9f192110069a8a9c1f1d441195fba90553dea3" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca220e4794c934dc6b1207c3b42856ad4c302f2df1712e9f8d2eec5afaacf1f" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b846f081361125bfc8dc9d3940c84e1fd83ba54bbca7b17cd29483c828be0704" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "data-encoding" version = "2.3.2" @@ -1140,7 +1225,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -1322,6 +1407,30 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "iana-time-zone" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + [[package]] name = "idna" version = "0.3.0" @@ -1516,6 +1625,15 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" +[[package]] +name = "link-cplusplus" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369" +dependencies = [ + "cc", +] + [[package]] name = "local-channel" version = "0.1.3" @@ -1616,7 +1734,7 @@ checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys", ] @@ -2163,6 +2281,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "scratch" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" + [[package]] name = "sec1" version = "0.3.0" @@ -2424,6 +2548,17 @@ dependencies = [ "weezl", ] +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + [[package]] name = "time" version = "0.3.17" @@ -2585,6 +2720,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + [[package]] name = "unicode-xid" version = "0.2.4" @@ -2659,6 +2800,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2888,7 +3035,7 @@ dependencies = [ "oid-registry", "rusticata-macros", "thiserror", - "time", + "time 0.3.17", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d0d09ae..b723d5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,3 +36,4 @@ webauthn-rs = { version = "0.4.7", features = ["danger-allow-state-serialisation url = "2.3.1" aes-gcm = { version = "0.10.1", features = ["aes"] } bincode = "1.3.3" +chrono = "0.4.22" \ No newline at end of file diff --git a/src/controllers/admin_controller.rs b/src/controllers/admin_controller.rs index 090b7df..ce98fa6 100644 --- a/src/controllers/admin_controller.rs +++ b/src/controllers/admin_controller.rs @@ -60,6 +60,7 @@ pub struct UpdateUserQuery { grant_type: String, granted_clients: String, two_factor: String, + clear_2fa_history: Option, } pub async fn users_route( @@ -114,10 +115,15 @@ pub async fn users_route( let temp_pass = rand_str(TEMPORARY_PASSWORDS_LEN); user.password = hash_password(&temp_pass).expect("Failed to hash password"); user.need_reset_password = true; + user.last_successful_2fa = Default::default(); Some(temp_pass) } }; + if update.0.clear_2fa_history.is_some() { + user.last_successful_2fa = Default::default(); + } + let res = users .send(users_actor::UpdateUserRequest(user.clone())) .await diff --git a/src/data/user.rs b/src/data/user.rs index c14266a..5b03fd1 100644 --- a/src/data/user.rs +++ b/src/data/user.rs @@ -1,3 +1,6 @@ +use std::collections::HashMap; +use std::net::IpAddr; + use crate::constants::SECOND_FACTOR_EXEMPTION_AFTER_SUCCESSFUL_LOGIN; use crate::data::client::ClientID; use crate::data::entity_manager::EntityManager; @@ -5,9 +8,7 @@ use crate::data::login_redirect::LoginRedirect; use crate::data::totp_key::TotpKey; use crate::data::webauthn_manager::WebauthnPubKey; use crate::utils::err::Res; -use crate::utils::time::time; -use std::collections::HashMap; -use std::net::IpAddr; +use crate::utils::time::{fmt_time, time}; #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub struct UserID(pub String); @@ -64,6 +65,19 @@ impl TwoFactor { } } +#[derive(Debug)] +pub struct Successful2FALogin { + pub ip: IpAddr, + pub time: u64, + pub can_bypass_2fa: bool, +} + +impl Successful2FALogin { + pub fn fmt_time(&self) -> String { + fmt_time(self.time) + } +} + #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct User { pub uid: UserID, @@ -175,6 +189,17 @@ impl User { }) .collect::>() } + + pub fn get_formatted_2fa_successful_logins(&self) -> Vec { + self.last_successful_2fa + .iter() + .map(|(ip, time)| Successful2FALogin { + ip: *ip, + time: *time, + can_bypass_2fa: self.can_bypass_two_factors_for_ip(*ip), + }) + .collect::>() + } } impl PartialEq for User { @@ -268,6 +293,7 @@ impl EntityManager { self.update_user(id, |mut user| { user.password = new_hash; user.need_reset_password = temporary; + user.two_factor_exemption_after_successful_login = Default::default(); user }) } diff --git a/src/utils/time.rs b/src/utils/time.rs index 14e1652..5eb474d 100644 --- a/src/utils/time.rs +++ b/src/utils/time.rs @@ -1,3 +1,4 @@ +use chrono::{DateTime, NaiveDateTime, Utc}; use std::time::{SystemTime, UNIX_EPOCH}; /// Get the current time since epoch @@ -7,3 +8,16 @@ pub fn time() -> u64 { .unwrap() .as_secs() } + +/// Format unix timestamp to a human-readable string +pub fn fmt_time(timestamp: u64) -> String { + // Create a NaiveDateTime from the timestamp + let naive = + NaiveDateTime::from_timestamp_opt(timestamp as i64, 0).expect("Failed to parse timestamp!"); + + // Create a normal DateTime from the NaiveDateTime + let datetime: DateTime = DateTime::from_utc(naive, Utc); + + // Format the datetime how you want + datetime.format("%Y-%m-%d %H:%M:%S").to_string() +} diff --git a/templates/settings/edit_user.html b/templates/settings/edit_user.html index 1cfadcd..2ded57e 100644 --- a/templates/settings/edit_user.html +++ b/templates/settings/edit_user.html @@ -98,6 +98,28 @@ {% endif %} + + {% if !u.last_successful_2fa.is_empty() %} +
+ Last successful 2FA authentications + + +
+ + +
+ +
    + {% for e in u.get_formatted_2fa_successful_logins() %} + {% if e.can_bypass_2fa %}
  • {{ e.ip }} - {{ e.fmt_time() }} - BYPASS 2FA
  • + {% else %}
  • {{ e.ip }} - {{ e.fmt_time() }}
  • {% endif %} + {% endfor %} +
+
+ {% endif %} +
Granted clients From 46bf14025b5cd15d03408475e3131a6a0bfd4986 Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Sat, 12 Nov 2022 11:18:40 +0100 Subject: [PATCH 3/6] cargo clippy --- src/actors/users_actor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/actors/users_actor.rs b/src/actors/users_actor.rs index dd11869..8f9c9d8 100644 --- a/src/actors/users_actor.rs +++ b/src/actors/users_actor.rs @@ -9,7 +9,7 @@ pub enum LoginResult { AccountNotFound, InvalidPassword, AccountDisabled, - Success(User), + Success(Box), } #[derive(Message)] @@ -98,7 +98,7 @@ impl Handler for UsersActor { return MessageResult(LoginResult::AccountDisabled); } - MessageResult(LoginResult::Success(user)) + MessageResult(LoginResult::Success(Box::new(user))) } } } From 1fa36c0aff16f3681c67720e989d30ff44d36612 Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Sat, 12 Nov 2022 11:27:19 +0100 Subject: [PATCH 4/6] Automatically remove outdated 2FA successful entries --- src/data/user.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/data/user.rs b/src/data/user.rs index 5b03fd1..1394a84 100644 --- a/src/data/user.rs +++ b/src/data/user.rs @@ -190,6 +190,11 @@ impl User { .collect::>() } + pub fn remove_outdated_successful_2fa_attempts(&mut self) { + self.last_successful_2fa + .retain(|_, t| *t + SECOND_FACTOR_EXEMPTION_AFTER_SUCCESSFUL_LOGIN > time()); + } + pub fn get_formatted_2fa_successful_logins(&self) -> Vec { self.last_successful_2fa .iter() @@ -301,6 +306,10 @@ impl EntityManager { pub fn save_new_successful_2fa_authentication(&mut self, id: &UserID, ip: IpAddr) -> bool { self.update_user(id, |mut user| { user.last_successful_2fa.insert(ip, time()); + + // Remove outdated successful attempts + user.remove_outdated_successful_2fa_attempts(); + user }) } From 7887ccaa4185e6e9eba906eb2d385010fc3ba6a8 Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Sat, 12 Nov 2022 11:37:15 +0100 Subject: [PATCH 5/6] Show 2FA successful login on 2FA user page --- templates/settings/two_factors_page.html | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/templates/settings/two_factors_page.html b/templates/settings/two_factors_page.html index 0c9732b..87ea375 100644 --- a/templates/settings/two_factors_page.html +++ b/templates/settings/two_factors_page.html @@ -34,6 +34,28 @@ +{% if !user.last_successful_2fa.is_empty() %} +
Successful 2FA login history
+ + + + + + + + + + {% for e in user.get_formatted_2fa_successful_logins() %} + + + + + + {% endfor %} + +
IP addressDateBypass 2FA
{{ e.ip }}{{ e.fmt_time() }}{% if e.can_bypass_2fa %}YES{% else %}NO{% endif %}
+{% endif %} + {% endblock content %}