Add token functions (create, refresh, validate)

Signed-off-by: trivernis <trivernis@protonmail.com>
pull/1/head
trivernis 4 years ago
parent c189b4bef9
commit 31f99d77fb
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

@ -0,0 +1,10 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<Languages>
<language minSize="52" name="Rust" />
</Languages>
</inspection_tool>
</profile>
</component>

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="CMakeRunConfigurationManager" shouldGenerate="true" shouldDeleteObsolete="true">
<generated />
</component>
@ -13,19 +16,17 @@
</component>
<component name="ChangeListManager">
<list default="true" id="db10ba0c-1d72-449d-b24f-24b5a4951941" name="Default Changelist" comment="">
<change afterPath="$PROJECT_DIR$/src/database/models.rs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/utils.rs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/database/redis_operations.rs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/database/tokens.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Cargo.lock" beforeDir="false" afterPath="$PROJECT_DIR$/Cargo.lock" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Cargo.toml" beforeDir="false" afterPath="$PROJECT_DIR$/Cargo.toml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/database/mod.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/database/mod.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/database/permissions.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/database/permissions.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/database/role_permissions.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/database/role_permissions.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/database/roles.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/database/roles.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/database/user_roles.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/database/user_roles.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/database/models.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/database/models.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/database/users.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/database/users.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/lib.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/lib.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/main.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/server/rpc_methods.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/server/rpc_methods.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/utils.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/utils.rs" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -47,6 +48,7 @@
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">
<property name="ASKED_SHARE_PROJECT_CONFIGURATION_FILES" value="true" />
<property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
<property name="WebServerToolWindowFactoryState" value="false" />
<property name="cf.first.check.clang-format" value="false" />
@ -96,7 +98,7 @@
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1599728150675</updated>
<workItem from="1599728152100" duration="9826000" />
<workItem from="1599728152100" duration="15199000" />
</task>
<servers />
</component>
@ -108,22 +110,22 @@
<screen x="0" y="0" width="1920" height="1158" />
</state>
<state x="769" y="295" key="#com.intellij.ide.util.MemberChooser/0.0.1920.1158/1920.0.1680.1050@0.0.1920.1158" timestamp="1599732127954" />
<state width="1874" height="304" key="GridCell.Tab.0.bottom" timestamp="1599740697819">
<state width="1874" height="304" key="GridCell.Tab.0.bottom" timestamp="1599746090075">
<screen x="0" y="0" width="1920" height="1158" />
</state>
<state width="1874" height="304" key="GridCell.Tab.0.bottom/0.0.1920.1158/1920.0.1680.1050@0.0.1920.1158" timestamp="1599740697819" />
<state width="1874" height="304" key="GridCell.Tab.0.center" timestamp="1599740697819">
<state width="1874" height="304" key="GridCell.Tab.0.bottom/0.0.1920.1158/1920.0.1680.1050@0.0.1920.1158" timestamp="1599746090075" />
<state width="1874" height="304" key="GridCell.Tab.0.center" timestamp="1599746090075">
<screen x="0" y="0" width="1920" height="1158" />
</state>
<state width="1874" height="304" key="GridCell.Tab.0.center/0.0.1920.1158/1920.0.1680.1050@0.0.1920.1158" timestamp="1599740697819" />
<state width="1874" height="304" key="GridCell.Tab.0.left" timestamp="1599740697819">
<state width="1874" height="304" key="GridCell.Tab.0.center/0.0.1920.1158/1920.0.1680.1050@0.0.1920.1158" timestamp="1599746090075" />
<state width="1874" height="304" key="GridCell.Tab.0.left" timestamp="1599746090075">
<screen x="0" y="0" width="1920" height="1158" />
</state>
<state width="1874" height="304" key="GridCell.Tab.0.left/0.0.1920.1158/1920.0.1680.1050@0.0.1920.1158" timestamp="1599740697819" />
<state width="1874" height="304" key="GridCell.Tab.0.right" timestamp="1599740697819">
<state width="1874" height="304" key="GridCell.Tab.0.left/0.0.1920.1158/1920.0.1680.1050@0.0.1920.1158" timestamp="1599746090075" />
<state width="1874" height="304" key="GridCell.Tab.0.right" timestamp="1599746090075">
<screen x="0" y="0" width="1920" height="1158" />
</state>
<state width="1874" height="304" key="GridCell.Tab.0.right/0.0.1920.1158/1920.0.1680.1050@0.0.1920.1158" timestamp="1599740697819" />
<state width="1874" height="304" key="GridCell.Tab.0.right/0.0.1920.1158/1920.0.1680.1050@0.0.1920.1158" timestamp="1599746090075" />
<state width="1874" height="374" key="GridCell.Tab.1.bottom" timestamp="1599739654429">
<screen x="0" y="0" width="1920" height="1158" />
</state>
@ -144,17 +146,17 @@
<screen x="0" y="0" width="1920" height="1158" />
</state>
<state x="457" y="211" key="SettingsEditor/0.0.1920.1158/1920.0.1680.1050@0.0.1920.1158" timestamp="1599731723387" />
<state x="763" y="489" key="com.intellij.openapi.vcs.update.UpdateOrStatusOptionsDialogupdate-v2" timestamp="1599732361387">
<state x="763" y="489" key="com.intellij.openapi.vcs.update.UpdateOrStatusOptionsDialogupdate-v2" timestamp="1599744702979">
<screen x="0" y="0" width="1920" height="1158" />
</state>
<state x="763" y="489" key="com.intellij.openapi.vcs.update.UpdateOrStatusOptionsDialogupdate-v2/0.0.1920.1158/1920.0.1680.1050@0.0.1920.1158" timestamp="1599732361387" />
<state x="763" y="489" key="com.intellij.openapi.vcs.update.UpdateOrStatusOptionsDialogupdate-v2/0.0.1920.1158/1920.0.1680.1050@0.0.1920.1158" timestamp="1599744702979" />
<state x="620" y="265" key="run.anything.popup" timestamp="1599731922994">
<screen x="0" y="0" width="1920" height="1158" />
</state>
<state x="620" y="265" key="run.anything.popup/0.0.1920.1158/1920.0.1680.1050@0.0.1920.1158" timestamp="1599731922994" />
<state x="623" y="256" width="672" height="678" key="search.everywhere.popup" timestamp="1599732839101">
<state x="623" y="256" width="672" height="678" key="search.everywhere.popup" timestamp="1599744968156">
<screen x="0" y="0" width="1920" height="1158" />
</state>
<state x="623" y="256" width="672" height="678" key="search.everywhere.popup/0.0.1920.1158/1920.0.1680.1050@0.0.1920.1158" timestamp="1599732839101" />
<state x="623" y="256" width="672" height="678" key="search.everywhere.popup/0.0.1920.1158/1920.0.1680.1050@0.0.1920.1158" timestamp="1599744968156" />
</component>
</project>

