vw_small

Hardened fork of Vaultwarden (https://github.com/dani-garcia/vaultwarden) with fewer features.
git clone https://git.philomathiclife.com/repos/vw_small
Log | Files | Refs | README

identity.rs (15441B)


      1 use crate::{
      2     api::{
      3         core::accounts::{PreloginData, RegisterData, _prelogin},
      4         ApiResult, EmptyResult, JsonResult,
      5     },
      6     auth::{ClientHeaders, ClientIp},
      7     config,
      8     db::{
      9         models::{Device, OrgPolicy, OrgPolicyType, TwoFactorType, User},
     10         DbConn,
     11     },
     12     error::{Error, MapResult},
     13     util,
     14 };
     15 use rocket::serde::json::Json;
     16 use rocket::{
     17     form::{Form, FromForm},
     18     Route,
     19 };
     20 use serde_json::Value;
     21 
     22 pub fn routes() -> Vec<Route> {
     23     routes![identity_register, login, prelogin]
     24 }
     25 
     26 #[post("/connect/token", data = "<data>")]
     27 async fn login(data: Form<ConnectData>, client_header: ClientHeaders, conn: DbConn) -> JsonResult {
     28     let data: ConnectData = data.into_inner();
     29     let mut user_uuid: Option<String> = None;
     30     let login_result = match data.grant_type.as_ref() {
     31         "refresh_token" => {
     32             _check_is_some(&data.refresh_token, "refresh_token cannot be blank")?;
     33             _refresh_login(data, &conn).await
     34         }
     35         "password" => {
     36             _check_is_some(&data.client_id, "client_id cannot be blank")?;
     37             _check_is_some(&data.password, "password cannot be blank")?;
     38             _check_is_some(&data.scope, "scope cannot be blank")?;
     39             _check_is_some(&data.username, "username cannot be blank")?;
     40             _check_is_some(&data.device_identifier, "device_identifier cannot be blank")?;
     41             _check_is_some(&data.device_name, "device_name cannot be blank")?;
     42             _check_is_some(&data.device_type, "device_type cannot be blank")?;
     43             _password_login(data, &mut user_uuid, &conn, &client_header.ip).await
     44         }
     45         t => err!("Invalid type", t),
     46     };
     47     login_result
     48 }
     49 
     50 async fn _refresh_login(data: ConnectData, conn: &DbConn) -> JsonResult {
     51     // Extract token
     52     let token = data.refresh_token.unwrap();
     53     // Get device by refresh token
     54     let mut device = Device::find_by_refresh_token(&token, conn)
     55         .await
     56         .map_res("Invalid refresh token")?;
     57     let scope = "api offline_access";
     58     let scope_vec = vec!["api".into(), "offline_access".into()];
     59     // Common
     60     let user = User::find_by_uuid(&device.user_uuid, conn).await.unwrap();
     61     // ---
     62     // Disabled this variable, it was used to generate the JWT
     63     // Because this might get used in the future, and is add by the Bitwarden Server, lets keep it, but then commented out
     64     // See: https://github.com/dani-garcia/vaultwarden/issues/4156
     65     // ---
     66     // let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
     67     let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec);
     68     device.save(conn).await?;
     69     let result = json!({
     70         "access_token": access_token,
     71         "expires_in": expires_in,
     72         "token_type": "Bearer",
     73         "refresh_token": device.refresh_token,
     74         "scope": scope,
     75     });
     76     Ok(Json(result))
     77 }
     78 #[expect(clippy::struct_excessive_bools, reason = "upstream")]
     79 #[derive(Default, Deserialize, Serialize)]
     80 #[serde(rename_all = "camelCase")]
     81 struct MasterPasswordPolicy {
     82     min_complexity: u8,
     83     min_length: u32,
     84     require_lower: bool,
     85     require_upper: bool,
     86     require_numbers: bool,
     87     require_special: bool,
     88     enforce_on_login: bool,
     89 }
     90 
     91 #[allow(clippy::else_if_without_else)]
     92 async fn _password_login(
     93     data: ConnectData,
     94     user_uuid: &mut Option<String>,
     95     conn: &DbConn,
     96     ip: &ClientIp,
     97 ) -> JsonResult {
     98     // Validate scope
     99     let scope = data.scope.as_ref().unwrap();
    100     if scope != "api offline_access" {
    101         err!("Scope not supported")
    102     }
    103     let scope_vec = vec!["api".into(), "offline_access".into()];
    104     // Get the user
    105     let username = data.username.as_ref().unwrap().trim();
    106     let Some(mut user) = User::find_by_mail(username, conn).await else {
    107         err!(
    108             "Username or password is incorrect. Try again",
    109             format!("IP: {}. Username: {}.", ip.ip, username)
    110         )
    111     };
    112     // Set the user_uuid here to be passed back used for event logging.
    113     *user_uuid = Some(user.uuid.clone());
    114     // Check password
    115     let password = data.password.as_ref().unwrap();
    116     if data.auth_request.is_some() {
    117         err!(
    118             "Auth request not found. Try again.",
    119             format!("IP: {}. Username: {}.", ip.ip, username)
    120         )
    121     } else if !user.check_valid_password(password) {
    122         err!(
    123             "Username or password is incorrect. Try again",
    124             format!("IP: {}. Username: {}.", ip.ip, username)
    125         )
    126     }
    127     // Change the KDF Iterations
    128     if user.password_iterations() != config::get_config().password_iterations {
    129         user.set_password_iterations(config::get_config().password_iterations);
    130         user.set_password(password, None, false, None);
    131         if let Err(e) = user.save(conn).await {
    132             panic!("Error updating user: {e:#?}");
    133         }
    134     }
    135     // Check if the user is disabled
    136     if !user.enabled {
    137         err!(
    138             "This user has been disabled",
    139             format!("IP: {}. Username: {}.", ip.ip, username)
    140         )
    141     }
    142     let (mut device, _) = get_device(&data, conn, &user).await;
    143     let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec);
    144     let kdf = user.client_kdf_type;
    145     let kdf_iter = user.client_kdf_iter();
    146     let kdf_mem = user.client_kdf_memory();
    147     let kdf_par = user.client_kdf_parallelism();
    148     let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, ip, conn).await?;
    149     // Common
    150     // ---
    151     // Disabled this variable, it was used to generate the JWT
    152     // Because this might get used in the future, and is add by the Bitwarden Server, lets keep it, but then commented out
    153     // See: https://github.com/dani-garcia/vaultwarden/issues/4156
    154     // ---
    155     // let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
    156     device.save(conn).await?;
    157     // Fetch all valid Master Password Policies and merge them into one with all true's and larges numbers as one policy
    158     let master_password_policies: Vec<MasterPasswordPolicy> =
    159         OrgPolicy::find_accepted_and_confirmed_by_user_and_active_policy(
    160             &user.uuid,
    161             OrgPolicyType::MasterPassword,
    162             conn,
    163         )
    164         .await
    165         .into_iter()
    166         .filter_map(|p| serde_json::from_str(&p.data).ok())
    167         .collect();
    168 
    169     let master_password_policy = if master_password_policies.is_empty() {
    170         json!({"object": "masterPasswordPolicy"})
    171     } else {
    172         let mut mpp_json = json!(master_password_policies.into_iter().reduce(|acc, policy| {
    173             MasterPasswordPolicy {
    174                 min_complexity: acc.min_complexity.max(policy.min_complexity),
    175                 min_length: acc.min_length.max(policy.min_length),
    176                 require_lower: acc.require_lower || policy.require_lower,
    177                 require_upper: acc.require_upper || policy.require_upper,
    178                 require_numbers: acc.require_numbers || policy.require_numbers,
    179                 require_special: acc.require_special || policy.require_special,
    180                 enforce_on_login: acc.enforce_on_login || policy.enforce_on_login,
    181             }
    182         }));
    183         mpp_json["object"] = json!("masterPasswordPolicy");
    184         mpp_json
    185     };
    186     let mut result = json!({
    187         "access_token": access_token,
    188         "expires_in": expires_in,
    189         "token_type": "Bearer",
    190         "refresh_token": device.refresh_token,
    191         "Key": user.akey,
    192         "PrivateKey": user.private_key,
    193         "Kdf": kdf,
    194         "KdfIterations": kdf_iter,
    195         "KdfMemory": kdf_mem,
    196         "KdfParallelism": kdf_par,
    197         "ResetMasterPassword": false, // TODO: Same as above
    198         "ForcePasswordReset": false,
    199         "MasterPasswordPolicy": master_password_policy,
    200         "scope": scope,
    201         "UserDecryptionOptions": {
    202             "HasMasterPassword": !user.password_hash.is_empty(),
    203             "Object": "userDecryptionOptions"
    204         },
    205     });
    206     if let Some(token) = twofactor_token {
    207         result["TwoFactorToken"] = Value::String(token);
    208     }
    209     info!("User {} logged in successfully. IP: {}", username, ip.ip);
    210     Ok(Json(result))
    211 }
    212 
    213 /// Retrieves an existing device or creates a new device from ConnectData and the User
    214 async fn get_device(data: &ConnectData, conn: &DbConn, user: &User) -> (Device, bool) {
    215     // On iOS, device_type sends "iOS", on others it sends a number
    216     // When unknown or unable to parse, return 14, which is 'Unknown Browser'
    217     let device_type = util::try_parse_string(data.device_type.as_ref()).unwrap_or(14i32);
    218     let device_id = data
    219         .device_identifier
    220         .clone()
    221         .expect("No device id provided");
    222     let device_name = data.device_name.clone().expect("No device name provided");
    223     let mut new_device = false;
    224     // Find device or create new
    225     let device = (Device::find_by_uuid_and_user(&device_id, &user.uuid, conn).await).map_or_else(
    226         || {
    227             new_device = true;
    228             Device::new(device_id, user.uuid.clone(), device_name, device_type)
    229         },
    230         |device| device,
    231     );
    232     (device, new_device)
    233 }
    234 
    235 async fn twofactor_auth(
    236     user_uuid: &str,
    237     data: &ConnectData,
    238     device: &mut Device,
    239     ip: &ClientIp,
    240     conn: &DbConn,
    241 ) -> ApiResult<Option<String>> {
    242     let (authn, totp_token) = TwoFactorType::get_factors(user_uuid, conn).await?;
    243     if authn || totp_token.is_some() {
    244         let Some(ref token) = data.two_factor_token else {
    245             err_json!(
    246                 _json_err_twofactor(authn, totp_token.is_some(), user_uuid, conn).await?,
    247                 "2FA token not provided"
    248             )
    249         };
    250         // If no provider is given, we use a fallback.
    251         // The fallback is prioritized to be WebAuthn if possible.
    252         let tf_type = data
    253             .two_factor_provider
    254             .map(|prov| {
    255                 TwoFactorType::try_from(prov)
    256                     .map_err(Error::from)
    257                     .and_then(|tf| {
    258                         if matches!(tf, TwoFactorType::WebAuthn) {
    259                             if authn {
    260                                 Ok(tf)
    261                             } else {
    262                                 const MSG: &str = "no webauthn registrations";
    263                                 Err(Error::new(MSG, MSG))
    264                             }
    265                         } else if totp_token.is_some() {
    266                             Ok(tf)
    267                         } else {
    268                             const MSG: &str = "no totp registrations";
    269                             Err(Error::new(MSG, MSG))
    270                         }
    271                     })
    272             })
    273             .transpose()?
    274             .unwrap_or({
    275                 if authn {
    276                     TwoFactorType::WebAuthn
    277                 } else {
    278                     TwoFactorType::Totp
    279                 }
    280             });
    281         use crate::api::core::two_factor as _tf;
    282         match tf_type {
    283             TwoFactorType::Totp => {
    284                 _tf::authenticator::validate_totp_code_str(
    285                     user_uuid,
    286                     token,
    287                     totp_token.unwrap_or_else(|| {
    288                         unreachable!("no totp registrations, but we verified there are")
    289                     }),
    290                     ip,
    291                     conn,
    292                 )
    293                 .await?;
    294             }
    295             TwoFactorType::WebAuthn => {
    296                 _tf::webauthn::validate_webauthn_login(user_uuid, token, conn).await?;
    297             }
    298         }
    299         device.delete_twofactor_remember();
    300     }
    301     Ok(None)
    302 }
    303 
    304 async fn _json_err_twofactor(
    305     authn: bool,
    306     totp: bool,
    307     user_uuid: &str,
    308     conn: &DbConn,
    309 ) -> ApiResult<Value> {
    310     use crate::api::core::two_factor;
    311     let auth_num = i32::from(TwoFactorType::WebAuthn).to_string();
    312     let totp_num = i32::from(TwoFactorType::Totp).to_string();
    313     let providers = [auth_num.as_str(), totp_num.as_str()];
    314     let mut result = json!({
    315         "error" : "invalid_grant",
    316         "error_description" : "Two factor required.",
    317         "TwoFactorProviders" : if authn { if totp { providers.as_slice() } else { &providers[..1] } } else if totp { &providers[1..] } else { [].as_slice() },
    318         "TwoFactorProviders2" : {},
    319         "MasterPasswordPolicy": {
    320             "Object": "masterPasswordPolicy"
    321         }
    322     });
    323     if authn {
    324         let request = two_factor::webauthn::generate_webauthn_login(user_uuid, conn).await?;
    325         result["TwoFactorProviders2"][auth_num] = request.0;
    326     }
    327     if totp {
    328         result["TwoFactorProviders2"][totp_num] = Value::Null;
    329     }
    330     Ok(result)
    331 }
    332 
    333 #[post("/accounts/prelogin", data = "<data>")]
    334 async fn prelogin(data: Json<PreloginData>, conn: DbConn) -> Json<Value> {
    335     _prelogin(data, conn).await
    336 }
    337 
    338 #[allow(unused_variables, clippy::needless_pass_by_value)]
    339 #[post("/accounts/register", data = "<data>")]
    340 fn identity_register(data: Json<RegisterData>) -> Error {
    341     const MSG: &str = "No more registerations allowed.";
    342     Error::new(MSG, MSG)
    343 }
    344 
    345 // https://github.com/bitwarden/jslib/blob/master/common/src/models/request/tokenRequest.ts
    346 // https://github.com/bitwarden/mobile/blob/master/src/Core/Models/Request/TokenRequest.cs
    347 #[derive(Clone, Default, FromForm)]
    348 struct ConnectData {
    349     #[field(name = uncased("grant_type"))]
    350     #[field(name = uncased("granttype"))]
    351     grant_type: String, // refresh_token, password, client_credentials (API key)
    352     // Needed for grant_type="refresh_token"
    353     #[field(name = uncased("refresh_token"))]
    354     #[field(name = uncased("refreshtoken"))]
    355     refresh_token: Option<String>,
    356     // Needed for grant_type = "password" | "client_credentials"
    357     #[field(name = uncased("client_id"))]
    358     #[field(name = uncased("clientid"))]
    359     client_id: Option<String>, // web, cli, desktop, browser, mobile
    360     #[field(name = uncased("client_secret"))]
    361     #[field(name = uncased("clientsecret"))]
    362     #[allow(dead_code)]
    363     client_secret: Option<String>,
    364     #[field(name = uncased("password"))]
    365     password: Option<String>,
    366     #[field(name = uncased("scope"))]
    367     scope: Option<String>,
    368     #[field(name = uncased("username"))]
    369     username: Option<String>,
    370     #[field(name = uncased("device_identifier"))]
    371     #[field(name = uncased("deviceidentifier"))]
    372     device_identifier: Option<String>,
    373     #[field(name = uncased("device_name"))]
    374     #[field(name = uncased("devicename"))]
    375     device_name: Option<String>,
    376     #[field(name = uncased("device_type"))]
    377     #[field(name = uncased("devicetype"))]
    378     device_type: Option<String>,
    379     #[field(name = uncased("device_push_token"))]
    380     #[field(name = uncased("devicepushtoken"))]
    381     _device_push_token: Option<String>, // Unused; mobile device push not yet supported.
    382     // Needed for two-factor auth
    383     #[field(name = uncased("two_factor_provider"))]
    384     #[field(name = uncased("twofactorprovider"))]
    385     two_factor_provider: Option<i32>,
    386     #[field(name = uncased("two_factor_token"))]
    387     #[field(name = uncased("twofactortoken"))]
    388     two_factor_token: Option<String>,
    389     #[field(name = uncased("two_factor_remember"))]
    390     #[field(name = uncased("twofactorremember"))]
    391     #[allow(dead_code)]
    392     two_factor_remember: Option<u32>,
    393     #[field(name = uncased("authrequest"))]
    394     auth_request: Option<String>,
    395 }
    396 
    397 fn _check_is_some<T>(value: &Option<T>, msg: &str) -> EmptyResult {
    398     if value.is_none() {
    399         err!(msg)
    400     }
    401     Ok(())
    402 }