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]] | [[package]] | ||||||
| name = "simple-auth" | name = "simple-auth" | ||||||
| version = "0.2.0" | version = "0.3.0" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "async-std", |  "async-std", | ||||||
|  "async-trait", |  "async-trait", | ||||||
| @ -1243,6 +1243,7 @@ dependencies = [ | |||||||
|  "lazy_static", |  "lazy_static", | ||||||
|  "log", |  "log", | ||||||
|  "regex", |  "regex", | ||||||
|  |  "serde", | ||||||
|  "simple_logger", |  "simple_logger", | ||||||
|  "tokio", |  "tokio", | ||||||
| ] | ] | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| [package] | [package] | ||||||
| name = "simple-auth" | name = "simple-auth" | ||||||
| version = "0.3.0" | version = "0.3.1" | ||||||
| edition = "2021" | edition = "2021" | ||||||
| 
 | 
 | ||||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | # 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] | [dependencies.async-std] | ||||||
| version = "1.6" | version = "1.6" | ||||||
| features = ["attributes"] | 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: | The store represents the credentials. For now, this a `.txt` file with plain passwords. You have to create one like: | ||||||
| ```txt | ```txt | ||||||
| # acts as a comment (only on a start line) | # acts as a comment (only on a start line) | ||||||
| <username>:<password> | <email>:<password> | ||||||
| ``` | ``` | ||||||
| **WARN**: the file should have a chmod to **600**. | **WARN**: the file should have a chmod to **600**. | ||||||
| 
 | 
 | ||||||
| @ -46,7 +46,7 @@ expiration_time = 2 # in hours | |||||||
| ./simple-auth <ini_path> | ./simple-auth <ini_path> | ||||||
| 
 | 
 | ||||||
