feat: #20 add email in JWT claims

This commit is contained in:
landrigun 2022-11-29 13:42:46 +00:00
parent 900dcebcad
commit 78e06756e2
11 changed files with 53 additions and 34 deletions

1
Cargo.lock generated
View File

@ -1243,6 +1243,7 @@ dependencies = [
"lazy_static", "lazy_static",
"log", "log",
"regex", "regex",
"serde",
"simple_logger", "simple_logger",
"tokio", "tokio",
] ]

View File

@ -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"]

View File

@ -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>"}

View File

@ -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();

View File

@ -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);

View File

@ -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) {

View File

@ -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");
} }

View File

@ -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};

View File

@ -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
} }
]; ];

View File

@ -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

View File

@ -17,7 +17,7 @@ class TestResponse(TestCase):
def test_get_target(self, pubkey=None): 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")
@ -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(