Start to create 2FA exemption after successful 2FA login
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				continuous-integration/drone/push Build is failing
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	continuous-integration/drone/push Build is failing
				
			This commit is contained in:
		@@ -1,4 +1,5 @@
 | 
				
			|||||||
use actix::{Actor, Context, Handler, Message, MessageResult};
 | 
					use actix::{Actor, Context, Handler, Message, MessageResult};
 | 
				
			||||||
 | 
					use std::net::IpAddr;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::data::entity_manager::EntityManager;
 | 
					use crate::data::entity_manager::EntityManager;
 | 
				
			||||||
use crate::data::user::{User, UserID};
 | 
					use crate::data::user::{User, UserID};
 | 
				
			||||||
@@ -50,6 +51,10 @@ pub struct ChangePasswordRequest {
 | 
				
			|||||||
#[derive(Debug)]
 | 
					#[derive(Debug)]
 | 
				
			||||||
pub struct ChangePasswordResult(pub bool);
 | 
					pub struct ChangePasswordResult(pub bool);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Message)]
 | 
				
			||||||
 | 
					#[rtype(result = "bool")]
 | 
				
			||||||
 | 
					pub struct AddSuccessful2FALogin(pub UserID, pub IpAddr);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug)]
 | 
					#[derive(Debug)]
 | 
				
			||||||
