Bypass 2FA after successful login #72

Merged
pierre merged 6 commits from bypass_2fa_after_successful_login into master 2022-11-12 11:21:34 +00:00
7 changed files with 88 additions and 7 deletions
Showing only changes of commit 7a3eaa944e - Show all commits

View File

@ -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>;

View File

@ -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;

View File

@ -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<_>>();

View File

@ -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!")
} }

View File

@ -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());
} }

View File

@ -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
})
}
} }

View File

@ -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 %}