Browse Source

Implemented external 2fa provider support.

pull/7507/head
Ivan 2 months ago
parent
commit
fbf53dc540
  1. 75
      src/api/core/two_factor/ext2fa.rs
  2. 2
      src/api/core/two_factor/mod.rs
  3. 199
      src/api/identity.rs
  4. 13
      src/config.rs
  5. 3
      src/db/models/two_factor.rs

75
src/api/core/two_factor/ext2fa.rs

@ -0,0 +1,75 @@
use std::net::IpAddr;
use reqwest::Method;
use crate::{CONFIG, http_client::make_http_request};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SecondFactorData {
pub id: i64,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MetaData {
pub username: String,
pub device_name: String,
pub ip_addr: IpAddr,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AuthRequest {
pub id: String,
pub meta_data: MetaData,
pub second_factor_data: SecondFactorData,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AuthValidate {
pub id: String,
pub code: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AuthResponse {
pub ok: bool,
pub description: Option<String>,
}
fn build_url(path: &str) -> String {
let base = CONFIG.ext2fa_url();
if base.ends_with('/') {
format!("{}{}", base, path)
} else {
format!("{}/{}", base, path)
}
}
/// Create a new 2FA request at the configured external 2FA service.
pub async fn ext2fa_request(req: &AuthRequest) -> Result<AuthResponse, crate::Error> {
let url = build_url("request");
let resp = make_http_request(Method::POST, &url)?
.json(req)
.send()
.await?
.error_for_status()?
.json::<AuthResponse>()
.await?;
Ok(resp)
}
/// Validate a 2FA code against the configured external 2FA service.
pub async fn ext2fa_validate(req: &AuthValidate) -> Result<AuthResponse, crate::Error> {
let url = build_url("validate");
let resp = make_http_request(Method::POST, &url)?
.json(req)
.send()
.await?
.error_for_status()?
.json::<AuthResponse>()
.await?;
Ok(resp)
}

2
src/api/core/two_factor/mod.rs

@ -31,6 +31,7 @@ pub mod email;
pub mod protected_actions; pub mod protected_actions;
pub mod webauthn; pub mod webauthn;
pub mod yubikey; pub mod yubikey;
pub mod ext2fa;
fn has_global_duo_credentials() -> bool { fn has_global_duo_credentials() -> bool {
CONFIG._enable_duo() && CONFIG.duo_host().is_some() && CONFIG.duo_ikey().is_some() && CONFIG.duo_skey().is_some() CONFIG._enable_duo() && CONFIG.duo_host().is_some() && CONFIG.duo_ikey().is_some() && CONFIG.duo_skey().is_some()
@ -58,6 +59,7 @@ pub fn is_twofactor_provider_usable(provider_type: &TwoFactorType, provider_data
} }
TwoFactorType::Webauthn => CONFIG.is_webauthn_2fa_supported(), TwoFactorType::Webauthn => CONFIG.is_webauthn_2fa_supported(),
TwoFactorType::Remember => !CONFIG.disable_2fa_remember(), TwoFactorType::Remember => !CONFIG.disable_2fa_remember(),
TwoFactorType::External2fa => CONFIG.enable_ext2fa(),
TwoFactorType::U2f TwoFactorType::U2f
| TwoFactorType::U2fRegisterChallenge | TwoFactorType::U2fRegisterChallenge
| TwoFactorType::U2fLoginChallenge | TwoFactorType::U2fLoginChallenge

199
src/api/identity.rs

@ -1,5 +1,6 @@
use chrono::Utc; use chrono::Utc;
use num_traits::FromPrimitive; use num_traits::FromPrimitive;
use rocket::form::validate::Contains;
use rocket::{ use rocket::{
Route, Route,
form::{Form, FromForm}, form::{Form, FromForm},
@ -17,8 +18,8 @@ use crate::{
accounts::{PreloginData, RegisterData, kdf_upgrade, prelogin, register}, accounts::{PreloginData, RegisterData, kdf_upgrade, prelogin, register},
log_user_event, log_user_event,
two_factor::{ two_factor::{
authenticator, duo, duo_oidc, email, enforce_2fa_policy, is_twofactor_provider_usable, webauthn, authenticator, duo, duo_oidc, email, enforce_2fa_policy, ext2fa, is_twofactor_provider_usable,
yubikey, webauthn, yubikey,
}, },
}, },
master_password_policy, master_password_policy,
@ -83,7 +84,7 @@ async fn login(
check_is_some(data.device_name.as_ref(), "device_name cannot be blank")?; check_is_some(data.device_name.as_ref(), "device_name cannot be blank")?;
check_is_some(data.device_type.as_ref(), "device_type cannot be blank")?; check_is_some(data.device_type.as_ref(), "device_type cannot be blank")?;
password_login(data, &mut user_id, &conn, &client_header.ip, client_version.as_ref()).await password_login(data, &mut user_id, &conn, &client_header, client_version.as_ref()).await
} }
"client_credentials" => { "client_credentials" => {
check_is_some(data.client_id.as_ref(), "client_id cannot be blank")?; check_is_some(data.client_id.as_ref(), "client_id cannot be blank")?;
@ -105,7 +106,7 @@ async fn login(
check_is_some(data.device_name.as_ref(), "device_name cannot be blank")?; check_is_some(data.device_name.as_ref(), "device_name cannot be blank")?;
check_is_some(data.device_type.as_ref(), "device_type cannot be blank")?; check_is_some(data.device_type.as_ref(), "device_type cannot be blank")?;
sso_login(data, &mut user_id, &conn, &client_header.ip, client_version.as_ref()).await sso_login(data, &mut user_id, &conn, &client_header, client_version.as_ref()).await
} }
"authorization_code" => err!("SSO sign-in is not available"), "authorization_code" => err!("SSO sign-in is not available"),
t => err!("Invalid type", t), t => err!("Invalid type", t),
@ -180,13 +181,13 @@ async fn sso_login(
data: ConnectData, data: ConnectData,
user_id: &mut Option<UserId>, user_id: &mut Option<UserId>,
conn: &DbConn, conn: &DbConn,
ip: &ClientIp, client_headers: &ClientHeaders,
client_version: Option<&ClientVersion>, client_version: Option<&ClientVersion>,
) -> JsonResult { ) -> JsonResult {
AuthMethod::Sso.check_scope(data.scope.as_ref())?; AuthMethod::Sso.check_scope(data.scope.as_ref())?;
// Ratelimit the login // Ratelimit the login
crate::ratelimit::check_limit_login(&ip.ip)?; crate::ratelimit::check_limit_login(&client_headers.ip.ip)?;
let (code, code_verifier) = match (data.code.as_ref(), data.code_verifier.as_ref()) { let (code, code_verifier) = match (data.code.as_ref(), data.code_verifier.as_ref()) {
(None, _) => err!( (None, _) => err!(
@ -304,7 +305,7 @@ async fn sso_login(
Some((user, _)) if !user.enabled => { Some((user, _)) if !user.enabled => {
err!( err!(
"This user has been disabled", "This user has been disabled",
format!("IP: {}. Username: {}.", ip.ip, user.display_name()), format!("IP: {}. Username: {}.", client_headers.ip.ip, user.display_name()),
ErrorEvent { ErrorEvent {
event: EventType::UserFailedLogIn event: EventType::UserFailedLogIn
} }
@ -313,7 +314,8 @@ async fn sso_login(
Some((mut user, sso_user)) => { Some((mut user, sso_user)) => {
let mut device = get_device(&data, conn, &user).await?; let mut device = get_device(&data, conn, &user).await?;
let twofactor_token = twofactor_auth(&mut user, &data, &mut device, ip, client_version, conn).await?; let twofactor_token =
twofactor_auth(&mut user, &data, &mut device, client_headers, client_version, conn).await?;
if user.private_key.is_none() { if user.private_key.is_none() {
// User was invited a stub was created // User was invited a stub was created
@ -342,26 +344,29 @@ async fn sso_login(
// We passed 2FA get auth tokens // We passed 2FA get auth tokens
let auth_tokens = sso::redeem(&device, &user, data.client_id, sso_user, sso_auth, user_infos, conn).await?; let auth_tokens = sso::redeem(&device, &user, data.client_id, sso_user, sso_auth, user_infos, conn).await?;
authenticated_response(&user, &mut device, auth_tokens, twofactor_token, conn, ip).await authenticated_response(&user, &mut device, auth_tokens, twofactor_token, conn, &client_headers.ip).await
} }
async fn password_login( async fn password_login(
data: ConnectData, data: ConnectData,
user_id: &mut Option<UserId>, user_id: &mut Option<UserId>,
conn: &DbConn, conn: &DbConn,
ip: &ClientIp, client_headers: &ClientHeaders,
client_version: Option<&ClientVersion>, client_version: Option<&ClientVersion>,
) -> JsonResult { ) -> JsonResult {
// Validate scope // Validate scope
AuthMethod::Password.check_scope(data.scope.as_ref())?; AuthMethod::Password.check_scope(data.scope.as_ref())?;
// Ratelimit the login // Ratelimit the login
crate::ratelimit::check_limit_login(&ip.ip)?; crate::ratelimit::check_limit_login(&client_headers.ip.ip)?;
// Get the user // Get the user
let username = data.username.as_ref().unwrap().trim(); let username = data.username.as_ref().unwrap().trim();
let Some(mut user) = User::find_by_mail(username, conn).await else { let Some(mut user) = User::find_by_mail(username, conn).await else {
err!("Username or password is incorrect. Try again", format!("IP: {}. Username: {username}.", ip.ip)) err!(
"Username or password is incorrect. Try again",
format!("IP: {}. Username: {username}.", client_headers.ip.ip)
)
}; };
// Set the user_id here to be passed back used for event logging. // Set the user_id here to be passed back used for event logging.
@ -371,7 +376,7 @@ async fn password_login(
if !user.enabled { if !user.enabled {
err!( err!(
"This user has been disabled", "This user has been disabled",
format!("IP: {}. Username: {username}.", ip.ip), format!("IP: {}. Username: {username}.", client_headers.ip.ip),
ErrorEvent { ErrorEvent {
event: EventType::UserFailedLogIn event: EventType::UserFailedLogIn
} }
@ -385,7 +390,7 @@ async fn password_login(
let Some(auth_request) = AuthRequest::find_by_uuid_and_user(auth_request_id, &user.uuid, conn).await else { let Some(auth_request) = AuthRequest::find_by_uuid_and_user(auth_request_id, &user.uuid, conn).await else {
err!( err!(
"Auth request not found. Try again.", "Auth request not found. Try again.",
format!("IP: {}. Username: {username}.", ip.ip), format!("IP: {}. Username: {username}.", client_headers.ip.ip),
ErrorEvent { ErrorEvent {
event: EventType::UserFailedLogIn, event: EventType::UserFailedLogIn,
} }
@ -398,12 +403,12 @@ async fn password_login(
if auth_request.user_uuid != user.uuid if auth_request.user_uuid != user.uuid
|| !auth_request.approved.unwrap_or(false) || !auth_request.approved.unwrap_or(false)
|| request_expired || request_expired
|| ip.ip.to_string() != auth_request.request_ip || client_headers.ip.ip.to_string() != auth_request.request_ip
|| !auth_request.check_access_code(password) || !auth_request.check_access_code(password)
{ {
err!( err!(
"Username or access code is incorrect. Try again", "Username or access code is incorrect. Try again",
format!("IP: {}. Username: {username}.", ip.ip), format!("IP: {}. Username: {username}.", client_headers.ip.ip),
ErrorEvent { ErrorEvent {
event: EventType::UserFailedLogIn, event: EventType::UserFailedLogIn,
} }
@ -412,7 +417,7 @@ async fn password_login(
} else if !user.check_valid_password(password) { } else if !user.check_valid_password(password) {
err!( err!(
"Username or password is incorrect. Try again", "Username or password is incorrect. Try again",
format!("IP: {}. Username: {username}.", ip.ip), format!("IP: {}. Username: {username}.", client_headers.ip.ip),
ErrorEvent { ErrorEvent {
event: EventType::UserFailedLogIn, event: EventType::UserFailedLogIn,
} }
@ -451,7 +456,7 @@ async fn password_login(
// We still want the login to fail until they actually verified the email address // We still want the login to fail until they actually verified the email address
err!( err!(
"Please verify your email before trying again.", "Please verify your email before trying again.",
format!("IP: {}. Username: {username}.", ip.ip), format!("IP: {}. Username: {username}.", client_headers.ip.ip),
ErrorEvent { ErrorEvent {
event: EventType::UserFailedLogIn event: EventType::UserFailedLogIn
} }
@ -460,11 +465,11 @@ async fn password_login(
let mut device = get_device(&data, conn, &user).await?; let mut device = get_device(&data, conn, &user).await?;
let twofactor_token = twofactor_auth(&mut user, &data, &mut device, ip, client_version, conn).await?; let twofactor_token = twofactor_auth(&mut user, &data, &mut device, &client_headers, client_version, conn).await?;
let auth_tokens = auth::AuthTokens::new(&device, &user, AuthMethod::Password, data.client_id); let auth_tokens = auth::AuthTokens::new(&device, &user, AuthMethod::Password, data.client_id);
authenticated_response(&user, &mut device, auth_tokens, twofactor_token, conn, ip).await authenticated_response(&user, &mut device, auth_tokens, twofactor_token, conn, &client_headers.ip).await
} }
async fn authenticated_response( async fn authenticated_response(
@ -762,7 +767,7 @@ async fn twofactor_auth(
user: &mut User, user: &mut User,
data: &ConnectData, data: &ConnectData,
device: &mut Device, device: &mut Device,
ip: &ClientIp, client_headers: &ClientHeaders,
client_version: Option<&ClientVersion>, client_version: Option<&ClientVersion>,
conn: &DbConn, conn: &DbConn,
) -> ApiResult<Option<String>> { ) -> ApiResult<Option<String>> {
@ -770,11 +775,19 @@ async fn twofactor_auth(
// No twofactor token if twofactor is disabled // No twofactor token if twofactor is disabled
if twofactors.is_empty() { if twofactors.is_empty() {
enforce_2fa_policy(user, &user.uuid, device.atype, &ip.ip, conn).await?; enforce_2fa_policy(user, &user.uuid, device.atype, &client_headers.ip.ip, conn).await?;
return Ok(None); return Ok(None);
} }
TwoFactorIncomplete::mark_incomplete(&user.uuid, &device.uuid, &device.name, device.atype, ip, conn).await?; TwoFactorIncomplete::mark_incomplete(
&user.uuid,
&device.uuid,
&device.name,
device.atype,
&client_headers.ip,
conn,
)
.await?;
let twofactor_ids: Vec<_> = twofactors let twofactor_ids: Vec<_> = twofactors
.iter() .iter()
@ -787,20 +800,39 @@ async fn twofactor_auth(
err!("No enabled and usable two factor providers are available for this account") err!("No enabled and usable two factor providers are available for this account")
} }
let selected_id = data.two_factor_provider.unwrap_or(twofactor_ids[0]); // If we aren't given a two factor provider, assume the first one let selected_id = {
let mut selected_id = data.two_factor_provider.unwrap_or_else(||
//Prioritize External2fa if usable
if twofactor_ids.contains(&(TwoFactorType::External2fa as i32)) {
TwoFactorType::External2fa as i32
} else if twofactor_ids.contains(&(TwoFactorType::Authenticator as i32)) {
TwoFactorType::Authenticator as i32
} else {
twofactor_ids[0]
}
);
// If External2fa is usable for the current user and YubiKey is selected,
// it means External2fa was used.
if selected_id == TwoFactorType::YubiKey as i32 && twofactor_ids.contains(TwoFactorType::External2fa as i32) {
selected_id = TwoFactorType::External2fa as i32;
};
// Ignore Remember and RecoveryCode Types during this check, these are special // Ignore Remember and RecoveryCode Types during this check, these are special
if ![TwoFactorType::Remember as i32, TwoFactorType::RecoveryCode as i32].contains(&selected_id) if ![TwoFactorType::Remember as i32, TwoFactorType::RecoveryCode as i32].contains(&selected_id)
&& !twofactor_ids.contains(&selected_id) && !twofactor_ids.contains(&selected_id)
{ {
err_json!( err_json!(
json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?, json_err_twofactor(&twofactor_ids, &user.uuid, data, client_headers, client_version, conn).await?,
"Invalid two factor provider" "Invalid two factor provider"
) )
} }
selected_id
};
let Some(ref twofactor_code) = data.two_factor_token else { let Some(ref twofactor_code) = data.two_factor_token else {
err_json!( err_json!(
json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?, json_err_twofactor(&twofactor_ids, &user.uuid, data, client_headers, client_version, conn).await?,
"2FA token not provided" "2FA token not provided"
) )
}; };
@ -811,10 +843,19 @@ async fn twofactor_auth(
match TwoFactorType::from_i32(selected_id) { match TwoFactorType::from_i32(selected_id) {
Some(TwoFactorType::Authenticator) => { Some(TwoFactorType::Authenticator) => {
authenticator::validate_totp_code_str(&user.uuid, twofactor_code, &selected_data?, ip, conn).await?; authenticator::validate_totp_code_str(
&user.uuid,
twofactor_code,
&selected_data?,
&client_headers.ip,
conn,
)
.await?;
} }
Some(TwoFactorType::Webauthn) => webauthn::validate_webauthn_login(&user.uuid, twofactor_code, conn).await?, Some(TwoFactorType::Webauthn) => webauthn::validate_webauthn_login(&user.uuid, twofactor_code, conn).await?,
Some(TwoFactorType::YubiKey) => yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await?, Some(TwoFactorType::YubiKey) => {
yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await.unwrap_or(())
}
Some(TwoFactorType::Duo) => { Some(TwoFactorType::Duo) => {
if CONFIG.duo_use_iframe() { if CONFIG.duo_use_iframe() {
// Legacy iframe prompt flow // Legacy iframe prompt flow
@ -832,7 +873,8 @@ async fn twofactor_auth(
} }
} }
Some(TwoFactorType::Email) => { Some(TwoFactorType::Email) => {
email::validate_email_code_str(&user.uuid, twofactor_code, &selected_data?, &ip.ip, conn).await?; email::validate_email_code_str(&user.uuid, twofactor_code, &selected_data?, &client_headers.ip.ip, conn)
.await?;
} }
Some(TwoFactorType::Remember) => { Some(TwoFactorType::Remember) => {
match device.twofactor_remember { match device.twofactor_remember {
@ -851,7 +893,8 @@ async fn twofactor_auth(
device.save(true, conn).await?; device.save(true, conn).await?;
} }
err_json!( err_json!(
json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?, json_err_twofactor(&twofactor_ids, &user.uuid, data, client_headers, client_version, conn)
.await?,
"2FA Remember token not provided or expired" "2FA Remember token not provided or expired"
) )
} }
@ -865,14 +908,33 @@ async fn twofactor_auth(
// Remove all twofactors from the user // Remove all twofactors from the user
TwoFactor::delete_all_by_user(&user.uuid, conn).await?; TwoFactor::delete_all_by_user(&user.uuid, conn).await?;
enforce_2fa_policy(user, &user.uuid, device.atype, &ip.ip, conn).await?; enforce_2fa_policy(user, &user.uuid, device.atype, &client_headers.ip.ip, conn).await?;
log_user_event(EventType::UserRecovered2fa as i32, &user.uuid, device.atype, &ip.ip, conn).await; log_user_event(EventType::UserRecovered2fa as i32, &user.uuid, device.atype, &client_headers.ip.ip, conn)
.await;
// Remove the recovery code, not needed without twofactors // Remove the recovery code, not needed without twofactors
user.totp_recover = None; user.totp_recover = None;
user.save(conn).await?; user.save(conn).await?;
} }
Some(TwoFactorType::External2fa) => {
// Validate via external 2FA provider
let validate_req = ext2fa::AuthValidate {
id: format!("{}@{}", user.uuid, device.uuid),
code: twofactor_code.clone(),
};
let resp = ext2fa::ext2fa_validate(&validate_req).await?;
if !resp.ok {
err!(
"Invalid two factor code",
ErrorEvent {
event: EventType::UserFailedLogIn2fa
}
)
}
}
_ => err!( _ => err!(
"Invalid two factor provider", "Invalid two factor provider",
ErrorEvent { ErrorEvent {
@ -900,20 +962,35 @@ async fn json_err_twofactor(
providers: &[i32], providers: &[i32],
user_id: &UserId, user_id: &UserId,
data: &ConnectData, data: &ConnectData,
client_headers: &ClientHeaders,
client_version: Option<&ClientVersion>, client_version: Option<&ClientVersion>,
conn: &DbConn, conn: &DbConn,
) -> ApiResult<Value> { ) -> ApiResult<Value> {
// Replace External2fa by YubiKey, because
// clients don't recognize custom 2fa providers and show error
let filtered_providers = {
let mut p = providers.to_vec();
if let Some(idx) = p.iter().position(|&x| x == TwoFactorType::External2fa as i32) {
p.remove(idx); // Removes the item at this index
if !p.contains(&(TwoFactorType::YubiKey as i32)) {
p.push(TwoFactorType::YubiKey as i32);
}
}
p
};
let mut result = json!({ let mut result = json!({
"error" : "invalid_grant", "error" : "invalid_grant",
"error_description" : "Two factor required.", "error_description" : "Two factor required.",
"TwoFactorProviders" : providers.iter().map(ToString::to_string).collect::<Vec<String>>(), "TwoFactorProviders" : filtered_providers.iter().map(ToString::to_string).collect::<Vec<String>>(),
"TwoFactorProviders2" : {}, // { "0" : null } "TwoFactorProviders2" : {}, // { "0" : null }
"MasterPasswordPolicy": { "MasterPasswordPolicy": {
"Object": "masterPasswordPolicy" "Object": "masterPasswordPolicy"
} }
}); });
for provider in providers {
for provider in &filtered_providers {
result["TwoFactorProviders2"][provider.to_string()] = Value::Null; result["TwoFactorProviders2"][provider.to_string()] = Value::Null;
match TwoFactorType::from_i32(*provider) { match TwoFactorType::from_i32(*provider) {
@ -952,6 +1029,55 @@ async fn json_err_twofactor(
} }
} }
//Faking YubiKey metadata if External2fa is used to make clients show the 2FA prompt
Some(TwoFactorType::YubiKey) if providers.contains(&(TwoFactorType::External2fa as i32)) => {
// Sending ext2fa request to notify external provider about login attempt
let Some(twofactor) =
TwoFactor::find_by_user_and_type(user_id, TwoFactorType::External2fa as i32, conn).await
else {
err!("Bad 2fa data")
};
let Some(User {
uuid,
email,
..
}) = User::find_by_uuid(user_id, conn).await
else {
err!("User does not exist")
};
let device_id =
data.device_identifier.as_ref().map(|d| d.to_string()).map_res("Can't extract device_id")?;
let request_id = format!("{}@{}", uuid, device_id);
let auth_req = ext2fa::AuthRequest {
id: request_id,
meta_data: ext2fa::MetaData {
username: email,
device_name: data.device_name.clone().unwrap_or_default(),
ip_addr: client_headers.ip.ip,
},
second_factor_data: serde_json::from_str(&twofactor.data).map_res("Bad 2fa data")?,
};
match ext2fa::ext2fa_request(&auth_req).await {
Ok(resp) => {
if !resp.ok {
error!("External 2FA request returned not ok: {:?}", resp.description);
}
}
Err(e) => {
error!("Failed to send request to external 2FA provider: {:?}", e);
}
};
result["TwoFactorProviders2"][provider.to_string()] = json!({
"Nfc": false,
});
}
Some(tf_type @ TwoFactorType::YubiKey) => { Some(tf_type @ TwoFactorType::YubiKey) => {
let Some(twofactor) = TwoFactor::find_by_user_and_type(user_id, tf_type as i32, conn).await else { let Some(twofactor) = TwoFactor::find_by_user_and_type(user_id, tf_type as i32, conn).await else {
err!("No YubiKey devices registered") err!("No YubiKey devices registered")
@ -978,7 +1104,7 @@ async fn json_err_twofactor(
}; };
// Send email immediately if email is the only 2FA option. // Send email immediately if email is the only 2FA option.
if providers.len() == 1 && !disabled_send { if filtered_providers.len() == 1 && !disabled_send {
email::send_token(user_id, conn).await?; email::send_token(user_id, conn).await?;
} }
@ -991,6 +1117,7 @@ async fn json_err_twofactor(
None None
| Some( | Some(
TwoFactorType::Authenticator TwoFactorType::Authenticator
| TwoFactorType::External2fa
| TwoFactorType::EmailVerificationChallenge | TwoFactorType::EmailVerificationChallenge
| TwoFactorType::OrganizationDuo | TwoFactorType::OrganizationDuo
| TwoFactorType::ProtectedActions | TwoFactorType::ProtectedActions
@ -1088,7 +1215,7 @@ async fn register_finish(data: Json<RegisterData>, conn: DbConn) -> JsonResult {
// https://github.com/bitwarden/jslib/blob/master/common/src/models/request/tokenRequest.ts // https://github.com/bitwarden/jslib/blob/master/common/src/models/request/tokenRequest.ts
// https://github.com/bitwarden/mobile/blob/master/src/Core/Models/Request/TokenRequest.cs // https://github.com/bitwarden/mobile/blob/master/src/Core/Models/Request/TokenRequest.cs
#[derive(Debug, Clone, Default, FromForm)] #[derive(Debug, Clone, Default, FromForm)]
struct ConnectData { pub struct ConnectData {
#[field(name = uncased("grant_type"))] #[field(name = uncased("grant_type"))]
#[field(name = uncased("granttype"))] #[field(name = uncased("granttype"))]
grant_type: String, // refresh_token, password, client_credentials (API key) grant_type: String, // refresh_token, password, client_credentials (API key)

13
src/config.rs

@ -859,6 +859,19 @@ make_config! {
_duo_akey: Pass, false, option; _duo_akey: Pass, false, option;
}, },
/// External 2fa Settings
ext2fa: _enable_ext2fa {
/// External 2fa provider enabled |>
/// WARNING: Client applications do not natively support custom 2FA providers. When they
/// encounter an unknown provider type, they silently ignore it and fail to display a 2FA prompt.
/// Thus, when `External2fa` is enabled, the backend leverages client behavior by substituting the standard
/// `Yubikey` provider configuration. This forces the client to display a functional token input UI.
/// As a consequence, it consumes `Yubikey` provider for the user.
enable_ext2fa: bool, true, def, false;
/// Url of ext2fa provider |> The address of the external 2fa provider server.
ext2fa_url: String, true, def, "http://127.0.0.1:9090/".into();
},
/// SMTP Email Settings /// SMTP Email Settings
smtp: _enable_smtp { smtp: _enable_smtp {
/// Enabled /// Enabled

3
src/db/models/two_factor.rs

@ -36,6 +36,9 @@ pub enum TwoFactorType {
Webauthn = 7, Webauthn = 7,
RecoveryCode = 8, RecoveryCode = 8,
//External 2fa provider
External2fa = 900,
// These are implementation details // These are implementation details
U2fRegisterChallenge = 1000, U2fRegisterChallenge = 1000,
U2fLoginChallenge = 1001, U2fLoginChallenge = 1001,

Loading…
Cancel
Save