feat: #13 impl the JWT validation + some fixes
This commit is contained in:
		
							parent
							
								
									6c79c3d708
								
							
						
					
					
						commit
						b73add00c5
					
				
							
								
								
									
										14
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								README.md
									
									
									
									
									
								
							| @ -48,6 +48,10 @@ expiration_time = 2 # in hours | |||||||
| curl http://<ip>:<port>/get/ -d '{"username":"<user>", "password":"<password>"}' | curl http://<ip>:<port>/get/ -d '{"username":"<user>", "password":"<password>"}' | ||||||
| # should returned | # should returned | ||||||
| {"token":"<header>.<payload>.<signature>"} | {"token":"<header>.<payload>.<signature>"} | ||||||
|  | 
 | ||||||
|  | curl http://<ip>:<port>/validate/ -d '{"token":"<header>.<payload>.<signature>"}' | ||||||
|  | # should returned (if valid) | ||||||
|  | {"valid":"true"} | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ## Test | ## Test | ||||||
| @ -58,7 +62,13 @@ cargo test | |||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ### integration tests | ### integration tests | ||||||
| * run the server locally or remotly (the URL must be changed if needed in `curling.bash` and `test_requests.py`) | * do the **configuration** step for your env tests | ||||||
|  | * set the following env variables: | ||||||
|  | ```bash | ||||||
|  | export SIMPLE_AUTH_URL="http://<url>:<port>" | ||||||
|  | export SIMPLE_AUTH_PUB_KEY="<path_to_pem_pub_key>" # DO NOT USE THE ONE IN PRODUCTION ! | ||||||
|  | ``` | ||||||
|  | * run the server (if no one is running remotly) | ||||||
| * run curl tests | * run curl tests | ||||||
| ```bash | ```bash | ||||||
| cd tests/bash/ | cd tests/bash/ | ||||||
| @ -75,7 +85,7 @@ source venv/bin/activate | |||||||
| pip install -r requirements | pip install -r requirements | ||||||
| 
 | 
 | ||||||
| # launch the tests | # launch the tests | ||||||
| python -m unitest | python -m unittest | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ## Documentation | ## Documentation | ||||||
|  | |||||||
| @ -8,6 +8,8 @@ use lazy_static::lazy_static; | |||||||
| use regex::Regex; | use regex::Regex; | ||||||
| use std::collections::VecDeque; | use std::collections::VecDeque; | ||||||
| 
 | 
 | ||||||
|  | use crate::utils::extract_json_value; | ||||||
|  | 
 | ||||||
| type RequestParts = (String, VecDeque<String>, String); | type RequestParts = (String, VecDeque<String>, String); | ||||||
| 
 | 
 | ||||||
