TOTP 2FA, App passwords and account disable support (closes #436 closes #479)

This commit is contained in:
mdecimus
2024-06-28 12:12:51 +02:00
parent 0693253dff
commit d8a73cd0e4
30 changed files with 250 additions and 85 deletions

View File

@@ -17,9 +17,8 @@ tokio-rustls = { version = "0.25.0"}
rustls = "0.22"
rustls-pki-types = { version = "1" }
ldap3 = { version = "0.11.1", default-features = false, features = ["tls-rustls"] }
deadpool = { version = "0.10.0", features = ["managed", "rt_tokio_1"] }
deadpool = { version = "0.12", features = ["managed", "rt_tokio_1"] }
parking_lot = "0.12"
async-trait = "0.1.68"
ahash = { version = "0.8" }
tracing = "0.1"
lru-cache = "0.1.2"
@@ -34,6 +33,7 @@ md5 = "0.7.0"
futures = "0.3"
regex = "1.7.0"
serde = { version = "1.0", features = ["derive"]}
totp-rs = { version = "5.5.1", features = ["otpauth"] }
[dev-dependencies]
tokio = { version = "1.23", features = ["full"] }

View File

@@ -6,14 +6,12 @@
use std::sync::atomic::Ordering;
use async_trait::async_trait;
use deadpool::managed;
use tokio::net::TcpStream;
use tokio_rustls::client::TlsStream;
use super::{ImapClient, ImapConnectionManager, ImapError};
#[async_trait]
impl managed::Manager for ImapConnectionManager {
type Type = ImapClient<TlsStream<TcpStream>>;
type Error = ImapError;

View File

@@ -59,7 +59,7 @@ impl DirectoryStore for Store {
.await?,
secret,
) {
(Some(mut principal), Some(secret)) if principal.verify_secret(secret).await => {
(Some(mut principal), Some(secret)) if principal.verify_secret(secret).await? => {
if return_member_of {
principal.member_of = self.get_member_of(principal.id).await?;
}

View File

@@ -414,6 +414,35 @@ impl ManageDirectory for Store {
) => {
principal.inner.secrets = secrets;
}
(
PrincipalAction::AddItem,
PrincipalField::Secrets,
PrincipalValue::String(secret),
) => {
let mut do_add = true;
let mut new_secrets = Vec::with_capacity(principal.inner.secrets.len() + 1);
for prev_secret in principal.inner.secrets {
if prev_secret == secret {
do_add = false;
} else if prev_secret.starts_with("otpauth://")
|| prev_secret == "$disabled$"
|| prev_secret.starts_with("$app$")
{
new_secrets.push(prev_secret);
}
}
if do_add {
new_secrets.push(secret);
}
principal.inner.secrets = new_secrets;
}
(
PrincipalAction::RemoveItem,
PrincipalField::Secrets,
PrincipalValue::String(secret),
) => {
principal.inner.secrets.retain(|v| *v != secret);
}
(
PrincipalAction::Set,
PrincipalField::Description,

View File

@@ -87,7 +87,7 @@ impl LdapDirectory {
.find_principal(&mut conn, &self.mappings.filter_name.build(username))
.await?
{
if principal.verify_secret(secret).await {
if principal.verify_secret(secret).await? {
principal
} else {
tracing::debug!(

View File

@@ -4,13 +4,11 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use async_trait::async_trait;
use deadpool::managed;
use ldap3::{exop::WhoAmI, Ldap, LdapConnAsync, LdapError};
use super::LdapConnectionManager;
#[async_trait]
impl managed::Manager for LdapConnectionManager {
type Type = Ldap;
type Error = LdapError;

View File

@@ -36,7 +36,7 @@ impl MemoryDirectory {
for principal in &self.principals {
if &principal.name == username {
return if principal.verify_secret(secret).await {
return if principal.verify_secret(secret).await? {
Ok(Some(principal.clone()))
} else {
Ok(None)

View File

@@ -4,13 +4,11 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use async_trait::async_trait;
use deadpool::managed;
use mail_send::{smtp::AssertReply, Error};
use super::{SmtpClient, SmtpConnectionManager};
#[async_trait]
impl managed::Manager for SmtpConnectionManager {
type Type = SmtpClient;
type Error = Error;
@@ -45,8 +43,10 @@ impl managed::Manager for SmtpConnectionManager {
.map(|_| ())
.map_err(managed::RecycleError::Backend)
} else {
Err(managed::RecycleError::StaticMessage(
"No longer valid: Too many authentication failures",
Err(managed::RecycleError::Message(
"No longer valid: Too many authentication failures"
.to_string()
.into(),
))
}
}

View File

@@ -68,7 +68,7 @@ impl SqlDirectory {
// Validate password
if let Some(secret) = secret {
if !principal.verify_secret(secret).await {
if !principal.verify_secret(secret).await? {
tracing::debug!(
context = "directory",
event = "invalid_password",

View File

@@ -16,17 +16,51 @@ use sha1::Sha1;
use sha2::Sha256;
use sha2::Sha512;
use tokio::sync::oneshot;
use totp_rs::TOTP;
use crate::DirectoryError;
use crate::Principal;
impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
pub async fn verify_secret(&self, secret: &str) -> bool {
for hashed_secret in &self.secrets {
if verify_secret_hash(hashed_secret, secret).await {
return true;
pub async fn verify_secret(&self, mut code: &str) -> crate::Result<bool> {
let mut totp_token = None;
for secret in &self.secrets {
let mut secret = secret.as_str();
if secret == "$disabled$" {
return Ok(false);
} else if secret.starts_with("otpauth://") && totp_token.is_none() {
let totp_token = if let Some(totp_token) = totp_token {
totp_token
} else {
let (_code, _totp_token) = code
.rsplit_once('$')
.filter(|(c, t)| !c.is_empty() && !t.is_empty())
.ok_or(DirectoryError::MissingTotpCode)?;
totp_token = Some(_totp_token);
code = _code;
_totp_token
};
if !TOTP::from_url(secret)
.map_err(DirectoryError::InvalidTotpUrl)?
.check_current(totp_token)
.unwrap_or(false)
{
return Ok(false);
}
} else if let Some((_, app_secret)) =
secret.strip_prefix("$app$").and_then(|s| s.split_once('$'))
{
secret = app_secret;
}
if verify_secret_hash(secret, code).await {
return Ok(true);
}
}
false
Ok(false)
}
}

View File

@@ -23,6 +23,7 @@ use deadpool::managed::PoolError;
use ldap3::LdapError;
use mail_send::Credentials;
use store::Store;
use totp_rs::TotpUrlError;
pub mod backend;
pub mod core;
@@ -81,6 +82,8 @@ pub enum DirectoryError {
Management(ManagementError),
TimedOut,
Unsupported,
InvalidTotpUrl(TotpUrlError),
MissingTotpCode,
}
#[derive(Debug, PartialEq, Eq)]
@@ -309,6 +312,8 @@ impl Display for DirectoryError {
Self::Management(error) => write!(f, "Management error: {:?}", error),
Self::TimedOut => write!(f, "Directory timed out"),
Self::Unsupported => write!(f, "Method not supported by directory"),
Self::InvalidTotpUrl(error) => write!(f, "Invalid TOTP URL: {}", error),
Self::MissingTotpCode => write!(f, "Missing TOTP code"),
}
}
}