From 88c2e99aa85d52dae71b24f49a7ed95961d8c421 Mon Sep 17 00:00:00 2001 From: landrigun Date: Thu, 13 Oct 2022 08:54:47 +0000 Subject: [PATCH 1/9] move Config into its own module cargo-fmt --- .gitignore | 1 + src/config/config.rs | 131 ++++++++++++++++++++++++++++++++++++++++++ src/config/mod.rs | 3 + src/http/mod.rs | 2 +- src/http/router.rs | 133 +------------------------------------------ src/main.rs | 4 +- 6 files changed, 140 insertions(+), 134 deletions(-) create mode 100644 src/config/config.rs create mode 100644 src/config/mod.rs diff --git a/.gitignore b/.gitignore index ddf9e12..8779a8a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ simple-auth tests/python/__pycache__ tests/bash/response.txt +tests/data/*.ini diff --git a/src/config/config.rs b/src/config/config.rs new file mode 100644 index 0000000..31d0bd8 --- /dev/null +++ b/src/config/config.rs @@ -0,0 +1,131 @@ +//! config module implements all the utilities to properly create and validate a router config + +use configparser::ini::Ini; +use std::str::FromStr; + +#[derive(Clone)] +pub struct Config { + pub jwt_exp_time: u64, + pub jwt_issuer: String, + pub jwt_priv_key: String, + pub jwt_pub_key: String, + pub filestore_path: String, +} + +impl Default for Config { + fn default() -> Self { + Config { + jwt_exp_time: 0, + jwt_issuer: "".to_string(), + jwt_priv_key: "".to_string(), + jwt_pub_key: "".to_string(), + filestore_path: "".to_string(), + } + } +} + +impl TryFrom for Config { + type Error = String; + fn try_from(config: Ini) -> Result { + let exp_time = config + .get("jwt", "expiration_time") + .unwrap_or("".to_string()); + let jwt_exp_time = { + match u64::from_str(&exp_time) { + Ok(v) => v, + Err(e) => { + eprintln!("unable to convert JWT expiration time into u64 err={}", e); + 0 + } + } + }; + let config = Config { + jwt_exp_time, + jwt_issuer: config.get("jwt", "issuer").unwrap_or("".to_string()), + jwt_pub_key: config.get("jwt", "public_key").unwrap_or("".to_string()), + jwt_priv_key: config.get("jwt", "private_key").unwrap_or("".to_string()), + filestore_path: config.get("store", "path").unwrap_or("".to_string()), + }; + + if !config.validate() { + return Err("ini file configuration validation failed".to_string()); + } + + Ok(config) + } +} + +impl Config { + /// validates config ini file + fn validate(&self) -> bool { + if self.jwt_exp_time <= 0 { + eprintln!("invalid config parameter: JWT expiration time is negative or equals to 0"); + return false; + } + + if self.jwt_issuer == "" { + eprintln!("invalid config parameter: JWT issuer is empty"); + return false; + } + + // TODO: check if the file exists and rights are ok + if self.jwt_pub_key == "" { + eprintln!("invalid config parameter: JWT public key file path is empty"); + return false; + } + + // TODO: check if the file exists and rights are ok + if self.jwt_priv_key == "" { + eprintln!("invalid config parameter: JWT private key file path is empty"); + return false; + } + + if self.filestore_path == "" { + eprintln!("invalid config parameter: filestore path is empty"); + return false; + } + + true + } +} + +#[test] +fn test_config() { + use std::env; + + let root_path = env::var("CARGO_MANIFEST_DIR").unwrap(); + + let config_path = format!("{}/{}/{}/{}", root_path, "tests", "data", "config.ini"); + let mut config = Ini::new(); + let _r = config.load(config_path); + + let router_config = Config::try_from(config); + assert!(router_config.is_ok()); +} + +#[test] +fn test_bad_config() { + use std::env; + + let root_path = env::var("CARGO_MANIFEST_DIR").unwrap(); + + let config_path = format!("{}/{}/{}/{}", root_path, "tests", "data", "bad_config.ini"); + let mut config = Ini::new(); + let _r = config.load(config_path); + + let router_config = Config::try_from(config); + assert!(router_config.is_err()); +} + +#[test] +fn test_bad_config_path() { + use std::env; + + let root_path = env::var("CARGO_MANIFEST_DIR").unwrap(); + + let config_path = format!("{}/{}/{}/{}", root_path, "tests", "data", "con.ini"); + let mut config = Ini::new(); + + let result = config.load(config_path); + assert!(result.is_err()); +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..43fe76c --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,3 @@ +mod config; + +pub use config::Config; diff --git a/src/http/mod.rs b/src/http/mod.rs index 38d58b0..f9dbf7c 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -6,4 +6,4 @@ pub mod router; pub use request::HTTPRequest; pub use response::{HTTPResponse, HTTPStatusCode}; -pub use router::{Config, ROUTER}; +pub use router::ROUTER; diff --git a/src/http/router.rs b/src/http/router.rs index c242fc7..f2a2089 100644 --- a/src/http/router.rs +++ b/src/http/router.rs @@ -2,105 +2,18 @@ //! it implements all the logic to build an `HTTPResponse` use super::{HTTPRequest, HTTPResponse}; +use crate::config::Config; use crate::stores::FileStore; use crate::stores::Store; -use configparser::ini::Ini; use jwt_simple::prelude::*; use lazy_static::lazy_static; use std::collections::HashMap; use std::future::Future; use std::pin::Pin; -use std::str::FromStr; type FuturePinned = Pin>>; type Handler = fn(HTTPRequest, Config) -> FuturePinned; -#[derive(Clone)] -pub struct Config { - jwt_exp_time: u64, - jwt_issuer: String, - jwt_priv_key: String, - jwt_pub_key: String, - filestore_path: String, -} - -impl Default for Config { - fn default() -> Self { - Config { - jwt_exp_time: 0, - jwt_issuer: "".to_string(), - jwt_priv_key: "".to_string(), - jwt_pub_key: "".to_string(), - filestore_path: "".to_string(), - } - } -} - -impl TryFrom for Config { - type Error = String; - fn try_from(config: Ini) -> Result { - let exp_time = config - .get("jwt", "expiration_time") - .unwrap_or("".to_string()); - let jwt_exp_time = { - match u64::from_str(&exp_time) { - Ok(v) => v, - Err(e) => { - eprintln!("unable to convert JWT expiration time into u64 err={}", e); - 0 - } - } - }; - let config = Config { - jwt_exp_time, - jwt_issuer: config.get("jwt", "issuer").unwrap_or("".to_string()), - jwt_pub_key: config.get("jwt", "public_key").unwrap_or("".to_string()), - jwt_priv_key: config.get("jwt", "private_key").unwrap_or("".to_string()), - filestore_path: config.get("store", "path").unwrap_or("".to_string()), - }; - - if !config.validate() { - return Err("ini file configuration validation failed".to_string()); - } - - Ok(config) - } -} - -impl Config { - /// validates config ini file - fn validate(&self) -> bool { - if self.jwt_exp_time <= 0 { - eprintln!("invalid config parameter: JWT expiration time is negative or equals to 0"); - return false; - } - - if self.jwt_issuer == "" { - eprintln!("invalid config parameter: JWT issuer is empty"); - return false; - } - - // TODO: check if the file exists and rights are ok - if self.jwt_pub_key == "" { - eprintln!("invalid config parameter: JWT public key file path is empty"); - return false; - } - - // TODO: check if the file exists and rights are ok - if self.jwt_priv_key == "" { - eprintln!("invalid config parameter: JWT private key file path is empty"); - return false; - } - - if self.filestore_path == "" { - eprintln!("invalid config parameter: filestore path is empty"); - return false; - } - - true - } -} - fn handle_get(request: HTTPRequest, config: Config) -> FuturePinned { Box::pin(async move { let mut store = FileStore::new(config.filestore_path); @@ -205,47 +118,3 @@ async fn test_route() { response.status_line.get_status_code() ); } - -#[test] -fn test_config() { - use std::env; - - let root_path = env::var("CARGO_MANIFEST_DIR").unwrap(); - - // TODO: path::Path should be better - let config_path = format!("{}/{}/{}/{}", root_path, "tests", "data", "config.ini"); - let mut config = Ini::new(); - let _r = config.load(config_path); - - let router_config = Config::try_from(config); - assert!(router_config.is_ok()); -} - -#[test] -fn test_bad_config() { - use std::env; - - let root_path = env::var("CARGO_MANIFEST_DIR").unwrap(); - - // TODO: path::Path should be better - let config_path = format!("{}/{}/{}/{}", root_path, "tests", "data", "bad_config.ini"); - let mut config = Ini::new(); - let _r = config.load(config_path); - - let router_config = Config::try_from(config); - assert!(router_config.is_err()); -} - -#[test] -fn test_bad_config_path() { - use std::env; - - let root_path = env::var("CARGO_MANIFEST_DIR").unwrap(); - - // TODO: path::Path should be better - let config_path = format!("{}/{}/{}/{}", root_path, "tests", "data", "con.ini"); - let mut config = Ini::new(); - - let result = config.load(config_path); - assert!(result.is_err()); -} diff --git a/src/main.rs b/src/main.rs index cb91c62..1fb6aae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +mod config; mod http; mod stores; @@ -8,7 +9,8 @@ use tokio::{ net::{TcpListener, TcpStream}, }; -use http::{Config, ROUTER}; +use config::Config; +use http::ROUTER; #[derive(Parser)] #[clap(author, version, about, long_about = None)] From 808cd3ee77a5254f80e32f30ca6b759afdc3b44a Mon Sep 17 00:00:00 2001 From: landrigun Date: Thu, 13 Oct 2022 10:33:11 +0000 Subject: [PATCH 2/9] impl an HTTPMessage corresponding to the JSON response body --- src/http/message.rs | 77 +++++++++++++++++++++++++++++++++++++++++++++ src/http/mod.rs | 2 ++ 2 files changed, 79 insertions(+) create mode 100644 src/http/message.rs diff --git a/src/http/message.rs b/src/http/message.rs new file mode 100644 index 0000000..58f8c4f --- /dev/null +++ b/src/http/message.rs @@ -0,0 +1,77 @@ +use json; +use std::collections::HashMap; + +const JSON_DELIMITER: &'static str = ","; + +/// `HashMap` wrapper, represents the JSON response body +pub struct HTTPMessage { + message: HashMap, +} + +impl Default for HTTPMessage { + fn default() -> Self { + HTTPMessage { + message: HashMap::new(), + } + } +} + +/// try to convert `HTTPMessage` in `json::JsonValue` +impl TryInto for HTTPMessage { + type Error = String; + fn try_into(self) -> Result { + let message = format!(r#"{{{}}}"#, self.build_json()); + println!("message: {}", message); + match json::parse(&message) { + Ok(r) => Ok(r), + Err(e) => Err(format!( + "unable to parse the HTTPMessage correctly: {}, err={}", + message, e + )), + } + } +} + +impl HTTPMessage { + fn put(&mut self, key: &str, value: &str) { + self.message.insert(key.to_string(), value.to_string()); + } + + /// loops over all the HashMap keys, builds a JSON key value for each one and join them with `JSON_DELIMITER` + fn build_json(self) -> String { + let unstruct: Vec = self + .message + .keys() + .map(|k| format!(r#""{}":{:?}"#, k, self.message.get(k).unwrap())) + .collect(); + let joined = unstruct.join(JSON_DELIMITER); + joined + } +} + +#[test] +fn test_message() { + let mut http_message = HTTPMessage::default(); + http_message.put("username", "toto"); + http_message.put("password", "tata"); + + let mut json_result: Result = http_message.try_into(); + assert!(json_result.is_ok()); + + let mut json = json_result.unwrap(); + assert!(json.has_key("username")); + assert!(json.has_key("password")); + + let empty_http_message = HTTPMessage::default(); + json_result = empty_http_message.try_into(); + assert!(json_result.is_ok()); + + json = json_result.unwrap(); + assert_eq!("{}", json.dump().to_string()); + + let mut bad_http_message = HTTPMessage::default(); + bad_http_message.put("\"", ""); + + json_result = bad_http_message.try_into(); + assert!(json_result.is_err()); +} diff --git a/src/http/mod.rs b/src/http/mod.rs index f9dbf7c..8431d8f 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -1,9 +1,11 @@ //! http module includes tools to parse an HTTP request and build and HTTP response +pub mod message; pub mod request; pub mod response; pub mod router; +pub use message::HTTPMessage; pub use request::HTTPRequest; pub use response::{HTTPResponse, HTTPStatusCode}; pub use router::ROUTER; From 7073a4b88e6187844035485bb177226bc53ece75 Mon Sep 17 00:00:00 2001 From: landrigun Date: Thu, 13 Oct 2022 12:20:30 +0000 Subject: [PATCH 3/9] improve HTTPResponse to include custom HTTPMessage --- src/http/message.rs | 19 ++++++++++++++++++- src/http/mod.rs | 2 +- src/http/response.rs | 39 +++++++++++++++++++++++++++++++++------ src/http/router.rs | 20 +++++++++++--------- src/main.rs | 11 +++++++---- 5 files changed, 70 insertions(+), 21 deletions(-) diff --git a/src/http/message.rs b/src/http/message.rs index 58f8c4f..6e786c7 100644 --- a/src/http/message.rs +++ b/src/http/message.rs @@ -33,10 +33,27 @@ impl TryInto for HTTPMessage { } impl HTTPMessage { - fn put(&mut self, key: &str, value: &str) { + pub fn put(&mut self, key: &str, value: &str) { self.message.insert(key.to_string(), value.to_string()); } + /// associated function to build an HTTPMessage error + pub fn error(message: &str) -> Option { + let mut http_message = HTTPMessage::default(); + http_message.put("error", message); + + match message.try_into() { + Ok(m) => Some(m), + Err(e) => { + eprintln!( + "unable to parse the message: {} into JSON, err={}", + message, e + ); + return None; + } + } + } + /// loops over all the HashMap keys, builds a JSON key value for each one and join them with `JSON_DELIMITER` fn build_json(self) -> String { let unstruct: Vec = self diff --git a/src/http/mod.rs b/src/http/mod.rs index 8431d8f..077d5b4 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -6,6 +6,6 @@ pub mod response; pub mod router; pub use message::HTTPMessage; -pub use request::HTTPRequest; +pub use request::{HTTPRequest, HTTPVersion}; pub use response::{HTTPResponse, HTTPStatusCode}; pub use router::ROUTER; diff --git a/src/http/response.rs b/src/http/response.rs index fcf7fc0..42283d2 100644 --- a/src/http/response.rs +++ b/src/http/response.rs @@ -2,7 +2,7 @@ //! it will build an HTTPResponse corresponding to the HTTP message specs. see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages //! NOTE: only few parts of the specification has been implemented -use crate::http::request::HTTPVersion; +use super::{HTTPMessage, HTTPVersion}; use json; #[derive(Debug, PartialEq, Clone)] @@ -92,13 +92,19 @@ impl Into for HTTPResponse { } impl HTTPResponse { - pub fn as_500() -> Self { + pub fn as_500(message: Option) -> Self { let mut response = Self::default(); response .status_line .set_status_code(HTTPStatusCode::Http500); - response.body = json::parse(r#"{"error": "unexpected error occurred"}"#).unwrap(); + + response.body = { + match message { + Some(m) => m, + None => json::parse(r#"{"error": "unexpected error occurred"}"#).unwrap(), + } + }; response } @@ -119,9 +125,11 @@ impl HTTPResponse { status_line: HTTPStatusLine::default(), body: json::parse(r#"{"error": "invalid credentials"}"#).unwrap(), }; + response .status_line .set_status_code(HTTPStatusCode::Http403); + response } @@ -130,16 +138,35 @@ impl HTTPResponse { Self::default() } - // TODO: need to be adjust to accept `json::JsonValue` - pub fn as_200(token: String) -> Self { + pub fn as_200(message: Option) -> Self { let mut response = Self::default(); response .status_line .set_status_code(HTTPStatusCode::Http200); - response.body = json::parse(format!(r#"{{"token": "{}"}}"#, token).as_str()).unwrap(); + response.body = { + match message { + Some(m) => m, + None => json::parse(r#"{"status": "ok"}"#).unwrap(), + } + }; response } + + /// build and HTTP 200 response with the generated JWT + pub fn send_token(token: &str) -> Self { + let mut http_message = HTTPMessage::default(); + http_message.put("token", token); + + let message = { + match http_message.try_into() { + Ok(m) => m, + Err(_e) => json::parse(r#"{"token": "error.generation.token"}"#).unwrap(), + } + }; + + HTTPResponse::as_200(Some(message)) + } } diff --git a/src/http/router.rs b/src/http/router.rs index f2a2089..f89a9b0 100644 --- a/src/http/router.rs +++ b/src/http/router.rs @@ -1,7 +1,7 @@ //! router aims to handle correctly the request corresponding to the target //! it implements all the logic to build an `HTTPResponse` -use super::{HTTPRequest, HTTPResponse}; +use super::{HTTPMessage, HTTPRequest, HTTPResponse}; use crate::config::Config; use crate::stores::FileStore; use crate::stores::Store; @@ -36,10 +36,11 @@ fn handle_get(request: HTTPRequest, config: Config) -> FuturePinned k, - // TODO: set error in the message body Err(e) => { - eprintln!("error occurred while getting private key err={}", e); - return HTTPResponse::as_500(); + let message: Option = HTTPMessage::error( + format!("unable to load the private key, err={}", e).as_str(), + ); + return HTTPResponse::as_500(message); } } }; @@ -47,11 +48,12 @@ fn handle_get(request: HTTPRequest, config: Config) -> FuturePinned HTTPResponse::as_200(token), - // TODO: set the error in the message body + Ok(token) => HTTPResponse::send_token(&token), Err(e) => { - eprintln!("error occurred while signing the token err={}", e); - return HTTPResponse::as_500(); + let message: Option = HTTPMessage::error( + format!("unable to sign the token, err={}", e).as_str(), + ); + return HTTPResponse::as_500(message); } } } @@ -67,7 +69,7 @@ fn handle_validate(request: HTTPRequest, _config: Config) -> FuturePinned { // TODO: impl the JWT validation - HTTPResponse::as_200("header.payload.signature".to_string()) + HTTPResponse::send_token("header.payload.signature") } None => HTTPResponse::as_400(), } diff --git a/src/main.rs b/src/main.rs index 1fb6aae..62c72ce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -46,10 +46,13 @@ async fn main() { } }; - let router_config: Config = if let Ok(c) = Config::try_from(config) { - c - } else { - std::process::exit(1); + let router_config: Config = { + match Config::try_from(config) { + Ok(c) => c, + Err(_e) => { + std::process::exit(1); + } + } }; loop { From 616631028359178ef62af310bcf79fa0371dc14f Mon Sep 17 00:00:00 2001 From: landrigun Date: Thu, 13 Oct 2022 16:06:27 +0000 Subject: [PATCH 4/9] refactor GET handler + impl JWTSigner --- src/config/config.rs | 2 - src/http/message.rs | 1 - src/http/request.rs | 2 - src/http/response.rs | 2 +- src/http/router.rs | 33 ++++---------- src/jwt/jwt.rs | 83 +++++++++++++++++++++++++++++++++++ src/jwt/mod.rs | 3 ++ src/main.rs | 1 + tests/bash/curling.bash | 2 +- tests/python/test_requests.py | 4 +- 10 files changed, 100 insertions(+), 33 deletions(-) create mode 100644 src/jwt/jwt.rs create mode 100644 src/jwt/mod.rs diff --git a/src/config/config.rs b/src/config/config.rs index 31d0bd8..1f77698 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -68,13 +68,11 @@ impl Config { return false; } - // TODO: check if the file exists and rights are ok if self.jwt_pub_key == "" { eprintln!("invalid config parameter: JWT public key file path is empty"); return false; } - // TODO: check if the file exists and rights are ok if self.jwt_priv_key == "" { eprintln!("invalid config parameter: JWT private key file path is empty"); return false; diff --git a/src/http/message.rs b/src/http/message.rs index 6e786c7..78b5ac4 100644 --- a/src/http/message.rs +++ b/src/http/message.rs @@ -21,7 +21,6 @@ impl TryInto for HTTPMessage { type Error = String; fn try_into(self) -> Result { let message = format!(r#"{{{}}}"#, self.build_json()); - println!("message: {}", message); match json::parse(&message) { Ok(r) => Ok(r), Err(e) => Err(format!( diff --git a/src/http/request.rs b/src/http/request.rs index d80471e..cc4388e 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -105,7 +105,6 @@ impl Default for HTTPStartLine { fn default() -> Self { HTTPStartLine { method: "".to_string(), - target: "".to_string(), version: HTTPVersion::Unknown, } @@ -335,7 +334,6 @@ fn test_request() { for (request, expect) in test_cases { let http_request = HTTPRequest::from(request.as_str()); - println!("{:?}", http_request); assert_eq!(expect.is_valid, http_request.is_valid()); let start_line: String = http_request.start_line.into(); diff --git a/src/http/response.rs b/src/http/response.rs index 42283d2..451e876 100644 --- a/src/http/response.rs +++ b/src/http/response.rs @@ -155,7 +155,7 @@ impl HTTPResponse { response } - /// build and HTTP 200 response with the generated JWT + /// builds an HTTP 200 response with the generated JWT pub fn send_token(token: &str) -> Self { let mut http_message = HTTPMessage::default(); http_message.put("token", token); diff --git a/src/http/router.rs b/src/http/router.rs index f89a9b0..8569553 100644 --- a/src/http/router.rs +++ b/src/http/router.rs @@ -3,9 +3,9 @@ use super::{HTTPMessage, HTTPRequest, HTTPResponse}; use crate::config::Config; +use crate::jwt::JWTSigner; use crate::stores::FileStore; use crate::stores::Store; -use jwt_simple::prelude::*; use lazy_static::lazy_static; use std::collections::HashMap; use std::future::Future; @@ -16,7 +16,7 @@ type Handler = fn(HTTPRequest, Config) -> FuturePinned; fn handle_get(request: HTTPRequest, config: Config) -> FuturePinned { Box::pin(async move { - let mut store = FileStore::new(config.filestore_path); + let mut store = FileStore::new(config.filestore_path.clone()); match &request.body { Some(ref b) => { let is_auth = store.is_auth(&b.get_data()).await; @@ -24,35 +24,20 @@ fn handle_get(request: HTTPRequest, config: Config) -> FuturePinned c, + let jwt_signer = { + match JWTSigner::new(config).await { + Ok(s) => s, Err(e) => { - eprintln!("error while reading JWT priv key content err={}", e); - "".to_string() - } - } - }; - let jwt_key = { - match RS384KeyPair::from_pem(priv_key_content.as_str()) { - Ok(k) => k, - Err(e) => { - let message: Option = HTTPMessage::error( - format!("unable to load the private key, err={}", e).as_str(), - ); + let message = HTTPMessage::error(&e); return HTTPResponse::as_500(message); } } }; - let mut claims = Claims::create(Duration::from_hours(config.jwt_exp_time)); - claims.issuer = Some(config.jwt_issuer); - match jwt_key.sign(claims) { - Ok(token) => HTTPResponse::send_token(&token), + match jwt_signer.sign() { + Ok(t) => HTTPResponse::send_token(&t), Err(e) => { - let message: Option = HTTPMessage::error( - format!("unable to sign the token, err={}", e).as_str(), - ); + let message = HTTPMessage::error(&e); return HTTPResponse::as_500(message); } } diff --git a/src/jwt/jwt.rs b/src/jwt/jwt.rs new file mode 100644 index 0000000..904a1fb --- /dev/null +++ b/src/jwt/jwt.rs @@ -0,0 +1,83 @@ +//! simple module to read `.pem` files and sign the token + +use crate::config::Config; +use jwt_simple::prelude::*; +use tokio::fs; + +pub struct JWTSigner { + private_key: String, + public_key: String, + issuer: String, + exp_time: u64, +} + +impl JWTSigner { + // NOTE: could be included in a Trait: `TryFrom` but difficult to handle with async + pub async fn new(config: Config) -> Result { + let mut jwt_signer = JWTSigner { + private_key: "".to_string(), + public_key: "".to_string(), + issuer: config.jwt_issuer, + exp_time: config.jwt_exp_time, + }; + + match fs::read_to_string(config.jwt_priv_key).await { + Ok(c) => { + jwt_signer.private_key = c; + } + Err(e) => { + return Err(format!("unable to read the private key, err={}", e)); + } + } + + match fs::read_to_string(config.jwt_pub_key).await { + Ok(c) => { + jwt_signer.public_key = c; + } + Err(e) => { + return Err(format!("unable to read the public key, err={}", e)); + } + } + + Ok(jwt_signer) + } + + /// builds and signs the token + pub fn sign(&self) -> Result { + let jwt_key = { + match RS384KeyPair::from_pem(&self.private_key) { + Ok(k) => k, + Err(e) => { + return Err(format!("unable to load the private key, err={}", e)); + } + } + }; + let mut claims = Claims::create(Duration::from_hours(self.exp_time)); + claims.issuer = Some(self.issuer.clone()); + + match jwt_key.sign(claims) { + Ok(token) => Ok(token), + Err(e) => { + return Err(format!("unable to sign the token, err={}", e)); + } + } + } +} + +#[tokio::test] +async fn test_signer() { + use configparser::ini::Ini; + use std::env; + + let root_path = env::var("CARGO_MANIFEST_DIR").unwrap(); + + let config_path = format!("{}/{}/{}/{}", root_path, "tests", "data", "config.ini"); + let mut config = Ini::new(); + let _r = config.load(config_path); + + let router_config = Config::try_from(config); + assert!(router_config.is_ok()); + + let jwt_signer = JWTSigner::new(router_config.unwrap()); + assert!(jwt_signer.await.is_ok()); +} diff --git a/src/jwt/mod.rs b/src/jwt/mod.rs new file mode 100644 index 0000000..051f27f --- /dev/null +++ b/src/jwt/mod.rs @@ -0,0 +1,3 @@ +mod jwt; + +pub use jwt::JWTSigner; diff --git a/src/main.rs b/src/main.rs index 62c72ce..587f34f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ mod config; mod http; +mod jwt; mod stores; use clap::Parser; diff --git a/tests/bash/curling.bash b/tests/bash/curling.bash index 5642cca..8fb3faa 100755 --- a/tests/bash/curling.bash +++ b/tests/bash/curling.bash @@ -6,7 +6,7 @@ # ####################################### -URL="https://dev.thegux.fr" +URL="http://localhost:9001" for i in {0..10} do diff --git a/tests/python/test_requests.py b/tests/python/test_requests.py index 19a88c2..bc7544e 100644 --- a/tests/python/test_requests.py +++ b/tests/python/test_requests.py @@ -5,7 +5,7 @@ from datetime import datetime from unittest import TestCase -URL = "https://dev.thegux.fr" +URL = "http://localhost:9001" class TestResponse(TestCase): @@ -62,7 +62,7 @@ class TestResponse(TestCase): resp = requests.post( URL + "/get/", json={"username": "tutu", "password": "titi"} ) - self.assertEqual(resp.status_code, 403, "bas status code returned") + self.assertEqual(resp.status_code, 403, "bad status code returned") self.assertIsNotNone(resp.json(), "response data must not be empty") self.assertEqual( resp.json()["error"], From 7336933642da932cb9e20adfa67fab1d93417b19 Mon Sep 17 00:00:00 2001 From: landrigun Date: Fri, 14 Oct 2022 10:37:40 +0000 Subject: [PATCH 5/9] bug: #15 fix fragmented TCPStream + spawn a tokio task on each connection --- src/http/router.rs | 87 +++++++++++++---------------------- src/main.rs | 23 +++++++-- tests/python/test_requests.py | 2 +- 3 files changed, 53 insertions(+), 59 deletions(-) diff --git a/src/http/router.rs b/src/http/router.rs index 8569553..b1bd817 100644 --- a/src/http/router.rs +++ b/src/http/router.rs @@ -6,72 +6,48 @@ use crate::config::Config; use crate::jwt::JWTSigner; use crate::stores::FileStore; use crate::stores::Store; -use lazy_static::lazy_static; -use std::collections::HashMap; -use std::future::Future; -use std::pin::Pin; -type FuturePinned = Pin>>; -type Handler = fn(HTTPRequest, Config) -> FuturePinned; +async fn handle_get(request: HTTPRequest, config: Config) -> HTTPResponse { + let mut store = FileStore::new(config.filestore_path.clone()); + match &request.body { + Some(ref b) => { + let is_auth = store.is_auth(&b.get_data()).await; + if !is_auth { + return HTTPResponse::as_403(); + } -fn handle_get(request: HTTPRequest, config: Config) -> FuturePinned { - Box::pin(async move { - let mut store = FileStore::new(config.filestore_path.clone()); - match &request.body { - Some(ref b) => { - let is_auth = store.is_auth(&b.get_data()).await; - if !is_auth { - return HTTPResponse::as_403(); - } - - let jwt_signer = { - match JWTSigner::new(config).await { - Ok(s) => s, - Err(e) => { - let message = HTTPMessage::error(&e); - return HTTPResponse::as_500(message); - } - } - }; - - match jwt_signer.sign() { - Ok(t) => HTTPResponse::send_token(&t), + let jwt_signer = { + match JWTSigner::new(config).await { + Ok(s) => s, Err(e) => { let message = HTTPMessage::error(&e); return HTTPResponse::as_500(message); } } + }; + + match jwt_signer.sign() { + Ok(t) => HTTPResponse::send_token(&t), + Err(e) => { + let message = HTTPMessage::error(&e); + return HTTPResponse::as_500(message); + } } - None => HTTPResponse::as_400(), } - }) + None => HTTPResponse::as_400(), + } } /// validates the token by checking: /// * expiration time -fn handle_validate(request: HTTPRequest, _config: Config) -> FuturePinned { - Box::pin(async move { - match &request.body { - Some(ref _b) => { - // TODO: impl the JWT validation - HTTPResponse::send_token("header.payload.signature") - } - None => HTTPResponse::as_400(), +async fn handle_validate(request: HTTPRequest, _config: Config) -> HTTPResponse { + match &request.body { + Some(ref _b) => { + // TODO: impl the JWT validation + HTTPResponse::send_token("header.payload.signature") } - }) -} - -lazy_static! { - /// defines the map between the URL and its associated callback - /// each authorized targets must implement a function returning `FuturePinned` - // TODO: a macro should be implemented to mask the implementation details - static ref HTTP_METHODS: HashMap<&'static str, Handler> = - HashMap::from( - [ - ("/get/", handle_get as Handler), - ("/validate/", handle_validate as Handler) - ] - ); + None => HTTPResponse::as_400(), + } } pub struct Router; @@ -81,9 +57,10 @@ impl Router { let request = HTTPRequest::from(request_str); let target = request.start_line.get_target(); - match HTTP_METHODS.get(target.as_str()) { - Some(f) => f(request, config).await, - None => HTTPResponse::as_404(), + match target.as_str() { + "/get/" => handle_get(request, config).await, + "/validate/" => handle_validate(request, config).await, + _ => HTTPResponse::as_404(), } } } diff --git a/src/main.rs b/src/main.rs index 587f34f..04c8f13 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ use configparser::ini::Ini; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, net::{TcpListener, TcpStream}, + time::{timeout, Duration}, }; use config::Config; @@ -58,16 +59,32 @@ async fn main() { loop { let (stream, _) = listener.accept().await.unwrap(); - handle_connection(stream, router_config.clone()).await; + let conf = router_config.clone(); + tokio::spawn(handle_connection(stream, conf.clone())); } } /// parses the incoming request (partial spec implementation) and build an HTTP response async fn handle_connection(mut stream: TcpStream, config: Config) { + let mut message = vec![]; let mut buffer: [u8; 1024] = [0; 1024]; - let n = stream.read(&mut buffer).await.unwrap(); - let request_string = std::str::from_utf8(&buffer[0..n]).unwrap(); + let duration = Duration::from_micros(500); + + // loop until the message is read + // the stream can be fragmented so, using a timout (500um should be enough) for the future for completion + // after the timeout, the message is "considered" as entirely read + loop { + match timeout(duration, stream.read(&mut buffer)).await { + Ok(v) => { + let n = v.unwrap(); + message.extend_from_slice(&buffer[0..n]); + } + Err(_e) => break, + } + } + + let request_string = std::str::from_utf8(&message).unwrap(); let response = ROUTER.route(request_string, config).await; let response_str: String = response.into(); diff --git a/tests/python/test_requests.py b/tests/python/test_requests.py index bc7544e..a79847d 100644 --- a/tests/python/test_requests.py +++ b/tests/python/test_requests.py @@ -5,7 +5,7 @@ from datetime import datetime from unittest import TestCase -URL = "http://localhost:9001" +URL = "http://127.0.0.1:9001" class TestResponse(TestCase): From baa8595a4a14dff11904e7b727f92f90c6aad051 Mon Sep 17 00:00:00 2001 From: landrigun Date: Fri, 14 Oct 2022 10:45:32 +0000 Subject: [PATCH 6/9] fix doc + set route target in const --- src/http/router.rs | 8 ++++++-- src/main.rs | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/http/router.rs b/src/http/router.rs index b1bd817..48c74d8 100644 --- a/src/http/router.rs +++ b/src/http/router.rs @@ -7,6 +7,10 @@ use crate::jwt::JWTSigner; use crate::stores::FileStore; use crate::stores::Store; +// TODO: must be mapped with corresponding handler +const GET_ROUTE: &'static str = "/get/"; +const VALIDATE_ROUTE: &'static str = "/validate/"; + async fn handle_get(request: HTTPRequest, config: Config) -> HTTPResponse { let mut store = FileStore::new(config.filestore_path.clone()); match &request.body { @@ -58,8 +62,8 @@ impl Router { let target = request.start_line.get_target(); match target.as_str() { - "/get/" => handle_get(request, config).await, - "/validate/" => handle_validate(request, config).await, + GET_ROUTE => handle_get(request, config).await, + VALIDATE_ROUTE => handle_validate(request, config).await, _ => HTTPResponse::as_404(), } } diff --git a/src/main.rs b/src/main.rs index 04c8f13..7a75f69 100644 --- a/src/main.rs +++ b/src/main.rs @@ -72,7 +72,7 @@ async fn handle_connection(mut stream: TcpStream, config: Config) { let duration = Duration::from_micros(500); // loop until the message is read - // the stream can be fragmented so, using a timout (500um should be enough) for the future for completion + // the stream can be fragmented so, using a timeout (500um should be enough) for the future for completion // after the timeout, the message is "considered" as entirely read loop { match timeout(duration, stream.read(&mut buffer)).await { From 6c79c3d708cbd0ffb66b74e944a638823ce0b1aa Mon Sep 17 00:00:00 2001 From: landrigun Date: Fri, 14 Oct 2022 11:29:26 +0000 Subject: [PATCH 7/9] fix README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 43810ba..02bb938 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # simple-auth -A little web server providing JWT token for auth auser. +A little web server providing JWT token for auth user. ## Build ```bash From b73add00c5f0824f5160335736b1acc688e1340e Mon Sep 17 00:00:00 2001 From: landrigun Date: Fri, 14 Oct 2022 14:45:03 +0000 Subject: [PATCH 8/9] feat: #13 impl the JWT validation + some fixes --- README.md | 14 ++++++++-- src/http/request.rs | 42 +++++++++++++++++++++++++++++- src/http/router.rs | 49 +++++++++++++++++++++++++++++------ src/jwt/jwt.rs | 38 ++++++++++++++++++++++++--- src/main.rs | 1 + src/stores/store.rs | 18 +++---------- src/utils/mod.rs | 3 +++ src/utils/utils.rs | 30 +++++++++++++++++++++ tests/bash/curling.bash | 6 ++++- tests/python/requirements.txt | 5 ++++ tests/python/test_requests.py | 44 ++++++++++++++++++++++++++----- 11 files changed, 214 insertions(+), 36 deletions(-) create mode 100644 src/utils/mod.rs create mode 100644 src/utils/utils.rs diff --git a/README.md b/README.md index 02bb938..b8bc60f 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,10 @@ expiration_time = 2 # in hours curl http://:/get/ -d '{"username":"", "password":""}' # should returned {"token":"
.."} + +curl http://:/validate/ -d '{"token":"
.."}' +# should returned (if valid) +{"valid":"true"} ``` ## Test @@ -58,7 +62,13 @@ cargo test ``` ### integration tests -* run the server locally or remotly (the URL must be changed if needed in `curling.bash` and `test_requests.py`) +* do the **configuration** step for your env tests +* set the following env variables: +```bash +export SIMPLE_AUTH_URL="http://:" +export SIMPLE_AUTH_PUB_KEY="" # DO NOT USE THE ONE IN PRODUCTION ! +``` +* run the server (if no one is running remotly) * run curl tests ```bash cd tests/bash/ @@ -75,7 +85,7 @@ source venv/bin/activate pip install -r requirements # launch the tests -python -m unitest +python -m unittest ``` ## Documentation diff --git a/src/http/request.rs b/src/http/request.rs index cc4388e..2d1bc4c 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -8,6 +8,8 @@ use lazy_static::lazy_static; use regex::Regex; use std::collections::VecDeque; +use crate::utils::extract_json_value; + type RequestParts = (String, VecDeque, String); const HTTP_REQUEST_SEPARATOR: &'static str = "\r\n"; @@ -205,6 +207,17 @@ impl HTTPRequest { } } + /// retrieve value in `HTTPBody` (returns None if empty or does not exist) + pub fn get_body_value(&self, key: &str) -> Option { + match self.body { + Some(ref b) => match &b.data { + json::JsonValue::Object(d) => extract_json_value(&d, key), + _ => None, + }, + None => None, + } + } + #[allow(dead_code)] pub fn is_valid(&self) -> bool { return self.start_line.is_valid(); @@ -238,15 +251,17 @@ fn test_request() { start_line: String, body: Option, is_valid: bool, + has_token: bool, } - let test_cases: [(String, Expect); 11] = [ + let test_cases: [(String, Expect); 12] = [ ( "POST /get/ HTTP/1.1\r\n\r\n".to_string(), Expect { start_line: "POST /get/ HTTP/1.1".to_string(), body: None, is_valid: true, + has_token: false, }, ), ( @@ -255,6 +270,7 @@ fn test_request() { start_line: "POST /refresh/ HTTP/2".to_string(), body: None, is_valid: true, + has_token: false, }, ), ( @@ -263,6 +279,7 @@ fn test_request() { start_line: "POST /validate/ HTTP/1.0".to_string(), body: None, is_valid: true, + has_token: false, }, ), ( @@ -271,6 +288,7 @@ fn test_request() { start_line: "GET / HTTP/1.1".to_string(), body: None, is_valid: true, + has_token: false, }, ), // intentionally add HTTP with no version number @@ -280,6 +298,7 @@ fn test_request() { start_line: " UNKNOWN".to_string(), body: None, is_valid: false, + has_token: false, }, ), ( @@ -288,6 +307,7 @@ fn test_request() { start_line: " UNKNOWN".to_string(), body: None, is_valid: false, + has_token: false } ), ( @@ -296,6 +316,7 @@ fn test_request() { start_line: " UNKNOWN".to_string(), body: None, is_valid: false, + has_token: false } ), ( @@ -304,6 +325,7 @@ fn test_request() { start_line: "fjlqskjd /oks?id=65 HTTP/2".to_string(), body: None, is_valid: true, + has_token: false } ), ( @@ -312,6 +334,7 @@ fn test_request() { start_line: " UNKNOWN".to_string(), body: None, is_valid: false, + has_token: false, } ), ( @@ -320,6 +343,7 @@ fn test_request() { start_line: " UNKNOWN".to_string(), body: Some(r#"{"access_token":"AAAAAAAAAAAA.BBBBBBBBBB.CCCCCCCCCC","refresh_token": "DDDDDDDDDDD.EEEEEEEEEEE.FFFFF"}"#.to_string()), is_valid: false, + has_token: false } ), ( @@ -328,6 +352,16 @@ fn test_request() { start_line: "POST /refresh/ HTTP/1.1".to_string(), body: Some(r#"{"access_token":"toto","refresh_token":"tutu"}"#.to_string()), is_valid: true, + has_token: false + } + ), + ( + format!("{}\r\nuselessheaders\r\n{}", "POST /get/ HTTP/1.1", r#"{"token": "toto", "refresh_token": "tutu"}"#), + Expect { + start_line: "POST /get/ HTTP/1.1".to_string(), + body: Some(r#"{"token":"toto","refresh_token":"tutu"}"#.to_string()), + is_valid: true, + has_token: true } ), ]; @@ -336,6 +370,7 @@ fn test_request() { let http_request = HTTPRequest::from(request.as_str()); assert_eq!(expect.is_valid, http_request.is_valid()); + let token = http_request.get_body_value("token"); let start_line: String = http_request.start_line.into(); assert_eq!(expect.start_line, start_line); @@ -345,6 +380,11 @@ fn test_request() { } None => continue, } + + match expect.has_token { + true => assert!(token.is_some()), + false => assert!(token.is_none()), + } } } diff --git a/src/http/router.rs b/src/http/router.rs index 48c74d8..d1f60ca 100644 --- a/src/http/router.rs +++ b/src/http/router.rs @@ -1,11 +1,12 @@ //! router aims to handle correctly the request corresponding to the target //! it implements all the logic to build an `HTTPResponse` +use json; + use super::{HTTPMessage, HTTPRequest, HTTPResponse}; use crate::config::Config; use crate::jwt::JWTSigner; -use crate::stores::FileStore; -use crate::stores::Store; +use crate::stores::{FileStore, Store}; // TODO: must be mapped with corresponding handler const GET_ROUTE: &'static str = "/get/"; @@ -44,14 +45,46 @@ async fn handle_get(request: HTTPRequest, config: Config) -> HTTPResponse { /// validates the token by checking: /// * expiration time -async fn handle_validate(request: HTTPRequest, _config: Config) -> HTTPResponse { - match &request.body { - Some(ref _b) => { - // TODO: impl the JWT validation - HTTPResponse::send_token("header.payload.signature") +/// * signature +async fn handle_validate(request: HTTPRequest, config: Config) -> HTTPResponse { + let token = { + match request.get_body_value("token") { + Some(t) => t, + None => { + let mut message = HTTPMessage::default(); + message.put("valid", "false"); + message.put("reason", "no token provided in the request body"); + let json = message.try_into().unwrap(); + + return HTTPResponse::as_200(Some(json)); + } + } + }; + + let jwt_signer = { + match JWTSigner::new(config).await { + Ok(s) => s, + Err(e) => { + let message = HTTPMessage::error(&e); + let json = message.try_into().unwrap(); + return HTTPResponse::as_500(Some(json)); + } + } + }; + + let mut message = HTTPMessage::default(); + match jwt_signer.validate(&token) { + Ok(()) => { + message.put("valid", "true"); + } + Err(e) => { + message.put("valid", "false"); + message.put("reason", &e); } - None => HTTPResponse::as_400(), } + + let json: json::JsonValue = message.try_into().unwrap(); + HTTPResponse::as_200(Some(json)) } pub struct Router; diff --git a/src/jwt/jwt.rs b/src/jwt/jwt.rs index 904a1fb..df9f932 100644 --- a/src/jwt/jwt.rs +++ b/src/jwt/jwt.rs @@ -1,7 +1,9 @@ //! simple module to read `.pem` files and sign the token use crate::config::Config; +use jwt_simple::common::VerificationOptions; use jwt_simple::prelude::*; +use std::collections::HashSet; use tokio::fs; pub struct JWTSigner { @@ -26,7 +28,7 @@ impl JWTSigner { jwt_signer.private_key = c; } Err(e) => { - return Err(format!("unable to read the private key, err={}", e)); + return Err(format!("unable to read the private key err={}", e)); } } @@ -35,20 +37,30 @@ impl JWTSigner { jwt_signer.public_key = c; } Err(e) => { - return Err(format!("unable to read the public key, err={}", e)); + return Err(format!("unable to read the public key err={}", e)); } } Ok(jwt_signer) } + fn get_verification_options(&self) -> VerificationOptions { + let mut verification_options = VerificationOptions::default(); + + let mut issuers: HashSet = HashSet::new(); + issuers.insert(self.issuer.clone()); + verification_options.allowed_issuers = Some(issuers); + + verification_options + } + /// builds and signs the token pub fn sign(&self) -> Result { let jwt_key = { match RS384KeyPair::from_pem(&self.private_key) { Ok(k) => k, Err(e) => { - return Err(format!("unable to load the private key, err={}", e)); + return Err(format!("unable to load the private key err={}", e)); } } }; @@ -58,10 +70,28 @@ impl JWTSigner { match jwt_key.sign(claims) { Ok(token) => Ok(token), Err(e) => { - return Err(format!("unable to sign the token, err={}", e)); + return Err(format!("unable to sign the token err={}", e)); } } } + + pub fn validate(&self, token: &str) -> Result<(), String> { + let verification_options = self.get_verification_options(); + match RS384PublicKey::from_pem(&self.public_key) { + Ok(key) => { + if let Err(e) = + key.verify_token::(token, Some(verification_options)) + { + return Err(format!("token validation failed err={}", e)); + } + Ok(()) + } + Err(e) => Err(format!( + "token validation failed can't read the public key err={}", + e + )), + } + } } #[tokio::test] diff --git a/src/main.rs b/src/main.rs index 7a75f69..87eb6cd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ mod config; mod http; mod jwt; mod stores; +mod utils; use clap::Parser; use configparser::ini::Ini; diff --git a/src/stores/store.rs b/src/stores/store.rs index f1c0a42..6011da5 100644 --- a/src/stores/store.rs +++ b/src/stores/store.rs @@ -1,23 +1,13 @@ use async_trait::async_trait; use json; -use json::object::Object; + +use crate::utils::extract_json_value; #[async_trait] pub trait Store { async fn is_auth(&mut self, data: &json::JsonValue) -> bool; } -/// extracts `String` json value from a key -fn extract_json_value(data: &Object, key: &str) -> String { - if let Some(u) = data.get(key) { - match u.as_str() { - Some(s) => return s.to_string(), - None => return "".to_string(), - } - }; - "".to_string() -} - #[derive(Default, Debug)] pub struct Credentials { pub username: String, @@ -39,8 +29,8 @@ impl From<&json::JsonValue> for Credentials { let mut credentials = Credentials::default(); match data { json::JsonValue::Object(ref d) => { - credentials.username = extract_json_value(&d, "username"); - credentials.password = extract_json_value(&d, "password"); + credentials.username = extract_json_value(&d, "username").unwrap_or("".to_string()); + credentials.password = extract_json_value(&d, "password").unwrap_or("".to_string()); } _ => return credentials, } diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..de6e774 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,3 @@ +mod utils; + +pub use utils::extract_json_value; diff --git a/src/utils/utils.rs b/src/utils/utils.rs new file mode 100644 index 0000000..e330062 --- /dev/null +++ b/src/utils/utils.rs @@ -0,0 +1,30 @@ +use json::object::Object; + +/// extracts JSON value from a key +pub fn extract_json_value(data: &Object, key: &str) -> Option { + match data.get(key) { + Some(u) => match u.as_str() { + Some(s) => return Some(s.to_string()), + None => None, + }, + None => None, + } +} + +#[test] +fn test_extract_json_value() { + let test_cases: [(json::JsonValue, bool, bool); 3] = [ + (json::parse(r#"{"test": ""}"#).unwrap(), true, true), + (json::parse(r#"{}"#).unwrap(), true, false), + (json::parse(r#"[]"#).unwrap(), false, false), + ]; + + for (value, is_valid, has_key) in test_cases { + match value { + json::JsonValue::Object(d) => { + assert_eq!(has_key, extract_json_value(&d, "test").is_some()); + } + _ => assert!(!is_valid), + } + } +} diff --git a/tests/bash/curling.bash b/tests/bash/curling.bash index 8fb3faa..0b2c7b6 100755 --- a/tests/bash/curling.bash +++ b/tests/bash/curling.bash @@ -6,7 +6,11 @@ # ####################################### -URL="http://localhost:9001" +if [ -z ${SIMPLE_AUTH_URL} ] +then + echo "[WARN]: SIMPLE_AUTH_URL is empty, set to http://localhost:9001" + URL="http://localhost:9001" +fi for i in {0..10} do diff --git a/tests/python/requirements.txt b/tests/python/requirements.txt index 0997272..7dc6ab4 100644 --- a/tests/python/requirements.txt +++ b/tests/python/requirements.txt @@ -1,8 +1,10 @@ attrs==22.1.0 black==22.8.0 certifi==2022.9.14 +cffi==1.15.1 charset-normalizer==2.1.1 click==8.1.3 +cryptography==38.0.1 idna==3.4 iniconfig==1.1.1 mypy-extensions==0.4.3 @@ -11,7 +13,10 @@ pathspec==0.10.1 platformdirs==2.5.2 pluggy==1.0.0 py==1.11.0 +pycparser==2.21 +PyJWT==2.5.0 pyparsing==3.0.9 requests==2.28.1 tomli==2.0.1 +types-cryptography==3.3.23 urllib3==1.26.12 diff --git a/tests/python/test_requests.py b/tests/python/test_requests.py index a79847d..ba45ce5 100644 --- a/tests/python/test_requests.py +++ b/tests/python/test_requests.py @@ -1,14 +1,19 @@ import jwt +import os import requests from datetime import datetime - from unittest import TestCase -URL = "http://127.0.0.1:9001" +URL = os.getenv("SIMPLE_AUTH_URL", "http://127.0.0.1:9001") +PUB_KEY_PATH = os.getenv("SIMPLE_AUTH_PUB_KEY", "") class TestResponse(TestCase): + def setUp(self): + with open(PUB_KEY_PATH, "r") as f: + self.pub_key = f.read() + def test_get_target(self): resp = requests.post( URL + "/get/", json={"username": "toto", "password": "tata"} @@ -17,25 +22,52 @@ class TestResponse(TestCase): self.assertIsNotNone(resp.json(), "response data can't be empty") token = resp.json()["token"] - jwt_decoded = jwt.decode(token, options={"verify_signature": False}) + jwt_decoded = jwt.decode( + token, + self.pub_key, + algorithms=["RS384"], + options={ + "verify_signature": True, + "verify_claims": True, + "verify_iss": True, + }, + ) self.assertEqual("thegux.fr", jwt_decoded["iss"]) jwt_exp = datetime.fromtimestamp(jwt_decoded["exp"]) jwt_iat = datetime.fromtimestamp(jwt_decoded["iat"]) date_exp = datetime.strptime(str(jwt_exp - jwt_iat), "%H:%M:%S") self.assertEqual(2, date_exp.hour) + return token - def test_validate_target(self): + def test_validate_target_no_token(self): resp = requests.post( URL + "/validate/", json={"username": "toto", "password": "tata"} ) self.assertEqual(resp.status_code, 200, "bad status code returned") self.assertIsNotNone(resp.json(), "response data can't be empty") + self.assertEqual(resp.json()["valid"], "false", "bad status returned") + self.assertEqual(resp.json()["reason"], "no token provided in the request body") + + def test_validate_target_empty_token(self): + resp = requests.post(URL + "/validate/", json={"tutu": "tutu", "token": ""}) + self.assertEqual(resp.status_code, 200, "bad status code returned") + self.assertIsNotNone(resp.json(), "response data can't be empty") + self.assertEqual(resp.json()["valid"], "false", "bad status returned") self.assertEqual( - resp.json()["token"], "header.payload.signature", "bad status returned" + resp.json()["reason"], + "token validation failed err=JWT compact encoding error", ) - # TODO: must be updated after implmenting `/refresh/` url handler + def test_validate_target(self): + token = self.test_get_target() + + resp = requests.post(URL + "/validate/", json={"token": token}) + self.assertEqual(resp.status_code, 200, "bad status code returned") + self.assertIsNotNone(resp.json(), "response data can't be empty") + self.assertEqual(resp.json()["valid"], "true", "bad status returned") + + # TODO: must be updated after implementing `/refresh/` url handler def test_refresh_target(self): resp = requests.post( URL + "/refresh/", json={"username": "toto", "password": "tata"} From e02849ca8eec0dfbe7050f2e19415cddf85ebb8a Mon Sep 17 00:00:00 2001 From: landrigun Date: Fri, 14 Oct 2022 14:50:21 +0000 Subject: [PATCH 9/9] release(v0.2.0): version number bumped --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b4e4710..c9a5d07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1205,7 +1205,7 @@ dependencies = [ [[package]] name = "simple-auth" -version = "0.1.0" +version = "0.2.0" dependencies = [ "async-std", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 95e5ae6..4121298 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "simple-auth" -version = "0.1.0" +version = "0.2.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html