Bypass 2FA after successful login #72
161
Cargo.lock
generated
161
Cargo.lock
generated
@ -222,7 +222,7 @@ dependencies = [
|
|||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"socket2",
|
"socket2",
|
||||||
"time",
|
"time 0.3.17",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -325,6 +325,15 @@ dependencies = [
|
|||||||
"alloc-no-stdlib",
|
"alloc-no-stdlib",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.66"
|
version = "1.0.66"
|
||||||
@ -392,7 +401,7 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
"rusticata-macros",
|
"rusticata-macros",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"time",
|
"time 0.3.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -495,6 +504,7 @@ dependencies = [
|
|||||||
"base64",
|
"base64",
|
||||||
"bcrypt",
|
"bcrypt",
|
||||||
"bincode",
|
"bincode",
|
||||||
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"digest",
|
"digest",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
@ -644,6 +654,21 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
|
||||||
|
dependencies = [
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
"time 0.1.44",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cipher"
|
name = "cipher"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@ -699,10 +724,20 @@ checksum = "454038500439e141804c655b4cd1bc6a70bcb95cd2bc9463af5661b6956f0e46"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"wasi",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "codespan-reporting"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
|
||||||
|
dependencies = [
|
||||||
|
"termcolor",
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "color_quant"
|
name = "color_quant"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@ -752,10 +787,16 @@ dependencies = [
|
|||||||
"rand",
|
"rand",
|
||||||
"sha2",
|
"sha2",
|
||||||
"subtle",
|
"subtle",
|
||||||
"time",
|
"time 0.3.17",
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
@ -861,6 +902,50 @@ dependencies = [
|
|||||||
"cipher",
|
"cipher",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cxx"
|
||||||
|
version = "1.0.81"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "97abf9f0eca9e52b7f81b945524e76710e6cb2366aead23b7d4fbf72e281f888"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cxxbridge-flags",
|
||||||
|
"cxxbridge-macro",
|
||||||
|
"link-cplusplus",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cxx-build"
|
||||||
|
version = "1.0.81"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7cc32cc5fea1d894b77d269ddb9f192110069a8a9c1f1d441195fba90553dea3"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"codespan-reporting",
|
||||||
|
"once_cell",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"scratch",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cxxbridge-flags"
|
||||||
|
version = "1.0.81"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ca220e4794c934dc6b1207c3b42856ad4c302f2df1712e9f8d2eec5afaacf1f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cxxbridge-macro"
|
||||||
|
version = "1.0.81"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b846f081361125bfc8dc9d3940c84e1fd83ba54bbca7b17cd29483c828be0704"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "data-encoding"
|
name = "data-encoding"
|
||||||
version = "2.3.2"
|
version = "2.3.2"
|
||||||
@ -1140,7 +1225,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1322,6 +1407,30 @@ version = "2.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.53"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
|
||||||
|
dependencies = [
|
||||||
|
"cxx",
|
||||||
|
"cxx-build",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
@ -1516,6 +1625,15 @@ version = "0.2.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb"
|
checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "link-cplusplus"
|
||||||
|
version = "1.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "local-channel"
|
name = "local-channel"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
@ -1616,7 +1734,7 @@ checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"wasi",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2163,6 +2281,12 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scratch"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sec1"
|
name = "sec1"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
@ -2424,6 +2548,17 @@ dependencies = [
|
|||||||
"weezl",
|
"weezl",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.1.44"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"wasi 0.10.0+wasi-snapshot-preview1",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.17"
|
version = "0.3.17"
|
||||||
@ -2585,6 +2720,12 @@ dependencies = [
|
|||||||
"tinyvec",
|
"tinyvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-xid"
|
name = "unicode-xid"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
@ -2659,6 +2800,12 @@ version = "0.9.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.10.0+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.0+wasi-snapshot-preview1"
|
version = "0.11.0+wasi-snapshot-preview1"
|
||||||
@ -2888,7 +3035,7 @@ dependencies = [
|
|||||||
"oid-registry",
|
"oid-registry",
|
||||||
"rusticata-macros",
|
"rusticata-macros",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"time",
|
"time 0.3.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -36,3 +36,4 @@ webauthn-rs = { version = "0.4.7", features = ["danger-allow-state-serialisation
|
|||||||
url = "2.3.1"
|
url = "2.3.1"
|
||||||
aes-gcm = { version = "0.10.1", features = ["aes"] }
|
aes-gcm = { version = "0.10.1", features = ["aes"] }
|
||||||
bincode = "1.3.3"
|
bincode = "1.3.3"
|
||||||
|
chrono = "0.4.22"
|
@ -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};
|
||||||
@ -8,7 +9,7 @@ pub enum LoginResult {
|
|||||||
AccountNotFound,
|
AccountNotFound,
|
||||||
InvalidPassword,
|
InvalidPassword,
|
||||||
AccountDisabled,
|
AccountDisabled,
|
||||||
Success(User),
|
Success(Box<User>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
@ -50,6 +51,14 @@ 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(Message)]
|
||||||
|
#[rtype(result = "bool")]
|
||||||
|
pub struct Clear2FALoginHistory(pub UserID);
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct UpdateUserResult(pub bool);
|
pub struct UpdateUserResult(pub bool);
|
||||||
|
|
||||||
@ -93,7 +102,7 @@ impl Handler<LoginRequest> for UsersActor {
|
|||||||
return MessageResult(LoginResult::AccountDisabled);
|
return MessageResult(LoginResult::AccountDisabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageResult(LoginResult::Success(user))
|
MessageResult(LoginResult::Success(Box::new(user)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -111,6 +120,22 @@ 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<Clear2FALoginHistory> for UsersActor {
|
||||||
|
type Result = <Clear2FALoginHistory as actix::Message>::Result;
|
||||||
|
fn handle(&mut self, msg: Clear2FALoginHistory, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
|
self.manager.clear_2fa_login_history(&msg.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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,10 +55,12 @@ 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,
|
||||||
two_factor: String,
|
two_factor: String,
|
||||||
|
clear_2fa_history: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn users_route(
|
pub async fn users_route(
|
||||||
@ -84,6 +86,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<_>>();
|
||||||
@ -109,10 +115,15 @@ pub async fn users_route(
|
|||||||
let temp_pass = rand_str(TEMPORARY_PASSWORDS_LEN);
|
let temp_pass = rand_str(TEMPORARY_PASSWORDS_LEN);
|
||||||
user.password = hash_password(&temp_pass).expect("Failed to hash password");
|
user.password = hash_password(&temp_pass).expect("Failed to hash password");
|
||||||
user.need_reset_password = true;
|
user.need_reset_password = true;
|
||||||
|
user.last_successful_2fa = Default::default();
|
||||||
Some(temp_pass)
|
Some(temp_pass)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if update.0.clear_2fa_history.is_some() {
|
||||||
|
user.last_successful_2fa = Default::default();
|
||||||
|
}
|
||||||
|
|
||||||
let res = users
|
let res = users
|
||||||
.send(users_actor::UpdateUserRequest(user.clone()))
|
.send(users_actor::UpdateUserRequest(user.clone()))
|
||||||
.await
|
.await
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
|
@ -120,3 +120,15 @@ pub async fn delete_factor(
|
|||||||
HttpResponse::Ok().body("Removed factor!")
|
HttpResponse::Ok().body("Removed factor!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn clear_login_history(
|
||||||
|
user: CurrentUser,
|
||||||
|
users: web::Data<Addr<UsersActor>>,
|
||||||
|
) -> impl Responder {
|
||||||
|
users
|
||||||
|
.send(users_actor::Clear2FALoginHistory(user.uid.clone()))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
HttpResponse::Ok().body("History successfully cleared")
|
||||||
|
}
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
|
||||||
|
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::{fmt_time, time};
|
||||||
|
|
||||||
#[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);
|
||||||
@ -60,6 +65,19 @@ impl TwoFactor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Successful2FALogin {
|
||||||
|
pub ip: IpAddr,
|
||||||
|
pub time: u64,
|
||||||
|
pub can_bypass_2fa: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Successful2FALogin {
|
||||||
|
pub fn fmt_time(&self) -> String {
|
||||||
|
fmt_time(self.time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub uid: UserID,
|
pub uid: UserID,
|
||||||
@ -76,6 +94,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 +128,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);
|
||||||
}
|
}
|
||||||
@ -155,6 +189,22 @@ impl User {
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn remove_outdated_successful_2fa_attempts(&mut self) {
|
||||||
|
self.last_successful_2fa
|
||||||
|
.retain(|_, t| *t + SECOND_FACTOR_EXEMPTION_AFTER_SUCCESSFUL_LOGIN > time());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_formatted_2fa_successful_logins(&self) -> Vec<Successful2FALogin> {
|
||||||
|
self.last_successful_2fa
|
||||||
|
.iter()
|
||||||
|
.map(|(ip, time)| Successful2FALogin {
|
||||||
|
ip: *ip,
|
||||||
|
time: *time,
|
||||||
|
can_bypass_2fa: self.can_bypass_two_factors_for_ip(*ip),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for User {
|
impl PartialEq for User {
|
||||||
@ -178,6 +228,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()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -246,6 +298,25 @@ impl EntityManager<User> {
|
|||||||
self.update_user(id, |mut user| {
|
self.update_user(id, |mut user| {
|
||||||
user.password = new_hash;
|
user.password = new_hash;
|
||||||
user.need_reset_password = temporary;
|
user.need_reset_password = temporary;
|
||||||
|
user.two_factor_exemption_after_successful_login = Default::default();
|
||||||
|
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());
|
||||||
|
|
||||||
|
// Remove outdated successful attempts
|
||||||
|
user.remove_outdated_successful_2fa_attempts();
|
||||||
|
|
||||||
|
user
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_2fa_login_history(&mut self, id: &UserID) -> bool {
|
||||||
|
self.update_user(id, |mut user| {
|
||||||
|
user.last_successful_2fa = Default::default();
|
||||||
user
|
user
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -192,6 +192,11 @@ async fn main() -> std::io::Result<()> {
|
|||||||
"/settings/api/two_factor/delete_factor",
|
"/settings/api/two_factor/delete_factor",
|
||||||
web::post().to(two_factor_api::delete_factor),
|
web::post().to(two_factor_api::delete_factor),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/settings/api/two_factor/clear_login_history",
|
||||||
|
// Use POST to prevent CSRF
|
||||||
|
web::post().to(two_factor_api::clear_login_history),
|
||||||
|
)
|
||||||
// Admin routes
|
// Admin routes
|
||||||
.route(
|
.route(
|
||||||
"/admin",
|
"/admin",
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
/// Get the current time since epoch
|
/// Get the current time since epoch
|
||||||
@ -7,3 +8,16 @@ pub fn time() -> u64 {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.as_secs()
|
.as_secs()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Format unix timestamp to a human-readable string
|
||||||
|
pub fn fmt_time(timestamp: u64) -> String {
|
||||||
|
// Create a NaiveDateTime from the timestamp
|
||||||
|
let naive =
|
||||||
|
NaiveDateTime::from_timestamp_opt(timestamp as i64, 0).expect("Failed to parse timestamp!");
|
||||||
|
|
||||||
|
// Create a normal DateTime from the NaiveDateTime
|
||||||
|
let datetime: DateTime<Utc> = DateTime::from_utc(naive, Utc);
|
||||||
|
|
||||||
|
// Format the datetime how you want
|
||||||
|
datetime.format("%Y-%m-%d %H:%M:%S").to_string()
|
||||||
|
}
|
||||||
|
@ -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>
|
||||||
@ -88,6 +98,28 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Two factor authentication history -->
|
||||||
|
{% if !u.last_successful_2fa.is_empty() %}
|
||||||
|
<fieldset class="form-group">
|
||||||
|
<legend class="mt-4">Last successful 2FA authentications</legend>
|
||||||
|
|
||||||
|
<!-- Clear 2FA history -->
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="clear_2fa_history" id="clear_2fa_history">
|
||||||
|
<label class="form-check-label" for="clear_2fa_history">
|
||||||
|
Clear 2FA authentication history
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{% for e in u.get_formatted_2fa_successful_logins() %}
|
||||||
|
{% if e.can_bypass_2fa %}<li style="font-weight: bold;">{{ e.ip }} - {{ e.fmt_time() }} - BYPASS 2FA</li>
|
||||||
|
{% else %}<li>{{ e.ip }} - {{ e.fmt_time() }}</li>{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</fieldset>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Granted clients -->
|
<!-- Granted clients -->
|
||||||
<fieldset class="form-group">
|
<fieldset class="form-group">
|
||||||
<legend class="mt-4">Granted clients</legend>
|
<legend class="mt-4">Granted clients</legend>
|
||||||
@ -190,6 +222,7 @@
|
|||||||
|
|
||||||
form.submit();
|
form.submit();
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock content %}
|
{% endblock content %}
|
@ -34,6 +34,33 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
{% if !user.last_successful_2fa.is_empty() %}
|
||||||
|
<div id="2fa_history_container">
|
||||||
|
<h5 style="margin-top: 50px">Successful 2FA login history</h5>
|
||||||
|
<p>
|
||||||
|
<a type="button" class="btn btn-danger btn-sm" onclick="clear_login_history()">Clear history</a>
|
||||||
|
</p>
|
||||||
|
<table class="table table-hover" style="max-width: 800px;" aria-describedby="Factors list">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">IP address</th>
|
||||||
|
<th scope="col">Date</th>
|
||||||
|
<th scope="col">Bypass 2FA</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for e in user.get_formatted_2fa_successful_logins() %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ e.ip }}</td>
|
||||||
|
<td>{{ e.fmt_time() }}</td>
|
||||||
|
<td>{% if e.can_bypass_2fa %}YES{% else %}NO{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<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?"))
|
||||||
@ -61,5 +88,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function clear_login_history() {
|
||||||
|
if (!confirm("Do you really want to clear your 2FA login history?"))
|
||||||
|
return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("/settings/api/two_factor/clear_login_history", {
|
||||||
|
method: "post"
|
||||||
|
});
|
||||||
|
|
||||||
|
let text = await res.text();
|
||||||
|
alert(text);
|
||||||
|
|
||||||
|
if (res.status == 200)
|
||||||
|
document.getElementById("2fa_history_container").remove();
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e);
|
||||||
|
alert("Failed to clear 2FA history!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
Loading…
Reference in New Issue
Block a user