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 (15918B)


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