Record successful 2FA authentication in session cookie
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Pierre HUBERT 2024-03-25 18:04:54 +01:00
parent b704e9868b
commit 5644e40763
6 changed files with 51 additions and 13 deletions

View File

@ -41,7 +41,9 @@ pub async fn auth_webauthn(
.await .await
.unwrap(); .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 { logger.log(Action::LoginWebauthnAttempt {
success: true, success: true,
user_id, user_id,

View File

@ -258,7 +258,7 @@ pub async fn reset_password_route(
let user_id = SessionIdentity(id.as_ref()).user_id(); 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 let Some(req) = &req {
if req.password.len() < MIN_PASS_LEN { if req.password.len() < MIN_PASS_LEN {
danger = Some("Password is too short!".to_string()); danger = Some("Password is too short!".to_string());
@ -408,7 +408,9 @@ pub async fn login_with_otp(
.await .await
.unwrap(); .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 { logger.log(Action::OTPLoginAttempt {
success: true, success: true,
user: &user, user: &user,

View File

@ -13,12 +13,14 @@ use crate::data::current_user::CurrentUser;
use crate::data::totp_key::TotpKey; use crate::data::totp_key::TotpKey;
use crate::data::user::User; use crate::data::user::User;
use crate::data::webauthn_manager::WebAuthManagerReq; use crate::data::webauthn_manager::WebAuthManagerReq;
use crate::utils::time::fmt_time;
#[derive(Template)] #[derive(Template)]
#[template(path = "settings/two_factors_page.html")] #[template(path = "settings/two_factors_page.html")]
struct TwoFactorsPage<'a> { struct TwoFactorsPage<'a> {
p: BaseSettingsPage<'a>, p: BaseSettingsPage<'a>,
user: &'a User, user: &'a User,
last_2fa_auth: Option<String>,
} }
#[derive(Template)] #[derive(Template)]
@ -46,6 +48,7 @@ pub async fn two_factors_route(user: CurrentUser) -> impl Responder {
TwoFactorsPage { TwoFactorsPage {
p: BaseSettingsPage::get("Two factor auth", &user, None, None), p: BaseSettingsPage::get("Two factor auth", &user, None, None),
user: user.deref(), user: user.deref(),
last_2fa_auth: user.last_2fa_auth.map(fmt_time),
} }
.render() .render()
.unwrap(), .unwrap(),

View File

@ -13,11 +13,15 @@ use crate::actors::users_actor::UsersActor;
use crate::data::session_identity::SessionIdentity; use crate::data::session_identity::SessionIdentity;
use crate::data::user::User; use crate::data::user::User;
pub struct CurrentUser(User); pub struct CurrentUser {
user: User,
pub auth_time: u64,
pub last_2fa_auth: Option<u64>,
}
impl From<CurrentUser> for User { impl From<CurrentUser> for User {
fn from(user: CurrentUser) -> Self { fn from(user: CurrentUser) -> Self {
user.0 user.user
} }
} }
@ -25,7 +29,7 @@ impl Deref for CurrentUser {
type Target = User; type Target = User;
fn deref(&self) -> &Self::Target { 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) let identity: Identity = Identity::from_request(req, payload)
.into_inner() .into_inner()
.expect("Failed to get identity!"); .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 { Box::pin(async move {
let user = match user_actor let user = match user_actor
@ -57,7 +64,11 @@ impl FromRequest for CurrentUser {
} }
}; };
Ok(CurrentUser(user)) Ok(CurrentUser {
user,
auth_time,
last_2fa_auth,
})
}) })
} }
} }

View File

@ -24,6 +24,7 @@ pub struct SessionIdentityData {
pub id: Option<UserID>, pub id: Option<UserID>,
pub is_admin: bool, pub is_admin: bool,
pub auth_time: u64, pub auth_time: u64,
pub last_2fa_auth: Option<u64>,
pub status: SessionStatus, pub status: SessionStatus,
} }
@ -75,6 +76,7 @@ impl<'a> SessionIdentity<'a> {
&SessionIdentityData { &SessionIdentityData {
id: Some(user.uid.clone()), id: Some(user.uid.clone()),
is_admin: user.admin, is_admin: user.admin,
last_2fa_auth: None,
auth_time: time(), auth_time: time(),
status, status,
}, },
@ -87,6 +89,12 @@ impl<'a> SessionIdentity<'a> {
self.set_session_data(req, &sess); 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 { pub fn is_authenticated(&self) -> bool {
self.get_session_data() self.get_session_data()
.map(|s| s.status == SessionStatus::SignedIn) .map(|s| s.status == SessionStatus::SignedIn)
@ -119,4 +127,8 @@ impl<'a> SessionIdentity<'a> {
pub fn auth_time(&self) -> u64 { pub fn auth_time(&self) -> u64 {
self.get_session_data().unwrap_or_default().auth_time self.get_session_data().unwrap_or_default().auth_time
} }
pub fn last_2fa_auth(&self) -> Option<u64> {
self.get_session_data().unwrap_or_default().last_2fa_auth
}
} }

View File

@ -26,7 +26,9 @@
<tbody> <tbody>
{% for f in user.two_factor %} {% for f in user.two_factor %}
<tr id="factor-{{ f.id.0 }}"> <tr id="factor-{{ f.id.0 }}">
<td><img src="{{ f.type_image() }}" alt="Factor icon" style="height: 1.5em; margin-right: 0.5em;" />{{ f.type_str() }}</td> <td><img src="{{ f.type_image() }}" alt="Factor icon" style="height: 1.5em; margin-right: 0.5em;"/>{{
f.type_str() }}
</td>
<td>{{ f.name }}</td> <td>{{ f.name }}</td>
<td><a href="javascript:delete_factor('{{ f.id.0 }}');">Delete</a></td> <td><a href="javascript:delete_factor('{{ f.id.0 }}');">Delete</a></td>
</tr> </tr>
@ -53,7 +55,9 @@
{% for e in user.get_formatted_2fa_successful_logins() %} {% for e in user.get_formatted_2fa_successful_logins() %}
<tr> <tr>
<td>{{ e.ip }}</td> <td>{{ e.ip }}</td>
<td><locateip ip="{{ e.ip }}"></locateip></td> <td>
<locateip ip="{{ e.ip }}"></locateip>
</td>
<td>{{ e.fmt_time() }}</td> <td>{{ e.fmt_time() }}</td>
<td>{% if e.can_bypass_2fa %}YES{% else %}NO{% endif %}</td> <td>{% if e.can_bypass_2fa %}YES{% else %}NO{% endif %}</td>
</tr> </tr>
@ -63,6 +67,10 @@
{% endif %} {% endif %}
</div> </div>
{% if let Some(last_2fa_auth) = last_2fa_auth %}
<p>Last successful 2FA authentication on this browser: {{ last_2fa_auth }}</p>
{% endif %}
<script> <script>
async function delete_factor(id) { async function delete_factor(id) {
if (!confirm("Do you really want to remove this factor?")) if (!confirm("Do you really want to remove this factor?"))
@ -72,7 +80,7 @@
const res = await fetch("/settings/api/two_factor/delete_factor", { const res = await fetch("/settings/api/two_factor/delete_factor", {
method: "post", method: "post",
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
id: id, id: id,
@ -84,7 +92,7 @@
if (res.status == 200) if (res.status == 200)
document.getElementById("factor-" + id).remove(); document.getElementById("factor-" + id).remove();
} catch(e) { } catch (e) {
console.error(e); console.error(e);
alert("Failed to remove factor!"); alert("Failed to remove factor!");
} }
@ -104,7 +112,7 @@
if (res.status == 200) if (res.status == 200)
document.getElementById("2fa_history_container").remove(); document.getElementById("2fa_history_container").remove();
} catch(e) { } catch (e) {
console.error(e); console.error(e);
alert("Failed to clear 2FA history!"); alert("Failed to clear 2FA history!");
} }