Merge branch 'release/v0.3.1'
This commit is contained in:
commit
24447dff9d
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