Record successful 2FA authentication in session cookie
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
b704e9868b
commit
5644e40763
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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(),
|
||||||
|
@ -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,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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!");
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user