diff --git a/src/controllers/login_api.rs b/src/controllers/login_api.rs index 532fdc1..4b1d001 100644 --- a/src/controllers/login_api.rs +++ b/src/controllers/login_api.rs @@ -41,7 +41,9 @@ pub async fn auth_webauthn( .await .unwrap(); - SessionIdentity(Some(&id)).set_status(&http_req, SessionStatus::SignedIn); + let session = SessionIdentity(Some(&id)); + session.record_2fa_auth(&http_req); + session.set_status(&http_req, SessionStatus::SignedIn); logger.log(Action::LoginWebauthnAttempt { success: true, user_id, diff --git a/src/controllers/login_controller.rs b/src/controllers/login_controller.rs index f76d1b7..b4f573e 100644 --- a/src/controllers/login_controller.rs +++ b/src/controllers/login_controller.rs @@ -258,7 +258,7 @@ pub async fn reset_password_route( let user_id = SessionIdentity(id.as_ref()).user_id(); - // Check if user is setting a new password + // Check if user is setting a new password if let Some(req) = &req { if req.password.len() < MIN_PASS_LEN { danger = Some("Password is too short!".to_string()); @@ -408,7 +408,9 @@ pub async fn login_with_otp( .await .unwrap(); - SessionIdentity(id.as_ref()).set_status(&http_req, SessionStatus::SignedIn); + let session = SessionIdentity(id.as_ref()); + session.record_2fa_auth(&http_req); + session.set_status(&http_req, SessionStatus::SignedIn); logger.log(Action::OTPLoginAttempt { success: true, user: &user, diff --git a/src/controllers/two_factors_controller.rs b/src/controllers/two_factors_controller.rs index aa64109..93d053f 100644 --- a/src/controllers/two_factors_controller.rs +++ b/src/controllers/two_factors_controller.rs @@ -13,12 +13,14 @@ use crate::data::current_user::CurrentUser; use crate::data::totp_key::TotpKey; use crate::data::user::User; use crate::data::webauthn_manager::WebAuthManagerReq; +use crate::utils::time::fmt_time; #[derive(Template)] #[template(path = "settings/two_factors_page.html")] struct TwoFactorsPage<'a> { p: BaseSettingsPage<'a>, user: &'a User, + last_2fa_auth: Option, } #[derive(Template)] @@ -46,6 +48,7 @@ pub async fn two_factors_route(user: CurrentUser) -> impl Responder { TwoFactorsPage { p: BaseSettingsPage::get("Two factor auth", &user, None, None), user: user.deref(), + last_2fa_auth: user.last_2fa_auth.map(fmt_time), } .render() .unwrap(), diff --git a/src/data/current_user.rs b/src/data/current_user.rs index 7606fee..41adfd0 100644 --- a/src/data/current_user.rs +++ b/src/data/current_user.rs @@ -13,11 +13,15 @@ use crate::actors::users_actor::UsersActor; use crate::data::session_identity::SessionIdentity; use crate::data::user::User; -pub struct CurrentUser(User); +pub struct CurrentUser { + user: User, + pub auth_time: u64, + pub last_2fa_auth: Option, +} impl From for User { fn from(user: CurrentUser) -> Self { - user.0 + user.user } } @@ -25,7 +29,7 @@ impl Deref for CurrentUser { type Target = User; fn deref(&self) -> &Self::Target { - &self.0 + &self.user } } @@ -40,7 +44,10 @@ impl FromRequest for CurrentUser { let identity: Identity = Identity::from_request(req, payload) .into_inner() .expect("Failed to get identity!"); - let user_id = SessionIdentity(Some(&identity)).user_id(); + let id = SessionIdentity(Some(&identity)); + let user_id = id.user_id(); + let last_2fa_auth = id.last_2fa_auth(); + let auth_time = id.auth_time(); Box::pin(async move { let user = match user_actor @@ -57,7 +64,11 @@ impl FromRequest for CurrentUser { } }; - Ok(CurrentUser(user)) + Ok(CurrentUser { + user, + auth_time, + last_2fa_auth, + }) }) } } diff --git a/src/data/session_identity.rs b/src/data/session_identity.rs index 20a6246..6f75391 100644 --- a/src/data/session_identity.rs +++ b/src/data/session_identity.rs @@ -24,6 +24,7 @@ pub struct SessionIdentityData { pub id: Option, pub is_admin: bool, pub auth_time: u64, + pub last_2fa_auth: Option, pub status: SessionStatus, } @@ -75,6 +76,7 @@ impl<'a> SessionIdentity<'a> { &SessionIdentityData { id: Some(user.uid.clone()), is_admin: user.admin, + last_2fa_auth: None, auth_time: time(), status, }, @@ -87,6 +89,12 @@ impl<'a> SessionIdentity<'a> { self.set_session_data(req, &sess); } + pub fn record_2fa_auth(&self, req: &HttpRequest) { + let mut sess = self.get_session_data().unwrap_or_default(); + sess.last_2fa_auth = Some(time()); + self.set_session_data(req, &sess); + } + pub fn is_authenticated(&self) -> bool { self.get_session_data() .map(|s| s.status == SessionStatus::SignedIn) @@ -119,4 +127,8 @@ impl<'a> SessionIdentity<'a> { pub fn auth_time(&self) -> u64 { self.get_session_data().unwrap_or_default().auth_time } + + pub fn last_2fa_auth(&self) -> Option { + self.get_session_data().unwrap_or_default().last_2fa_auth + } } diff --git a/templates/settings/two_factors_page.html b/templates/settings/two_factors_page.html index 765217f..bab594c 100644 --- a/templates/settings/two_factors_page.html +++ b/templates/settings/two_factors_page.html @@ -26,7 +26,9 @@ {% for f in user.two_factor %} - Factor icon{{ f.type_str() }} + Factor icon{{ + f.type_str() }} + {{ f.name }} Delete @@ -53,7 +55,9 @@ {% for e in user.get_formatted_2fa_successful_logins() %} {{ e.ip }} - + + + {{ e.fmt_time() }} {% if e.can_bypass_2fa %}YES{% else %}NO{% endif %} @@ -63,6 +67,10 @@ {% endif %} +{% if let Some(last_2fa_auth) = last_2fa_auth %} +

Last successful 2FA authentication on this browser: {{ last_2fa_auth }}

+{% endif %} +