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 --- src/custom_filters.rs | 4 ++ src/handlers.rs | 60 +++++++++++++++++++++-- src/routes.rs | 132 +++++++++++++++++++++++++++++++++++++++++++++----- src/schema.rs | 14 +++--- 4 files changed, 189 insertions(+), 21 deletions(-) (limited to 'src') 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