|
|
@ -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 = { |
|
|
// Ignore Remember and RecoveryCode Types during this check, these are special
|
|
|
let mut selected_id = data.two_factor_provider.unwrap_or_else(|| |
|
|
if ![TwoFactorType::Remember as i32, TwoFactorType::RecoveryCode as i32].contains(&selected_id) |
|
|
//Prioritize External2fa if usable
|
|
|
&& !twofactor_ids.contains(&selected_id) |
|
|
if twofactor_ids.contains(&(TwoFactorType::External2fa as i32)) { |
|
|
{ |
|
|
TwoFactorType::External2fa as i32 |
|
|
err_json!( |
|
|
} else if twofactor_ids.contains(&(TwoFactorType::Authenticator as i32)) { |
|
|
json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?, |
|
|
TwoFactorType::Authenticator as i32 |
|
|
"Invalid two factor provider" |
|
|
} 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
|
|
|
|
|
|
if ![TwoFactorType::Remember as i32, TwoFactorType::RecoveryCode as i32].contains(&selected_id) |
|
|
|
|
|
&& !twofactor_ids.contains(&selected_id) |
|
|
|
|
|
{ |
|
|
|
|
|
err_json!( |
|
|
|
|
|
json_err_twofactor(&twofactor_ids, &user.uuid, data, client_headers, client_version, conn).await?, |
|
|
|
|
|
"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)
|
|
|
|