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,