35
Cargo.lock generated

@ -305,6 +305,7 @@ checksum = "5c85295147490b8fcf2ea3d104080a105a8b2c63f9c319e82c02d8e952388919"
name = "flotte-user-management"
version = "0.1.0"
dependencies = [
"byteorder",
"dotenv",
"msgrpc",
"postgres",
@ -313,6 +314,7 @@ dependencies = [
"scrypt",
"serde",
"serde_postgres",
"zeroize",
]
[[package]]
@ -1130,6 +1132,18 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "synstructure"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701"
dependencies = [
"proc-macro2",
"quote",
"syn",
"unicode-xid",
]
[[package]]
name = "tinyvec"
version = "0.3.4"
@ -1384,3 +1398,24 @@ dependencies = [
"winapi 0.2.8",
"winapi-build",
]
[[package]]
name = "zeroize"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cbac2ed2ba24cc90f5e06485ac8c7c1e5449fe8911aef4d8877218af021a5b8"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de251eec69fc7c1bc3923403d18ececb929380e016afe103da75f396704f8ca2"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]

@ -14,4 +14,6 @@ dotenv = "0.15.0"
redis = "0.17.0"
serde = {version = "1.0.115", features = ["serde_derive"]}
rand = "0.7.3"
scrypt = "0.4.1"
scrypt = "0.4.1"
zeroize = {version = "1.1.0", features = ["zeroize_derive"]}
byteorder = "1.3.4"

