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:
		@@ -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!");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user