| # get a JWT | # 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 | # should returned | ||||||
| {"token":"<header>.<payload>.<signature>"} | {"token":"<header>.<payload>.<signature>"} | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -68,14 +68,14 @@ impl HTTPMessage { | |||||||
| #[test] | #[test] | ||||||
| fn test_message() { | fn test_message() { | ||||||
|     let mut http_message = HTTPMessage::default(); |     let mut http_message = HTTPMessage::default(); | ||||||
|     http_message.put("username", "toto"); |     http_message.put("email", "toto@toto.fr"); | ||||||
|     http_message.put("password", "tata"); |     http_message.put("password", "tata"); | ||||||
| 
 | 
 | ||||||
|     let mut json_result: Result<json::JsonValue, String> = http_message.try_into(); |     let mut json_result: Result<json::JsonValue, String> = http_message.try_into(); | ||||||
|     assert!(json_result.is_ok()); |     assert!(json_result.is_ok()); | ||||||
| 
 | 
 | ||||||
|     let mut json = json_result.unwrap(); |     let mut json = json_result.unwrap(); | ||||||
|     assert!(json.has_key("username")); |     assert!(json.has_key("email")); | ||||||
|     assert!(json.has_key("password")); |     assert!(json.has_key("password")); | ||||||
| 
 | 
 | ||||||
|     let empty_http_message = HTTPMessage::default(); |     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()); |     let mut store = FileStore::new(config.filestore_path.clone()); | ||||||
|     match &request.body { |     match &request.body { | ||||||
|         Some(ref b) => { |         Some(ref b) => { | ||||||
|             let is_auth = store.is_auth(&b.get_data()).await; |             let credentials = store.is_auth(&b.get_data()).await; | ||||||
|             if !is_auth { |             if credentials.is_none() { | ||||||
|                 return HTTPResponse::as_403(); |                 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), |                 Ok(t) => HTTPResponse::send_token(&t), | ||||||
|                 Err(e) => { |                 Err(e) => { | ||||||
|                     let message = HTTPMessage::error(&e); |                     let message = HTTPMessage::error(&e); | ||||||
|  | |||||||
| @ -1,9 +1,15 @@ | |||||||
| use crate::config::Config; | use crate::config::Config; | ||||||
| use jwt_simple::common::VerificationOptions; | use jwt_simple::common::VerificationOptions; | ||||||
| use jwt_simple::prelude::*; | use jwt_simple::prelude::*; | ||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
| use std::collections::HashSet; | use std::collections::HashSet; | ||||||
| use tokio::fs; | use tokio::fs; | ||||||
| 
 | 
 | ||||||
|  | #[derive(Serialize, Deserialize)] | ||||||
|  | struct JWTCustomClaims { | ||||||
|  |     email: String, | ||||||
|  | } | ||||||
|  | 
 | ||||||
| pub struct JWTSigner { | pub struct JWTSigner { | ||||||
|     private_key: String, |     private_key: String, | ||||||
|     public_key: String, |     public_key: String, | ||||||
| @ -53,7 +59,7 @@ impl JWTSigner { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// builds and signs the token
 |     /// builds and signs the token
 | ||||||
|     pub fn sign(&self) -> Result<String, String> { |     pub fn sign(&self, email: String) -> Result<String, String> { | ||||||
|         let jwt_key = { |         let jwt_key = { | ||||||
|             match RS384KeyPair::from_pem(&self.private_key) { |             match RS384KeyPair::from_pem(&self.private_key) { | ||||||
|                 Ok(k) => k, |                 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()); |         claims.issuer = Some(self.issuer.clone()); | ||||||
| 
 | 
 | ||||||
|         match jwt_key.sign(claims) { |         match jwt_key.sign(claims) { | ||||||
|  | |||||||
| @ -48,36 +48,40 @@ impl FileStore { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// checks if the credentials exist in the `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 |         let credentials: Vec<&Credentials> = self | ||||||
|             .credentials |             .credentials | ||||||
|             .iter() |             .iter() | ||||||
|             .filter(|x| x.username == username && x.password == password) |             .filter(|x| x.email == email && x.password == password) | ||||||
|             .collect(); |             .collect(); | ||||||
|         if credentials.len() == 1 { |         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] | #[async_trait] | ||||||
| impl Store for FileStore { | 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
 |         // ensure that the store file already exists even after its instanciation
 | ||||||
|         if !Path::new(&self.path).is_file() { |         if !Path::new(&self.path).is_file() { | ||||||
|             log::error!("{} path referencing file store does not exist", self.path); |             log::error!("{} path referencing file store does not exist", self.path); | ||||||
|             return false; |             return None; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         let credentials = Credentials::from(data); |         let credentials = Credentials::from(data); | ||||||
|         if credentials.is_empty() { |         if credentials.is_empty() { | ||||||
|             log::error!("unable to parse the credentials correctly from the incoming request"); |             log::error!("unable to parse the credentials correctly from the incoming request"); | ||||||
|             return false; |             return None; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         self.parse_contents().await; |         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 mut store = FileStore::new(store_path); | ||||||
| 
 | 
 | ||||||
|     let data = json::parse(r#"{"username": "toto", "password": "tata"}"#).unwrap(); |     let data = json::parse(r#"{"email": "toto@toto.fr", "password": "tata"}"#).unwrap(); | ||||||
|     assert_eq!(store.is_auth(&data).await, true); |     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; | mod store; | ||||||
| 
 | 
 | ||||||
| pub use file::FileStore; | 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] | #[async_trait] | ||||||
| pub trait Store { | 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)] | #[derive(Default, Debug)] | ||||||
| pub struct Credentials { | pub struct Credentials { | ||||||
|     pub username: String, |     pub email: String, | ||||||
|     pub password: String, |     pub password: String, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl Credentials { | impl Credentials { | ||||||
|     pub fn new(username: String, password: String) -> Self { |     pub fn new(email: String, password: String) -> Self { | ||||||
|         Credentials { username, password } |         Credentials { email, password } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub fn is_empty(&self) -> bool { |     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(); |         let mut credentials = Credentials::default(); | ||||||
|         match data { |         match data { | ||||||
|             json::JsonValue::Object(ref d) => { |             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()); |                 credentials.password = extract_json_value(&d, "password").unwrap_or("".to_string()); | ||||||
|             } |             } | ||||||
|             _ => return credentials, |             _ => return credentials, | ||||||
| @ -50,7 +50,7 @@ fn test_credentials() { | |||||||
|             is_empty: true |             is_empty: true | ||||||
|         }, |         }, | ||||||
|         Expect { |         Expect { | ||||||
|             data: json::parse(r#"{"username":"toto","password": "tata"}"#).unwrap(), |             data: json::parse(r#"{"email":"toto@toto.fr","password": "tata"}"#).unwrap(), | ||||||
|             is_empty: false |             is_empty: false | ||||||
|         } |         } | ||||||
|     ]; |     ]; | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| # this a test password storage with password in clear | # this a test password storage with password in clear | ||||||
| # need to be updated in the future to encrypt or hash the password | # need to be updated in the future to encrypt or hash the password | ||||||
| # <username>:<password> | # <email>:<password> | ||||||
| toto:tata | toto@toto.fr:tata | ||||||
|  | |||||||
| @ -15,9 +15,9 @@ class TestResponse(TestCase): | |||||||
|         with open(PUB_KEY_PATH, "r") as f: |         with open(PUB_KEY_PATH, "r") as f: | ||||||
|             self.pub_key = f.read() |             self.pub_key = f.read() | ||||||
| 
 | 
 | ||||||
|     def test_get_target(self): |     def test_get_target(self, pubkey=None): | ||||||
|         resp = requests.post( |         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.assertEqual(resp.status_code, 200, "bad status code returned") | ||||||
|         self.assertIsNotNone(resp.json(), "response data can't be empty") |         self.assertIsNotNone(resp.json(), "response data can't be empty") | ||||||
| @ -25,7 +25,7 @@ class TestResponse(TestCase): | |||||||
|         token = resp.json()["token"] |         token = resp.json()["token"] | ||||||
|         jwt_decoded = jwt.decode( |         jwt_decoded = jwt.decode( | ||||||
|             token, |             token, | ||||||
|             self.pub_key, |             pubkey or self.pub_key, | ||||||
|             algorithms=["RS384"], |             algorithms=["RS384"], | ||||||
|             options={ |             options={ | ||||||
|                 "verify_signature": True, |                 "verify_signature": True, | ||||||
| @ -34,6 +34,7 @@ class TestResponse(TestCase): | |||||||
|             }, |             }, | ||||||
|         ) |         ) | ||||||
|         self.assertEqual("thegux.fr", jwt_decoded["iss"]) |         self.assertEqual("thegux.fr", jwt_decoded["iss"]) | ||||||
|  |         self.assertEqual("toto@toto.fr", jwt_decoded["email"]) | ||||||
| 
 | 
 | ||||||
|         jwt_exp = datetime.fromtimestamp(jwt_decoded["exp"]) |         jwt_exp = datetime.fromtimestamp(jwt_decoded["exp"]) | ||||||
|         jwt_iat = datetime.fromtimestamp(jwt_decoded["iat"]) |         jwt_iat = datetime.fromtimestamp(jwt_decoded["iat"]) | ||||||
| @ -43,7 +44,7 @@ class TestResponse(TestCase): | |||||||
| 
 | 
 | ||||||
|     def test_validate_target_no_token(self): |     def test_validate_target_no_token(self): | ||||||
|         resp = requests.post( |         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.assertEqual(resp.status_code, 200, "bad status code returned") | ||||||
|         self.assertIsNotNone(resp.json(), "response data can't be empty") |         self.assertIsNotNone(resp.json(), "response data can't be empty") | ||||||
| @ -92,9 +93,7 @@ class TestResponse(TestCase): | |||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|     def test_bad_credentials(self): |     def test_bad_credentials(self): | ||||||
|         resp = requests.post( |         resp = requests.post(URL + "/get/", json={"email": "tutu", "password": "titi"}) | ||||||
|             URL + "/get/", json={"username": "tutu", "password": "titi"} |  | ||||||
|         ) |  | ||||||
|         self.assertEqual(resp.status_code, 403, "bad status code returned") |         self.assertEqual(resp.status_code, 403, "bad status code returned") | ||||||
|         self.assertIsNotNone(resp.json(), "response data must not be empty") |         self.assertIsNotNone(resp.json(), "response data must not be empty") | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
| @ -123,7 +122,10 @@ class TestResponse(TestCase): | |||||||
| 
 | 
 | ||||||
|         b64_pubkey = base64.b64decode(resp.json()["pubkey"]) |         b64_pubkey = base64.b64decode(resp.json()["pubkey"]) | ||||||
|         self.assertIsNotNone(b64_pubkey, "public key b64 decoded can't be empty") |         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): |     def test_get_pubkey_bad_method(self): | ||||||
|         resp = requests.post(URL + "/pubkey/", json={"tutu": "toto"}) |         resp = requests.post(URL + "/pubkey/", json={"tutu": "toto"}) | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user