drop JWT and use database-based session tokens

This commit is contained in:
Peter Cai 2020-02-21 18:57:39 +08:00
parent 5d5e88fcef
commit 6e37ed9e62
No known key found for this signature in database
GPG Key ID: 71F5FB4E4F3FD54F
11 changed files with 144 additions and 122 deletions

View File

@ -1,3 +1,2 @@
SFRS_ENV=development
SFRS_JWT_SECRET=whatever_a_secret_is
DATABASE_URL=./db/database.test.db

102
Cargo.lock generated
View File

@ -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",
]

View File

@ -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"] }
uuid = { version = "0.8", features = ["v4"] }
chrono = "0.4"
serde_json = "1.0"

View File

@ -0,0 +1 @@
DROP TABLE tokens

View File

@ -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)
)

View File

@ -76,7 +76,7 @@ fn auth_sign_in(db: DbConn, params: Json<SignInParams>) -> Custom<JsonResp<AuthR
fn _sign_in(db: DbConn, mail: &str, passwd: &str) -> Custom<JsonResp<AuthResult>> {
// 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 {

View File

@ -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;

View File

@ -12,6 +12,14 @@ table! {
}
}
table! {
tokens (id) {
id -> Text,
uid -> Integer,
timestamp -> Nullable<Timestamp>,
}
}
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,
);

View File

@ -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::<serde_json::Value>(&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::<serde_json::Value>(&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::<serde_json::Value>(&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();

52
src/tokens.rs Normal file
View File

@ -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<NaiveDateTime>
}
impl Token {
// Return user id if any
pub fn find_token_by_id(db: &SqliteConnection, tid: &str) -> Option<i32> {
(lock_db_read!() as Result<RwLockReadGuard<()>, String>).ok()
.and_then(|_| {
tokens.filter(id.eq(tid))
.load::<Token>(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<String> {
let tid = Uuid::new_v4().to_hyphenated().to_string();
(lock_db_write!() as Result<RwLockWriteGuard<()>, 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)
})
}
}

View File

@ -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<User, UserOpError> {
let parsed: jwt::Token<jwt::Claims, jwt::Registered> =
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<User, UserOpError> {
let mut results = lock_db_read!()
.and_then(|_| users.filter(id.eq(user_id))
.limit(1)
.load::<UserQuery>(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<User, UserOpError> {
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<String, UserOpError> {
pub fn create_token(&self, db: &SqliteConnection, passwd: &str) -> Result<String, UserOpError> {
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())
}
}