@ -17,15 +17,14 @@ 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_oidc , email , enforce_2fa_policy , is_twofactor_provider_usable , webauthn , yubikey ,
yubikey ,
} ,
} ,
} ,
} ,
master_password_policy ,
identity_ master_password_policy,
push ::register_push_device ,
push ::register_push_device ,
} ,
} ,
auth ,
auth ,
auth ::{ AuthMethod , ClientHeaders , ClientIp , ClientVersion , Secure , generate_organization_api_key_login_claims } ,
auth ::{ AuthMethod , ClientHeaders , ClientIp , Secure , generate_organization_api_key_login_claims } ,
crypto ,
crypto ,
db ::{
db ::{
DbConn ,
DbConn ,
@ -46,7 +45,6 @@ pub fn routes() -> Vec<Route> {
login ,
login ,
post_prelogin ,
post_prelogin ,
prelogin_password ,
prelogin_password ,
identity_register ,
register_verification_email ,
register_verification_email ,
register_finish ,
register_finish ,
prevalidate ,
prevalidate ,
@ -57,12 +55,7 @@ pub fn routes() -> Vec<Route> {
}
}
#[ post( " /connect/token " , data = " <data> " ) ]
#[ post( " /connect/token " , data = " <data> " ) ]
async fn login (
async fn login ( data : Form < ConnectData > , client_header : ClientHeaders , conn : DbConn ) -> JsonResult {
data : Form < ConnectData > ,
client_header : ClientHeaders ,
client_version : Option < ClientVersion > ,
conn : DbConn ,
) -> JsonResult {
let data : ConnectData = data . into_inner ( ) ;
let data : ConnectData = data . into_inner ( ) ;
let mut user_id : Option < UserId > = None ;
let mut user_id : Option < UserId > = None ;
@ -83,17 +76,13 @@ 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 . ip ) . 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" ) ? ;
check_is_some ( data . client_secret . as_ref ( ) , "client_secret cannot be blank" ) ? ;
check_is_some ( data . client_secret . as_ref ( ) , "client_secret cannot be blank" ) ? ;
check_is_some ( data . scope . as_ref ( ) , "scope cannot be blank" ) ? ;
check_is_some ( data . scope . as_ref ( ) , "scope cannot be blank" ) ? ;
check_is_some ( data . device_identifier . as_ref ( ) , "device_identifier 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" ) ? ;
api_key_login ( data , & mut user_id , & conn , & client_header . ip ) . await
api_key_login ( data , & mut user_id , & conn , & client_header . ip ) . await
}
}
"authorization_code" if CONFIG . sso_enabled ( ) = > {
"authorization_code" if CONFIG . sso_enabled ( ) = > {
@ -105,7 +94,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 . ip ) . await
}
}
"authorization_code" = > err ! ( "SSO sign-in is not available" ) ,
"authorization_code" = > err ! ( "SSO sign-in is not available" ) ,
"send_access" = > {
"send_access" = > {
@ -129,7 +118,7 @@ async fn login(
match & login_result {
match & login_result {
Ok ( _ ) = > {
Ok ( _ ) = > {
log_user_event (
log_user_event (
EventType ::UserLoggedIn as i32 ,
EventType ::UserLoggedIn ,
& user_id ,
& user_id ,
client_header . device_type ,
client_header . device_type ,
& client_header . ip . ip ,
& client_header . ip . ip ,
@ -139,8 +128,7 @@ async fn login(
}
}
Err ( e ) = > {
Err ( e ) = > {
if let Some ( ev ) = e . get_event ( ) {
if let Some ( ev ) = e . get_event ( ) {
log_user_event ( ev . event as i32 , & user_id , client_header . device_type , & client_header . ip . ip , & conn )
log_user_event ( ev . event , & user_id , client_header . device_type , & client_header . ip . ip , & conn ) . await ;
. await ;
}
}
}
}
}
}
@ -190,13 +178,7 @@ async fn refresh_login(data: ConnectData, conn: &DbConn, ip: &ClientIp) -> JsonR
}
}
// After exchanging the code we need to check first if 2FA is needed before continuing
// After exchanging the code we need to check first if 2FA is needed before continuing
async fn sso_login (
async fn sso_login ( data : ConnectData , user_id : & mut Option < UserId > , conn : & DbConn , ip : & ClientIp ) -> JsonResult {
data : ConnectData ,
user_id : & mut Option < UserId > ,
conn : & DbConn ,
ip : & ClientIp ,
client_version : Option < & ClientVersion > ,
) -> JsonResult {
AuthMethod ::Sso . check_scope ( data . scope . as_ref ( ) ) ? ;
AuthMethod ::Sso . check_scope ( data . scope . as_ref ( ) ) ? ;
// Ratelimit the login
// Ratelimit the login
@ -365,7 +347,7 @@ async fn sso_login(
let twofactor_token = if skip_2fa {
let twofactor_token = if skip_2fa {
None
None
} else {
} else {
twofactor_auth ( & mut user , & data , & mut device , ip , client_version , c onn ) . await ?
twofactor_auth ( & mut user , & data , & mut device , ip , conn ) . await ?
} ;
} ;
if user . private_key . is_none ( ) {
if user . private_key . is_none ( ) {
@ -398,13 +380,7 @@ async fn sso_login(
authenticated_response ( & user , & mut device , auth_tokens , twofactor_token , conn , ip ) . await
authenticated_response ( & user , & mut device , auth_tokens , twofactor_token , conn , ip ) . await
}
}
async fn password_login (
async fn password_login ( data : ConnectData , user_id : & mut Option < UserId > , conn : & DbConn , ip : & ClientIp ) -> JsonResult {
data : ConnectData ,
user_id : & mut Option < UserId > ,
conn : & DbConn ,
ip : & ClientIp ,
client_version : Option < & ClientVersion > ,
) -> JsonResult {
// Validate scope
// Validate scope
AuthMethod ::Password . check_scope ( data . scope . as_ref ( ) ) ? ;
AuthMethod ::Password . check_scope ( data . scope . as_ref ( ) ) ? ;
@ -413,24 +389,14 @@ async fn password_login(
// Get the user
// Get the user
let username = data . username . as_ref ( ) . unwrap ( ) . trim ( ) ;
let username = data . username . as_ref ( ) . unwrap ( ) . trim ( ) ;
let log_username = username . escape_debug ( ) ;
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: {log_ username}." , 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.
* user_id = Some ( user . uuid . clone ( ) ) ;
* user_id = Some ( user . uuid . clone ( ) ) ;
// Check if the user is disabled
if ! user . enabled {
err ! (
"This user has been disabled" ,
format ! ( "IP: {}. Username: {username}." , ip . ip ) ,
ErrorEvent {
event : EventType ::UserFailedLogIn
}
)
}
let password = data . password . as_ref ( ) . unwrap ( ) ;
let password = data . password . as_ref ( ) . unwrap ( ) ;
// If we get an auth request, we don't check the user's password, but the access code of the auth request
// If we get an auth request, we don't check the user's password, but the access code of the auth request
@ -438,7 +404,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: {log_ username}." , ip . ip ) ,
ErrorEvent {
ErrorEvent {
event : EventType ::UserFailedLogIn ,
event : EventType ::UserFailedLogIn ,
}
}
@ -450,13 +416,14 @@ 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 )
| | auth_request . authentication_date . is_some ( )
| | request_expired
| | request_expired
| | ip . ip . to_string ( ) ! = auth_request . request_ip
| | 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: {log_ username}." , ip . ip ) ,
ErrorEvent {
ErrorEvent {
event : EventType ::UserFailedLogIn ,
event : EventType ::UserFailedLogIn ,
}
}
@ -465,13 +432,24 @@ 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: {log_ username}." , ip . ip ) ,
ErrorEvent {
ErrorEvent {
event : EventType ::UserFailedLogIn ,
event : EventType ::UserFailedLogIn ,
}
}
)
)
}
}
// Check if the user is disabled
if ! user . enabled {
err ! (
"This user has been disabled" ,
format ! ( "IP: {}. Username: {log_username}." , ip . ip ) ,
ErrorEvent {
event : EventType ::UserFailedLogIn
}
)
}
// Change the KDF Iterations (only when not logging in with an auth request)
// Change the KDF Iterations (only when not logging in with an auth request)
if data . auth_request . is_none ( ) {
if data . auth_request . is_none ( ) {
kdf_upgrade ( & mut user , password , conn ) . await ? ;
kdf_upgrade ( & mut user , password , conn ) . await ? ;
@ -504,7 +482,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: {log_ username}." , ip . ip ) ,
ErrorEvent {
ErrorEvent {
event : EventType ::UserFailedLogIn
event : EventType ::UserFailedLogIn
}
}
@ -513,7 +491,20 @@ 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 , ip , conn ) . await ? ;
// Like upstream, an auth request can be used for one login only. Marked after 2FA so a 2FA prompt doesn't use it up.
if let Some ( ref auth_request_id ) = data . auth_request
& & ! AuthRequest ::set_authentication_date ( auth_request_id , conn ) . await ?
{
err ! (
"Username or access code is incorrect. Try again" ,
format ! ( "IP: {}. Username: {log_username}." , ip . ip ) ,
ErrorEvent {
event : EventType ::UserFailedLogIn ,
}
)
}
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 ) ;
@ -552,7 +543,7 @@ async fn authenticated_response(
// Save to update `device.updated_at` to track usage and toggle new status
// Save to update `device.updated_at` to track usage and toggle new status
device . save ( true , conn ) . await ? ;
device . save ( true , conn ) . await ? ;
let master_password_policy = master_password_policy ( user , conn ) . await ;
let master_password_policy = identity_ master_password_policy( user , conn ) . await ;
let has_master_password = ! user . password_hash . is_empty ( ) ;
let has_master_password = ! user . password_hash . is_empty ( ) ;
let master_password_unlock = if has_master_password {
let master_password_unlock = if has_master_password {
@ -596,7 +587,6 @@ async fn authenticated_response(
"KdfIterations" : user . client_kdf_iter ,
"KdfIterations" : user . client_kdf_iter ,
"KdfMemory" : user . client_kdf_memory ,
"KdfMemory" : user . client_kdf_memory ,
"KdfParallelism" : user . client_kdf_parallelism ,
"KdfParallelism" : user . client_kdf_parallelism ,
"ResetMasterPassword" : false , // TODO: Same as above
"ForcePasswordReset" : false ,
"ForcePasswordReset" : false ,
"MasterPasswordPolicy" : master_password_policy ,
"MasterPasswordPolicy" : master_password_policy ,
"scope" : auth_tokens . scope ( ) ,
"scope" : auth_tokens . scope ( ) ,
@ -626,7 +616,14 @@ async fn api_key_login(data: ConnectData, user_id: &mut Option<UserId>, conn: &D
// Validate scope
// Validate scope
match data . scope . as_ref ( ) {
match data . scope . as_ref ( ) {
Some ( scope ) if scope = = & AuthMethod ::UserApiKey . scope ( ) = > user_api_key_login ( data , user_id , conn , ip ) . await ,
Some ( scope ) if scope = = & AuthMethod ::UserApiKey . scope ( ) = > {
// Like upstream, only the user API key logs in a device
check_is_some ( data . device_identifier . as_ref ( ) , "device_identifier 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" ) ? ;
user_api_key_login ( data , user_id , conn , ip ) . await
}
Some ( scope ) if scope = = & AuthMethod ::OrgApiKey . scope ( ) = > organization_api_key_login ( data , conn , ip ) . await ,
Some ( scope ) if scope = = & AuthMethod ::OrgApiKey . scope ( ) = > organization_api_key_login ( data , conn , ip ) . await ,
_ = > err ! ( "Scope not supported" ) ,
_ = > err ! ( "Scope not supported" ) ,
}
}
@ -750,7 +747,6 @@ async fn user_api_key_login(
"KdfIterations" : user . client_kdf_iter ,
"KdfIterations" : user . client_kdf_iter ,
"KdfMemory" : user . client_kdf_memory ,
"KdfMemory" : user . client_kdf_memory ,
"KdfParallelism" : user . client_kdf_parallelism ,
"KdfParallelism" : user . client_kdf_parallelism ,
"ResetMasterPassword" : false , // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing
"ForcePasswordReset" : false ,
"ForcePasswordReset" : false ,
"scope" : AuthMethod ::UserApiKey . scope ( ) ,
"scope" : AuthMethod ::UserApiKey . scope ( ) ,
"AccountKeys" : account_keys ,
"AccountKeys" : account_keys ,
@ -816,7 +812,6 @@ async fn twofactor_auth(
data : & ConnectData ,
data : & ConnectData ,
device : & mut Device ,
device : & mut Device ,
ip : & ClientIp ,
ip : & ClientIp ,
client_version : Option < & ClientVersion > ,
conn : & DbConn ,
conn : & DbConn ,
) -> ApiResult < Option < String > > {
) -> ApiResult < Option < String > > {
let twofactors = TwoFactor ::find_by_user ( & user . uuid , conn ) . await ;
let twofactors = TwoFactor ::find_by_user ( & user . uuid , conn ) . await ;
@ -845,17 +840,12 @@ async fn twofactor_auth(
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 , data , conn ) . await ? , "Invalid two factor provider" )
json_err_twofactor ( & twofactor_ids , & user . uuid , data , client_version , conn ) . await ? ,
"Invalid two factor provider"
)
}
}
let Some ( ref twofactor_code ) = data . two_factor_token else {
// Like upstream, a blank token counts as not provided
err_json ! (
let Some ( twofactor_code ) = data . two_factor_token . as_deref ( ) . filter ( | t | ! t . trim ( ) . is_empty ( ) ) else {
json_err_twofactor ( & twofactor_ids , & user . uuid , data , client_version , conn ) . await ? ,
err_json ! ( json_err_twofactor ( & twofactor_ids , user , data , conn ) . await ? , "2FA token not provided" )
"2FA token not provided"
)
} ;
} ;
let selected_twofactor = twofactors . into_iter ( ) . find ( | tf | tf . atype = = selected_id & & tf . enabled ) ;
let selected_twofactor = twofactors . into_iter ( ) . find ( | tf | tf . atype = = selected_id & & tf . enabled ) ;
@ -869,11 +859,6 @@ async fn twofactor_auth(
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 ? ,
Some ( TwoFactorType ::Duo ) = > {
Some ( TwoFactorType ::Duo ) = > {
if CONFIG . duo_use_iframe ( ) {
// Legacy iframe prompt flow
duo ::validate_duo_login ( & user . email , twofactor_code , conn ) . await ? ;
} else {
// OIDC based flow
duo_oidc ::validate_duo_login (
duo_oidc ::validate_duo_login (
& user . email ,
& user . email ,
twofactor_code ,
twofactor_code ,
@ -883,7 +868,6 @@ async fn twofactor_auth(
)
)
. await ? ;
. await ? ;
}
}
}
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 ? , & ip . ip , conn ) . await ? ;
}
}
@ -904,16 +888,22 @@ 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 , data , conn ) . await ? ,
"2FA Remember token not provided or expired"
"2FA Remember token not provided or expired"
)
)
}
}
}
}
}
}
Some ( TwoFactorType ::RecoveryCode ) = > {
Some ( TwoFactorType ::RecoveryCode ) = > {
// Check if recovery code is correct
// Like upstream, spaces and case don't matter
if ! user . check_valid_recovery_code ( twofactor_code ) {
let recovery_code = twofactor_code . replace ( ' ' , "" ) . trim ( ) . to_lowercase ( ) ;
err ! ( "Recovery code is incorrect. Try again." )
if ! user . check_valid_recovery_code ( & recovery_code ) {
err ! (
"Two-step token is invalid. Try again." ,
ErrorEvent {
event : EventType ::UserFailedLogIn2fa
}
)
}
}
// Remove all twofactors from the user
// Remove all twofactors from the user
@ -926,7 +916,14 @@ async fn twofactor_auth(
enforce_2fa_policy ( user , & user . uuid , device . atype , & ip . ip , conn ) . await ? ;
enforce_2fa_policy ( user , & user . uuid , device . atype , & ip . ip , conn ) . await ? ;
log_user_event ( EventType ::UserRecovered2fa as i32 , & user . uuid , device . atype , & ip . ip , conn ) . await ;
log_user_event ( EventType ::UserRecovered2fa , & user . uuid , device . atype , & ip . ip , conn ) . await ;
if CONFIG . mail_enabled ( )
& & let Err ( e ) =
mail ::send_recover_twofactor ( & user . email , & ip . ip . to_string ( ) , & Utc ::now ( ) . naive_utc ( ) ) . await
{
error ! ( "Error sending two-step login recovered email: {e:#?}" ) ;
}
// Remove the recovery code, not needed without twofactors
// Remove the recovery code, not needed without twofactors
user . totp_recover = None ;
user . totp_recover = None ;
@ -955,21 +952,14 @@ fn selected_data(tf: Option<TwoFactor>) -> ApiResult<String> {
tf . map ( | t | t . data ) . map_res ( "Two factor doesn't exist" )
tf . map ( | t | t . data ) . map_res ( "Two factor doesn't exist" )
}
}
async fn json_err_twofactor (
async fn json_err_twofactor ( providers : & [ i32 ] , user : & User , data : & ConnectData , conn : & DbConn ) -> ApiResult < Value > {
providers : & [ i32 ] ,
let user_id = & user . uuid ;
user_id : & UserId ,
data : & ConnectData ,
client_version : Option < & ClientVersion > ,
conn : & DbConn ,
) -> ApiResult < Value > {
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" : providers . iter ( ) . map ( ToString ::to_string ) . collect ::< Vec < String > > ( ) ,
"TwoFactorProviders2" : { } , // { "0" : null }
"TwoFactorProviders2" : { } , // { "0" : null }
"MasterPasswordPolicy" : {
"MasterPasswordPolicy" : identity_master_password_policy ( user , conn ) . await ,
"Object" : "masterPasswordPolicy"
}
} ) ;
} ) ;
for provider in providers {
for provider in providers {
@ -982,23 +972,8 @@ async fn json_err_twofactor(
}
}
Some ( TwoFactorType ::Duo ) = > {
Some ( TwoFactorType ::Duo ) = > {
let email = if let Some ( u ) = User ::find_by_uuid ( user_id , conn ) . await {
u . email
} else {
err ! ( "User does not exist" )
} ;
if CONFIG . duo_use_iframe ( ) {
// Legacy iframe prompt flow
let ( signature , host ) = duo ::generate_duo_signature ( & email , conn ) . await ? ;
result [ "TwoFactorProviders2" ] [ provider . to_string ( ) ] = json ! ( {
"Host" : host ,
"Signature" : signature ,
} ) ;
} else {
// OIDC based flow
let auth_url = duo_oidc ::get_duo_auth_url (
let auth_url = duo_oidc ::get_duo_auth_url (
& email ,
& user . email ,
data . client_id . as_ref ( ) . unwrap ( ) ,
data . client_id . as_ref ( ) . unwrap ( ) ,
data . device_identifier . as_ref ( ) . unwrap ( ) ,
data . device_identifier . as_ref ( ) . unwrap ( ) ,
conn ,
conn ,
@ -1009,10 +984,9 @@ async fn json_err_twofactor(
"AuthUrl" : auth_url ,
"AuthUrl" : auth_url ,
} ) ;
} ) ;
}
}
}
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 , conn ) . await else {
err ! ( "No YubiKey devices registered" )
err ! ( "No YubiKey devices registered" )
} ;
} ;
@ -1024,23 +998,10 @@ async fn json_err_twofactor(
}
}
Some ( tf_type @ TwoFactorType ::Email ) = > {
Some ( tf_type @ TwoFactorType ::Email ) = > {
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 , conn ) . await else {
err ! ( "No twofactor email registered" )
err ! ( "No twofactor email registered" )
} ;
} ;
// Starting with version 2025.5.0 the client will call `/api/two-factor/send-email-login`.
let disabled_send = if let Some ( cv ) = client_version {
let ver_match = semver ::VersionReq ::parse ( ">=2025.5.0" ) . unwrap ( ) ;
ver_match . matches ( & cv . 0 )
} else {
false
} ;
// Send email immediately if email is the only 2FA option.
if providers . len ( ) = = 1 & & ! disabled_send {
email ::send_token ( user_id , conn ) . await ? ;
}
let email_data = email ::EmailTokenData ::from_json ( & twofactor . data ) ? ;
let email_data = email ::EmailTokenData ::from_json ( & twofactor . data ) ? ;
result [ "TwoFactorProviders2" ] [ provider . to_string ( ) ] = json ! ( {
result [ "TwoFactorProviders2" ] [ provider . to_string ( ) ] = json ! ( {
"Email" : email ::obscure_email ( & email_data . email ) ,
"Email" : email ::obscure_email ( & email_data . email ) ,
@ -1078,11 +1039,6 @@ async fn prelogin_password(data: Json<PreloginData>, ip: ClientIp, conn: DbConn)
prelogin ( data , ip , conn ) . await
prelogin ( data , ip , conn ) . await
}
}
#[ post( " /accounts/register " , data = " <data> " ) ]
async fn identity_register ( data : Json < RegisterData > , conn : DbConn ) -> JsonResult {
register ( data , false , conn ) . await
}
#[ derive(Debug, Deserialize) ]
#[ derive(Debug, Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
#[ serde(rename_all = " camelCase " ) ]
struct RegisterVerificationData {
struct RegisterVerificationData {
@ -1154,8 +1110,9 @@ async fn register_verification_email(
}
}
#[ post( " /accounts/register/finish " , data = " <data> " ) ]
#[ post( " /accounts/register/finish " , data = " <data> " ) ]
async fn register_finish ( data : Json < RegisterData > , conn : DbConn ) -> JsonResult {
async fn register_finish ( data : Json < RegisterData > , ip : ClientIp , conn : DbConn ) -> JsonResult {
register ( data , true , conn ) . await
crate ::ratelimit ::check_limit_unauthenticated ( & ip . ip ) ? ;
register ( data , conn ) . await
}
}
// 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
@ -1285,7 +1242,7 @@ async fn oidcsignin_redirect(
let state = sso ::decode_state ( & base64_state ) ? ;
let state = sso ::decode_state ( & base64_state ) ? ;
let Some ( mut sso_auth ) = SsoAuth ::find ( & state , conn ) . await else {
let Some ( mut sso_auth ) = SsoAuth ::find ( & state , conn ) . await else {
err ! ( format ! ( "Cannot retrieve sso_auth for {state }" ) )
err ! ( format ! ( "Cannot retrieve sso_auth for {}" , state . escape_debug ( ) ) )
} ;
} ;
// Browser-binding check
// Browser-binding check
@ -1294,7 +1251,7 @@ async fn oidcsignin_redirect(
let provided_hash = cookie_value . as_deref ( ) . map ( | v | crypto ::sha256_hex ( v . as_bytes ( ) ) ) ;
let provided_hash = cookie_value . as_deref ( ) . map ( | v | crypto ::sha256_hex ( v . as_bytes ( ) ) ) ;
match ( sso_auth . binding_hash . as_deref ( ) , provided_hash . as_deref ( ) ) {
match ( sso_auth . binding_hash . as_deref ( ) , provided_hash . as_deref ( ) ) {
( Some ( expected ) , Some ( actual ) ) if crypto ::ct_eq ( expected , actual ) = > { }
( Some ( expected ) , Some ( actual ) ) if crypto ::ct_eq ( expected , actual ) = > { }
_ = > err ! ( format ! ( "SSO session binding mismatch for {state }" ) ) ,
_ = > err ! ( format ! ( "SSO session binding mismatch for {}" , state . escape_debug ( ) ) ) ,
}
}
cookies
cookies
. remove ( Cookie ::build ( SSO_BINDING_COOKIE ) . path ( format ! ( "{}/identity/connect/" , CONFIG . domain_path ( ) ) ) . build ( ) ) ;
. remove ( Cookie ::build ( SSO_BINDING_COOKIE ) . path ( format ! ( "{}/identity/connect/" , CONFIG . domain_path ( ) ) ) . build ( ) ) ;