@ -10,8 +10,10 @@ use std::sync::{Arc, Mutex};
pub mod models;
pub mod permissions;
pub mod redis_operations;
pub mod role_permissions;
pub mod roles;
pub mod tokens;
pub mod user_roles;
pub mod users;
@ -40,6 +42,7 @@ pub enum Error {
RecordExists,
ScryptError,
DeserializeError(serde_postgres::DeError),
GenericError(String),
}
pub type DatabaseError = Error;

@ -1,7 +1,8 @@
use postgres::Row;
use serde::Deserialize;
use zeroize::Zeroize;
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Zeroize)]
#[zeroize(drop)]
pub struct UserRecord {
pub id: i32,
pub name: String,

@ -0,0 +1,5 @@
pub const SET: &str = "SET";
pub const EXPIRE: &str = "EXPIRE";
pub const TTL: &str = "TTL";
pub const EX: &str = "EX";
pub const GET: &str = "GET";

@ -1,5 +1,4 @@
use crate::database::{DatabaseClient, DatabaseError, DatabaseResult, RedisConnection, Table};
use postgres::{Client, Error};
use std::sync::{Arc, Mutex};
#[derive(Clone)]

@ -1,6 +1,6 @@
use crate::database::role_permissions::RolePermissions;
use crate::database::{DatabaseError, DatabaseResult, RedisConnection, Table};
use postgres::{Client, Error};
use postgres::Client;
use std::sync::{Arc, Mutex};
#[derive(Clone)]

@ -0,0 +1,91 @@
use crate::database::redis_operations::{EX, GET, SET};
use crate::database::RedisConnection;
use crate::utils::create_user_token;
use byteorder::{BigEndian, ByteOrder};
use redis::RedisResult;
use zeroize::Zeroize;
const REQUEST_TOKEN_EXPIRE_SECONDS: usize = 60 * 10;
const REFRESH_TOKEN_EXPIRE_SECONDS: usize = 60 * 60 * 24;
#[derive(Clone, Debug, Zeroize)]
#[zeroize(drop)]
pub struct SessionTokens {
pub request_token: [u8; 32],
pub refresh_token: [u8; 32],
}
impl SessionTokens {
pub fn new(user_id: i32) -> Self {
Self {
request_token: create_user_token(user_id),
refresh_token: create_user_token(user_id),
}
}
pub fn from_tokens(request_token: [u8; 32], refresh_token: [u8; 32]) -> Self {
Self {
request_token,
refresh_token,
}
}
pub fn retrieve(user_id: i32, redis_connection: &mut RedisConnection) -> RedisResult<Self> {
let redis_request_key = format!("user-{}_request", user_id);
let request_token_vec: Vec<u8> = redis::cmd(GET)
.arg(redis_request_key)
.query(redis_connection)?;
let redis_refresh_key = format!("user-{}_refresh", user_id);
let refresh_token_vec: Vec<u8> = redis::cmd(GET)
.arg(redis_refresh_key)
.query(redis_connection)?;
let mut request_token = [0u8; 32];
let mut refresh_token = [0u8; 32];
if request_token_vec.len() == 32 {
request_token.copy_from_slice(&request_token_vec);
}
if refresh_token_vec.len() == 32 {
refresh_token.copy_from_slice(&refresh_token_vec);
}
Ok(Self {
request_token,
refresh_token,
})
}
pub fn refresh(&mut self) {
self.request_token = create_user_token(self.get_user_id());
self.refresh_token = create_user_token(self.get_user_id());
}
/// Returns the user id that is stored in the first four bytes of the refresh token
pub fn get_user_id(&self) -> i32 {
BigEndian::read_i32(&self.refresh_token[0..4])
}
/// Saves the tokens into the database
pub fn store(&self, redis_connection: &mut RedisConnection) -> RedisResult<()> {
let id = self.get_user_id();
let redis_request_key = format!("user-{}_request", id);
redis::cmd(SET)
.arg(&redis_request_key)
.arg(&self.request_token)
.arg(EX)
.arg(REQUEST_TOKEN_EXPIRE_SECONDS)
.query(&mut *redis_connection)?;
let redis_refresh_key = format!("user-{}_refresh", id);
redis::cmd(SET)
.arg(&redis_refresh_key)
.arg(&self.refresh_token)
.arg(EX)
.arg(REFRESH_TOKEN_EXPIRE_SECONDS)
.query(&mut *redis_connection)?;
Ok(())
}
}

@ -1,5 +1,5 @@
use crate::database::{DatabaseError, DatabaseResult, RedisConnection, Table};
use postgres::{Client, Error};
use postgres::Client;
use std::sync::{Arc, Mutex};
#[derive(Clone)]

@ -1,10 +1,12 @@
use crate::database::models::UserRecord;
use crate::database::tokens::SessionTokens;
use crate::database::user_roles::UserRoles;
use crate::database::{DatabaseError, DatabaseResult, RedisConnection, Table};
use crate::utils::create_salt;
use postgres::{Client, Error};
use crate::utils::{create_salt, get_user_id_from_token, TOKEN_LENGTH};
use postgres::Client;
use scrypt::ScryptParams;
use std::sync::{Arc, Mutex};
use zeroize::{Zeroize, Zeroizing};
#[derive(Clone)]
pub struct Users {
@ -13,8 +15,6 @@ pub struct Users {
user_roles: UserRoles,
}
const SALT_LENGTH: usize = 16;
impl Table for Users {
fn new(
database_connection: Arc<Mutex<Client>>,
@ -55,6 +55,7 @@ impl Users {
password: String,
) -> DatabaseResult<UserRecord> {
let mut connection = self.database_connection.lock().unwrap();
let mut password = Zeroizing::new(password);
if !connection
.query("SELECT email FROM users WHERE email = $1", &[&email])
@ -63,19 +64,105 @@ impl Users {
{
return Err(DatabaseError::RecordExists);
}
let salt = create_salt(SALT_LENGTH);
let mut pw_hash = [0u8; 32];
let salt = Zeroizing::new(create_salt());
let mut pw_hash = Zeroizing::new([0u8; 32]);
scrypt::scrypt(
password.as_bytes(),
&salt,
&*salt,
&ScryptParams::recommended(),
&mut pw_hash,
&mut *pw_hash,
)
.map_err(|_| DatabaseError::ScryptError)?;
password.zeroize();
let row = connection.query_one("
INSERT INTO users (name, email, password_hash, salt) VALUES ($1, $2, $3, $4) RETURNING *;
", &[&name, &email, &pw_hash.to_vec(), &salt.to_vec()]).map_err(|e|DatabaseError::Postgres(e))?;
Ok(UserRecord::from_ordered_row(&row))
}
pub fn create_token(&self, email: String, password: String) -> DatabaseResult<SessionTokens> {
if self.validate_login(&email, password)? {
let mut connection = self.database_connection.lock().unwrap();
let row = connection
.query_one("SELECT id FROM users WHERE email = $1", &[&email])
.map_err(|e| DatabaseError::Postgres(e))?;
let id: i32 = row.get(0);
let mut redis_connection = self.redis_connection.lock().unwrap();
let tokens = SessionTokens::new(id);
tokens
.store(&mut redis_connection)
.map_err(|e| DatabaseError::Redis(e))?;
Ok(tokens)
} else {
Err(DatabaseError::GenericError("Invalid password".to_string()))
}
}
pub fn validate_request_token(&self, token: &[u8; TOKEN_LENGTH]) -> DatabaseResult<bool> {
let id = get_user_id_from_token(token);
let mut redis_connection = self.redis_connection.lock().unwrap();
let tokens = SessionTokens::retrieve(id, &mut redis_connection)
.map_err(|e| DatabaseError::Redis(e))?;
Ok(tokens.request_token == *token)
}
pub fn validate_refresh_token(&self, token: &[u8; TOKEN_LENGTH]) -> DatabaseResult<bool> {
let id = get_user_id_from_token(token);
let mut redis_connection = self.redis_connection.lock().unwrap();
let tokens = SessionTokens::retrieve(id, &mut redis_connection)
.map_err(|e| DatabaseError::Redis(e))?;
Ok(tokens.refresh_token == *token)
}
pub fn refresh_tokens(
&self,
refresh_token: &[u8; TOKEN_LENGTH],
) -> DatabaseResult<SessionTokens> {
let id = get_user_id_from_token(refresh_token);
let mut redis_connection = self.redis_connection.lock().unwrap();
let mut tokens = SessionTokens::retrieve(id, &mut redis_connection)
.map_err(|e| DatabaseError::Redis(e))?;
if tokens.refresh_token == *refresh_token {
tokens.refresh();
tokens
.store(&mut redis_connection)
.map_err(|e| DatabaseError::Redis(e))?;
Ok(tokens)
} else {
Err(DatabaseError::GenericError(
"Invalid refresh token!".to_string(),
))
}
}
fn validate_login(&self, email: &String, password: String) -> DatabaseResult<bool> {
let password = Zeroizing::new(password);
let mut connection = self.database_connection.lock().unwrap();
let row = connection
.query_one(
"SELECT password_hash, salt FROM users WHERE email = $1",
&[&email],
)
.map_err(|e| DatabaseError::Postgres(e))?;
let original_pw_hash: Zeroizing<Vec<u8>> = Zeroizing::new(row.get(0));
let salt: Zeroizing<Vec<u8>> = Zeroizing::new(row.get(1));
let mut pw_hash = Zeroizing::new([0u8; 32]);
scrypt::scrypt(
password.as_bytes(),
&*salt,
&ScryptParams::recommended(),
&mut *pw_hash,
)
.map_err(|_| DatabaseError::ScryptError)?;
Ok(*pw_hash == *original_pw_hash.as_slice())
}
}

@ -3,15 +3,4 @@ use flotte_user_management::database::Database;
fn main() {
let database = Database::new().unwrap();
database.init().unwrap();
println!(
"{:?}",
database
.users
.create_user(
"John Doe".to_string(),
"johndoe@protonmail.com".to_string(),
"ttest".to_string()
)
.unwrap()
)
}

@ -1,3 +1,4 @@
#![allow(dead_code)]
const NULL: [u8; 4] = [0x00, 0x00, 0x00, 0x00];
const VALIDATE_TOKEN: [u8; 4] = [0x56, 0x41, 0x4c, 0x49];
const GET_ROLES: [u8; 4] = [0x52, 0x4f, 0x4c, 0x45];

@ -1,9 +1,26 @@
use byteorder::{BigEndian, ByteOrder};
use rand::Rng;
pub fn create_salt(length: usize) -> [u8; 16] {
pub const TOKEN_LENGTH: usize = 32;
const SALT_LENGTH: usize = 16;
pub fn create_salt() -> [u8; SALT_LENGTH] {
let mut rng = rand::thread_rng();
let mut salt = [0u8; 16];
let mut salt = [0u8; SALT_LENGTH];
rng.fill(&mut salt);
salt
}
pub fn create_user_token(user_id: i32) -> [u8; TOKEN_LENGTH] {
let mut rng = rand::thread_rng();
let mut value = [0u8; TOKEN_LENGTH];
rng.fill(&mut value);
BigEndian::write_i32(&mut value, user_id);
value
}
pub fn get_user_id_from_token(token: &[u8]) -> i32 {
BigEndian::read_i32(token)
}

Loading…
Cancel
Save