From aa169ad1b3c277859f01413a945ea2d6f1375615 Mon Sep 17 00:00:00 2001 From: alpaylan Date: Mon, 12 Apr 2021 22:15:17 +0300 Subject: implement user authentication using jwt --- Cargo.lock | 178 ++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + TODO.md | 12 ++-- examples/mining.rs | 4 +- src/custom_filters.rs | 4 ++ src/handlers.rs | 60 +++++++++++++++-- src/routes.rs | 132 +++++++++++++++++++++++++++++++++---- src/schema.rs | 14 ++-- 8 files changed, 374 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1116c65..9b91b30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,6 +74,12 @@ dependencies = [ "safemem", ] +[[package]] +name = "bumpalo" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" + [[package]] name = "byteorder" version = "1.4.3" @@ -92,6 +98,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" +[[package]] +name = "cc" +version = "1.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" + [[package]] name = "cfg-if" version = "0.1.10" @@ -304,8 +316,10 @@ dependencies = [ "blake2", "chrono", "hex-literal", + "jsonwebtoken", "lazy_static", "log", + "md-5", "parking_lot", "pretty_env_logger", "serde", @@ -492,6 +506,29 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +[[package]] +name = "js-sys" +version = "0.3.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d99f9e3e84b8f67f846ef5b4cbbc3b1c29f6c759fcbce6f01aa0e73d932a24c" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afabcc15e437a6484fc4f12d0fd63068fe457bf93f1c148d3d9649c60b103f32" +dependencies = [ + "base64 0.12.3", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -538,6 +575,17 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +[[package]] +name = "md-5" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" +dependencies = [ + "block-buffer", + "digest", + "opaque-debug", +] + [[package]] name = "memchr" version = "2.3.4" @@ -620,6 +668,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.44" @@ -639,6 +698,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "once_cell" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" + [[package]] name = "opaque-debug" version = "0.3.0" @@ -669,6 +734,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "pem" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb" +dependencies = [ + "base64 0.13.0", + "once_cell", + "regex", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -896,6 +972,21 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi 0.3.9", +] + [[package]] name = "ryu" version = "1.0.5" @@ -985,6 +1076,17 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "simple_asn1" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692ca13de57ce0613a363c8c2f1de925adebc81b04c923ac60c5488bb44abe4b" +dependencies = [ + "chrono", + "num-bigint", + "num-traits", +] + [[package]] name = "slab" version = "0.4.2" @@ -1008,6 +1110,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "subtle" version = "2.4.0" @@ -1240,6 +1348,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "url" version = "2.2.1" @@ -1320,6 +1434,70 @@ version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +[[package]] +name = "wasm-bindgen" +version = "0.2.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83240549659d187488f91f33c0f8547cbfef0b2088bc470c116d1d260ef623d9" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae70622411ca953215ca6d06d3ebeb1e915f0f6613e3b495122878d7ebec7dae" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e734d91443f177bfdb41969de821e15c516931c3c3db3d318fa1b68975d0f6f" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53739ff08c8a68b0fdbcd54c372b8ab800b1449ab3c9d706503bc7dd1621b2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9a543ae66aa233d14bb765ed9af4a33e81b8b58d1584cf1b47ff8cd0b9e4489" + +[[package]] +name = "web-sys" +version = "0.3.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a905d57e488fec8861446d3393670fb50d27a262344013181c2cdf9fff5481be" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.2.8" diff --git a/Cargo.toml b/Cargo.toml index 7e12c2d..6701daf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,8 @@ serde_json = "1.0.59" lazy_static = "1.4.0" blake2 = "0.9.1" hex-literal = "0.3.1" +jsonwebtoken = "7.2.0" +md-5 = "0.9.1" [dev-dependencies] serde_test = "1.0.117" diff --git a/TODO.md b/TODO.md index b311777..622c95a 100644 --- a/TODO.md +++ b/TODO.md @@ -3,14 +3,7 @@ ## Proof-of-work So far so good -## Authentication -- [X] pick a user authentication scheme = [JWT](https://tools.ietf.org/html/rfc7519) Seems perfect -- [ ] implement JWT - - https://blog.logrocket.com/jwt-authentication-in-rust/ - - https://crates.io/crates/jsonwebtoken - - https://jwt.io/introduction/ - - https://jwt.io/#debugger-io -- [ ] users should be able to _sign_ their transactions +## Authentication(DONE/DONE) ## Verbosity - [ ] Verbose error messages (use error.rs?) @@ -31,3 +24,6 @@ So far so good - [x] We need our own representation of students and their grades, "there is no blockchain" (done at 2021-04-12 00:05) - [x] pick a block proposal scheme (= pick hash function) [list of hash functions](https://en.bitcoinwiki.org/wiki/List_of_hash_functions) (done at 2021-04-12 05:30) - [x] check the nonce for incoming blocks (done at 2021-04-12 05:30) +- [X] pick a user authentication scheme = [JWT](https://tools.ietf.org/html/rfc7519) Seems perfect +- [X] implement JWT +- [X] users should be able to _sign_ their transactions diff --git a/examples/mining.rs b/examples/mining.rs index c95c214..21fdc58 100644 --- a/examples/mining.rs +++ b/examples/mining.rs @@ -7,9 +7,7 @@ use blake2::{Blake2s, Digest}; pub fn main() { let mut b = NakedBlock { - transaction_list: vec![ - "hash_value".to_owned(), - ], + transaction_list: vec!["hash_value".to_owned()], nonce: 0, timestamp: NaiveDate::from_ymd(2021, 04, 08).and_hms(12, 30, 30), }; diff --git a/src/custom_filters.rs b/src/custom_filters.rs index 315ba4a..f93f572 100644 --- a/src/custom_filters.rs +++ b/src/custom_filters.rs @@ -20,6 +20,10 @@ pub fn transaction_json_body() -> impl Filter impl Filter + Clone { + warp::header::header::("Authorization") +} + // Accept only json encoded Block body and reject big payloads // TODO: find a good limit for this <11-04-21, yigit> // pub fn block_json_body() -> impl Filter + Clone { diff --git a/src/handlers.rs b/src/handlers.rs index 38bd459..07986f5 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -1,16 +1,29 @@ +use blake2::{Blake2s, Digest}; +use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; /// API handlers, the ends of each filter chain use log::debug; +use md5::Md5; use parking_lot::RwLockUpgradableReadGuard; +use serde::{Deserialize, Serialize}; use serde_json; use serde_json::json; use std::convert::Infallible; -use warp::{http::Response, http::StatusCode, reply}; +use std::fs; +use warp::{http::Response, http::StatusCode, reject, reply}; -use blake2::{Blake2s, Digest}; +use gradecoin::schema::{ + AuthRequest, Block, Db, MetuId, NakedBlock, PublicKeySignature, Transaction, User, +}; -use std::fs; +const BEARER: &str = "Bearer "; -use gradecoin::schema::{AuthRequest, Block, Db, MetuId, NakedBlock, Transaction, User}; +/// tha: Transaction Hash, String +/// iat: Issued At, Unix Time, epoch +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub tha: String, + pub iat: usize, +} /// POST /register /// Enables a student to introduce themselves to the system @@ -167,3 +180,42 @@ pub async fn propose_block(new_block: Block, db: Db) -> Result Result { + debug!("new transaction request {:?}", new_transaction); + let raw_jwt = token.trim_start_matches(BEARER).to_owned(); + + let decoded = jsonwebtoken::decode::( + &token, + &DecodingKey::from_rsa_pem( + db.users + .read() + .get(&new_transaction.by) + .unwrap() + .public_key + .as_bytes(), + ) + .unwrap(), + // todo@keles: If user is not found return user not found error + &Validation::new(Algorithm::PS256), + ) + .unwrap(); + // todo: If user is found but header is not validated, return header not valid + + let hashed_transaction = Md5::digest(&serde_json::to_vec(&new_transaction).unwrap()); + + // let mut transactions = db.lock().await; + if decoded.claims.tha == format!("{:x}", hashed_transaction) { + let mut transactions = db.pending_transactions.write(); + + transactions.insert(new_transaction.source.to_owned(), new_transaction); + + Ok(StatusCode::CREATED) + } else { + Ok(StatusCode::BAD_REQUEST) + } +} diff --git a/src/routes.rs b/src/routes.rs index 03a2569..ed2acad 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -1,14 +1,14 @@ +use gradecoin::schema::Db; use warp::{Filter, Rejection, Reply}; use crate::custom_filters; use crate::handlers; -use gradecoin::schema::Db; /// Root, all routes combined pub fn consensus_routes(db: Db) -> impl Filter + Clone { transaction_list(db.clone()) .or(register_user(db.clone())) - .or(transaction_propose(db.clone())) + .or(auth_transaction_propose(db.clone())) .or(block_propose(db.clone())) .or(block_list(db.clone())) } @@ -47,6 +47,18 @@ pub fn transaction_propose(db: Db) -> impl Filter impl Filter + Clone { + warp::path!("transaction") + .and(warp::post()) + .and(custom_filters::transaction_json_body()) + .and(custom_filters::auth_header()) + .and(custom_filters::with_db(db)) + .and_then(handlers::auth_propose_transaction) +} + /// POST /block warp route pub fn block_propose(db: Db) -> impl Filter + Clone { warp::path!("block") @@ -58,22 +70,69 @@ pub fn block_propose(db: Db) -> impl Filter Db { let db = create_database(); + db.users.write().insert( + "mock_transaction_source".to_owned(), + User { + user_id: MetuId::new("e254275".to_owned()).unwrap(), + public_key: +"-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4nU0G4WjkmcQUx0hq6LQ +uV5Q+ACmUFL/OjoYMDwC/O/6pCd1UZgCfgHN2xEffDPznzcTn8OiFRxr4oWyBiny +rUpnY4mhy0SQUwoeCw7YkcHAyhCjNT74aR/ohX0MCj0qRRdbt5ZQXM/GC3HJuXE1 +ptSuhFgQxziItamn8maoJ6JUSVEXVO1NOrrjoM3r7Q+BK2B+sX4/bLZ+VG5g1q2n +EbFdTHS6pHqtZNHQndTmEKwRfh0RYtzEzOXuO6e1gQY42Tujkof40dhGCIU7TeIG +GHwdFxy1niLkXwtHNjV7lnIOkTbx6+sSPamRfQAlZqUWM2Lf5o+7h3qWP3ENB138 +sQIDAQAB +-----END PUBLIC KEY-----" + .to_owned(), + balance: 0, + }, + ); db.pending_transactions.write().insert( "hash_value".to_owned(), Transaction { + by: "source_account".to_owned(), source: "source_account".to_owned(), target: "target_account".to_owned(), amount: 20, @@ -95,6 +154,15 @@ mod tests { db } + fn mocked_jwt() -> String { + + let claims = Claims { + tha: "6692e774eba7fb92dc0fe6cf7347591e".to_owned(), + iat: 1516239022, + }; + let header = Header::new(Algorithm::RS256); + encode(&header, &claims, &EncodingKey::from_rsa_pem(private_key_pem.as_bytes()).unwrap()).unwrap() + } /// Create a mock user that is allowed to be in gradecoin to be used in tests fn priviliged_mocked_user() -> AuthRequest { AuthRequest { @@ -114,6 +182,7 @@ mod tests { /// Create a mock transaction to be used in tests fn mocked_transaction() -> Transaction { Transaction { + by: "mock_transaction_source".to_owned(), source: "mock_transaction_source".to_owned(), target: "mock_transaction_target".to_owned(), amount: 25, @@ -125,9 +194,9 @@ mod tests { fn mocked_block() -> Block { Block { transaction_list: vec!["hash_value".to_owned()], - nonce: 560108, + nonce: 3831993, timestamp: chrono::NaiveDate::from_ymd(2021, 04, 08).and_hms(12, 30, 30), - hash: "c7d053f3e5b056ba948db3f5c0d30408fb0c29a328a0c3c1cf435fb68d700000".to_owned(), + hash: "2b648ffab5d9af1d5d5fc052fc9e51b882fc4fb0c998608c99232f9282000000".to_owned(), } } @@ -158,7 +227,7 @@ mod tests { assert_eq!(res.status(), StatusCode::OK); - let expected_json_body = r#"[{"source":"source_account","target":"target_account","amount":20,"timestamp":"2021-04-09T01:30:30"}]"#; + let expected_json_body = r#"[{"by":"source_account","source":"source_account","target":"target_account","amount":20,"timestamp":"2021-04-09T01:30:30"}]"#; assert_eq!(res.body(), expected_json_body); } @@ -208,10 +277,30 @@ mod tests { async fn post_json_201() { let db = mocked_db(); let filter = consensus_routes(db.clone()); + let res = warp::test::request() + .method("POST") + .json(&mocked_transaction()) + .path("/transaction") + .reply(&filter) + .await; + + assert_eq!(res.status(), StatusCode::CREATED); + assert_eq!(db.pending_transactions.read().len(), 2); + } + + /// Test a POST request to /transaction, a resource that exists + /// https://tools.ietf.org/html/rfc7231#section-6.3.2 + /// Should accept the json request, create + /// the transaction and add it to pending transactions in the db + #[tokio::test] + async fn post_auth_json_201() { + let db = mocked_db(); + let filter = consensus_routes(db.clone()); let res = warp::test::request() .method("POST") .json(&mocked_transaction()) + .header("Authorization", format!("Bearer {}", &mocked_jwt())) .path("/transaction") .reply(&filter) .await; @@ -220,6 +309,27 @@ mod tests { assert_eq!(db.pending_transactions.read().len(), 2); } + /// Test a POST request to /transaction, a resource that exists + /// https://tools.ietf.org/html/rfc7231#section-6.3.2 + /// Should accept the json request, create + /// the transaction and add it to pending transactions in the db + #[tokio::test] + async fn post_auth_json_400() { + let db = mocked_db(); + let filter = consensus_routes(db.clone()); + + let res = warp::test::request() + .method("POST") + .json(&mocked_transaction()) + .header("Authorization", "Bearer aaaaaaaasdlkjaldkasljdaskjlaaaaaaaaaaaaaa") + .path("/transaction") + .reply(&filter) + .await; + + assert_eq!(res.status(), StatusCode::BAD_REQUEST); + assert_eq!(db.pending_transactions.read().len(), 1); + } + /// Test a POST request to /block, a resource that exists /// https://tools.ietf.org/html/rfc7231#section-6.3.2 /// Should accept the json request, create @@ -239,7 +349,7 @@ mod tests { assert_eq!(res.status(), StatusCode::CREATED); assert_eq!( *db.blockchain.read().hash, - "c7d053f3e5b056ba948db3f5c0d30408fb0c29a328a0c3c1cf435fb68d700000".to_owned() + "2b648ffab5d9af1d5d5fc052fc9e51b882fc4fb0c998608c99232f9282000000".to_owned() ); } @@ -279,7 +389,7 @@ mod tests { println!("{:?}", res.body()); assert_eq!(res.status(), StatusCode::CREATED); - assert_eq!(db.users.read().len(), 1); + assert_eq!(db.users.read().len(), 2); } /// Test a POST request to /transaction, an endpoint that exists @@ -299,7 +409,7 @@ mod tests { println!("{:?}", res.body()); assert_eq!(res.status(), StatusCode::BAD_REQUEST); - assert_eq!(db.users.read().len(), 0); + assert_eq!(db.users.read().len(), 1); } /// Test a POST request to /transaction, a resource that exists with a longer than expected diff --git a/src/schema.rs b/src/schema.rs index 98291d7..39921b8 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -41,18 +41,20 @@ impl Db { } } +pub type PublicKeySignature = String; + /// A transaction between `source` and `target` that moves `amount` #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Transaction { - // TODO: new field by <11-04-21, yigit> // - pub source: String, - pub target: String, + pub by: PublicKeySignature, + pub source: PublicKeySignature, + pub target: PublicKeySignature, pub amount: i32, pub timestamp: NaiveDateTime, } /// A block that was proposed with `transaction_list` and `nonce` that made `hash` valid -/// https://serde.rs/container-attrs.html might be valueable to normalize the serialize/deserialize +/// https://serde.rs/container-attrs.html might be valuable to normalize the serialize/deserialize /// conventions as these will be hashed #[derive(Serialize, Deserialize, Debug)] pub struct Block { @@ -61,7 +63,7 @@ pub struct Block { // we can leave this as is and whenever we have a new block we _could_ just log it to file // somewhere // I want to keep this as a String vector because it makes things easier elsewhere - pub transaction_list: Vec, // hashes of the transactions (or just "source" for now) + pub transaction_list: Vec, // hashes of the transactions (or just "source" for now) pub nonce: u32, pub timestamp: NaiveDateTime, pub hash: String, // future proof'd baby @@ -70,7 +72,7 @@ pub struct Block { /// For prototyping and letting serde handle everything json #[derive(Serialize, Deserialize, Debug)] pub struct NakedBlock { - pub transaction_list: Vec, + pub transaction_list: Vec, pub nonce: u32, pub timestamp: NaiveDateTime, } -- cgit v1.2.3-70-g09d2