From ed53fbc9097370feeda1c5507878933643a9bcc5 Mon Sep 17 00:00:00 2001 From: Yigit Sever Date: Sat, 10 Apr 2021 14:16:41 +0300 Subject: Trying to auth --- src/auth.rs | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/error.rs | 63 +++++++++++++++++++++++++++++++ src/handlers.rs | 18 +++++++++ src/main.rs | 2 + src/routes.rs | 38 +++++++++++++++++-- src/schema.rs | 14 ++++++- 6 files changed, 242 insertions(+), 5 deletions(-) create mode 100644 src/auth.rs create mode 100644 src/error.rs (limited to 'src') diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..e22262c --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,112 @@ +use crate::error::Error; +use crate::schema::{Db, Transaction}; +use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; +use serde::{Deserialize, Serialize}; +use warp::header::headers_cloned; +use warp::http::header::{HeaderMap, HeaderValue, AUTHORIZATION}; +use warp::{reject, Filter, Rejection}; +use thiserror::Error; +use anyhow::*; + +const BEARER: &str = "Bearer "; +const PUBLIC_KEY_PEM: &str = "-----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-----"; + +// const private_key_pem: &str = "-----BEGIN RSA PRIVATE KEY----- +// MIIEpAIBAAKCAQEA4nU0G4WjkmcQUx0hq6LQuV5Q+ACmUFL/OjoYMDwC/O/6pCd1 +// UZgCfgHN2xEffDPznzcTn8OiFRxr4oWyBinyrUpnY4mhy0SQUwoeCw7YkcHAyhCj +// NT74aR/ohX0MCj0qRRdbt5ZQXM/GC3HJuXE1ptSuhFgQxziItamn8maoJ6JUSVEX +// VO1NOrrjoM3r7Q+BK2B+sX4/bLZ+VG5g1q2nEbFdTHS6pHqtZNHQndTmEKwRfh0R +// YtzEzOXuO6e1gQY42Tujkof40dhGCIU7TeIGGHwdFxy1niLkXwtHNjV7lnIOkTbx +// 6+sSPamRfQAlZqUWM2Lf5o+7h3qWP3ENB138sQIDAQABAoIBAD23nYTmrganag6M +// wPFrBSGP79c3Lhx0EjUHQjJbGKFgsdltG48qM3ut+DF9ACy0Z+/7bbC7+39vaIOq +// 1jLR2d6aiYTaLKseO4s2FawD1sgamvU3BZPsXn0gAhnnU5Gyy8Nas1dccvhoc9wI +// neaZUPrvucQ90AzLfo6r9yacDbYHB1lOyomApUvpJxOgHISGEtc9qGPDrdH19aF0 +// 8fCv2bbQRh+TChgN3IB0o5w0wXaI7YAyAouAv/AzHCoEMpt7OGjFTkjh/ujlPL9O +// +FLuJNsQRHDN0gJo2pcvwGwDCsioMixQ9bZ7ZrUu2BNpEQygyeSbj9ZI1iRvhosO +// JU3rwEECgYEA9MppTYA6A9WQbCCwPH1QMpUAmPNVSWVhUVag4lGOEhdCDRcz9ook +// DohQMKctiEB1luKuvDokxo0uMOfMO9/YwjsRB7qjQip7Th1zMJIjD+A+juLzHK4r +// /RiRtWYGAnF8mptDvE+93JsPb3C/lQLvIhio5GQYWBqPJu6SpeosIskCgYEA7NPi +// Gbffzr2UQhW8BNKmctEEh8yFRVojFo3wwwWxSNUVXGSmSm31CL+Q8h817R+2OkPV +// 1ZMUOBU4UJiqFt28kIvTDFqbAJlJQGCpY2mY7OLQiD2A+TVLcFrHmoCaPfCAK1Qd +// hQ0PmFK7Mf8qClpA3E5chop/WfKQfiu46sZv1qkCgYAhGdXPcw1lQ1W6KVlrdI6J +// qHhiNlVMDXdxZkNvFxQdAiQeXQrbxaZGiMw/J/wSNpUwCAsUzM/4QVMDrfSCDCzl +// ZtNQtj4pTlFKKNVQthIjrXEIJUw2jp7IJLBfVSJu5iWxSlmId0f3MsiNizN81N69 +// P5Rm/doE3+KHoy8VXGsHcQKBgQCkNh62enqjHWypjex6450qS6f6iWN3PRLLVsw0 +// TcQpniZblCaBwVCAKmRUnjOEIdL2/4ZLutnwMTaFG/YEOOfAylMiY8jKV38lNmD9 +// X4D78CFr9klxgvS2CRwSE03f2NzmLkLxuKaxldvaxPTfjMkgeO1LFMlNExYBhkuH +// 7uQpUQKBgQCKX6qMNh2gSdgG7qyxfTFZ4y5EGOBoKe/dE+IcVF3Vnh6DZVbCAbBL +// 5EdFWZSrCnDjA4xiKW55mwp95Ud9EZsZAb13L8V9t82eK+UDBoWlb7VRNYpda/x1 +// 5/i4qQJ28x2UNJDStpYFpnp4Ba1lvXjKngIbDPkjU+hbBJ+BNGAIeg== +// -----END RSA PRIVATE KEY-----"; + +/// sub: Subject, user identifier +/// exp: Expiration date, Unix Time, epoch +/// puk: Public Key? Not 100% on this +#[derive(Debug, Serialize, Deserialize)] +struct Claims { + sub: String, + exp: usize, + puk: String, +} + +// #[derive(Error, Debug)] +// pub enum Nope { +// #[error("Invalid header")] +// InvalidHeader { +// expected: String, +// found: String, +// }, +// } + +pub fn with_auth( + db: Db, + t: Transaction, +) -> impl Filter + Clone { + headers_cloned() + .map(move |headers: HeaderMap| (db.clone(), headers)) + .and_then(authorize) +} + +impl warp::reject::Reject for Nope {} + +async fn authorize((db, headers): (Db, HeaderMap)) -> Result { + match jwt_from_header(&headers) { + Ok(jwt) => { + let decoded = decode::( + &jwt, + // TODO: what key are we using here? pass db/pw store here to get the claimant's + // public key <10-04-21, yigit> // + &DecodingKey::from_rsa_pem(PUBLIC_KEY_PEM.as_bytes()).unwrap(), + &Validation::new(Algorithm::HS512), + ) + .map_err(|_| reject::custom(Error::JWTTokenError)) + .unwrap(); + + Ok(decoded.claims.puk) + } + Err(e) => return Err(anyhow!("missing!")); + } +} + +fn jwt_from_header(headers: &HeaderMap) -> Result { + let header = match headers.get(AUTHORIZATION) { + Some(v) => v, + None => return Err(Error::NoAuthHeaderError), + }; + let auth_header = match std::str::from_utf8(header.as_bytes()) { + Ok(v) => v, + Err(_) => return Err(Error::NoAuthHeaderError), + }; + if !auth_header.starts_with(BEARER) { + return Err(Error::InvalidAuthHeaderError); + } + Ok(auth_header.trim_start_matches(BEARER).to_owned()) +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..0db26c4 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,63 @@ +use serde::Serialize; +use std::convert::Infallible; +use thiserror::Error; +use warp::{http::StatusCode, Rejection, Reply}; + +#[derive(Error, Debug)] +pub enum Error { + // #[error("wrong credentials")] + // WrongCredentialsError, + #[error("jwt token not valid")] + JWTTokenError, + // #[error("jwt token creation error")] + // JWTTokenCreationError, + #[error("no auth header")] + NoAuthHeaderError, + #[error("invalid auth header")] + InvalidAuthHeaderError, + // #[error("no permission")] + // NoPermissionError, +} + +#[derive(Serialize, Debug)] +struct ErrorResponse { + message: String, + status: String, +} + +impl warp::reject::Reject for Error {} + +pub async fn handle_rejection(err: Rejection) -> std::result::Result { + let (code, message) = if err.is_not_found() { + (StatusCode::NOT_FOUND, "Not Found".to_string()) + } else if let Some(e) = err.find::() { + match e { + // Error::WrongCredentialsError => (StatusCode::FORBIDDEN, e.to_string()), + // Error::NoPermissionError => (StatusCode::UNAUTHORIZED, e.to_string()), + Error::JWTTokenError => (StatusCode::UNAUTHORIZED, e.to_string()), + // Error::JWTTokenCreationError => ( + // StatusCode::INTERNAL_SERVER_ERROR, + // "Internal Server Error".to_string(), + // ), + _ => (StatusCode::BAD_REQUEST, e.to_string()), + } + } else if err.find::().is_some() { + ( + StatusCode::METHOD_NOT_ALLOWED, + "Method Not Allowed".to_string(), + ) + } else { + eprintln!("unhandled error: {:?}", err); + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Internal Server Error".to_string(), + ) + }; + + let json = warp::reply::json(&ErrorResponse { + status: code.to_string(), + message, + }); + + Ok(warp::reply::with_status(json, code)) +} diff --git a/src/handlers.rs b/src/handlers.rs index 856970d..256e72a 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -58,6 +58,24 @@ pub async fn propose_transaction( Ok(StatusCode::CREATED) } +/// POST /transaction, authenticated +/// The transaction arrived in this method has been authored by the public key in the source +pub async fn propose_authenticated_transaction( + pubkey: String, + new_transaction: Transaction, + db: Db, +) -> Result { + debug!("new transaction request {:?}", new_transaction); + + // let mut transactions = db.lock().await; + let mut transactions = db.pending_transactions.write(); + + transactions.insert(new_transaction.source.to_owned(), new_transaction); + + Ok(StatusCode::CREATED) +} + + /// POST /block /// Proposes a new block for the next round /// Can reject the block diff --git a/src/main.rs b/src/main.rs index 7ef2597..91f6757 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,8 @@ mod custom_filters; mod handlers; mod routes; mod schema; +mod auth; +mod error; // mod validators; #[tokio::main] diff --git a/src/routes.rs b/src/routes.rs index 95138e6..499ba35 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -1,8 +1,9 @@ use warp::{Filter, Rejection, Reply}; +use crate::auth::with_auth; use crate::custom_filters; use crate::handlers; -use crate::schema::Db; +use crate::schema::{Db, Transaction}; /// Root, all routes combined pub fn consensus_routes(db: Db) -> impl Filter + Clone { @@ -14,7 +15,8 @@ pub fn consensus_routes(db: Db) -> impl Filter impl Filter + Clone { - warp::path!("transaction") + warp::path("transaction") + .and(warp::path::end()) .and(warp::get()) .and(custom_filters::with_db(db)) .and_then(handlers::list_transactions) @@ -30,13 +32,43 @@ pub fn block_list(db: Db) -> impl Filter impl Filter + Clone { - warp::path!("transaction") + warp::path("transaction") + .and(warp::path::end()) .and(warp::post()) .and(custom_filters::transaction_json_body()) .and(custom_filters::with_db(db)) .and_then(handlers::propose_transaction) } +/// POST /transaction warp route with authentication +pub fn authenticated_transaction_propose( + db: Db, +) -> impl Filter + Clone { + warp::path("transaction") + .and(warp::path::end()) + .and(warp::post()) + .and(custom_filters::transaction_json_body()) + .map(|t: Transaction| { + with_auth(db.clone(), t) + }) + .untuple_one() + .and(custom_filters::transaction_json_body()) + .and(custom_filters::with_db(db)) + .and_then(handlers::propose_authenticated_transaction) + + // .and(custom_filters::transaction_json_body()) + // // TODO: you might have to restore this + // // what we're trying to do is knowing which public key to use to decode the jwt in the + // // header of the request, we will either request it through a header (ugly, ugh) or get it + // // from json (then how do we chain these ugh) or we can just validate/check (move the + // // header/jwt logic to propose_transaction but that doesn't feel right either + // // good luck <10-04-21, yigit> // + // .map(|t: Transaction| with_auth(db.clone(), t)) + // .and(custom_filters::transaction_json_body()) + // .and(custom_filters::with_db(db)) + // .and_then(handlers::propose_transaction) +} + /// POST /block warp route pub fn block_propose(db: Db) -> impl Filter + Clone { warp::path!("block") diff --git a/src/schema.rs b/src/schema.rs index 556e625..c4917ab 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -29,6 +29,8 @@ pub struct Db { pub blockchain: Arc>>, // every proposer can have _one_ pending transaction, a way to enforce this, String is proposer identifier pub pending_transactions: Arc>>, + // this was bound to happen eventually + pub users: Arc>>, } impl Db { @@ -36,12 +38,15 @@ impl Db { Db { blockchain: Arc::new(RwLock::new(Vec::new())), pending_transactions: Arc::new(RwLock::new(HashMap::new())), + users: Arc::new(RwLock::new(HashMap::new())), } } } -/// A transaction between `source` and `target` that moves `amount` -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +/// A transaction between `source` and `target` that moves `amount` Note: +/// https://serde.rs/container-attrs.html might be valueable to normalize the serialize/deserialize +/// conventions as these will be hashed +#[derive(Serialize, Deserialize, Debug)] pub struct Transaction { pub source: String, pub target: String, @@ -65,5 +70,10 @@ pub struct Block { pub hash: String, // future proof'd baby } +#[derive(Serialize, Deserialize, Debug)] +pub struct User { + username: String, + token: String +} // TODO: write schema tests using the original repo <09-04-21, yigit> // -- cgit v1.2.3-70-g09d2