diff --git a/.env.test b/.env.test index de4f2e9..a9be4ee 100644 --- a/.env.test +++ b/.env.test @@ -1,3 +1,2 @@ SFRS_ENV=development -SFRS_JWT_SECRET=whatever_a_secret_is DATABASE_URL=./db/database.test.db \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 76c9b58..f4ac9f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,6 +114,17 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +[[package]] +name = "chrono" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31850b4a4d6bae316f7a09e691c944c28299298837edc0a03f755618c23cbc01" +dependencies = [ + "num-integer", + "num-traits", + "time", +] + [[package]] name = "cloudabi" version = "0.0.3" @@ -184,6 +195,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d7cc03b910de9935007861dce440881f69102aaaedfd4bc5a6f40340ca5840c" dependencies = [ "byteorder", + "chrono", "diesel_derives", "libsqlite3-sys", "r2d2", @@ -287,12 +299,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" -[[package]] -name = "gcc" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" - [[package]] name = "generic-array" version = "0.12.3" @@ -412,16 +418,6 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" -[[package]] -name = "jwt" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa2b51232f4dba9bcbdc082f4ea5bee58d5c2866770b4dc80c868d09bd82569" -dependencies = [ - "rust-crypto", - "rustc-serialize", -] - [[package]] name = "kernel32-sys" version = "0.2.2" @@ -613,6 +609,25 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "num-integer" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.12.0" @@ -750,29 +765,6 @@ dependencies = [ "scheduled-thread-pool", ] -[[package]] -name = "rand" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" -dependencies = [ - "libc", - "rand 0.4.6", -] - -[[package]] -name = "rand" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" -dependencies = [ - "fuchsia-cprng", - "libc", - "rand_core 0.3.1", - "rdrand", - "winapi 0.3.8", -] - [[package]] name = "rand" version = "0.5.6" @@ -842,15 +834,6 @@ dependencies = [ "rand_core 0.5.1", ] -[[package]] -name = "rdrand" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" -dependencies = [ - "rand_core 0.3.1", -] - [[package]] name = "redox_syscall" version = "0.1.56" @@ -1005,25 +988,6 @@ dependencies = [ "unicode-xid 0.1.0", ] -[[package]] -name = "rust-crypto" -version = "0.2.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a" -dependencies = [ - "gcc", - "libc", - "rand 0.3.23", - "rustc-serialize", - "time", -] - -[[package]] -name = "rustc-serialize" -version = "0.3.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" - [[package]] name = "ryu" version = "1.0.2" @@ -1111,17 +1075,17 @@ dependencies = [ name = "sfrs" version = "0.1.0" dependencies = [ + "chrono", "diesel", "diesel_migrations", "dotenv", - "jwt", "lazy_static", "rocket", "rocket_contrib", "rocket_cors", - "rust-crypto", "scrypt", "serde", + "serde_json", "uuid", ] diff --git a/Cargo.toml b/Cargo.toml index ac11bd9..f0abaf5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,12 +8,12 @@ edition = "2018" rocket = "0.4.2" rocket_contrib = { version = "0.4.2", features = ["diesel_sqlite_pool"] } rocket_cors = "0.5.1" -jwt = "0.4.0" -diesel = { version = "1.4.3", features = ["sqlite"] } +diesel = { version = "1.4.3", features = ["sqlite", "chrono"] } diesel_migrations = "1.4.0" dotenv = "0.9.0" lazy_static = "1.4.0" serde = { version = "1.0.104", features = ["derive"] } scrypt = "0.2.0" -rust-crypto = "0.2.36" -uuid = { version = "0.8", features = ["v4"] } \ No newline at end of file +uuid = { version = "0.8", features = ["v4"] } +chrono = "0.4" +serde_json = "1.0" \ No newline at end of file diff --git a/migrations/2020-02-21-102019_create_tokens/down.sql b/migrations/2020-02-21-102019_create_tokens/down.sql new file mode 100644 index 0000000..1f701b3 --- /dev/null +++ b/migrations/2020-02-21-102019_create_tokens/down.sql @@ -0,0 +1 @@ +DROP TABLE tokens \ No newline at end of file diff --git a/migrations/2020-02-21-102019_create_tokens/up.sql b/migrations/2020-02-21-102019_create_tokens/up.sql new file mode 100644 index 0000000..4e5a142 --- /dev/null +++ b/migrations/2020-02-21-102019_create_tokens/up.sql @@ -0,0 +1,7 @@ +CREATE TABLE tokens ( + id VARCHAR PRIMARY KEY NOT NULL, + uid INTEGER NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (uid) + REFERENCES users (id) +) \ No newline at end of file diff --git a/src/api.rs b/src/api.rs index 2f623ff..8251bcb 100644 --- a/src/api.rs +++ b/src/api.rs @@ -76,7 +76,7 @@ fn auth_sign_in(db: DbConn, params: Json) -> Custom Custom> { // Try to find the user first let res = user::User::find_user_by_email(&db, mail) - .and_then(|u| u.create_token(passwd) + .and_then(|u| u.create_token(&db, passwd) .map(|x| (u.uuid, u.email, x))); match res { Ok((uuid, email, token)) => success_resp(AuthResult { diff --git a/src/main.rs b/src/main.rs index df51e42..b80ff7e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,7 @@ extern crate diesel_migrations; extern crate dotenv; #[macro_use] extern crate serde; -extern crate crypto; +extern crate serde_json; extern crate scrypt; #[macro_use] extern crate lazy_static; @@ -20,6 +20,7 @@ extern crate uuid; mod schema; mod api; +mod tokens; mod user; mod item; diff --git a/src/schema.rs b/src/schema.rs index e0925ff..e14b962 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -12,6 +12,14 @@ table! { } } +table! { + tokens (id) { + id -> Text, + uid -> Integer, + timestamp -> Nullable, + } +} + table! { users (id) { id -> Integer, @@ -25,8 +33,10 @@ table! { } joinable!(items -> users (owner)); +joinable!(tokens -> users (uid)); allow_tables_to_appear_in_same_query!( items, + tokens, users, ); diff --git a/src/tests.rs b/src/tests.rs index 7cacd14..5df3440 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -21,13 +21,14 @@ fn should_add_user() { .body(r#"{ "email": "test@example.com", "password": "testpw", - "pw_cost": "100", + "pw_cost": 100, "pw_nonce": "whatever", "version": "001" }"#) .dispatch(); assert_eq!(resp.status(), Status::Ok); - assert!(resp.body_string().unwrap().contains(r#"{"token":"#)); + serde_json::from_str::(&resp.body_string().unwrap()).unwrap() + .get("token").unwrap().as_str().unwrap(); } #[test] @@ -37,7 +38,7 @@ fn should_not_add_user_twice() { .body(r#"{ "email": "test1@example.com", "password": "testpw", - "pw_cost": "100", + "pw_cost": 100, "pw_nonce": "whatever", "version": "001" }"#) @@ -50,7 +51,7 @@ fn should_not_add_user_twice() { .body(r#"{ "email": "test1@example.com", "password": "does not matter", - "pw_cost": "100", + "pw_cost": 100, "pw_nonce": "whatever", "version": "001" }"#) @@ -65,7 +66,7 @@ fn should_log_in_successfully() { .body(r#"{ "email": "test2@example.com", "password": "testpw", - "pw_cost": "100", + "pw_cost": 100, "pw_nonce": "whatever", "version": "001" }"#) @@ -81,9 +82,8 @@ fn should_log_in_successfully() { }"#) .dispatch(); assert_eq!(resp.status(), Status::Ok); - let body = resp.body_string().unwrap(); - //println!("{}", body); - assert!(body.contains(r#"{"token":"#)); + serde_json::from_str::(&resp.body_string().unwrap()).unwrap() + .get("token").unwrap().as_str().unwrap(); } #[test] @@ -93,7 +93,7 @@ fn should_log_in_fail() { .body(r#"{ "email": "test3@example.com", "password": "testpw", - "pw_cost": "100", + "pw_cost": 100, "pw_nonce": "whatever", "version": "001" }"#) @@ -118,7 +118,7 @@ fn should_change_pw_successfully() { .body(r#"{ "email": "test4@example.com", "password": "testpw", - "pw_cost": "100", + "pw_cost": 100, "pw_nonce": "whatever", "version": "001" }"#) @@ -144,7 +144,7 @@ fn should_change_pw_fail() { .body(r#"{ "email": "test5@example.com", "password": "testpw", - "pw_cost": "100", + "pw_cost": 100, "pw_nonce": "whatever", "version": "001" }"#) @@ -170,7 +170,7 @@ fn should_change_pw_successfully_and_log_in_successfully() { .body(r#"{ "email": "test6@example.com", "password": "testpw", - "pw_cost": "100", + "pw_cost": 100, "pw_nonce": "whatever", "version": "001" }"#) @@ -219,15 +219,15 @@ fn should_success_authorize() { .body(r#"{ "email": "test7@example.com", "password": "testpw", - "pw_cost": "100", + "pw_cost": 100, "pw_nonce": "whatever", "version": "001" }"#) .dispatch() .body_string() - .unwrap() - .replace("{\"token\":\"", "") - .replace("\"}", ""); + .unwrap(); + let val = serde_json::from_str::(&token).unwrap(); + let token = val.get("token").unwrap().as_str().unwrap(); let mut resp = CLIENT.get("/auth/ping") .header(Header::new("Authorization", format!("Bearer {}", token))) .dispatch(); diff --git a/src/tokens.rs b/src/tokens.rs new file mode 100644 index 0000000..caf367d --- /dev/null +++ b/src/tokens.rs @@ -0,0 +1,52 @@ +use crate::schema::tokens; +use crate::schema::tokens::dsl::*; +use crate::{lock_db_write, lock_db_read}; +use chrono::NaiveDateTime; +use diesel::sqlite::SqliteConnection; +use diesel::prelude::*; +use std::sync::{RwLockReadGuard, RwLockWriteGuard}; +use uuid::Uuid; + +#[derive(Queryable, Insertable)] +#[table_name = "tokens"] +pub struct Token { + id: String, + uid: i32, + timestamp: Option +} + +impl Token { + // Return user id if any + pub fn find_token_by_id(db: &SqliteConnection, tid: &str) -> Option { + (lock_db_read!() as Result, String>).ok() + .and_then(|_| { + tokens.filter(id.eq(tid)) + .load::(db) + .ok() + .and_then(|mut v| { + if !v.is_empty() { + Some(v.remove(0).uid) + } else { + None + } + }) + }) + } + + // Create a new token for a user + pub fn create_token(db: &SqliteConnection, user: i32) -> Option { + let tid = Uuid::new_v4().to_hyphenated().to_string(); + (lock_db_write!() as Result, String>).ok() + .and_then(|_| { + diesel::insert_into(tokens::table) + .values(Token { + id: tid.clone(), + uid: user, + timestamp: None // There's default value from SQLite + }) + .execute(db) + .ok() + .map(|_| tid) + }) + } +} \ No newline at end of file diff --git a/src/user.rs b/src/user.rs index 7523dd6..5ee8c6f 100644 --- a/src/user.rs +++ b/src/user.rs @@ -7,13 +7,6 @@ use diesel::sqlite::SqliteConnection; use rocket::request; use rocket::http::Status; use serde::Deserialize; -use std::env; -use std::time::{SystemTime, UNIX_EPOCH}; - -lazy_static! { - static ref JWT_SECRET: String = env::var("SFRS_JWT_SECRET") - .expect("Please have SFRS_JWT_SECRET set"); -} #[derive(Debug)] pub struct UserOpError(pub String); @@ -156,37 +149,32 @@ impl User { } } - pub fn find_user_by_token(db: &SqliteConnection, token: &str) -> Result { - let parsed: jwt::Token = - jwt::Token::parse(token).map_err(|_| "Invalid JWT Token".into())?; - if !parsed.verify(JWT_SECRET.as_bytes(), crypto::sha2::Sha256::new()) { - Err("JWT Signature Verification Error".into()) + pub fn find_user_by_id(db: &SqliteConnection, user_id: i32) -> Result { + let mut results = lock_db_read!() + .and_then(|_| users.filter(id.eq(user_id)) + .limit(1) + .load::(db) + .map_err(|_| UserOpError::new("Database error")))?; + if results.is_empty() { + Result::Err(UserOpError::new("No matching user found")) } else { - parsed.claims.sub - .ok_or("Malformed Token".into()) - .and_then(|mail| Self::find_user_by_email(db, &mail)) + Result::Ok(results.remove(0).into()) // Take ownership, kill the stupid Vec } } + pub fn find_user_by_token(db: &SqliteConnection, token: &str) -> Result { + crate::tokens::Token::find_token_by_id(db, token) + .ok_or("Invalid token".into()) + .and_then(|uid| Self::find_user_by_id(db, uid)) + } + // Create a JWT token for the current user if password matches - pub fn create_token(&self, passwd: &str) -> Result { + pub fn create_token(&self, db: &SqliteConnection, passwd: &str) -> Result { if self.password != passwd { Err(UserOpError::new("Password mismatch")) } else { - jwt::Token::new( - jwt::Header::default(), - jwt::Claims::new(jwt::Registered { - iss: None, - sub: Some(self.email.clone()), - exp: None, - aud: None, - iat: Some(SystemTime::now().duration_since(UNIX_EPOCH) - .expect("wtf????").as_secs()), - nbf: None, - jti: None - }) - ).signed(JWT_SECRET.as_bytes(), crypto::sha2::Sha256::new()) - .map_err(|_| UserOpError::new("Failed to generate token")) + crate::tokens::Token::create_token(db, self.id) + .ok_or("Failed to generate token".into()) } }