pub struct UpdateUserResult(pub bool);
 | 
					pub struct UpdateUserResult(pub bool);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -111,6 +116,15 @@ impl Handler<ChangePasswordRequest> for UsersActor {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Handler<AddSuccessful2FALogin> for UsersActor {
 | 
				
			||||||
 | 
					    type Result = <AddSuccessful2FALogin as actix::Message>::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<GetUserRequest> for UsersActor {
 | 
					impl Handler<GetUserRequest> for UsersActor {
 | 
				
			||||||
    type Result = MessageResult<GetUserRequest>;
 | 
					    type Result = MessageResult<GetUserRequest>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,6 +19,10 @@ pub const MAX_INACTIVITY_DURATION: u64 = 60 * 30;
 | 
				
			|||||||
/// Maximum session duration (6 hours)
 | 
					/// Maximum session duration (6 hours)
 | 
				
			||||||
pub const MAX_SESSION_DURATION: u64 = 3600 * 6;
 | 
					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
 | 
					/// Minimum password length
 | 
				
			||||||
pub const MIN_PASS_LEN: usize = 4;
 | 
					pub const MIN_PASS_LEN: usize = 4;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,6 +55,7 @@ pub struct UpdateUserQuery {
 | 
				
			|||||||
    email: String,
 | 
					    email: String,
 | 
				
			||||||
    gen_new_password: Option<String>,
 | 
					    gen_new_password: Option<String>,
 | 
				
			||||||
    enabled: Option<String>,
 | 
					    enabled: Option<String>,
 | 
				
			||||||
 | 
					    two_factor_exemption_after_successful_login: Option<String>,
 | 
				
			||||||
    admin: Option<String>,
 | 
					    admin: Option<String>,
 | 
				
			||||||
    grant_type: String,
 | 
					    grant_type: String,
 | 
				
			||||||
    granted_clients: String,
 | 
					    granted_clients: String,
 | 
				
			||||||
@@ -84,6 +85,10 @@ pub async fn users_route(
 | 
				
			|||||||
        user.last_name = update.0.last_name;
 | 
					        user.last_name = update.0.last_name;
 | 
				
			||||||
        user.email = update.0.email;
 | 
					        user.email = update.0.email;
 | 
				
			||||||
        user.enabled = update.0.enabled.is_some();
 | 
					        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();
 | 
					        user.admin = update.0.admin.is_some();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let factors_to_keep = update.0.two_factor.split(';').collect::<Vec<_>>();
 | 
					        let factors_to_keep = update.0.two_factor.split(';').collect::<Vec<_>>();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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_identity::Identity;
 | 
				
			||||||
use actix_web::{web, HttpRequest, HttpResponse, Responder};
 | 
					use actix_web::{web, HttpRequest, HttpResponse, Responder};
 | 
				
			||||||
use webauthn_rs::prelude::PublicKeyCredential;
 | 
					use webauthn_rs::prelude::PublicKeyCredential;
 | 
				
			||||||
@@ -16,6 +20,8 @@ pub async fn auth_webauthn(
 | 
				
			|||||||
    req: web::Json<AuthWebauthnRequest>,
 | 
					    req: web::Json<AuthWebauthnRequest>,
 | 
				
			||||||
    manager: WebAuthManagerReq,
 | 
					    manager: WebAuthManagerReq,
 | 
				
			||||||
    http_req: HttpRequest,
 | 
					    http_req: HttpRequest,
 | 
				
			||||||
 | 
					    remote_ip: RemoteIP,
 | 
				
			||||||
 | 
					    users: web::Data<Addr<UsersActor>>,
 | 
				
			||||||
) -> impl Responder {
 | 
					) -> impl Responder {
 | 
				
			||||||
    if !SessionIdentity(Some(&id)).need_2fa_auth() {
 | 
					    if !SessionIdentity(Some(&id)).need_2fa_auth() {
 | 
				
			||||||
        return HttpResponse::Unauthorized().json("No 2FA required!");
 | 
					        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) {
 | 
					    match manager.finish_authentication(&user_id, &req.opaque_state, &req.credential) {
 | 
				
			||||||
        Ok(_) => {
 | 
					        Ok(_) => {
 | 
				
			||||||
 | 
					            users
 | 
				
			||||||
 | 
					                .send(users_actor::AddSuccessful2FALogin(user_id, remote_ip.0))
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					                .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            SessionIdentity(Some(&id)).set_status(&http_req, SessionStatus::SignedIn);
 | 
					            SessionIdentity(Some(&id)).set_status(&http_req, SessionStatus::SignedIn);
 | 
				
			||||||
            HttpResponse::Ok().body("You are authenticated!")
 | 
					            HttpResponse::Ok().body("You are authenticated!")
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -139,7 +139,8 @@ pub async fn login_route(
 | 
				
			|||||||
            LoginResult::Success(user) => {
 | 
					            LoginResult::Success(user) => {
 | 
				
			||||||
                let status = if user.need_reset_password {
 | 
					                let status = if user.need_reset_password {
 | 
				
			||||||
                    SessionStatus::NeedNewPassword
 | 
					                    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
 | 
					                    SessionStatus::Need2FA
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    SessionStatus::SignedIn
 | 
					                    SessionStatus::SignedIn
 | 
				
			||||||
@@ -326,6 +327,7 @@ pub async fn login_with_otp(
 | 
				
			|||||||
    form: Option<web::Form<LoginWithOTPForm>>,
 | 
					    form: Option<web::Form<LoginWithOTPForm>>,
 | 
				
			||||||
    users: web::Data<Addr<UsersActor>>,
 | 
					    users: web::Data<Addr<UsersActor>>,
 | 
				
			||||||
    http_req: HttpRequest,
 | 
					    http_req: HttpRequest,
 | 
				
			||||||
 | 
					    remote_ip: RemoteIP,
 | 
				
			||||||
) -> impl Responder {
 | 
					) -> impl Responder {
 | 
				
			||||||
    let mut danger = None;
 | 
					    let mut danger = None;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -354,6 +356,11 @@ pub async fn login_with_otp(
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
            danger = Some("Specified code is invalid!".to_string());
 | 
					            danger = Some("Specified code is invalid!".to_string());
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
 | 
					            users
 | 
				
			||||||
 | 
					                .send(users_actor::AddSuccessful2FALogin(user.uid, remote_ip.0))
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					                .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            SessionIdentity(id.as_ref()).set_status(&http_req, SessionStatus::SignedIn);
 | 
					            SessionIdentity(id.as_ref()).set_status(&http_req, SessionStatus::SignedIn);
 | 
				
			||||||
            return redirect_user(query.redirect.get());
 | 
					            return redirect_user(query.redirect.get());
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,13 @@
 | 
				
			|||||||
 | 
					use crate::constants::SECOND_FACTOR_EXEMPTION_AFTER_SUCCESSFUL_LOGIN;
 | 
				
			||||||
use crate::data::client::ClientID;
 | 
					use crate::data::client::ClientID;
 | 
				
			||||||
use crate::data::entity_manager::EntityManager;
 | 
					use crate::data::entity_manager::EntityManager;
 | 
				
			||||||
use crate::data::login_redirect::LoginRedirect;
 | 
					use crate::data::login_redirect::LoginRedirect;
 | 
				
			||||||
use crate::data::totp_key::TotpKey;
 | 
					use crate::data::totp_key::TotpKey;
 | 
				
			||||||
use crate::data::webauthn_manager::WebauthnPubKey;
 | 
					use crate::data::webauthn_manager::WebauthnPubKey;
 | 
				
			||||||
use crate::utils::err::Res;
 | 
					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)]
 | 
					#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
 | 
				
			||||||
pub struct UserID(pub String);
 | 
					pub struct UserID(pub String);
 | 
				
			||||||
@@ -76,6 +80,15 @@ pub struct User {
 | 
				
			|||||||
    #[serde(default)]
 | 
					    #[serde(default)]
 | 
				
			||||||
    pub two_factor: Vec<TwoFactor>,
 | 
					    pub two_factor: Vec<TwoFactor>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// 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<IpAddr, u64>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// None = all services
 | 
					    /// None = all services
 | 
				
			||||||
    /// Some([]) = no service
 | 
					    /// Some([]) = no service
 | 
				
			||||||
    pub authorized_clients: Option<Vec<ClientID>>,
 | 
					    pub authorized_clients: Option<Vec<ClientID>>,
 | 
				
			||||||
@@ -101,6 +114,13 @@ impl User {
 | 
				
			|||||||
        !self.two_factor.is_empty()
 | 
					        !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) {
 | 
					    pub fn add_factor(&mut self, factor: TwoFactor) {
 | 
				
			||||||
        self.two_factor.push(factor);
 | 
					        self.two_factor.push(factor);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -178,6 +198,8 @@ impl Default for User {
 | 
				
			|||||||
            enabled: true,
 | 
					            enabled: true,
 | 
				
			||||||
            admin: false,
 | 
					            admin: false,
 | 
				
			||||||
            two_factor: vec![],
 | 
					            two_factor: vec![],
 | 
				
			||||||
 | 
					            two_factor_exemption_after_successful_login: false,
 | 
				
			||||||
 | 
					            last_successful_2fa: Default::default(),
 | 
				
			||||||
            authorized_clients: Some(Vec::new()),
 | 
					            authorized_clients: Some(Vec::new()),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -249,4 +271,11 @@ impl EntityManager<User> {
 | 
				
			|||||||
            user
 | 
					            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
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,7 +12,7 @@
 | 
				
			|||||||
    <!-- User name -->
 | 
					    <!-- User name -->
 | 
				
			||||||
    <div class="form-group">
 | 
					    <div class="form-group">
 | 
				
			||||||
        <label class="form-label mt-4" for="username">Username</label>
 | 
					        <label class="form-label mt-4" for="username">Username</label>
 | 
				
			||||||
        <input class="form-control" id="username" type="text"
 | 
					        <input class="form-control" id="username" type="text" autocomplete="nope"
 | 
				
			||||||
               name="username" value="{{ u.username }}" required/>
 | 
					               name="username" value="{{ u.username }}" required/>
 | 
				
			||||||
        <div class="valid-feedback">This username is valid</div>
 | 
					        <div class="valid-feedback">This username is valid</div>
 | 
				
			||||||
        <div class="invalid-feedback">This username is already taken.</div>
 | 
					        <div class="invalid-feedback">This username is already taken.</div>
 | 
				
			||||||
@@ -51,17 +51,27 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        <!-- Enabled -->
 | 
					        <!-- Enabled -->
 | 
				
			||||||
        <div class="form-check">
 | 
					        <div class="form-check">
 | 
				
			||||||
            <input class="form-check-input" type="checkbox" name="enabled" id="enabled" {% if u.enabled %} checked="" {%
 | 
					            <input class="form-check-input" type="checkbox" name="enabled" id="enabled"
 | 
				
			||||||
                   endif %}>
 | 
					                   {% if u.enabled %} checked="" {% endif %}>
 | 
				
			||||||
            <label class="form-check-label" for="enabled">
 | 
					            <label class="form-check-label" for="enabled">
 | 
				
			||||||
                Enabled
 | 
					                Enabled
 | 
				
			||||||
            </label>
 | 
					            </label>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <!-- 2FA exemption after successful login -->
 | 
				
			||||||
 | 
					        <div class="form-check">
 | 
				
			||||||
 | 
					            <input class="form-check-input" type="checkbox" name="two_factor_exemption_after_successful_login"
 | 
				
			||||||
 | 
					                   id="two_factor_exemption_after_successful_login"
 | 
				
			||||||
 | 
					                   {% if u.two_factor_exemption_after_successful_login %} checked="" {% endif %}>
 | 
				
			||||||
 | 
					            <label class="form-check-label" for="two_factor_exemption_after_successful_login">
 | 
				
			||||||
 | 
					                Exempt user from 2FA authentication for an IP address after a successful login for a limited time
 | 
				
			||||||
 | 
					            </label>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <!-- Admin -->
 | 
					        <!-- Admin -->
 | 
				
			||||||
        <div class="form-check">
 | 
					        <div class="form-check">
 | 
				
			||||||
            <input class="form-check-input" type="checkbox" name="admin" id="admin" {% if u.admin %} checked="" {% endif
 | 
					            <input class="form-check-input" type="checkbox" name="admin" id="admin"
 | 
				
			||||||
                   %}>
 | 
					                   {% if u.admin %} checked="" {% endif %}>
 | 
				
			||||||
            <label class="form-check-label" for="admin">
 | 
					            <label class="form-check-label" for="admin">
 | 
				
			||||||
                Grant admin privileges
 | 
					                Grant admin privileges
 | 
				
			||||||
            </label>
 | 
					            </label>
 | 
				
			||||||
@@ -80,7 +90,7 @@
 | 
				
			|||||||
                <input type="checkbox" class="form-check-input two-fact-checkbox"
 | 
					                <input type="checkbox" class="form-check-input two-fact-checkbox"
 | 
				
			||||||
                       value="{{ f.id.0 }}"
 | 
					                       value="{{ f.id.0 }}"
 | 
				
			||||||
                       checked=""/>
 | 
					                       checked=""/>
 | 
				
			||||||
                {{ f.name }} (<img src="{{ f.type_image() }}" alt="Factor icon" style="height:1em;" />
 | 
					                {{ f.name }} (<img src="{{ f.type_image() }}" alt="Factor icon" style="height:1em;"/>
 | 
				
			||||||
                {{ f.type_str() }})
 | 
					                {{ f.type_str() }})
 | 
				
			||||||
            </label>
 | 
					            </label>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
@@ -190,6 +200,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        form.submit();
 | 
					        form.submit();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% endblock content %}
 | 
					{% endblock content %}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user