Improved two factor auth

This commit is contained in:
Daniel García 2018-06-01 15:08:03 +02:00
parent dc188211d8
commit b0ee5f6570
8 changed files with 251 additions and 107 deletions

View file

@ -0,0 +1 @@
-- This file should undo anything in `up.sql`

View file

@ -0,0 +1,3 @@
ALTER TABLE devices
ADD COLUMN
twofactor_remember TEXT;

View file

@ -96,22 +96,49 @@ pub fn routes() -> Vec<Route> {
use rocket::Route; use rocket::Route;
use rocket_contrib::Json; use rocket_contrib::{Json, Value};
use db::DbConn; use db::DbConn;
use db::models::*;
use api::{JsonResult, EmptyResult, JsonUpcase}; use api::{JsonResult, EmptyResult, JsonUpcase};
use auth::Headers; use auth::Headers;
#[put("/devices/identifier/<uuid>/clear-token")] #[put("/devices/identifier/<uuid>/clear-token", data = "<data>")]
fn clear_device_token(uuid: String, _conn: DbConn) -> JsonResult { fn clear_device_token(uuid: String, data: Json<Value>, headers: Headers, conn: DbConn) -> EmptyResult {
println!("{}", uuid); println!("UUID: {:#?}", uuid);
err!("Not implemented") println!("DATA: {:#?}", data);
let device = match Device::find_by_uuid(&uuid, &conn) {
Some(device) => device,
None => err!("Device not found")
};
if device.user_uuid != headers.user.uuid {
err!("Device not owned by user")
} }
#[put("/devices/identifier/<uuid>/token")] device.delete(&conn);
fn put_device_token(uuid: String, _conn: DbConn) -> JsonResult {
println!("{}", uuid); Ok(())
}
#[put("/devices/identifier/<uuid>/token", data = "<data>")]
fn put_device_token(uuid: String, data: Json<Value>, headers: Headers, conn: DbConn) -> JsonResult {
println!("UUID: {:#?}", uuid);
println!("DATA: {:#?}", data);
let device = match Device::find_by_uuid(&uuid, &conn) {
Some(device) => device,
None => err!("Device not found")
};
if device.user_uuid != headers.user.uuid {
err!("Device not owned by user")
}
// TODO: What does this do?
err!("Not implemented") err!("Not implemented")
} }

View file

@ -135,7 +135,7 @@ fn activate_authenticator(data: JsonUpcase<EnableTwoFactorData>, headers: Header
user.totp_secret = Some(key.to_uppercase()); user.totp_secret = Some(key.to_uppercase());
// Validate the token provided with the key // Validate the token provided with the key
if !user.check_totp_code(Some(token)) { if !user.check_totp_code(token) {
err!("Invalid totp code") err!("Invalid totp code")
} }

View file

@ -3,7 +3,7 @@ use std::collections::HashMap;
use rocket::{Route, Outcome}; use rocket::{Route, Outcome};
use rocket::request::{self, Request, FromRequest, Form, FormItems, FromForm}; use rocket::request::{self, Request, FromRequest, Form, FormItems, FromForm};
use rocket_contrib::Json; use rocket_contrib::{Json, Value};
use db::DbConn; use db::DbConn;
use db::models::*; use db::models::*;
@ -19,19 +19,42 @@ pub fn routes() -> Vec<Route> {
#[post("/connect/token", data = "<connect_data>")] #[post("/connect/token", data = "<connect_data>")]
fn login(connect_data: Form<ConnectData>, device_type: DeviceType, conn: DbConn) -> JsonResult { fn login(connect_data: Form<ConnectData>, device_type: DeviceType, conn: DbConn) -> JsonResult {
let data = connect_data.get(); let data = connect_data.get();
println!("{:#?}", data);
let mut device = match data.grant_type { match data.grant_type {
GrantType::RefreshToken => { GrantType::RefreshToken =>_refresh_login(data, device_type, conn),
GrantType::Password => _password_login(data, device_type, conn)
}
}
fn _refresh_login(data: &ConnectData, _device_type: DeviceType, conn: DbConn) -> JsonResult {
// Extract token // Extract token
let token = data.get("refresh_token").unwrap(); let token = data.get("refresh_token").unwrap();
// Get device by refresh token // Get device by refresh token
match Device::find_by_refresh_token(token, &conn) { let mut device = match Device::find_by_refresh_token(token, &conn) {
Some(device) => device, Some(device) => device,
None => err!("Invalid refresh token") None => err!("Invalid refresh token")
};
// COMMON
let user = User::find_by_uuid(&device.user_uuid, &conn).unwrap();
let orgs = UserOrganization::find_by_user(&user.uuid, &conn);
let (access_token, expires_in) = device.refresh_tokens(&user, orgs);
device.save(&conn);
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,
})))
} }
}
GrantType::Password => { fn _password_login(data: &ConnectData, device_type: DeviceType, conn: DbConn) -> JsonResult {
// Validate scope // Validate scope
let scope = data.get("scope").unwrap(); let scope = data.get("scope").unwrap();
if scope != "api offline_access" { if scope != "api offline_access" {
@ -51,19 +74,6 @@ fn login(connect_data: Form<ConnectData>, device_type: DeviceType, conn: DbConn)
err!("Username or password is incorrect. Try again.") err!("Username or password is incorrect. Try again.")
} }
// Check if totp code is required and the value is correct
let totp_code = util::parse_option_string(data.get("twoFactorToken"));
if !user.check_totp_code(totp_code) {
// Return error 400
err_json!(json!({
"error" : "invalid_grant",
"error_description" : "Two factor required.",
"TwoFactorProviders" : [ 0 ],
"TwoFactorProviders2" : { "0" : null }
}))
}
// Let's only use the header and ignore the 'devicetype' parameter // Let's only use the header and ignore the 'devicetype' parameter
let device_type_num = device_type.0; let device_type_num = device_type.0;
@ -78,7 +88,7 @@ fn login(connect_data: Form<ConnectData>, device_type: DeviceType, conn: DbConn)
}; };
// Find device or create new // Find device or create new
match Device::find_by_uuid(&device_id, &conn) { let mut device = match Device::find_by_uuid(&device_id, &conn) {
Some(device) => { Some(device) => {
// Check if valid device // Check if valid device
if device.user_uuid != user.uuid { if device.user_uuid != user.uuid {
@ -90,28 +100,112 @@ fn login(connect_data: Form<ConnectData>, device_type: DeviceType, conn: DbConn)
} }
None => { None => {
// Create new device // Create new device
Device::new(device_id, user.uuid, device_name, device_type_num) Device::new(device_id, user.uuid.clone(), device_name, device_type_num)
}
}
} }
}; };
let twofactor_token = if user.requires_twofactor() {
let twofactor_provider = util::parse_option_string(data.get("twoFactorProvider")).unwrap_or(0);
let twofactor_code = match data.get("twoFactorToken") {
Some(code) => code,
None => err_json!(_json_err_twofactor())
};
match twofactor_provider {
0 /* TOTP */ => {
let totp_code: u64 = match twofactor_code.parse() {
Ok(code) => code,
Err(_) => err!("Invalid Totp code")
};
if !user.check_totp_code(totp_code) {
err_json!(_json_err_twofactor())
}
if util::parse_option_string(data.get("twoFactorRemember")).unwrap_or(0) == 1 {
device.refresh_twofactor_remember();
device.twofactor_remember.clone()
} else {
device.delete_twofactor_remember();
None
}
},
5 /* Remember */ => {
match device.twofactor_remember {
Some(ref remember) if remember == twofactor_code => (),
_ => err_json!(_json_err_twofactor())
};
None // No twofactor token needed here
},
_ => err!("Invalid two factor provider"),
}
} else { None }; // No twofactor token if twofactor is disabled
// Common
let user = User::find_by_uuid(&device.user_uuid, &conn).unwrap(); let user = User::find_by_uuid(&device.user_uuid, &conn).unwrap();
let orgs = UserOrganization::find_by_user(&user.uuid, &conn); let orgs = UserOrganization::find_by_user(&user.uuid, &conn);
let (access_token, expires_in) = device.refresh_tokens(&user, orgs); let (access_token, expires_in) = device.refresh_tokens(&user, orgs);
device.save(&conn); device.save(&conn);
Ok(Json(json!({ let mut result = json!({
"access_token": access_token, "access_token": access_token,
"expires_in": expires_in, "expires_in": expires_in,
"token_type": "Bearer", "token_type": "Bearer",
"refresh_token": device.refresh_token, "refresh_token": device.refresh_token,
"Key": user.key, "Key": user.key,
"PrivateKey": user.private_key "PrivateKey": user.private_key,
}))) //"TwoFactorToken": "11122233333444555666777888999"
});
if let Some(token) = twofactor_token {
result["TwoFactorToken"] = Value::String(token);
} }
Ok(Json(result))
}
fn _json_err_twofactor() -> Value {
json!({
"error" : "invalid_grant",
"error_description" : "Two factor required.",
"TwoFactorProviders" : [ 0 ],
"TwoFactorProviders2" : { "0" : null }
})
}
/*
ConnectData {
grant_type: Password,
is_device: false,
data: {
"scope": "api offline_access",
"client_id": "web",
"grant_type": "password",
"username": "dani@mail",
"password": "8IuV1sJ94tPjyYIK+E+PTjblzjm4W6C4N5wqM0KKsSg="
}
}
RETURNS "TwoFactorToken": "11122233333444555666777888999"
Next login
ConnectData {
grant_type: Password,
is_device: false,
data: {
"scope": "api offline_access",
"username": "dani@mail",
"client_id": "web",
"twofactorprovider": "5",
"twofactortoken": "11122233333444555666777888999",
"grant_type": "password",
"twofactorremember": "0",
"password": "8IuV1sJ94tPjyYIK+E+PTjblzjm4W6C4N5wqM0KKsSg="
}
}
*/
struct DeviceType(i32); struct DeviceType(i32);

