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> | ||||||
| @@ -190,6 +200,7 @@ | |||||||
|  |  | ||||||
|         form.submit(); |         form.submit(); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| {% endblock content %} | {% endblock content %} | ||||||
		Reference in New Issue
	
	Block a user