From 3b6e208004714dda95046344596af28a7d805bf6 Mon Sep 17 00:00:00 2001 From: landrigun Date: Thu, 16 Feb 2023 09:11:15 +0000 Subject: [PATCH 1/7] update is_auth trait + deserialize credentials with serde --- Cargo.lock | 5 +++-- Cargo.toml | 3 ++- src/jwt/jwt.rs | 6 ++++-- src/router/router.rs | 14 ++++++++++---- src/stores/file.rs | 27 ++++++++------------------- src/stores/store.rs | 33 ++++++++++++++++++++------------- 6 files changed, 47 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d925400..18db34c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -635,8 +635,8 @@ dependencies = [ [[package]] name = "http" -version = "0.1.3" -source = "git+https://gitea.thegux.fr/rmanach/http#b8c0fbba0b62906823a79e34bb2eadc1fe419d90" +version = "0.1.4" +source = "git+https://gitea.thegux.fr/rmanach/http#7d4aabad2c6d2cc07359b64214ba6e61f42ed80f" dependencies = [ "json", "lazy_static", @@ -1239,6 +1239,7 @@ dependencies = [ "log", "regex", "serde", + "serde_json", "simple_logger", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index a0dc9e1..4b2a916 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,8 +15,9 @@ jwt-simple = "0.11.1" simple_logger = "4.0.0" log = "0.4.17" base64 = "0.13.1" +serde_json = "1.0" -http = { git = "https://gitea.thegux.fr/rmanach/http", version = "0.1.3" } +http = { git = "https://gitea.thegux.fr/rmanach/http", version = "0.1.4" } # useful for tests (embedded files should be delete in release ?) #rust-embed="6.4.1" diff --git a/src/jwt/jwt.rs b/src/jwt/jwt.rs index 56f3aa8..4b9d7e0 100644 --- a/src/jwt/jwt.rs +++ b/src/jwt/jwt.rs @@ -5,6 +5,8 @@ use serde::{Deserialize, Serialize}; use std::collections::HashSet; use tokio::fs; +use crate::stores::Credentials; + #[derive(Serialize, Deserialize)] struct JWTCustomClaims { email: String, @@ -59,7 +61,7 @@ impl JWTSigner { } /// builds and signs the token - pub fn sign(&self, email: String) -> Result { + pub fn sign(&self, credentials: Credentials) -> Result { let jwt_key = { match RS384KeyPair::from_pem(&self.private_key) { Ok(k) => k, @@ -69,7 +71,7 @@ impl JWTSigner { } }; let mut claims = Claims::with_custom_claims( - JWTCustomClaims { email }, + JWTCustomClaims { email: credentials.get_email() }, Duration::from_hours(self.exp_time), ); claims.issuer = Some(self.issuer.clone()); diff --git a/src/router/router.rs b/src/router/router.rs index 3000e13..810b66b 100644 --- a/src/router/router.rs +++ b/src/router/router.rs @@ -7,7 +7,7 @@ use json::JsonValue; use crate::config::Config; use crate::jwt::JWTSigner; -use crate::stores::{FileStore, Store}; +use crate::stores::{Credentials, FileStore, Store}; // TODO: must be mapped with corresponding handler const GET_ROUTE: &'static str = "/get/"; @@ -23,8 +23,14 @@ async fn handle_get(request: HTTPRequest<'_>, config: Config, method: &str) -> H match request.get_body() { Some(d) => { - let credentials = store.is_auth(d).await; - if credentials.is_none() { + + let credentials = Credentials::from(d); + if credentials.is_empty() { + log::error!("unable to parse the credentials correctly from the incoming request"); + return HTTPResponse::as_400(); + } + + if !store.is_auth(&credentials).await { return HTTPResponse::as_403(); } @@ -38,7 +44,7 @@ async fn handle_get(request: HTTPRequest<'_>, config: Config, method: &str) -> H } }; - match jwt_signer.sign(credentials.unwrap().email) { + match jwt_signer.sign(credentials) { Ok(t) => send_token(&t), Err(e) => { let message = JSONMessage::error(&e); diff --git a/src/stores/file.rs b/src/stores/file.rs index f3839d0..90a5b12 100644 --- a/src/stores/file.rs +++ b/src/stores/file.rs @@ -1,5 +1,4 @@ use async_trait::async_trait; -use json::JsonValue; use std::path::Path; use super::store::{Credentials, Store}; @@ -47,41 +46,31 @@ impl FileStore { self.credentials = credentials; } - /// checks if the credentials exist in the `FileStore` - fn auth(&self, email: String, password: String) -> Option { + /// auth checks if the credentials exist in the `FileStore` + fn auth(&self, email: String, password: String) -> bool { let credentials: Vec<&Credentials> = self .credentials .iter() - .filter(|x| x.email == email && x.password == password) + .filter(|x| *x.get_email() == email && *x.get_password() == password) .collect(); if credentials.len() == 1 { - // no need to store the password again - return Some(Credentials::new( - credentials[0].email.clone(), - "".to_string(), - )); + return true; } - None + false } } #[async_trait] impl Store for FileStore { - async fn is_auth(&mut self, data: &JsonValue) -> Option { + async fn is_auth(&mut self, credentials: &Credentials) -> bool { // ensure that the store file already exists even after its instanciation if !Path::new(&self.path).is_file() { log::error!("{} path referencing file store does not exist", self.path); - return None; - } - - let credentials = Credentials::from(data); - if credentials.is_empty() { - log::error!("unable to parse the credentials correctly from the incoming request"); - return None; + return false; } self.parse_contents().await; - self.auth(credentials.email, credentials.password) + self.auth(credentials.get_email(), credentials.get_password()) } } diff --git a/src/stores/store.rs b/src/stores/store.rs index ec84e04..9cf23ef 100644 --- a/src/stores/store.rs +++ b/src/stores/store.rs @@ -1,17 +1,16 @@ use async_trait::async_trait; use json::JsonValue; - -use http::extract_json_value; +use serde::Deserialize; #[async_trait] pub trait Store { - async fn is_auth(&mut self, data: &JsonValue) -> Option; + async fn is_auth(&mut self, data: &Credentials) -> bool; } -#[derive(Default, Debug)] +#[derive(Default, Debug, Deserialize)] pub struct Credentials { - pub email: String, - pub password: String, + email: String, + password: String, } impl Credentials { @@ -19,22 +18,30 @@ impl Credentials { Credentials { email, password } } + pub fn get_email(&self) -> String { + self.email.clone() + } + + pub fn get_password(&self) -> String { + self.password.clone() + } + pub fn is_empty(&self) -> bool { self.email == "" || self.password == "" } } +// TODO: could be less restrictive with `From<&str>` impl From<&JsonValue> for Credentials { fn from(data: &JsonValue) -> Self { - let mut credentials = Credentials::default(); - match data { - JsonValue::Object(ref d) => { - credentials.email = extract_json_value(&d, "email").unwrap_or("".to_string()); - credentials.password = extract_json_value(&d, "password").unwrap_or("".to_string()); + let res = serde_json::from_str(&data.dump()); + match res { + Ok(c) => c, + Err(e) => { + log::warn!("unable to deserialize credentials: {}", e); + return Credentials::default(); } - _ => return credentials, } - credentials } } From e992f7a3ce3295973bf9075409aa160e40b1335b Mon Sep 17 00:00:00 2001 From: landrigun Date: Thu, 16 Feb 2023 10:45:50 +0000 Subject: [PATCH 2/7] use serde_json to build http response with JWT json body --- Cargo.lock | 4 +-- Cargo.toml | 2 +- src/jwt/jwt.rs | 40 +++++++++++++++++++++++++++--- src/jwt/mod.rs | 5 ++-- src/router/router.rs | 46 +++++++++++------------------------ src/stores/file.rs | 13 +++++----- tests/bash/curling.bash | 24 +++++++++++++++--- tests/python/test_requests.py | 2 +- 8 files changed, 85 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 18db34c..fcbf360 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -635,8 +635,8 @@ dependencies = [ [[package]] name = "http" -version = "0.1.4" -source = "git+https://gitea.thegux.fr/rmanach/http#7d4aabad2c6d2cc07359b64214ba6e61f42ed80f" +version = "0.1.5" +source = "git+https://gitea.thegux.fr/rmanach/http#72bf34127b185f5a52895c08a3d8b229c0588dc0" dependencies = [ "json", "lazy_static", diff --git a/Cargo.toml b/Cargo.toml index 4b2a916..ad33220 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ log = "0.4.17" base64 = "0.13.1" serde_json = "1.0" -http = { git = "https://gitea.thegux.fr/rmanach/http", version = "0.1.4" } +http = { git = "https://gitea.thegux.fr/rmanach/http", version = "0.1.5" } # useful for tests (embedded files should be delete in release ?) #rust-embed="6.4.1" diff --git a/src/jwt/jwt.rs b/src/jwt/jwt.rs index 4b9d7e0..3ab3428 100644 --- a/src/jwt/jwt.rs +++ b/src/jwt/jwt.rs @@ -7,6 +7,35 @@ use tokio::fs; use crate::stores::Credentials; +#[derive(Serialize)] +/// JWTMessage aims to have a generic struct to build HTTP response message with JWT +pub struct JWTMessage { + #[serde(skip_serializing_if = "String::is_empty")] + access_token: String, + #[serde(skip_serializing_if = "String::is_empty")] + refresh_token: String, + #[serde(skip_serializing_if = "String::is_empty")] + pubkey: String, +} + +impl JWTMessage { + pub fn with_access(access_token: String) -> Self { + JWTMessage { + access_token: access_token, + refresh_token: "".to_string(), + pubkey: "".to_string(), + } + } + + pub fn with_pubkey(pubkey: String) -> Self { + JWTMessage { + access_token: "".to_string(), + refresh_token: "".to_string(), + pubkey: base64::encode(pubkey), + } + } +} + #[derive(Serialize, Deserialize)] struct JWTCustomClaims { email: String, @@ -60,7 +89,7 @@ impl JWTSigner { verification_options } - /// builds and signs the token + /// sign builds and signs the token pub fn sign(&self, credentials: Credentials) -> Result { let jwt_key = { match RS384KeyPair::from_pem(&self.private_key) { @@ -71,13 +100,18 @@ impl JWTSigner { } }; let mut claims = Claims::with_custom_claims( - JWTCustomClaims { email: credentials.get_email() }, + JWTCustomClaims { + email: credentials.get_email(), + }, Duration::from_hours(self.exp_time), ); claims.issuer = Some(self.issuer.clone()); match jwt_key.sign(claims) { - Ok(token) => Ok(token), + Ok(token) => { + // TODO: need to generate the refresh token + return Ok(serde_json::to_string(&JWTMessage::with_access(token)).unwrap()); + } Err(e) => { return Err(format!("unable to sign the token details={}", e)); } diff --git a/src/jwt/mod.rs b/src/jwt/mod.rs index 9b43d55..b447102 100644 --- a/src/jwt/mod.rs +++ b/src/jwt/mod.rs @@ -1,4 +1,5 @@ -//! simple module to read `.pem` files and sign the token +//! jwt module aims to read `.pem` files, sign/validate the token and have useful functions to +//! manage HTTP response message mod jwt; -pub use jwt::JWTSigner; +pub use jwt::{JWTMessage, JWTSigner}; diff --git a/src/router/router.rs b/src/router/router.rs index 810b66b..9fbe360 100644 --- a/src/router/router.rs +++ b/src/router/router.rs @@ -1,12 +1,11 @@ //! router aims to handle correctly the request corresponding to the target //! it implements all the logic to build an `HTTPResponse` -use base64; use http::{HTTPRequest, HTTPResponse, JSONMessage}; use json::JsonValue; use crate::config::Config; -use crate::jwt::JWTSigner; +use crate::jwt::{JWTMessage, JWTSigner}; use crate::stores::{Credentials, FileStore, Store}; // TODO: must be mapped with corresponding handler @@ -23,7 +22,6 @@ async fn handle_get(request: HTTPRequest<'_>, config: Config, method: &str) -> H match request.get_body() { Some(d) => { - let credentials = Credentials::from(d); if credentials.is_empty() { log::error!("unable to parse the credentials correctly from the incoming request"); @@ -59,11 +57,7 @@ async fn handle_get(request: HTTPRequest<'_>, config: Config, method: &str) -> H /// validates the token by checking: /// * expiration time /// * signature -async fn handle_validate( - request: HTTPRequest<'_>, - config: Config, - method: &str, -) -> HTTPResponse { +async fn handle_validate(request: HTTPRequest<'_>, config: Config, method: &str) -> HTTPResponse { if request.get_method().trim().to_lowercase() != method { return HTTPResponse::as_400(); } @@ -109,11 +103,7 @@ async fn handle_validate( } /// returns the JWT public key in base64 encoded -async fn handle_public_key( - request: HTTPRequest<'_>, - config: Config, - method: &str, -) -> HTTPResponse { +async fn handle_public_key(request: HTTPRequest<'_>, config: Config, method: &str) -> HTTPResponse { if request.get_method().trim().to_lowercase() != method { return HTTPResponse::as_400(); } @@ -130,18 +120,15 @@ async fn handle_public_key( }; let public_key = jwt_signer.get_public_key(); + let message = serde_json::to_string(&JWTMessage::with_pubkey(public_key)).unwrap(); - let mut message = JSONMessage::default(); - message.put("pubkey", &base64::encode(public_key)); - - let json = message.try_into().unwrap(); - HTTPResponse::as_200(Some(json)) + HTTPResponse::as_200(Some(json::parse(&message).unwrap())) } pub struct Router; impl Router { - /// routes the request to the corresponding handling method + /// route routes the request to the corresponding handling method pub async fn route(&self, request_str: &str, config: Config) -> HTTPResponse { let request = HTTPRequest::from(request_str); match request.get_target() { @@ -154,18 +141,13 @@ impl Router { } /// send_token generates an HTTPResponse with the new token -pub fn send_token(token: &str) -> HTTPResponse { - let mut message = JSONMessage::default(); - message.put("token", token); - - let json = { - match message.try_into() { - Ok(m) => m, - Err(_e) => json::parse(r#"{"token": "error.generation.token"}"#).unwrap(), - } +pub fn send_token(jwt_message: &str) -> HTTPResponse { + let message = if jwt_message != "" { + jwt_message + } else { + r#"{"token": "error.generation.token"}"# }; - - HTTPResponse::as_200(Some(json)) + HTTPResponse::as_200(Some(json::parse(message).unwrap())) } // this MUST be used like a Singleton @@ -179,9 +161,9 @@ async fn test_route() { let config: Config = Config::default(); let request_str = "POST /get/ HTTP/1.1\r\n\r\n"; - let response: HTTPResponse = router.route(request_str, "".to_string(), config).await; + let response: HTTPResponse = router.route(request_str, config).await; assert_eq!( HTTPStatusCode::Http400, - response.status_line.get_status_code() + response.get_status_code() ); } diff --git a/src/stores/file.rs b/src/stores/file.rs index 90a5b12..6526cac 100644 --- a/src/stores/file.rs +++ b/src/stores/file.rs @@ -3,7 +3,7 @@ use std::path::Path; use super::store::{Credentials, Store}; -/// references a credentials store file +/// FileStore references a credentials store file pub struct FileStore { path: String, credentials: Vec, @@ -17,7 +17,7 @@ impl FileStore { } } - /// loads and reads the file asynchonously + /// parse_contents loads and reads the file asynchonously /// parses the file line by line to retrieve the credentials async fn parse_contents(&mut self) { let contents = tokio::fs::read_to_string(&self.path).await; @@ -79,13 +79,14 @@ async fn test_store() { use std::env; let root_path = env::var("CARGO_MANIFEST_DIR").unwrap(); - // TODO: path::Path should be better let store_path = format!("{}/{}/{}/{}", root_path, "tests", "data", "store.txt"); let mut store = FileStore::new(store_path); let data = json::parse(r#"{"email": "toto@toto.fr", "password": "tata"}"#).unwrap(); - let credentials = store.is_auth(&data).await; - assert_eq!(false, credentials.is_none()); - assert_eq!(credentials.unwrap().email, "toto@toto.fr"); + let credentials = Credentials::from(&data); + assert_eq!(credentials.get_email(), "toto@toto.fr"); + + let is_auth = store.is_auth(&credentials).await; + assert_eq!(true, is_auth); } diff --git a/tests/bash/curling.bash b/tests/bash/curling.bash index fe863c4..bee216a 100755 --- a/tests/bash/curling.bash +++ b/tests/bash/curling.bash @@ -16,15 +16,31 @@ fi for i in {0..10} do http_response=$(curl -s -o response.txt -w "%{http_code}" ${URL}/get/ -d '{"username":"toto", "password":"tutu"}') + if [ $http_response != "400" ] + then + echo "bad http status code : ${http_response}, expect 400" + exit 1 + fi + + if [ "$(cat response.txt | jq -r '.error')" != "bad request" ] + then + echo "bad data returned, expect : bad request" + exit 1 + fi +done + +for i in {0..10} +do + http_response=$(curl -s -o response.txt -w "%{http_code}" ${URL}/get/ -d '{"email":"toto", "password":"tutu"}') if [ $http_response != "403" ] then - echo "bad http status code : ${http_response}, expect 200" + echo "bad http status code : ${http_response}, expect 403" exit 1 fi if [ "$(cat response.txt | jq -r '.error')" != "url forbidden" ] then - echo "bad data returned, expect : invalid credentials" + echo "bad data returned, expect : url forbidden" exit 1 fi done @@ -35,7 +51,7 @@ do http_response=$(curl -s -o response.txt -w "%{http_code}" ${URL}/ge/ -d '{"username":"toto", "password":"tutu"}') if [ $http_response != "404" ] then - echo "bad http status code : ${http_response}, expect 400" + echo "bad http status code : ${http_response}, expect 404" exit 1 fi done @@ -46,7 +62,7 @@ do http_response=$(curl -s -o response.txt -w "%{http_code}" ${URL}/pubkey/) if [ $http_response != "200" ] then - echo "bad http status code : ${http_response}, expect 400" + echo "bad http status code : ${http_response}, expect 200" exit 1 fi done diff --git a/tests/python/test_requests.py b/tests/python/test_requests.py index 8701f04..11e6a39 100644 --- a/tests/python/test_requests.py +++ b/tests/python/test_requests.py @@ -22,7 +22,7 @@ class TestResponse(TestCase): self.assertEqual(resp.status_code, 200, "bad status code returned") self.assertIsNotNone(resp.json(), "response data can't be empty") - token = resp.json()["token"] + token = resp.json()["access_token"] jwt_decoded = jwt.decode( token, pubkey or self.pub_key, From 530063f8ffab7c4542883bc891294352979d2e24 Mon Sep 17 00:00:00 2001 From: landrigun Date: Thu, 16 Feb 2023 10:46:30 +0000 Subject: [PATCH 3/7] format code --- src/router/router.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/router/router.rs b/src/router/router.rs index 9fbe360..36e10e8 100644 --- a/src/router/router.rs +++ b/src/router/router.rs @@ -162,8 +162,5 @@ async fn test_route() { let request_str = "POST /get/ HTTP/1.1\r\n\r\n"; let response: HTTPResponse = router.route(request_str, config).await; - assert_eq!( - HTTPStatusCode::Http400, - response.get_status_code() - ); + assert_eq!(HTTPStatusCode::Http400, response.get_status_code()); } From f108c592a96dfa8996c4b0a371cc936a615358c3 Mon Sep 17 00:00:00 2001 From: landrigun Date: Thu, 16 Feb 2023 11:00:11 +0000 Subject: [PATCH 4/7] impl ValidationMessage using serde_json to build json response body --- src/router/router.rs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/router/router.rs b/src/router/router.rs index 36e10e8..2182fed 100644 --- a/src/router/router.rs +++ b/src/router/router.rs @@ -3,6 +3,7 @@ use http::{HTTPRequest, HTTPResponse, JSONMessage}; use json::JsonValue; +use serde::Serialize; use crate::config::Config; use crate::jwt::{JWTMessage, JWTSigner}; @@ -13,6 +14,14 @@ const GET_ROUTE: &'static str = "/get/"; const VALIDATE_ROUTE: &'static str = "/validate/"; const PUBKEY_ROUTE: &'static str = "/pubkey/"; +#[derive(Serialize, Default)] +/// ValidationMessage aims to build a JSON HTTP response body for JWT validation +struct ValidationMessage { + is_valid: bool, + #[serde(skip_serializing_if = "String::is_empty")] + reason: String, +} + async fn handle_get(request: HTTPRequest<'_>, config: Config, method: &str) -> HTTPResponse { if method.trim().to_lowercase() != "post" { return HTTPResponse::as_400(); @@ -54,7 +63,7 @@ async fn handle_get(request: HTTPRequest<'_>, config: Config, method: &str) -> H } } -/// validates the token by checking: +/// handle_validate validates the token by checking: /// * expiration time /// * signature async fn handle_validate(request: HTTPRequest<'_>, config: Config, method: &str) -> HTTPResponse { @@ -87,22 +96,21 @@ async fn handle_validate(request: HTTPRequest<'_>, config: Config, method: &str) } }; - let mut message = JSONMessage::default(); + let mut message = ValidationMessage::default(); match jwt_signer.validate(&token) { Ok(()) => { - message.put("valid", "true"); + message.is_valid = true; } Err(e) => { - message.put("valid", "false"); - message.put("reason", &e); + message.reason = e; } } - let json: JsonValue = message.try_into().unwrap(); + let json: JsonValue = json::parse(&serde_json::to_string(&message).unwrap()).unwrap(); HTTPResponse::as_200(Some(json)) } -/// returns the JWT public key in base64 encoded +/// handle_public_key returns the JWT public key in base64 encoded async fn handle_public_key(request: HTTPRequest<'_>, config: Config, method: &str) -> HTTPResponse { if request.get_method().trim().to_lowercase() != method { return HTTPResponse::as_400(); From 734ec7901d4635e7f55fc96bd1304ac9d9c5c7b2 Mon Sep 17 00:00:00 2001 From: landrigun Date: Thu, 16 Feb 2023 11:29:21 +0000 Subject: [PATCH 5/7] add a message module holding struct to manager json response body + fix tests --- src/jwt/jwt.rs | 30 +--------------------- src/jwt/mod.rs | 5 ++-- src/main.rs | 1 + src/message/message.rs | 48 +++++++++++++++++++++++++++++++++++ src/message/mod.rs | 4 +++ src/router/router.rs | 23 +++++------------ tests/python/test_requests.py | 6 ++--- 7 files changed, 66 insertions(+), 51 deletions(-) create mode 100644 src/message/message.rs create mode 100644 src/message/mod.rs diff --git a/src/jwt/jwt.rs b/src/jwt/jwt.rs index 3ab3428..f642376 100644 --- a/src/jwt/jwt.rs +++ b/src/jwt/jwt.rs @@ -5,37 +5,9 @@ use serde::{Deserialize, Serialize}; use std::collections::HashSet; use tokio::fs; +use crate::message::JWTMessage; use crate::stores::Credentials; -#[derive(Serialize)] -/// JWTMessage aims to have a generic struct to build HTTP response message with JWT -pub struct JWTMessage { - #[serde(skip_serializing_if = "String::is_empty")] - access_token: String, - #[serde(skip_serializing_if = "String::is_empty")] - refresh_token: String, - #[serde(skip_serializing_if = "String::is_empty")] - pubkey: String, -} - -impl JWTMessage { - pub fn with_access(access_token: String) -> Self { - JWTMessage { - access_token: access_token, - refresh_token: "".to_string(), - pubkey: "".to_string(), - } - } - - pub fn with_pubkey(pubkey: String) -> Self { - JWTMessage { - access_token: "".to_string(), - refresh_token: "".to_string(), - pubkey: base64::encode(pubkey), - } - } -} - #[derive(Serialize, Deserialize)] struct JWTCustomClaims { email: String, diff --git a/src/jwt/mod.rs b/src/jwt/mod.rs index b447102..ae4d8f4 100644 --- a/src/jwt/mod.rs +++ b/src/jwt/mod.rs @@ -1,5 +1,4 @@ -//! jwt module aims to read `.pem` files, sign/validate the token and have useful functions to -//! manage HTTP response message +//! jwt module aims to read `.pem` files, sign/validate the token mod jwt; -pub use jwt::{JWTMessage, JWTSigner}; +pub use jwt::JWTSigner; diff --git a/src/main.rs b/src/main.rs index 9b9384d..d103358 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ mod config; mod jwt; +mod message; mod router; mod stores; diff --git a/src/message/message.rs b/src/message/message.rs new file mode 100644 index 0000000..3dc0cb7 --- /dev/null +++ b/src/message/message.rs @@ -0,0 +1,48 @@ +use serde::Serialize; + +#[derive(Serialize)] +/// JWTMessage aims to have a generic struct to build HTTP response message with JWT +pub struct JWTMessage { + #[serde(skip_serializing_if = "String::is_empty")] + access_token: String, + #[serde(skip_serializing_if = "String::is_empty")] + refresh_token: String, + #[serde(skip_serializing_if = "String::is_empty")] + pubkey: String, +} + +impl JWTMessage { + pub fn with_access(access_token: String) -> Self { + JWTMessage { + access_token: access_token, + refresh_token: "".to_string(), + pubkey: "".to_string(), + } + } + + pub fn with_pubkey(pubkey: String) -> Self { + JWTMessage { + access_token: "".to_string(), + refresh_token: "".to_string(), + pubkey: base64::encode(pubkey), + } + } +} + +#[derive(Serialize, Default)] +/// ValidationMessage aims to build a JSON HTTP response body for JWT validation +pub struct ValidationMessage { + valid: bool, + #[serde(skip_serializing_if = "String::is_empty")] + reason: String, +} + +impl ValidationMessage { + pub fn set_valid(&mut self, valid: bool) { + self.valid = valid; + } + + pub fn set_reason(&mut self, reason: &str) { + self.reason = reason.to_string(); + } +} diff --git a/src/message/mod.rs b/src/message/mod.rs new file mode 100644 index 0000000..5da0789 --- /dev/null +++ b/src/message/mod.rs @@ -0,0 +1,4 @@ +//! message lib holds all structs to manager auth JSON response body +mod message; + +pub use message::{JWTMessage, ValidationMessage}; diff --git a/src/router/router.rs b/src/router/router.rs index 2182fed..d284234 100644 --- a/src/router/router.rs +++ b/src/router/router.rs @@ -3,10 +3,10 @@ use http::{HTTPRequest, HTTPResponse, JSONMessage}; use json::JsonValue; -use serde::Serialize; use crate::config::Config; -use crate::jwt::{JWTMessage, JWTSigner}; +use crate::jwt::JWTSigner; +use crate::message::{JWTMessage, ValidationMessage}; use crate::stores::{Credentials, FileStore, Store}; // TODO: must be mapped with corresponding handler @@ -14,14 +14,6 @@ const GET_ROUTE: &'static str = "/get/"; const VALIDATE_ROUTE: &'static str = "/validate/"; const PUBKEY_ROUTE: &'static str = "/pubkey/"; -#[derive(Serialize, Default)] -/// ValidationMessage aims to build a JSON HTTP response body for JWT validation -struct ValidationMessage { - is_valid: bool, - #[serde(skip_serializing_if = "String::is_empty")] - reason: String, -} - async fn handle_get(request: HTTPRequest<'_>, config: Config, method: &str) -> HTTPResponse { if method.trim().to_lowercase() != "post" { return HTTPResponse::as_400(); @@ -75,11 +67,10 @@ async fn handle_validate(request: HTTPRequest<'_>, config: Config, method: &str) match request.get_body_value("token") { Some(t) => t, None => { - let mut message = JSONMessage::default(); - message.put("valid", "false"); - message.put("reason", "no token provided in the request body"); - let json = message.try_into().unwrap(); + let mut message = ValidationMessage::default(); + message.set_reason("no token provided in the request body"); + let json = json::parse(&serde_json::to_string(&message).unwrap()).unwrap(); return HTTPResponse::as_200(Some(json)); } } @@ -99,10 +90,10 @@ async fn handle_validate(request: HTTPRequest<'_>, config: Config, method: &str) let mut message = ValidationMessage::default(); match jwt_signer.validate(&token) { Ok(()) => { - message.is_valid = true; + message.set_valid(true); } Err(e) => { - message.reason = e; + message.set_reason(&e); } } diff --git a/tests/python/test_requests.py b/tests/python/test_requests.py index 11e6a39..e1c7b19 100644 --- a/tests/python/test_requests.py +++ b/tests/python/test_requests.py @@ -48,14 +48,14 @@ class TestResponse(TestCase): ) 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()["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()["valid"], False, "bad status returned") self.assertEqual( resp.json()["reason"], "token validation failed details=JWT compact encoding error", @@ -67,7 +67,7 @@ class TestResponse(TestCase): 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") + self.assertEqual(resp.json()["valid"], True, "bad status returned") # TODO: must be updated after implementing `/refresh/` url handler def test_refresh_target(self): From b07d4e7e0e0f18be03353558bdc180361e12a353 Mon Sep 17 00:00:00 2001 From: rmanach Date: Thu, 16 Feb 2023 15:43:11 +0100 Subject: [PATCH 6/7] fix documentation + update http lib version --- Cargo.toml | 2 +- README.md | 9 ++++----- src/config/mod.rs | 3 ++- src/jwt/mod.rs | 3 ++- src/main.rs | 2 +- src/message/message.rs | 2 +- src/message/mod.rs | 3 ++- src/router/mod.rs | 2 +- src/router/router.rs | 7 ++----- src/stores/file.rs | 3 ++- src/stores/mod.rs | 7 +++---- src/stores/store.rs | 1 + 12 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ad33220..f327306 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ log = "0.4.17" base64 = "0.13.1" serde_json = "1.0" -http = { git = "https://gitea.thegux.fr/rmanach/http", version = "0.1.5" } +http = { git = "https://gitea.thegux.fr/rmanach/http", version = "0.1.6" } # useful for tests (embedded files should be delete in release ?) #rust-embed="6.4.1" diff --git a/README.md b/README.md index b5e57fc..a0238c6 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ cargo build --release ``` ## Configuration - ### Store The store represents the credentials. For now, this a `.txt` file with plain passwords. You have to create one like: ```txt @@ -72,7 +71,7 @@ cargo test * set the following env variables: ```bash export SIMPLE_AUTH_URL="http://:" -export SIMPLE_AUTH_PUB_KEY="" # DO NOT USE THE ONE IN PRODUCTION ! +export SIMPLE_AUTH_PUB_KEY="" # DO NOT USE THIS ONE IN PRODUCTION ! ``` * run the server (if no one is running remotly) * run curl tests @@ -80,14 +79,14 @@ export SIMPLE_AUTH_PUB_KEY="" # DO NOT USE THE ONE IN PRODU cd tests/bash/ ./curling.bash && echo "passed" ``` -* run python requests tests +* run python tests ```bash # create a python venv cd tests/python python3 -m venv venv source venv/bin/activate -# intall the requirements +# install the requirements pip install -r requirements # launch the tests @@ -97,5 +96,5 @@ python -m unittest ## Documentation ```bash # add the '--open' arg to open the doc on a browser -cargo doc --no-deps +cargo doc -r --no-deps ``` diff --git a/src/config/mod.rs b/src/config/mod.rs index b82b271..bd20f7d 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,4 +1,5 @@ -//! provides `Config` struct to load and validate `.ini` file +//! **config** module provides `Config` struct to load and validate `.ini` file. + mod config; pub use config::Config; diff --git a/src/jwt/mod.rs b/src/jwt/mod.rs index ae4d8f4..7eb999b 100644 --- a/src/jwt/mod.rs +++ b/src/jwt/mod.rs @@ -1,4 +1,5 @@ -//! jwt module aims to read `.pem` files, sign/validate the token +//! **jwt** module aims to read `.pem` files and sign/validate the token. + mod jwt; pub use jwt::JWTSigner; diff --git a/src/main.rs b/src/main.rs index d103358..90f47ee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -67,7 +67,7 @@ async fn main() { } } -/// parses the incoming request (partial spec implementation) and build an HTTP response +/// handle_connection parses the incoming request and builds an HTTP response async fn handle_connection(mut stream: TcpStream, addr: String, config: Config) { log::info!("client connected: {}", addr); diff --git a/src/message/message.rs b/src/message/message.rs index 3dc0cb7..8b365c8 100644 --- a/src/message/message.rs +++ b/src/message/message.rs @@ -1,7 +1,7 @@ use serde::Serialize; #[derive(Serialize)] -/// JWTMessage aims to have a generic struct to build HTTP response message with JWT +/// JWTMessage aims to have a generic struct to build JSON HTTP response message with JWT informations pub struct JWTMessage { #[serde(skip_serializing_if = "String::is_empty")] access_token: String, diff --git a/src/message/mod.rs b/src/message/mod.rs index 5da0789..072badf 100644 --- a/src/message/mod.rs +++ b/src/message/mod.rs @@ -1,4 +1,5 @@ -//! message lib holds all structs to manager auth JSON response body +//! **message** module holds all structs to manage JSON response body for the authentication. + mod message; pub use message::{JWTMessage, ValidationMessage}; diff --git a/src/router/mod.rs b/src/router/mod.rs index 16b5ebb..343295a 100644 --- a/src/router/mod.rs +++ b/src/router/mod.rs @@ -1,4 +1,4 @@ -//! router module includes all the handlers to get and validate JWT +//! **router** module includes all the handlers to get and validate JWT. mod router; pub use router::ROUTER; diff --git a/src/router/router.rs b/src/router/router.rs index d284234..63dcd00 100644 --- a/src/router/router.rs +++ b/src/router/router.rs @@ -1,6 +1,3 @@ -//! router aims to handle correctly the request corresponding to the target -//! it implements all the logic to build an `HTTPResponse` - use http::{HTTPRequest, HTTPResponse, JSONMessage}; use json::JsonValue; @@ -127,7 +124,7 @@ async fn handle_public_key(request: HTTPRequest<'_>, config: Config, method: &st pub struct Router; impl Router { - /// route routes the request to the corresponding handling method + /// route routes the request to the corresponding handler pub async fn route(&self, request_str: &str, config: Config) -> HTTPResponse { let request = HTTPRequest::from(request_str); match request.get_target() { @@ -149,7 +146,7 @@ pub fn send_token(jwt_message: &str) -> HTTPResponse { HTTPResponse::as_200(Some(json::parse(message).unwrap())) } -// this MUST be used like a Singleton +// this **MUST** be used like a Singleton pub const ROUTER: Router = Router {}; #[tokio::test] diff --git a/src/stores/file.rs b/src/stores/file.rs index 6526cac..5783320 100644 --- a/src/stores/file.rs +++ b/src/stores/file.rs @@ -18,7 +18,8 @@ impl FileStore { } /// parse_contents loads and reads the file asynchonously - /// parses the file line by line to retrieve the credentials + /// + /// It parses the file line by line to retrieve the credentials async fn parse_contents(&mut self) { let contents = tokio::fs::read_to_string(&self.path).await; let mut credentials: Vec = vec![]; diff --git a/src/stores/mod.rs b/src/stores/mod.rs index 2a768fc..ae56d8e 100644 --- a/src/stores/mod.rs +++ b/src/stores/mod.rs @@ -1,8 +1,7 @@ -//! store module lists interfaces available to check request credentials -//! each store must implement the trait `is_auth` -//! two stores are available : +//! **store** module lists interfaces available to check credentials. Each store must implement the trait `is_auth`. +//! +//! For now one store is available: //! * `FileStore`: credentials stored in a text file (like **/etc/passwd**) -//! * `DBStore`: credentials stored in a database (TODO) mod file; mod store; diff --git a/src/stores/store.rs b/src/stores/store.rs index 9cf23ef..e5fa8fb 100644 --- a/src/stores/store.rs +++ b/src/stores/store.rs @@ -13,6 +13,7 @@ pub struct Credentials { password: String, } +/// Credentials represents the incoming user credentials for authentication checking impl Credentials { pub fn new(email: String, password: String) -> Self { Credentials { email, password } From d974cd718be61c4a407c1403c0af217afbb9e7e5 Mon Sep 17 00:00:00 2001 From: landrigun Date: Thu, 16 Feb 2023 14:46:57 +0000 Subject: [PATCH 7/7] take Cargo.lock updates --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fcbf360..da33ad9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -635,8 +635,8 @@ dependencies = [ [[package]] name = "http" -version = "0.1.5" -source = "git+https://gitea.thegux.fr/rmanach/http#72bf34127b185f5a52895c08a3d8b229c0588dc0" +version = "0.1.6" +source = "git+https://gitea.thegux.fr/rmanach/http#0e616570907f3427be99f4bfc227bd57a252c8c1" dependencies = [ "json", "lazy_static",