| const HTTP_REQUEST_SEPARATOR: &'static str = "\r\n"; | const HTTP_REQUEST_SEPARATOR: &'static str = "\r\n"; | ||||||
| @ -205,6 +207,17 @@ impl HTTPRequest { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /// retrieve value in `HTTPBody` (returns None if empty or does not exist)
 | ||||||
|  |     pub fn get_body_value(&self, key: &str) -> Option<String> { | ||||||
|  |         match self.body { | ||||||
|  |             Some(ref b) => match &b.data { | ||||||
|  |                 json::JsonValue::Object(d) => extract_json_value(&d, key), | ||||||
|  |                 _ => None, | ||||||
|  |             }, | ||||||
|  |             None => None, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     #[allow(dead_code)] |     #[allow(dead_code)] | ||||||
|     pub fn is_valid(&self) -> bool { |     pub fn is_valid(&self) -> bool { | ||||||
|         return self.start_line.is_valid(); |         return self.start_line.is_valid(); | ||||||
| @ -238,15 +251,17 @@ fn test_request() { | |||||||
|         start_line: String, |         start_line: String, | ||||||
|         body: Option<String>, |         body: Option<String>, | ||||||
|         is_valid: bool, |         is_valid: bool, | ||||||
|  |         has_token: bool, | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     let test_cases: [(String, Expect); 11] = [ |     let test_cases: [(String, Expect); 12] = [ | ||||||
|         ( |         ( | ||||||
|             "POST /get/ HTTP/1.1\r\n\r\n".to_string(), |             "POST /get/ HTTP/1.1\r\n\r\n".to_string(), | ||||||
|             Expect { |             Expect { | ||||||
|                 start_line: "POST /get/ HTTP/1.1".to_string(), |                 start_line: "POST /get/ HTTP/1.1".to_string(), | ||||||
|                 body: None, |                 body: None, | ||||||
|                 is_valid: true, |                 is_valid: true, | ||||||
|  |                 has_token: false, | ||||||
|             }, |             }, | ||||||
|         ), |         ), | ||||||
|         ( |         ( | ||||||
| @ -255,6 +270,7 @@ fn test_request() { | |||||||
|                 start_line: "POST /refresh/ HTTP/2".to_string(), |                 start_line: "POST /refresh/ HTTP/2".to_string(), | ||||||
|                 body: None, |                 body: None, | ||||||
|                 is_valid: true, |                 is_valid: true, | ||||||
|  |                 has_token: false, | ||||||
|             }, |             }, | ||||||
|         ), |         ), | ||||||
|         ( |         ( | ||||||
| @ -263,6 +279,7 @@ fn test_request() { | |||||||
|                 start_line: "POST /validate/ HTTP/1.0".to_string(), |                 start_line: "POST /validate/ HTTP/1.0".to_string(), | ||||||
|                 body: None, |                 body: None, | ||||||
|                 is_valid: true, |                 is_valid: true, | ||||||
|  |                 has_token: false, | ||||||
|             }, |             }, | ||||||
|         ), |         ), | ||||||
|         ( |         ( | ||||||
| @ -271,6 +288,7 @@ fn test_request() { | |||||||
|                 start_line: "GET / HTTP/1.1".to_string(), |                 start_line: "GET / HTTP/1.1".to_string(), | ||||||
|                 body: None, |                 body: None, | ||||||
|                 is_valid: true, |                 is_valid: true, | ||||||
|  |                 has_token: false, | ||||||
|             }, |             }, | ||||||
|         ), |         ), | ||||||
|         // intentionally add HTTP with no version number
 |         // intentionally add HTTP with no version number
 | ||||||
| @ -280,6 +298,7 @@ fn test_request() { | |||||||
|                 start_line: "  UNKNOWN".to_string(), |                 start_line: "  UNKNOWN".to_string(), | ||||||
|                 body: None, |                 body: None, | ||||||
|                 is_valid: false, |                 is_valid: false, | ||||||
|  |                 has_token: false, | ||||||
|             }, |             }, | ||||||
|         ), |         ), | ||||||
|         ( |         ( | ||||||
| @ -288,6 +307,7 @@ fn test_request() { | |||||||
|                 start_line: "  UNKNOWN".to_string(), |                 start_line: "  UNKNOWN".to_string(), | ||||||
|                 body: None, |                 body: None, | ||||||
|                 is_valid: false, |                 is_valid: false, | ||||||
|  |                 has_token: false | ||||||
|             } |             } | ||||||
|         ), |         ), | ||||||
|         ( |         ( | ||||||
| @ -296,6 +316,7 @@ fn test_request() { | |||||||
|                 start_line: "  UNKNOWN".to_string(), |                 start_line: "  UNKNOWN".to_string(), | ||||||
|                 body: None, |                 body: None, | ||||||
|                 is_valid: false, |                 is_valid: false, | ||||||
|  |                 has_token: false | ||||||
|             } |             } | ||||||
|         ), |         ), | ||||||
|         ( |         ( | ||||||
| @ -304,6 +325,7 @@ fn test_request() { | |||||||
|                 start_line: "fjlqskjd /oks?id=65 HTTP/2".to_string(), |                 start_line: "fjlqskjd /oks?id=65 HTTP/2".to_string(), | ||||||
|                 body: None, |                 body: None, | ||||||
|                 is_valid: true, |                 is_valid: true, | ||||||
|  |                 has_token: false | ||||||
|             } |             } | ||||||
|         ), |         ), | ||||||
|         ( |         ( | ||||||
| @ -312,6 +334,7 @@ fn test_request() { | |||||||
|                 start_line: "  UNKNOWN".to_string(), |                 start_line: "  UNKNOWN".to_string(), | ||||||
|                 body: None, |                 body: None, | ||||||
|                 is_valid: false, |                 is_valid: false, | ||||||
|  |                 has_token: false, | ||||||
|             } |             } | ||||||
|         ), |         ), | ||||||
|         ( |         ( | ||||||
| @ -320,6 +343,7 @@ fn test_request() { | |||||||
|                 start_line: "  UNKNOWN".to_string(), |                 start_line: "  UNKNOWN".to_string(), | ||||||
|                 body: Some(r#"{"access_token":"AAAAAAAAAAAA.BBBBBBBBBB.CCCCCCCCCC","refresh_token": "DDDDDDDDDDD.EEEEEEEEEEE.FFFFF"}"#.to_string()), |                 body: Some(r#"{"access_token":"AAAAAAAAAAAA.BBBBBBBBBB.CCCCCCCCCC","refresh_token": "DDDDDDDDDDD.EEEEEEEEEEE.FFFFF"}"#.to_string()), | ||||||
|                 is_valid: false, |                 is_valid: false, | ||||||
|  |                 has_token: false | ||||||
|             } |             } | ||||||
|         ), |         ), | ||||||
|         ( |         ( | ||||||
| @ -328,6 +352,16 @@ fn test_request() { | |||||||
|                 start_line: "POST /refresh/ HTTP/1.1".to_string(), |                 start_line: "POST /refresh/ HTTP/1.1".to_string(), | ||||||
|                 body: Some(r#"{"access_token":"toto","refresh_token":"tutu"}"#.to_string()), |                 body: Some(r#"{"access_token":"toto","refresh_token":"tutu"}"#.to_string()), | ||||||
|                 is_valid: true, |                 is_valid: true, | ||||||
|  |                 has_token: false | ||||||
|  |             } | ||||||
|  |         ), | ||||||
|  |         ( | ||||||
|  |             format!("{}\r\nuselessheaders\r\n{}", "POST /get/ HTTP/1.1", r#"{"token": "toto", "refresh_token": "tutu"}"#), | ||||||
|  |             Expect { | ||||||
|  |                 start_line: "POST /get/ HTTP/1.1".to_string(), | ||||||
|  |                 body: Some(r#"{"token":"toto","refresh_token":"tutu"}"#.to_string()), | ||||||
|  |                 is_valid: true, | ||||||
|  |                 has_token: true | ||||||
|             } |             } | ||||||
|         ), |         ), | ||||||
|     ]; |     ]; | ||||||
| @ -336,6 +370,7 @@ fn test_request() { | |||||||
|         let http_request = HTTPRequest::from(request.as_str()); |         let http_request = HTTPRequest::from(request.as_str()); | ||||||
|         assert_eq!(expect.is_valid, http_request.is_valid()); |         assert_eq!(expect.is_valid, http_request.is_valid()); | ||||||
| 
 | 
 | ||||||
|  |         let token = http_request.get_body_value("token"); | ||||||
|         let start_line: String = http_request.start_line.into(); |         let start_line: String = http_request.start_line.into(); | ||||||
|         assert_eq!(expect.start_line, start_line); |         assert_eq!(expect.start_line, start_line); | ||||||
| 
 | 
 | ||||||
| @ -345,6 +380,11 @@ fn test_request() { | |||||||
|             } |             } | ||||||
|             None => continue, |             None => continue, | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         match expect.has_token { | ||||||
|  |             true => assert!(token.is_some()), | ||||||
|  |             false => assert!(token.is_none()), | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,11 +1,12 @@ | |||||||
| //! router aims to handle correctly the request corresponding to the target
 | //! router aims to handle correctly the request corresponding to the target
 | ||||||
| //! it implements all the logic to build an `HTTPResponse`
 | //! it implements all the logic to build an `HTTPResponse`
 | ||||||
| 
 | 
 | ||||||
|  | use json; | ||||||
|  | 
 | ||||||
| use super::{HTTPMessage, HTTPRequest, HTTPResponse}; | use super::{HTTPMessage, HTTPRequest, HTTPResponse}; | ||||||
| use crate::config::Config; | use crate::config::Config; | ||||||
| use crate::jwt::JWTSigner; | use crate::jwt::JWTSigner; | ||||||
| use crate::stores::FileStore; | use crate::stores::{FileStore, Store}; | ||||||
| use crate::stores::Store; |  | ||||||
| 
 | 
 | ||||||
| // TODO: must be mapped with corresponding handler
 | // TODO: must be mapped with corresponding handler
 | ||||||
| const GET_ROUTE: &'static str = "/get/"; | const GET_ROUTE: &'static str = "/get/"; | ||||||
| @ -44,14 +45,46 @@ async fn handle_get(request: HTTPRequest, config: Config) -> HTTPResponse { | |||||||
| 
 | 
 | ||||||
| /// validates the token by checking:
 | /// validates the token by checking:
 | ||||||
| /// * expiration time
 | /// * expiration time
 | ||||||
| async fn handle_validate(request: HTTPRequest, _config: Config) -> HTTPResponse { | /// * signature
 | ||||||
|     match &request.body { | async fn handle_validate(request: HTTPRequest, config: Config) -> HTTPResponse { | ||||||
|         Some(ref _b) => { |     let token = { | ||||||
|             // TODO: impl the JWT validation
 |         match request.get_body_value("token") { | ||||||
|             HTTPResponse::send_token("header.payload.signature") |             Some(t) => t, | ||||||
|  |             None => { | ||||||
|  |                 let mut message = HTTPMessage::default(); | ||||||
|  |                 message.put("valid", "false"); | ||||||
|  |                 message.put("reason", "no token provided in the request body"); | ||||||
|  |                 let json = message.try_into().unwrap(); | ||||||
|  | 
 | ||||||
|  |                 return HTTPResponse::as_200(Some(json)); | ||||||
|             } |             } | ||||||
|         None => HTTPResponse::as_400(), |  | ||||||
|         } |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     let jwt_signer = { | ||||||
|  |         match JWTSigner::new(config).await { | ||||||
|  |             Ok(s) => s, | ||||||
|  |             Err(e) => { | ||||||
|  |                 let message = HTTPMessage::error(&e); | ||||||
|  |                 let json = message.try_into().unwrap(); | ||||||
|  |                 return HTTPResponse::as_500(Some(json)); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     let mut message = HTTPMessage::default(); | ||||||
|  |     match jwt_signer.validate(&token) { | ||||||
|  |         Ok(()) => { | ||||||
|  |             message.put("valid", "true"); | ||||||
|  |         } | ||||||
|  |         Err(e) => { | ||||||
|  |             message.put("valid", "false"); | ||||||
|  |             message.put("reason", &e); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let json: json::JsonValue = message.try_into().unwrap(); | ||||||
|  |     HTTPResponse::as_200(Some(json)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub struct Router; | pub struct Router; | ||||||
|  | |||||||
| @ -1,7 +1,9 @@ | |||||||
| //! simple module to read `.pem` files and sign the token
 | //! simple module to read `.pem` files and sign the token
 | ||||||
| 
 | 
 | ||||||
| use crate::config::Config; | use crate::config::Config; | ||||||
|  | use jwt_simple::common::VerificationOptions; | ||||||
| use jwt_simple::prelude::*; | use jwt_simple::prelude::*; | ||||||
|  | use std::collections::HashSet; | ||||||
| use tokio::fs; | use tokio::fs; | ||||||
| 
 | 
 | ||||||
| pub struct JWTSigner { | pub struct JWTSigner { | ||||||
| @ -26,7 +28,7 @@ impl JWTSigner { | |||||||
|                 jwt_signer.private_key = c; |                 jwt_signer.private_key = c; | ||||||
|             } |             } | ||||||
|             Err(e) => { |             Err(e) => { | ||||||
|                 return Err(format!("unable to read the private key, err={}", e)); |                 return Err(format!("unable to read the private key err={}", e)); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -35,20 +37,30 @@ impl JWTSigner { | |||||||
|                 jwt_signer.public_key = c; |                 jwt_signer.public_key = c; | ||||||
|             } |             } | ||||||
|             Err(e) => { |             Err(e) => { | ||||||
|                 return Err(format!("unable to read the public key, err={}", e)); |                 return Err(format!("unable to read the public key err={}", e)); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         Ok(jwt_signer) |         Ok(jwt_signer) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     fn get_verification_options(&self) -> VerificationOptions { | ||||||
|  |         let mut verification_options = VerificationOptions::default(); | ||||||
|  | 
 | ||||||
|  |         let mut issuers: HashSet<String> = HashSet::new(); | ||||||
|  |         issuers.insert(self.issuer.clone()); | ||||||
|  |         verification_options.allowed_issuers = Some(issuers); | ||||||
|  | 
 | ||||||
|  |         verification_options | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /// builds and signs the token
 |     /// builds and signs the token
 | ||||||
|     pub fn sign(&self) -> Result<String, String> { |     pub fn sign(&self) -> 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, | ||||||
|                 Err(e) => { |                 Err(e) => { | ||||||
|                     return Err(format!("unable to load the private key, err={}", e)); |                     return Err(format!("unable to load the private key err={}", e)); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         }; |         }; | ||||||
| @ -58,10 +70,28 @@ impl JWTSigner { | |||||||
|         match jwt_key.sign(claims) { |         match jwt_key.sign(claims) { | ||||||
|             Ok(token) => Ok(token), |             Ok(token) => Ok(token), | ||||||
|             Err(e) => { |             Err(e) => { | ||||||
|                 return Err(format!("unable to sign the token, err={}", e)); |                 return Err(format!("unable to sign the token err={}", e)); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     pub fn validate(&self, token: &str) -> Result<(), String> { | ||||||
|  |         let verification_options = self.get_verification_options(); | ||||||
|  |         match RS384PublicKey::from_pem(&self.public_key) { | ||||||
|  |             Ok(key) => { | ||||||
|  |                 if let Err(e) = | ||||||
|  |                     key.verify_token::<NoCustomClaims>(token, Some(verification_options)) | ||||||
|  |                 { | ||||||
|  |                     return Err(format!("token validation failed err={}", e)); | ||||||
|  |                 } | ||||||
|  |                 Ok(()) | ||||||
|  |             } | ||||||
|  |             Err(e) => Err(format!( | ||||||
|  |                 "token validation failed can't read the public key err={}", | ||||||
|  |                 e | ||||||
|  |             )), | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[tokio::test] | #[tokio::test] | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ mod config; | |||||||
| mod http; | mod http; | ||||||
| mod jwt; | mod jwt; | ||||||
| mod stores; | mod stores; | ||||||
|  | mod utils; | ||||||
| 
 | 
 | ||||||
| use clap::Parser; | use clap::Parser; | ||||||
| use configparser::ini::Ini; | use configparser::ini::Ini; | ||||||
|  | |||||||
| @ -1,23 +1,13 @@ | |||||||
| use async_trait::async_trait; | use async_trait::async_trait; | ||||||
| use json; | use json; | ||||||
| use json::object::Object; | 
 | ||||||
|  | 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) -> bool; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// extracts `String` json value from a key
 |  | ||||||
| fn extract_json_value(data: &Object, key: &str) -> String { |  | ||||||
|     if let Some(u) = data.get(key) { |  | ||||||
|         match u.as_str() { |  | ||||||
|             Some(s) => return s.to_string(), |  | ||||||
|             None => return "".to_string(), |  | ||||||
|         } |  | ||||||
|     }; |  | ||||||
|     "".to_string() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Default, Debug)] | #[derive(Default, Debug)] | ||||||
| pub struct Credentials { | pub struct Credentials { | ||||||
|     pub username: String, |     pub username: String, | ||||||
| @ -39,8 +29,8 @@ 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"); |                 credentials.username = extract_json_value(&d, "username").unwrap_or("".to_string()); | ||||||
|                 credentials.password = extract_json_value(&d, "password"); |                 credentials.password = extract_json_value(&d, "password").unwrap_or("".to_string()); | ||||||
|             } |             } | ||||||
|             _ => return credentials, |             _ => return credentials, | ||||||
|         } |         } | ||||||
|  | |||||||
							
								
								
									
										3
									
								
								src/utils/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/utils/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | mod utils; | ||||||
|  | 
 | ||||||
|  | pub use utils::extract_json_value; | ||||||
							
								
								
									
										30
									
								
								src/utils/utils.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/utils/utils.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | |||||||
|  | use json::object::Object; | ||||||
|  | 
 | ||||||
|  | /// extracts JSON value from a key
 | ||||||
|  | pub fn extract_json_value(data: &Object, key: &str) -> Option<String> { | ||||||
|  |     match data.get(key) { | ||||||
|  |         Some(u) => match u.as_str() { | ||||||
|  |             Some(s) => return Some(s.to_string()), | ||||||
|  |             None => None, | ||||||
|  |         }, | ||||||
|  |         None => None, | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn test_extract_json_value() { | ||||||
|  |     let test_cases: [(json::JsonValue, bool, bool); 3] = [ | ||||||
|  |         (json::parse(r#"{"test": ""}"#).unwrap(), true, true), | ||||||
|  |         (json::parse(r#"{}"#).unwrap(), true, false), | ||||||
|  |         (json::parse(r#"[]"#).unwrap(), false, false), | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     for (value, is_valid, has_key) in test_cases { | ||||||
|  |         match value { | ||||||
|  |             json::JsonValue::Object(d) => { | ||||||
|  |                 assert_eq!(has_key, extract_json_value(&d, "test").is_some()); | ||||||
|  |             } | ||||||
|  |             _ => assert!(!is_valid), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -6,7 +6,11 @@ | |||||||
| # | # | ||||||
| ####################################### | ####################################### | ||||||
| 
 | 
 | ||||||
| URL="http://localhost:9001" | if [ -z ${SIMPLE_AUTH_URL} ] | ||||||
|  | then | ||||||
|  | 	echo "[WARN]: SIMPLE_AUTH_URL is empty, set to http://localhost:9001" | ||||||
|  | 	URL="http://localhost:9001" | ||||||
|  | fi | ||||||
| 
 | 
 | ||||||
| for i in {0..10} | for i in {0..10} | ||||||
| do | do | ||||||
|  | |||||||
| @ -1,8 +1,10 @@ | |||||||
| attrs==22.1.0 | attrs==22.1.0 | ||||||
| black==22.8.0 | black==22.8.0 | ||||||
| certifi==2022.9.14 | certifi==2022.9.14 | ||||||
|  | cffi==1.15.1 | ||||||
| charset-normalizer==2.1.1 | charset-normalizer==2.1.1 | ||||||
| click==8.1.3 | click==8.1.3 | ||||||
|  | cryptography==38.0.1 | ||||||
| idna==3.4 | idna==3.4 | ||||||
| iniconfig==1.1.1 | iniconfig==1.1.1 | ||||||
| mypy-extensions==0.4.3 | mypy-extensions==0.4.3 | ||||||
| @ -11,7 +13,10 @@ pathspec==0.10.1 | |||||||
| platformdirs==2.5.2 | platformdirs==2.5.2 | ||||||
| pluggy==1.0.0 | pluggy==1.0.0 | ||||||
| py==1.11.0 | py==1.11.0 | ||||||
|  | pycparser==2.21 | ||||||
|  | PyJWT==2.5.0 | ||||||
| pyparsing==3.0.9 | pyparsing==3.0.9 | ||||||
| requests==2.28.1 | requests==2.28.1 | ||||||
| tomli==2.0.1 | tomli==2.0.1 | ||||||
|  | types-cryptography==3.3.23 | ||||||
| urllib3==1.26.12 | urllib3==1.26.12 | ||||||
|  | |||||||
| @ -1,14 +1,19 @@ | |||||||
| import jwt | import jwt | ||||||
|  | import os | ||||||
| import requests | import requests | ||||||
| 
 | 
 | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| 
 |  | ||||||
| from unittest import TestCase | from unittest import TestCase | ||||||
| 
 | 
 | ||||||
| URL = "http://127.0.0.1:9001" | URL = os.getenv("SIMPLE_AUTH_URL", "http://127.0.0.1:9001") | ||||||
|  | PUB_KEY_PATH = os.getenv("SIMPLE_AUTH_PUB_KEY", "") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TestResponse(TestCase): | class TestResponse(TestCase): | ||||||
|  |     def setUp(self): | ||||||
|  |         with open(PUB_KEY_PATH, "r") as f: | ||||||
|  |             self.pub_key = f.read() | ||||||
|  | 
 | ||||||
|     def test_get_target(self): |     def test_get_target(self): | ||||||
|         resp = requests.post( |         resp = requests.post( | ||||||
|             URL + "/get/", json={"username": "toto", "password": "tata"} |             URL + "/get/", json={"username": "toto", "password": "tata"} | ||||||
| @ -17,25 +22,52 @@ class TestResponse(TestCase): | |||||||
|         self.assertIsNotNone(resp.json(), "response data can't be empty") |         self.assertIsNotNone(resp.json(), "response data can't be empty") | ||||||
| 
 | 
 | ||||||
|         token = resp.json()["token"] |         token = resp.json()["token"] | ||||||
|         jwt_decoded = jwt.decode(token, options={"verify_signature": False}) |         jwt_decoded = jwt.decode( | ||||||
|  |             token, | ||||||
|  |             self.pub_key, | ||||||
|  |             algorithms=["RS384"], | ||||||
|  |             options={ | ||||||
|  |                 "verify_signature": True, | ||||||
|  |                 "verify_claims": True, | ||||||
|  |                 "verify_iss": True, | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|         self.assertEqual("thegux.fr", jwt_decoded["iss"]) |         self.assertEqual("thegux.fr", jwt_decoded["iss"]) | ||||||
| 
 | 
 | ||||||
|         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"]) | ||||||
|         date_exp = datetime.strptime(str(jwt_exp - jwt_iat), "%H:%M:%S") |         date_exp = datetime.strptime(str(jwt_exp - jwt_iat), "%H:%M:%S") | ||||||
|         self.assertEqual(2, date_exp.hour) |         self.assertEqual(2, date_exp.hour) | ||||||
|  |         return token | ||||||
| 
 | 
 | ||||||
|     def test_validate_target(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", "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") | ||||||
|  |         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( |         self.assertEqual( | ||||||
|             resp.json()["token"], "header.payload.signature", "bad status returned" |             resp.json()["reason"], | ||||||
|  |             "token validation failed err=JWT compact encoding error", | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|     # TODO: must be updated after implmenting `/refresh/` url handler |     def test_validate_target(self): | ||||||
|  |         token = self.test_get_target() | ||||||
|  | 
 | ||||||
|  |         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") | ||||||
|  | 
 | ||||||
|  |     # TODO: must be updated after implementing `/refresh/` url handler | ||||||
|     def test_refresh_target(self): |     def test_refresh_target(self): | ||||||
|         resp = requests.post( |         resp = requests.post( | ||||||
|             URL + "/refresh/", json={"username": "toto", "password": "tata"} |             URL + "/refresh/", json={"username": "toto", "password": "tata"} | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user