From bc532f54d5ae5728704d87fe6323843819c1fc8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Wed, 17 Oct 2018 22:25:28 +0200 Subject: [PATCH] Improve login query parsing --- src/api/identity.rs | 211 ++++++++++++++++++-------------------------- 1 file changed, 86 insertions(+), 125 deletions(-) diff --git a/src/api/identity.rs b/src/api/identity.rs index 1493692d..ac48b2f9 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -1,8 +1,7 @@ -use std::collections::HashMap; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use rocket::request::{self, Form, FormItems, FromForm, FromRequest, Request}; -use rocket::{Outcome, Route}; +use rocket::request::LenientForm; +use rocket::Route; use rocket_contrib::json::Json; use serde_json::Value; @@ -14,7 +13,7 @@ use db::DbConn; use util::{self, JsonMap}; -use api::{ApiResult, JsonResult}; +use api::{ApiResult, EmptyResult, JsonResult}; use CONFIG; @@ -23,21 +22,22 @@ pub fn routes() -> Vec { } #[post("/connect/token", data = "")] -fn login(data: Form, device_type: DeviceType, conn: DbConn, socket: Option) -> JsonResult { +fn login(data: LenientForm, conn: DbConn, socket: Option) -> JsonResult { let data: ConnectData = data.into_inner(); + validate_data(&data)?; match data.grant_type { - GrantType::RefreshToken => _refresh_login(data, device_type, conn), - GrantType::Password => _password_login(data, device_type, conn, socket), + GrantType::refresh_token => _refresh_login(data, conn), + GrantType::password => _password_login(data, conn, socket), } } -fn _refresh_login(data: ConnectData, _device_type: DeviceType, conn: DbConn) -> JsonResult { +fn _refresh_login(data: ConnectData, conn: DbConn) -> JsonResult { // Extract token - let token = data.get("refresh_token"); + let token = data.refresh_token.unwrap(); // Get device by refresh token - let mut device = match Device::find_by_refresh_token(token, &conn) { + let mut device = match Device::find_by_refresh_token(&token, &conn) { Some(device) => device, None => err!("Invalid refresh token"), }; @@ -48,19 +48,19 @@ fn _refresh_login(data: ConnectData, _device_type: DeviceType, conn: DbConn) -> let (access_token, expires_in) = device.refresh_tokens(&user, orgs); match device.save(&conn) { - Ok(()) => Ok(Json(json!({ - "access_token": access_token, - "expires_in": expires_in, - "token_type": "Bearer", - "refresh_token": device.refresh_token, - "Key": user.key, - "PrivateKey": user.private_key, - }))), - Err(_) => err!("Failed to add device to user") + Ok(()) => Ok(Json(json!({ + "access_token": access_token, + "expires_in": expires_in, + "token_type": "Bearer", + "refresh_token": device.refresh_token, + "Key": user.key, + "PrivateKey": user.private_key, + }))), + Err(_) => err!("Failed to add device to user"), } } -fn _password_login(data: ConnectData, device_type: DeviceType, conn: DbConn, remote: Option) -> JsonResult { +fn _password_login(data: ConnectData, conn: DbConn, remote: Option) -> JsonResult { // Get the ip for error reporting let ip = match remote { Some(ip) => ip.ip(), @@ -68,13 +68,13 @@ fn _password_login(data: ConnectData, device_type: DeviceType, conn: DbConn, rem }; // Validate scope - let scope = data.get("scope"); + let scope = data.scope.as_ref().unwrap(); if scope != "api offline_access" { err!("Scope not supported") } // Get the user - let username = data.get("username"); + let username = data.username.as_ref().unwrap(); let user = match User::find_by_mail(username, &conn) { Some(user) => user, None => err!(format!( @@ -84,7 +84,7 @@ fn _password_login(data: ConnectData, device_type: DeviceType, conn: DbConn, rem }; // Check password - let password = data.get("password"); + let password = data.password.as_ref().unwrap(); if !user.check_valid_password(password) { err!(format!( "Username or password is incorrect. Try again. IP: {}. Username: {}.", @@ -92,17 +92,9 @@ fn _password_login(data: ConnectData, device_type: DeviceType, conn: DbConn, rem )) } - // Let's only use the header and ignore the 'devicetype' parameter - let device_type_num = device_type.0; - - let (device_id, device_name) = if data.is_device { - ( - data.get("deviceidentifier").clone(), - data.get("devicename").clone(), - ) - } else { - (format!("web-{}", user.uuid), String::from("web")) - }; + let device_type: i32 = util::try_parse_string(data.device_type.as_ref()).expect("Invalid type"); + let device_id = data.device_identifier.clone().expect("Missing device id"); + let device_name = data.device_name.clone().expect("Missing device name"); // Find device or create new let mut device = match Device::find_by_uuid(&device_id, &conn) { @@ -110,8 +102,8 @@ fn _password_login(data: ConnectData, device_type: DeviceType, conn: DbConn, rem // Check if valid device if device.user_uuid != user.uuid { match device.delete(&conn) { - Ok(()) => Device::new(device_id, user.uuid.clone(), device_name, device_type_num), - Err(_) => err!("Tried to delete device not owned by user, but failed") + Ok(()) => Device::new(device_id, user.uuid.clone(), device_name, device_type), + Err(_) => err!("Tried to delete device not owned by user, but failed"), } } else { device @@ -119,11 +111,11 @@ fn _password_login(data: ConnectData, device_type: DeviceType, conn: DbConn, rem } None => { // Create new device - Device::new(device_id, user.uuid.clone(), device_name, device_type_num) + Device::new(device_id, user.uuid.clone(), device_name, device_type) } }; - let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, &conn)?; + let twofactor_token = twofactor_auth(&user.uuid, &data.clone(), &mut device, &conn)?; // Common let user = User::find_by_uuid(&device.user_uuid, &conn).unwrap(); @@ -168,13 +160,10 @@ fn twofactor_auth( return Ok(None); } - let provider = match util::try_parse_string(data.get_opt("twoFactorProvider")) { - Some(provider) => provider, - None => providers[0], // If we aren't given a two factor provider, asume the first one - }; + let provider = data.two_factor_provider.unwrap_or(providers[0]); // If we aren't given a two factor provider, asume the first one - let twofactor_code = match data.get_opt("twoFactorToken") { - Some(code) => code, + let twofactor_code = match data.two_factor_token { + Some(ref code) => code, None => err_json!(_json_err_twofactor(&providers, user_uuid, conn)?), }; @@ -182,8 +171,8 @@ fn twofactor_auth( match TwoFactorType::from_i32(provider) { Some(TwoFactorType::Remember) => { - match &device.twofactor_remember { - Some(remember) if remember == twofactor_code => return Ok(None), // No twofactor token needed here + match device.twofactor_remember { + Some(ref remember) if remember == twofactor_code => return Ok(None), // No twofactor token needed here _ => err_json!(_json_err_twofactor(&providers, user_uuid, conn)?), } } @@ -207,13 +196,13 @@ fn twofactor_auth( Some(TwoFactorType::U2f) => { use api::core::two_factor; - two_factor::validate_u2f_login(user_uuid, twofactor_code, conn)?; + two_factor::validate_u2f_login(user_uuid, &twofactor_code, conn)?; } _ => err!("Invalid two factor provider"), } - if util::try_parse_string_or(data.get_opt("twoFactorRemember"), 0) == 1 { + if data.two_factor_remember.unwrap_or(0) == 1 { Ok(Some(device.refresh_twofactor_remember())) } else { device.delete_twofactor_remember(); @@ -271,91 +260,63 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api Ok(result) } -#[derive(Clone, Copy)] -struct DeviceType(i32); - -impl<'a, 'r> FromRequest<'a, 'r> for DeviceType { - type Error = &'static str; - - fn from_request(request: &'a Request<'r>) -> request::Outcome { - let headers = request.headers(); - let type_opt = headers.get_one("Device-Type"); - let type_num = util::try_parse_string_or(type_opt, 0); - - Outcome::Success(DeviceType(type_num)) - } -} - -#[derive(Debug)] +#[derive(FromForm, Debug, Clone)] +#[allow(non_snake_case)] struct ConnectData { grant_type: GrantType, - is_device: bool, - data: HashMap, + + // Needed for grant_type="refresh_token" + refresh_token: Option, + + // Needed for grant_type="password" + client_id: Option, // web, cli, desktop, browser, mobile + password: Option, + scope: Option, + username: Option, + + #[form(field = "deviceIdentifier")] + device_identifier: Option, + #[form(field = "deviceName")] + device_name: Option, + #[form(field = "deviceType")] + device_type: Option, + + // Needed for two-factor auth + #[form(field = "twoFactorProvider")] + two_factor_provider: Option, + #[form(field = "twoFactorToken")] + two_factor_token: Option, + #[form(field = "twoFactorRemember")] + two_factor_remember: Option, } -#[derive(Debug, Copy, Clone)] +#[derive(FromFormValue, Debug, Clone, Copy)] +#[allow(non_camel_case_types)] enum GrantType { - RefreshToken, - Password, + refresh_token, + password, } -impl ConnectData { - fn get(&self, key: &str) -> &String { - &self.data[&key.to_lowercase()] - } - - fn get_opt(&self, key: &str) -> Option<&String> { - self.data.get(&key.to_lowercase()) - } -} - -const VALUES_REFRESH: [&str; 1] = ["refresh_token"]; -const VALUES_PASSWORD: [&str; 5] = ["client_id", "grant_type", "password", "scope", "username"]; -const VALUES_DEVICE: [&str; 3] = ["deviceidentifier", "devicename", "devicetype"]; - -impl<'f> FromForm<'f> for ConnectData { - type Error = String; - - fn from_form(items: &mut FormItems<'f>, _strict: bool) -> Result { - let mut data = HashMap::new(); - - // Insert data into map - for item in items { - let (key, value) = item.key_value_decoded(); - data.insert(key.to_lowercase(), value); +fn validate_data(data: &ConnectData) -> EmptyResult { + match data.grant_type { + GrantType::refresh_token => { + _check_is_some(&data.refresh_token, "refresh_token cannot be blank") } - - // Validate needed values - let (grant_type, is_device) = match data.get("grant_type").map(String::as_ref) { - Some("refresh_token") => { - check_values(&data, &VALUES_REFRESH)?; - (GrantType::RefreshToken, false) // Device doesn't matter here - } - Some("password") => { - check_values(&data, &VALUES_PASSWORD)?; - - let is_device = match data["client_id"].as_ref() { - "browser" | "mobile" => check_values(&data, &VALUES_DEVICE)?, - _ => false, - }; - (GrantType::Password, is_device) - } - _ => return Err("Grant type not supported".to_string()), - }; - - Ok(ConnectData { - grant_type, - is_device, - data, - }) - } -} - -fn check_values(map: &HashMap, values: &[&str]) -> Result { - for value in values { - if !map.contains_key(*value) { - return Err(format!("{} cannot be blank", value)); + GrantType::password => { + _check_is_some(&data.client_id, "client_id cannot be blank")?; + _check_is_some(&data.password, "password cannot be blank")?; + _check_is_some(&data.scope, "scope cannot be blank")?; + _check_is_some(&data.username, "username cannot be blank")?; + _check_is_some(&data.device_identifier, "device_identifier cannot be blank")?; + _check_is_some(&data.device_name, "device_name cannot be blank")?; + _check_is_some(&data.device_type, "device_type cannot be blank") } } - Ok(true) +} + +fn _check_is_some(value: &Option, msg: &str) -> EmptyResult { + if value.is_none() { + err!(msg) + } + Ok(()) }