Merge branch 'feature/auth-refactor' into develop
This commit is contained in:
commit
bdd34a3490
5
Cargo.lock
generated
5
Cargo.lock
generated
@ -635,8 +635,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.1.3"
|
version = "0.1.6"
|
||||||
source = "git+https://gitea.thegux.fr/rmanach/http#b8c0fbba0b62906823a79e34bb2eadc1fe419d90"
|
source = "git+https://gitea.thegux.fr/rmanach/http#0e616570907f3427be99f4bfc227bd57a252c8c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"json",
|
"json",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
@ -1239,6 +1239,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"simple_logger",
|
"simple_logger",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -15,8 +15,9 @@ jwt-simple = "0.11.1"
|
|||||||
simple_logger = "4.0.0"
|
simple_logger = "4.0.0"
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
base64 = "0.13.1"
|
base64 = "0.13.1"
|
||||||
|
serde_json = "1.0"
|
||||||
|
|
||||||
http = { git = "https://gitea.thegux.fr/rmanach/http", version = "0.1.3" }
|
http = { git = "https://gitea.thegux.fr/rmanach/http", version = "0.1.6" }
|
||||||
|
|
||||||
# useful for tests (embedded files should be delete in release ?)
|
# useful for tests (embedded files should be delete in release ?)
|
||||||
#rust-embed="6.4.1"
|
#rust-embed="6.4.1"
|
||||||
|
|||||||
@ -8,7 +8,6 @@ cargo build --release
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
### Store
|
### Store
|
||||||
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
|
||||||
@ -72,7 +71,7 @@ cargo test
|
|||||||
* set the following env variables:
|
* set the following env variables:
|
||||||
```bash
|
```bash
|
||||||
export SIMPLE_AUTH_URL="http://<url>:<port>"
|
export SIMPLE_AUTH_URL="http://<url>:<port>"
|
||||||
export SIMPLE_AUTH_PUB_KEY="<path_to_pem_pub_key>" # DO NOT USE THE ONE IN PRODUCTION !
|
export SIMPLE_AUTH_PUB_KEY="<path_to_pem_pub_key>" # DO NOT USE THIS ONE IN PRODUCTION !
|
||||||
```
|
```
|
||||||
* run the server (if no one is running remotly)
|
* run the server (if no one is running remotly)
|
||||||
* run curl tests
|
* run curl tests
|
||||||
@ -80,14 +79,14 @@ export SIMPLE_AUTH_PUB_KEY="<path_to_pem_pub_key>" # DO NOT USE THE ONE IN PRODU
|
|||||||
cd tests/bash/
|
cd tests/bash/
|
||||||
./curling.bash && echo "passed"
|
./curling.bash && echo "passed"
|
||||||
```
|
```
|
||||||
* run python requests tests
|
* run python tests
|
||||||
```bash
|
```bash
|
||||||
# create a python venv
|
# create a python venv
|
||||||
cd tests/python
|
cd tests/python
|
||||||
python3 -m venv venv
|
python3 -m venv venv
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
|
|
||||||
# intall the requirements
|
# install the requirements
|
||||||
pip install -r requirements
|
pip install -r requirements
|
||||||
|
|
||||||
# launch the tests
|
# launch the tests
|
||||||
@ -97,5 +96,5 @@ python -m unittest
|
|||||||
## Documentation
|
## Documentation
|
||||||
```bash
|
```bash
|
||||||
# add the '--open' arg to open the doc on a browser
|
# add the '--open' arg to open the doc on a browser
|
||||||
cargo doc --no-deps
|
cargo doc -r --no-deps
|
||||||
```
|
```
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
//! provides `Config` struct to load and validate `.ini` file
|
//! **config** module provides `Config` struct to load and validate `.ini` file.
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
|
|
||||||
pub use config::Config;
|
pub use config::Config;
|
||||||
|
|||||||
@ -5,6 +5,9 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
|
use crate::message::JWTMessage;
|
||||||
|
use crate::stores::Credentials;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
struct JWTCustomClaims {
|
struct JWTCustomClaims {
|
||||||
email: String,
|
email: String,
|
||||||
@ -58,8 +61,8 @@ impl JWTSigner {
|
|||||||
verification_options
|
verification_options
|
||||||
}
|
}
|
||||||
|
|
||||||
/// builds and signs the token
|
/// sign builds and signs the token
|
||||||
pub fn sign(&self, email: String) -> Result<String, String> {
|
pub fn sign(&self, credentials: Credentials) -> 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,
|
||||||
@ -69,13 +72,18 @@ impl JWTSigner {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut claims = Claims::with_custom_claims(
|
let mut claims = Claims::with_custom_claims(
|
||||||
JWTCustomClaims { email },
|
JWTCustomClaims {
|
||||||
|
email: credentials.get_email(),
|
||||||
|
},
|
||||||
Duration::from_hours(self.exp_time),
|
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) {
|
||||||
Ok(token) => Ok(token),
|
Ok(token) => {
|
||||||
|
// TODO: need to generate the refresh token
|
||||||
|
return Ok(serde_json::to_string(&JWTMessage::with_access(token)).unwrap());
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return Err(format!("unable to sign the token details={}", e));
|
return Err(format!("unable to sign the token details={}", e));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
//! simple module to read `.pem` files and sign the token
|
//! **jwt** module aims to read `.pem` files and sign/validate the token.
|
||||||
|
|
||||||
mod jwt;
|
mod jwt;
|
||||||
|
|
||||||
pub use jwt::JWTSigner;
|
pub use jwt::JWTSigner;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
mod config;
|
mod config;
|
||||||
mod jwt;
|
mod jwt;
|
||||||
|
mod message;
|
||||||
mod router;
|
mod router;
|
||||||
mod stores;
|
mod stores;
|
||||||
|
|
||||||
@ -66,7 +67,7 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// parses the incoming request (partial spec implementation) and build an HTTP response
|
/// handle_connection parses the incoming request and builds an HTTP response
|
||||||
async fn handle_connection(mut stream: TcpStream, addr: String, config: Config) {
|
async fn handle_connection(mut stream: TcpStream, addr: String, config: Config) {
|
||||||
log::info!("client connected: {}", addr);
|
log::info!("client connected: {}", addr);
|
||||||
|
|
||||||
|
|||||||
48
src/message/message.rs
Normal file
48
src/message/message.rs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
/// JWTMessage aims to have a generic struct to build JSON HTTP response message with JWT informations
|
||||||
|
pub struct JWTMessage {
|
||||||
|
#[serde(skip_serializing_if = "String::is_empty")]
|
||||||
|
access_token: String,
|
||||||
|
#[serde(skip_serializing_if = "String::is_empty")]
|
||||||
|
refresh_token: String,
|
||||||
|
#[serde(skip_serializing_if = "String::is_empty")]
|
||||||
|
pubkey: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JWTMessage {
|
||||||
|
pub fn with_access(access_token: String) -> Self {
|
||||||
|
JWTMessage {
|
||||||
|
access_token: access_token,
|
||||||
|
refresh_token: "".to_string(),
|
||||||
|
pubkey: "".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_pubkey(pubkey: String) -> Self {
|
||||||
|
JWTMessage {
|
||||||
|
access_token: "".to_string(),
|
||||||
|
refresh_token: "".to_string(),
|
||||||
|
pubkey: base64::encode(pubkey),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Default)]
|
||||||
|
/// ValidationMessage aims to build a JSON HTTP response body for JWT validation
|
||||||
|
pub struct ValidationMessage {
|
||||||
|
valid: bool,
|
||||||
|
#[serde(skip_serializing_if = "String::is_empty")]
|
||||||
|
reason: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValidationMessage {
|
||||||
|
pub fn set_valid(&mut self, valid: bool) {
|
||||||
|
self.valid = valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_reason(&mut self, reason: &str) {
|
||||||
|
self.reason = reason.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/message/mod.rs
Normal file
5
src/message/mod.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
//! **message** module holds all structs to manage JSON response body for the authentication.
|
||||||
|
|
||||||
|
mod message;
|
||||||
|
|
||||||
|
pub use message::{JWTMessage, ValidationMessage};
|
||||||
@ -1,4 +1,4 @@
|
|||||||
//! router module includes all the handlers to get and validate JWT
|
//! **router** module includes all the handlers to get and validate JWT.
|
||||||
|
|
||||||
mod router;
|
mod router;
|
||||||
pub use router::ROUTER;
|
pub use router::ROUTER;
|
||||||
|
|||||||
@ -1,13 +1,10 @@
|
|||||||
//! router aims to handle correctly the request corresponding to the target
|
|
||||||
//! it implements all the logic to build an `HTTPResponse`
|
|
||||||
|
|
||||||
use base64;
|
|
||||||
use http::{HTTPRequest, HTTPResponse, JSONMessage};
|
use http::{HTTPRequest, HTTPResponse, JSONMessage};
|
||||||
use json::JsonValue;
|
use json::JsonValue;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::jwt::JWTSigner;
|
use crate::jwt::JWTSigner;
|
||||||
use crate::stores::{FileStore, Store};
|
use crate::message::{JWTMessage, ValidationMessage};
|
||||||
|
use crate::stores::{Credentials, FileStore, 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/";
|
||||||
@ -23,8 +20,13 @@ async fn handle_get(request: HTTPRequest<'_>, config: Config, method: &str) -> H
|
|||||||
|
|
||||||
match request.get_body() {
|
match request.get_body() {
|
||||||
Some(d) => {
|
Some(d) => {
|
||||||
let credentials = store.is_auth(d).await;
|
let credentials = Credentials::from(d);
|
||||||
if credentials.is_none() {
|
if credentials.is_empty() {
|
||||||
|
log::error!("unable to parse the credentials correctly from the incoming request");
|
||||||
|
return HTTPResponse::as_400();
|
||||||
|
}
|
||||||
|
|
||||||
|
if !store.is_auth(&credentials).await {
|
||||||
return HTTPResponse::as_403();
|
return HTTPResponse::as_403();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,7 +40,7 @@ async fn handle_get(request: HTTPRequest<'_>, config: Config, method: &str) -> H
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match jwt_signer.sign(credentials.unwrap().email) {
|
match jwt_signer.sign(credentials) {
|
||||||
Ok(t) => send_token(&t),
|
Ok(t) => send_token(&t),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let message = JSONMessage::error(&e);
|
let message = JSONMessage::error(&e);
|
||||||
@ -50,14 +52,10 @@ async fn handle_get(request: HTTPRequest<'_>, config: Config, method: &str) -> H
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// validates the token by checking:
|
/// handle_validate validates the token by checking:
|
||||||
/// * expiration time
|
/// * expiration time
|
||||||
/// * signature
|
/// * signature
|
||||||
async fn handle_validate(
|
async fn handle_validate(request: HTTPRequest<'_>, config: Config, method: &str) -> HTTPResponse {
|
||||||
request: HTTPRequest<'_>,
|
|
||||||
config: Config,
|
|
||||||
method: &str,
|
|
||||||
) -> HTTPResponse {
|
|
||||||
if request.get_method().trim().to_lowercase() != method {
|
if request.get_method().trim().to_lowercase() != method {
|
||||||
return HTTPResponse::as_400();
|
return HTTPResponse::as_400();
|
||||||
}
|
}
|
||||||
@ -66,11 +64,10 @@ async fn handle_validate(
|
|||||||
match request.get_body_value("token") {
|
match request.get_body_value("token") {
|
||||||
Some(t) => t,
|
Some(t) => t,
|
||||||
None => {
|
None => {
|
||||||
let mut message = JSONMessage::default();
|
let mut message = ValidationMessage::default();
|
||||||
message.put("valid", "false");
|
message.set_reason("no token provided in the request body");
|
||||||
message.put("reason", "no token provided in the request body");
|
|
||||||
let json = message.try_into().unwrap();
|
|
||||||
|
|
||||||
|
let json = json::parse(&serde_json::to_string(&message).unwrap()).unwrap();
|
||||||
return HTTPResponse::as_200(Some(json));
|
return HTTPResponse::as_200(Some(json));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -87,27 +84,22 @@ async fn handle_validate(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut message = JSONMessage::default();
|
let mut message = ValidationMessage::default();
|
||||||
match jwt_signer.validate(&token) {
|
match jwt_signer.validate(&token) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
message.put("valid", "true");
|
message.set_valid(true);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
message.put("valid", "false");
|
message.set_reason(&e);
|
||||||
message.put("reason", &e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let json: JsonValue = message.try_into().unwrap();
|
let json: JsonValue = json::parse(&serde_json::to_string(&message).unwrap()).unwrap();
|
||||||
HTTPResponse::as_200(Some(json))
|
HTTPResponse::as_200(Some(json))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// returns the JWT public key in base64 encoded
|
/// handle_public_key returns the JWT public key in base64 encoded
|
||||||
async fn handle_public_key(
|
async fn handle_public_key(request: HTTPRequest<'_>, config: Config, method: &str) -> HTTPResponse {
|
||||||
request: HTTPRequest<'_>,
|
|
||||||
config: Config,
|
|
||||||
method: &str,
|
|
||||||
) -> HTTPResponse {
|
|
||||||
if request.get_method().trim().to_lowercase() != method {
|
if request.get_method().trim().to_lowercase() != method {
|
||||||
return HTTPResponse::as_400();
|
return HTTPResponse::as_400();
|
||||||
}
|
}
|
||||||
@ -124,18 +116,15 @@ async fn handle_public_key(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let public_key = jwt_signer.get_public_key();
|
let public_key = jwt_signer.get_public_key();
|
||||||
|
let message = serde_json::to_string(&JWTMessage::with_pubkey(public_key)).unwrap();
|
||||||
|
|
||||||
let mut message = JSONMessage::default();
|
HTTPResponse::as_200(Some(json::parse(&message).unwrap()))
|
||||||
message.put("pubkey", &base64::encode(public_key));
|
|
||||||
|
|
||||||
let json = message.try_into().unwrap();
|
|
||||||
HTTPResponse::as_200(Some(json))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Router;
|
pub struct Router;
|
||||||
|
|
||||||
impl Router {
|
impl Router {
|
||||||
/// routes the request to the corresponding handling method
|
/// route routes the request to the corresponding handler
|
||||||
pub async fn route(&self, request_str: &str, config: Config) -> HTTPResponse {
|
pub async fn route(&self, request_str: &str, config: Config) -> HTTPResponse {
|
||||||
let request = HTTPRequest::from(request_str);
|
let request = HTTPRequest::from(request_str);
|
||||||
match request.get_target() {
|
match request.get_target() {
|
||||||
@ -148,21 +137,16 @@ impl Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// send_token generates an HTTPResponse with the new token
|
/// send_token generates an HTTPResponse with the new token
|
||||||
pub fn send_token(token: &str) -> HTTPResponse {
|
pub fn send_token(jwt_message: &str) -> HTTPResponse {
|
||||||
let mut message = JSONMessage::default();
|
let message = if jwt_message != "" {
|
||||||
message.put("token", token);
|
jwt_message
|
||||||
|
} else {
|
||||||
let json = {
|
r#"{"token": "error.generation.token"}"#
|
||||||
match message.try_into() {
|
|
||||||
Ok(m) => m,
|
|
||||||
Err(_e) => json::parse(r#"{"token": "error.generation.token"}"#).unwrap(),
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
HTTPResponse::as_200(Some(json::parse(message).unwrap()))
|
||||||
HTTPResponse::as_200(Some(json))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// this MUST be used like a Singleton
|
// this **MUST** be used like a Singleton
|
||||||
pub const ROUTER: Router = Router {};
|
pub const ROUTER: Router = Router {};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@ -173,9 +157,6 @@ async fn test_route() {
|
|||||||
let config: Config = Config::default();
|
let config: Config = Config::default();
|
||||||
let request_str = "POST /get/ HTTP/1.1\r\n\r\n";
|
let request_str = "POST /get/ HTTP/1.1\r\n\r\n";
|
||||||
|
|
||||||
let response: HTTPResponse = router.route(request_str, "".to_string(), config).await;
|
let response: HTTPResponse = router.route(request_str, config).await;
|
||||||
assert_eq!(
|
assert_eq!(HTTPStatusCode::Http400, response.get_status_code());
|
||||||
HTTPStatusCode::Http400,
|
|
||||||
response.status_line.get_status_code()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use json::JsonValue;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use super::store::{Credentials, Store};
|
use super::store::{Credentials, Store};
|
||||||
|
|
||||||
/// references a credentials store file
|
/// FileStore references a credentials store file
|
||||||
pub struct FileStore {
|
pub struct FileStore {
|
||||||
path: String,
|
path: String,
|
||||||
credentials: Vec<Credentials>,
|
credentials: Vec<Credentials>,
|
||||||
@ -18,8 +17,9 @@ impl FileStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// loads and reads the file asynchonously
|
/// parse_contents loads and reads the file asynchonously
|
||||||
/// parses the file line by line to retrieve the credentials
|
///
|
||||||
|
/// It parses the file line by line to retrieve the credentials
|
||||||
async fn parse_contents(&mut self) {
|
async fn parse_contents(&mut self) {
|
||||||
let contents = tokio::fs::read_to_string(&self.path).await;
|
let contents = tokio::fs::read_to_string(&self.path).await;
|
||||||
let mut credentials: Vec<Credentials> = vec![];
|
let mut credentials: Vec<Credentials> = vec![];
|
||||||
@ -47,41 +47,31 @@ impl FileStore {
|
|||||||
self.credentials = credentials;
|
self.credentials = credentials;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// checks if the credentials exist in the `FileStore`
|
/// auth checks if the credentials exist in the `FileStore`
|
||||||
fn auth(&self, email: String, password: String) -> Option<Credentials> {
|
fn auth(&self, email: String, password: String) -> bool {
|
||||||
let credentials: Vec<&Credentials> = self
|
let credentials: Vec<&Credentials> = self
|
||||||
.credentials
|
.credentials
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|x| x.email == email && x.password == password)
|
.filter(|x| *x.get_email() == email && *x.get_password() == password)
|
||||||
.collect();
|
.collect();
|
||||||
if credentials.len() == 1 {
|
if credentials.len() == 1 {
|
||||||
// no need to store the password again
|
return true;
|
||||||
return Some(Credentials::new(
|
|
||||||
credentials[0].email.clone(),
|
|
||||||
"".to_string(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
None
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Store for FileStore {
|
impl Store for FileStore {
|
||||||
async fn is_auth(&mut self, data: &JsonValue) -> Option<Credentials> {
|
async fn is_auth(&mut self, credentials: &Credentials) -> bool {
|
||||||
// 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 None;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
let credentials = Credentials::from(data);
|
|
||||||
if credentials.is_empty() {
|
|
||||||
log::error!("unable to parse the credentials correctly from the incoming request");
|
|
||||||
return None;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.parse_contents().await;
|
self.parse_contents().await;
|
||||||
self.auth(credentials.email, credentials.password)
|
self.auth(credentials.get_email(), credentials.get_password())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,13 +80,14 @@ async fn test_store() {
|
|||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
let root_path = env::var("CARGO_MANIFEST_DIR").unwrap();
|
let root_path = env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||||
// TODO: path::Path should be better
|
|
||||||
let store_path = format!("{}/{}/{}/{}", root_path, "tests", "data", "store.txt");
|
let store_path = format!("{}/{}/{}/{}", root_path, "tests", "data", "store.txt");
|
||||||
|
|
||||||
let mut store = FileStore::new(store_path);
|
let mut store = FileStore::new(store_path);
|
||||||
|
|
||||||
let data = json::parse(r#"{"email": "toto@toto.fr", "password": "tata"}"#).unwrap();
|
let data = json::parse(r#"{"email": "toto@toto.fr", "password": "tata"}"#).unwrap();
|
||||||
let credentials = store.is_auth(&data).await;
|
let credentials = Credentials::from(&data);
|
||||||
assert_eq!(false, credentials.is_none());
|
assert_eq!(credentials.get_email(), "toto@toto.fr");
|
||||||
assert_eq!(credentials.unwrap().email, "toto@toto.fr");
|
|
||||||
|
let is_auth = store.is_auth(&credentials).await;
|
||||||
|
assert_eq!(true, is_auth);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
//! store module lists interfaces available to check request credentials
|
//! **store** module lists interfaces available to check credentials. Each store must implement the trait `is_auth`.
|
||||||
//! each store must implement the trait `is_auth`
|
//!
|
||||||
//! two stores are available :
|
//! For now one store is available:
|
||||||
//! * `FileStore`: credentials stored in a text file (like **/etc/passwd**)
|
//! * `FileStore`: credentials stored in a text file (like **/etc/passwd**)
|
||||||
//! * `DBStore`: credentials stored in a database (TODO)
|
|
||||||
|
|
||||||
mod file;
|
mod file;
|
||||||
mod store;
|
mod store;
|
||||||
|
|||||||
@ -1,40 +1,48 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use json::JsonValue;
|
use json::JsonValue;
|
||||||
|
use serde::Deserialize;
|
||||||
use http::extract_json_value;
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Store {
|
pub trait Store {
|
||||||
async fn is_auth(&mut self, data: &JsonValue) -> Option<Credentials>;
|
async fn is_auth(&mut self, data: &Credentials) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug, Deserialize)]
|
||||||
pub struct Credentials {
|
pub struct Credentials {
|
||||||
pub email: String,
|
email: String,
|
||||||
pub password: String,
|
password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Credentials represents the incoming user credentials for authentication checking
|
||||||
impl Credentials {
|
impl Credentials {
|
||||||
pub fn new(email: String, password: String) -> Self {
|
pub fn new(email: String, password: String) -> Self {
|
||||||
Credentials { email, password }
|
Credentials { email, password }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_email(&self) -> String {
|
||||||
|
self.email.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_password(&self) -> String {
|
||||||
|
self.password.clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.email == "" || self.password == ""
|
self.email == "" || self.password == ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: could be less restrictive with `From<&str>`
|
||||||
impl From<&JsonValue> for Credentials {
|
impl From<&JsonValue> for Credentials {
|
||||||
fn from(data: &JsonValue) -> Self {
|
fn from(data: &JsonValue) -> Self {
|
||||||
let mut credentials = Credentials::default();
|
let res = serde_json::from_str(&data.dump());
|
||||||
match data {
|
match res {
|
||||||
JsonValue::Object(ref d) => {
|
Ok(c) => c,
|
||||||
credentials.email = extract_json_value(&d, "email").unwrap_or("".to_string());
|
Err(e) => {
|
||||||
credentials.password = extract_json_value(&d, "password").unwrap_or("".to_string());
|
log::warn!("unable to deserialize credentials: {}", e);
|
||||||
|
return Credentials::default();
|
||||||
}
|
}
|
||||||
_ => return credentials,
|
|
||||||
}
|
}
|
||||||
credentials
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,15 +16,31 @@ fi
|
|||||||
for i in {0..10}
|
for i in {0..10}
|
||||||
do
|
do
|
||||||
http_response=$(curl -s -o response.txt -w "%{http_code}" ${URL}/get/ -d '{"username":"toto", "password":"tutu"}')
|
http_response=$(curl -s -o response.txt -w "%{http_code}" ${URL}/get/ -d '{"username":"toto", "password":"tutu"}')
|
||||||
|
if [ $http_response != "400" ]
|
||||||
|
then
|
||||||
|
echo "bad http status code : ${http_response}, expect 400"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$(cat response.txt | jq -r '.error')" != "bad request" ]
|
||||||
|
then
|
||||||
|
echo "bad data returned, expect : bad request"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
for i in {0..10}
|
||||||
|
do
|
||||||
|
http_response=$(curl -s -o response.txt -w "%{http_code}" ${URL}/get/ -d '{"email":"toto", "password":"tutu"}')
|
||||||
if [ $http_response != "403" ]
|
if [ $http_response != "403" ]
|
||||||
then
|
then
|
||||||
echo "bad http status code : ${http_response}, expect 200"
|
echo "bad http status code : ${http_response}, expect 403"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$(cat response.txt | jq -r '.error')" != "url forbidden" ]
|
if [ "$(cat response.txt | jq -r '.error')" != "url forbidden" ]
|
||||||
then
|
then
|
||||||
echo "bad data returned, expect : invalid credentials"
|
echo "bad data returned, expect : url forbidden"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
@ -35,7 +51,7 @@ do
|
|||||||
http_response=$(curl -s -o response.txt -w "%{http_code}" ${URL}/ge/ -d '{"username":"toto", "password":"tutu"}')
|
http_response=$(curl -s -o response.txt -w "%{http_code}" ${URL}/ge/ -d '{"username":"toto", "password":"tutu"}')
|
||||||
if [ $http_response != "404" ]
|
if [ $http_response != "404" ]
|
||||||
then
|
then
|
||||||
echo "bad http status code : ${http_response}, expect 400"
|
echo "bad http status code : ${http_response}, expect 404"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
@ -46,7 +62,7 @@ do
|
|||||||
http_response=$(curl -s -o response.txt -w "%{http_code}" ${URL}/pubkey/)
|
http_response=$(curl -s -o response.txt -w "%{http_code}" ${URL}/pubkey/)
|
||||||
if [ $http_response != "200" ]
|
if [ $http_response != "200" ]
|
||||||
then
|
then
|
||||||
echo "bad http status code : ${http_response}, expect 400"
|
echo "bad http status code : ${http_response}, expect 200"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|||||||
@ -22,7 +22,7 @@ class TestResponse(TestCase):
|
|||||||
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")
|
||||||
|
|
||||||
token = resp.json()["token"]
|
token = resp.json()["access_token"]
|
||||||
jwt_decoded = jwt.decode(
|
jwt_decoded = jwt.decode(
|
||||||
token,
|
token,
|
||||||
pubkey or self.pub_key,
|
pubkey or self.pub_key,
|
||||||
@ -48,14 +48,14 @@ class TestResponse(TestCase):
|
|||||||
)
|
)
|
||||||
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()["valid"], False, "bad status returned")
|
||||||
self.assertEqual(resp.json()["reason"], "no token provided in the request body")
|
self.assertEqual(resp.json()["reason"], "no token provided in the request body")
|
||||||
|
|
||||||
def test_validate_target_empty_token(self):
|
def test_validate_target_empty_token(self):
|
||||||
resp = requests.post(URL + "/validate/", json={"tutu": "tutu", "token": ""})
|
resp = requests.post(URL + "/validate/", json={"tutu": "tutu", "token": ""})
|
||||||
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()["valid"], False, "bad status returned")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
resp.json()["reason"],
|
resp.json()["reason"],
|
||||||
"token validation failed details=JWT compact encoding error",
|
"token validation failed details=JWT compact encoding error",
|
||||||
@ -67,7 +67,7 @@ class TestResponse(TestCase):
|
|||||||
resp = requests.post(URL + "/validate/", json={"token": token})
|
resp = requests.post(URL + "/validate/", json={"token": token})
|
||||||
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"], "true", "bad status returned")
|
self.assertEqual(resp.json()["valid"], True, "bad status returned")
|
||||||
|
|
||||||
# TODO: must be updated after implementing `/refresh/` url handler
|
# TODO: must be updated after implementing `/refresh/` url handler
|
||||||
def test_refresh_target(self):
|
def test_refresh_target(self):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user