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:
parent
c24318f6b8
commit
7a3eaa944e
@ -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 %}
|
Loading…
Reference in New Issue
Block a user