View file

@ -19,6 +19,8 @@ pub struct Device {
pub push_token: Option<String>, pub push_token: Option<String>,
pub refresh_token: String, pub refresh_token: String,
pub twofactor_remember: Option<String>,
} }
/// Local methods /// Local methods
@ -37,9 +39,22 @@ impl Device {
push_token: None, push_token: None,
refresh_token: String::new(), refresh_token: String::new(),
twofactor_remember: None,
} }
} }
pub fn refresh_twofactor_remember(&mut self) {
use data_encoding::BASE64;
use crypto;
self.twofactor_remember = Some(BASE64.encode(&crypto::get_random(vec![0u8; 180])));
}
pub fn delete_twofactor_remember(&mut self) {
self.twofactor_remember = None;
}
pub fn refresh_tokens(&mut self, user: &super::User, orgs: Vec<super::UserOrganization>) -> (String, i64) { pub fn refresh_tokens(&mut self, user: &super::User, orgs: Vec<super::UserOrganization>) -> (String, i64) {
// If there is no refresh token, we create one // If there is no refresh token, we create one
if self.refresh_token.is_empty() { if self.refresh_token.is_empty() {

View file

@ -26,8 +26,10 @@ pub struct User {
pub key: String, pub key: String,
pub private_key: Option<String>, pub private_key: Option<String>,
pub public_key: Option<String>, pub public_key: Option<String>,
pub totp_secret: Option<String>, pub totp_secret: Option<String>,
pub totp_recover: Option<String>, pub totp_recover: Option<String>,
pub security_stamp: String, pub security_stamp: String,
pub equivalent_domains: String, pub equivalent_domains: String,
@ -61,6 +63,7 @@ impl User {
password_hint: None, password_hint: None,
private_key: None, private_key: None,
public_key: None, public_key: None,
totp_secret: None, totp_secret: None,
totp_recover: None, totp_recover: None,
@ -95,9 +98,12 @@ impl User {
self.security_stamp = Uuid::new_v4().to_string(); self.security_stamp = Uuid::new_v4().to_string();
} }
pub fn check_totp_code(&self, totp_code: Option<u64>) -> bool { pub fn requires_twofactor(&self) -> bool {
self.totp_secret.is_some()
}
pub fn check_totp_code(&self, totp_code: u64) -> bool {
if let Some(ref totp_secret) = self.totp_secret { if let Some(ref totp_secret) = self.totp_secret {
if let Some(code) = totp_code {
// Validate totp // Validate totp
use data_encoding::BASE32; use data_encoding::BASE32;
use oath::{totp_raw_now, HashType}; use oath::{totp_raw_now, HashType};
@ -108,10 +114,7 @@ impl User {
}; };
let generated = totp_raw_now(&decoded_secret, 6, 0, 30, &HashType::SHA1); let generated = totp_raw_now(&decoded_secret, 6, 0, 30, &HashType::SHA1);
generated == code generated == totp_code
} else {
false
}
} else { } else {
true true
} }

View file

@ -24,6 +24,13 @@ table! {
} }
} }
table! {
ciphers_collections (cipher_uuid, collection_uuid) {
cipher_uuid -> Text,
collection_uuid -> Text,
}
}
table! { table! {
collections (uuid) { collections (uuid) {
uuid -> Text, uuid -> Text,
@ -43,6 +50,7 @@ table! {
type_ -> Integer, type_ -> Integer,
push_token -> Nullable<Text>, push_token -> Nullable<Text>,
refresh_token -> Text, refresh_token -> Text,
twofactor_remember -> Nullable<Text>,
} }
} }
@ -101,13 +109,6 @@ table! {
} }
} }
table! {
ciphers_collections (cipher_uuid, collection_uuid) {
cipher_uuid -> Text,
collection_uuid -> Text,
}
}
table! { table! {
users_organizations (uuid) { users_organizations (uuid) {
uuid -> Text, uuid -> Text,
@ -124,6 +125,8 @@ table! {
joinable!(attachments -> ciphers (cipher_uuid)); joinable!(attachments -> ciphers (cipher_uuid));
joinable!(ciphers -> organizations (organization_uuid)); joinable!(ciphers -> organizations (organization_uuid));
joinable!(ciphers -> users (user_uuid)); joinable!(ciphers -> users (user_uuid));
joinable!(ciphers_collections -> ciphers (cipher_uuid));
joinable!(ciphers_collections -> collections (collection_uuid));
joinable!(collections -> organizations (org_uuid)); joinable!(collections -> organizations (org_uuid));
joinable!(devices -> users (user_uuid)); joinable!(devices -> users (user_uuid));
joinable!(folders -> users (user_uuid)); joinable!(folders -> users (user_uuid));
@ -131,14 +134,13 @@ joinable!(folders_ciphers -> ciphers (cipher_uuid));
joinable!(folders_ciphers -> folders (folder_uuid)); joinable!(folders_ciphers -> folders (folder_uuid));
joinable!(users_collections -> collections (collection_uuid)); joinable!(users_collections -> collections (collection_uuid));
joinable!(users_collections -> users (user_uuid)); joinable!(users_collections -> users (user_uuid));
joinable!(ciphers_collections -> collections (collection_uuid));
joinable!(ciphers_collections -> ciphers (cipher_uuid));
joinable!(users_organizations -> organizations (org_uuid)); joinable!(users_organizations -> organizations (org_uuid));
joinable!(users_organizations -> users (user_uuid)); joinable!(users_organizations -> users (user_uuid));
allow_tables_to_appear_in_same_query!( allow_tables_to_appear_in_same_query!(
attachments, attachments,
ciphers, ciphers,
ciphers_collections,
collections, collections,
devices, devices,
folders, folders,
@ -146,6 +148,5 @@ allow_tables_to_appear_in_same_query!(
organizations, organizations,
users, users,
users_collections, users_collections,
ciphers_collections,
users_organizations, users_organizations,
); );