diff --git a/Cargo.lock b/Cargo.lock index 79283d8..cbfb905 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1231,7 +1231,7 @@ dependencies = [ [[package]] name = "simple-auth" -version = "0.2.0" +version = "0.3.0" dependencies = [ "async-std", "async-trait", @@ -1243,6 +1243,7 @@ dependencies = [ "lazy_static", "log", "regex", + "serde", "simple_logger", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index e10da92..2434003 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "simple-auth" -version = "0.3.0" +version = "0.3.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -30,3 +30,7 @@ features = ["derive"] [dependencies.async-std] version = "1.6" features = ["attributes"] + +[dependencies.serde] +version = "1.0" +features = ["derive"] diff --git a/README.md b/README.md index e58d5c2..b5e57fc 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ cargo build --release The store represents the credentials. For now, this a `.txt` file with plain passwords. You have to create one like: ```txt # acts as a comment (only on a start line) -: +: ``` **WARN**: the file should have a chmod to **600**. @@ -46,7 +46,7 @@ expiration_time = 2 # in hours ./simple-auth # get a JWT -curl http://:/get/ -d '{"username":"", "password":""}' +curl http://:/get/ -d '{"email":"", "password":""}' # should returned {"token":"
.."} diff --git a/src/http/message.rs b/src/http/message.rs index 78b5ac4..cbf4287 100644 --- a/src/http/message.rs +++ b/src/http/message.rs @@ -68,14 +68,14 @@ impl HTTPMessage { #[test] fn test_message() { let mut http_message = HTTPMessage::default(); - http_message.put("username", "toto"); + http_message.put("email", "toto@toto.fr"); 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("email")); assert!(json.has_key("password")); let empty_http_message = HTTPMessage::default(); diff --git a/src/http/router.rs b/src/http/router.rs index f12b677..2ddaae5 100644 --- a/src/http/router.rs +++ b/src/http/router.rs @@ -22,8 +22,8 @@ async fn handle_get(request: HTTPRequest, config: Config, method: &str) -> HTTPR 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 { + let credentials = store.is_auth(&b.get_data()).await; + if credentials.is_none() { return HTTPResponse::as_403(); } @@ -37,7 +37,7 @@ async fn handle_get(request: HTTPRequest, config: Config, method: &str) -> HTTPR } }; - match jwt_signer.sign() { + match jwt_signer.sign(credentials.unwrap().email) { Ok(t) => HTTPResponse::send_token(&t), Err(e) => { let message = HTTPMessage::error(&e); diff --git a/src/jwt/jwt.rs b/src/jwt/jwt.rs index 8772517..56f3aa8 100644 --- a/src/jwt/jwt.rs +++ b/src/jwt/jwt.rs @@ -1,9 +1,15 @@ use crate::config::Config; use jwt_simple::common::VerificationOptions; use jwt_simple::prelude::*; +use serde::{Deserialize, Serialize}; use std::collections::HashSet; use tokio::fs; +#[derive(Serialize, Deserialize)] +struct JWTCustomClaims { + email: String, +} + pub struct JWTSigner { private_key: String, public_key: String, @@ -53,7 +59,7 @@ impl JWTSigner { } /// builds and signs the token - pub fn sign(&self) -> Result { + pub fn sign(&self, email: String) -> Result { let jwt_key = { match RS384KeyPair::from_pem(&self.private_key) { Ok(k) => k, @@ -62,7 +68,10 @@ impl JWTSigner { } } }; - let mut claims = Claims::create(Duration::from_hours(self.exp_time)); + let mut claims = Claims::with_custom_claims( + JWTCustomClaims { email }, + Duration::from_hours(self.exp_time), + ); claims.issuer = Some(self.issuer.clone()); match jwt_key.sign(claims) { diff --git a/src/stores/file.rs b/src/stores/file.rs index d5ab489..3f0cb1b 100644 --- a/src/stores/file.rs +++ b/src/stores/file.rs @@ -48,36 +48,40 @@ impl FileStore { } /// checks if the credentials exist in the `FileStore` - fn auth(&self, username: String, password: String) -> bool { + fn auth(&self, email: String, password: String) -> Option { let credentials: Vec<&Credentials> = self .credentials .iter() - .filter(|x| x.username == username && x.password == password) + .filter(|x| x.email == email && x.password == password) .collect(); if credentials.len() == 1 { - return true; + // no need to store the password again + return Some(Credentials::new( + credentials[0].email.clone(), + "".to_string(), + )); } - false + None } } #[async_trait] impl Store for FileStore { - async fn is_auth(&mut self, data: &json::JsonValue) -> bool { + async fn is_auth(&mut self, data: &json::JsonValue) -> Option { // 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 false; + return None; } let credentials = Credentials::from(data); if credentials.is_empty() { log::error!("unable to parse the credentials correctly from the incoming request"); - return false; + return None; } self.parse_contents().await; - self.auth(credentials.username, credentials.password) + self.auth(credentials.email, credentials.password) } } @@ -91,6 +95,8 @@ async fn test_store() { let mut store = FileStore::new(store_path); - let data = json::parse(r#"{"username": "toto", "password": "tata"}"#).unwrap(); - assert_eq!(store.is_auth(&data).await, true); + 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"); } diff --git a/src/stores/mod.rs b/src/stores/mod.rs index 470c347..2a768fc 100644 --- a/src/stores/mod.rs +++ b/src/stores/mod.rs @@ -8,4 +8,4 @@ mod file; mod store; pub use file::FileStore; -pub use store::Store; +pub use store::{Credentials, Store}; diff --git a/src/stores/store.rs b/src/stores/store.rs index 6011da5..f034103 100644 --- a/src/stores/store.rs +++ b/src/stores/store.rs @@ -5,22 +5,22 @@ use crate::utils::extract_json_value; #[async_trait] pub trait Store { - async fn is_auth(&mut self, data: &json::JsonValue) -> bool; + async fn is_auth(&mut self, data: &json::JsonValue) -> Option; } #[derive(Default, Debug)] pub struct Credentials { - pub username: String, + pub email: String, pub password: String, } impl Credentials { - pub fn new(username: String, password: String) -> Self { - Credentials { username, password } + pub fn new(email: String, password: String) -> Self { + Credentials { email, password } } pub fn is_empty(&self) -> bool { - self.username == "" || self.password == "" + self.email == "" || self.password == "" } } @@ -29,7 +29,7 @@ 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").unwrap_or("".to_string()); + credentials.email = extract_json_value(&d, "email").unwrap_or("".to_string()); credentials.password = extract_json_value(&d, "password").unwrap_or("".to_string()); } _ => return credentials, @@ -50,7 +50,7 @@ fn test_credentials() { is_empty: true }, Expect { - data: json::parse(r#"{"username":"toto","password": "tata"}"#).unwrap(), + data: json::parse(r#"{"email":"toto@toto.fr","password": "tata"}"#).unwrap(), is_empty: false } ]; diff --git a/tests/data/store.txt b/tests/data/store.txt index a9b0316..5bfa03f 100644 --- a/tests/data/store.txt +++ b/tests/data/store.txt @@ -1,4 +1,4 @@ # this a test password storage with password in clear # need to be updated in the future to encrypt or hash the password -# : -toto:tata +# : +toto@toto.fr:tata diff --git a/tests/python/test_requests.py b/tests/python/test_requests.py index 9247873..16be103 100644 --- a/tests/python/test_requests.py +++ b/tests/python/test_requests.py @@ -15,9 +15,9 @@ class TestResponse(TestCase): with open(PUB_KEY_PATH, "r") as f: self.pub_key = f.read() - def test_get_target(self): + def test_get_target(self, pubkey=None): resp = requests.post( - URL + "/get/", json={"username": "toto", "password": "tata"} + URL + "/get/", json={"email": "toto@toto.fr", "password": "tata"} ) self.assertEqual(resp.status_code, 200, "bad status code returned") self.assertIsNotNone(resp.json(), "response data can't be empty") @@ -25,7 +25,7 @@ class TestResponse(TestCase): token = resp.json()["token"] jwt_decoded = jwt.decode( token, - self.pub_key, + pubkey or self.pub_key, algorithms=["RS384"], options={ "verify_signature": True, @@ -34,6 +34,7 @@ class TestResponse(TestCase): }, ) self.assertEqual("thegux.fr", jwt_decoded["iss"]) + self.assertEqual("toto@toto.fr", jwt_decoded["email"]) jwt_exp = datetime.fromtimestamp(jwt_decoded["exp"]) jwt_iat = datetime.fromtimestamp(jwt_decoded["iat"]) @@ -43,7 +44,7 @@ class TestResponse(TestCase): def test_validate_target_no_token(self): resp = requests.post( - URL + "/validate/", json={"username": "toto", "password": "tata"} + URL + "/validate/", json={"username": "toto@toto.fr", "password": "tata"} ) self.assertEqual(resp.status_code, 200, "bad status code returned") self.assertIsNotNone(resp.json(), "response data can't be empty") @@ -92,9 +93,7 @@ class TestResponse(TestCase): ) def test_bad_credentials(self): - resp = requests.post( - URL + "/get/", json={"username": "tutu", "password": "titi"} - ) + resp = requests.post(URL + "/get/", json={"email": "tutu", "password": "titi"}) self.assertEqual(resp.status_code, 403, "bad status code returned") self.assertIsNotNone(resp.json(), "response data must not be empty") self.assertEqual( @@ -123,7 +122,10 @@ class TestResponse(TestCase): b64_pubkey = base64.b64decode(resp.json()["pubkey"]) self.assertIsNotNone(b64_pubkey, "public key b64 decoded can't be empty") - self.assertIn("-BEGIN PUBLIC KEY-", b64_pubkey.decode()) + b64_pubkey_decoded = b64_pubkey.decode() + self.assertIn("-BEGIN PUBLIC KEY-", b64_pubkey_decoded) + + self.test_get_target(b64_pubkey_decoded) def test_get_pubkey_bad_method(self): resp = requests.post(URL + "/pubkey/", json={"tutu": "toto"})