drop JWT and use database-based session tokens
This commit is contained in:
parent
5d5e88fcef
commit
6e37ed9e62
|
@ -1,3 +1,2 @@
|
|||
SFRS_ENV=development
|
||||
SFRS_JWT_SECRET=whatever_a_secret_is
|
||||
DATABASE_URL=./db/database.test.db
|
102
Cargo.lock
generated
102
Cargo.lock
generated
|
@ -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",
|
||||
]
|
||||
|
||||
|
|
|
@ -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"
|
1
migrations/2020-02-21-102019_create_tokens/down.sql
Normal file
1
migrations/2020-02-21-102019_create_tokens/down.sql
Normal file
|
@ -0,0 +1 @@
|
|||
DROP TABLE tokens
|
7
migrations/2020-02-21-102019_create_tokens/up.sql
Normal file
7
migrations/2020-02-21-102019_create_tokens/up.sql
Normal 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)
|
||||
)
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
32
src/tests.rs
32
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::<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
52
src/tokens.rs
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
48
src/user.rs
48
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<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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue