Merge branch 'release/v0.3.1'
This commit is contained in:
		
						commit
						83a9c74dcc
					
				
							
								
								
									
										3
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -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", | ||||
| ] | ||||
|  | ||||
| @ -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"] | ||||
|  | ||||
| @ -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) | ||||
| <username>:<password> | ||||
| <email>:<password> | ||||
| ``` | ||||
| **WARN**: the file should have a chmod to **600**. | ||||
| 
 | ||||
| @ -46,7 +46,7 @@ expiration_time = 2 # in hours | ||||
| ./simple-auth <ini_path> | ||||
| 
 | ||||
| # get a JWT | ||||
| curl http://<ip>:<port>/get/ -d '{"username":"<user>", "password":"<password>"}' | ||||
| curl http://<ip>:<port>/get/ -d '{"email":"<email>", "password":"<password>"}' | ||||
| # should returned | ||||
| {"token":"<header>.<payload>.<signature>"} | ||||
| 
 | ||||
|  | ||||
| @ -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<json::JsonValue, String> = 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(); | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
| @ -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<String, String> { | ||||
|     pub fn sign(&self, email: String) -> Result<String, String> { | ||||
|         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) { | ||||
|  | ||||
| @ -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<Credentials> { | ||||
|         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<Credentials> { | ||||
|         // 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"); | ||||
| } | ||||
|  | ||||
| @ -8,4 +8,4 @@ mod file; | ||||
| mod store; | ||||
| 
 | ||||
| pub use file::FileStore; | ||||
| pub use store::Store; | ||||
| pub use store::{Credentials, Store}; | ||||
|  | ||||
| @ -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<Credentials>; | ||||
| } | ||||
| 
 | ||||
| #[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 | ||||
|         } | ||||
|     ]; | ||||
|  | ||||
| @ -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 | ||||
| # <username>:<password> | ||||
| toto:tata | ||||
| # <email>:<password> | ||||
| toto@toto.fr:tata | ||||
|  | ||||
| @ -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"}) | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user