use serde_json to build http response with JWT json body

This commit is contained in:
landrigun 2023-02-16 10:45:50 +00:00
parent 3b6e208004
commit e992f7a3ce
8 changed files with 85 additions and 51 deletions

4
Cargo.lock generated
View File

@ -635,8 +635,8 @@ dependencies = [
[[package]]
name = "http"
version = "0.1.4"
source = "git+https://gitea.thegux.fr/rmanach/http#7d4aabad2c6d2cc07359b64214ba6e61f42ed80f"
version = "0.1.5"
source = "git+https://gitea.thegux.fr/rmanach/http#72bf34127b185f5a52895c08a3d8b229c0588dc0"
dependencies = [
"json",
"lazy_static",

View File

@ -17,7 +17,7 @@ log = "0.4.17"
base64 = "0.13.1"
serde_json = "1.0"
http = { git = "https://gitea.thegux.fr/rmanach/http", version = "0.1.4" }
http = { git = "https://gitea.thegux.fr/rmanach/http", version = "0.1.5" }
# useful for tests (embedded files should be delete in release ?)
#rust-embed="6.4.1"

View File

@ -7,6 +7,35 @@ use tokio::fs;
use crate::stores::Credentials;
#[derive(Serialize)]
/// JWTMessage aims to have a generic struct to build HTTP response message with JWT
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, Deserialize)]
struct JWTCustomClaims {
email: String,
@ -60,7 +89,7 @@ impl JWTSigner {
verification_options
}
/// builds and signs the token
/// sign builds and signs the token
pub fn sign(&self, credentials: Credentials) -> Result<String, String> {
let jwt_key = {
match RS384KeyPair::from_pem(&self.private_key) {
@ -71,13 +100,18 @@ impl JWTSigner {
}
};
let mut claims = Claims::with_custom_claims(
JWTCustomClaims { email: credentials.get_email() },
JWTCustomClaims {
email: credentials.get_email(),
},
Duration::from_hours(self.exp_time),
);
claims.issuer = Some(self.issuer.clone());
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) => {
return Err(format!("unable to sign the token details={}", e));
}

View File

@ -1,4 +1,5 @@
//! simple module to read `.pem` files and sign the token
//! jwt module aims to read `.pem` files, sign/validate the token and have useful functions to
//! manage HTTP response message
mod jwt;
pub use jwt::JWTSigner;
pub use jwt::{JWTMessage, JWTSigner};

View File

@ -1,12 +1,11 @@
//! 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 json::JsonValue;
use crate::config::Config;
use crate::jwt::JWTSigner;
use crate::jwt::{JWTMessage, JWTSigner};
use crate::stores::{Credentials, FileStore, Store};
// TODO: must be mapped with corresponding handler
@ -23,7 +22,6 @@ async fn handle_get(request: HTTPRequest<'_>, config: Config, method: &str) -> H
match request.get_body() {
Some(d) => {
let credentials = Credentials::from(d);
if credentials.is_empty() {
log::error!("unable to parse the credentials correctly from the incoming request");
@ -59,11 +57,7 @@ async fn handle_get(request: HTTPRequest<'_>, config: Config, method: &str) -> H
/// validates the token by checking:
/// * expiration time
/// * signature
async fn handle_validate(
request: HTTPRequest<'_>,
config: Config,
method: &str,
) -> HTTPResponse {
async fn handle_validate(request: HTTPRequest<'_>, config: Config, method: &str) -> HTTPResponse {
if request.get_method().trim().to_lowercase() != method {
return HTTPResponse::as_400();
}
@ -109,11 +103,7 @@ async fn handle_validate(
}
/// returns the JWT public key in base64 encoded
async fn handle_public_key(
request: HTTPRequest<'_>,
config: Config,
method: &str,
) -> HTTPResponse {
async fn handle_public_key(request: HTTPRequest<'_>, config: Config, method: &str) -> HTTPResponse {
if request.get_method().trim().to_lowercase() != method {
return HTTPResponse::as_400();
}
@ -130,18 +120,15 @@ async fn handle_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();
message.put("pubkey", &base64::encode(public_key));
let json = message.try_into().unwrap();
HTTPResponse::as_200(Some(json))
HTTPResponse::as_200(Some(json::parse(&message).unwrap()))
}
pub struct Router;
impl Router {
/// routes the request to the corresponding handling method
/// route routes the request to the corresponding handling method
pub async fn route(&self, request_str: &str, config: Config) -> HTTPResponse {
let request = HTTPRequest::from(request_str);
match request.get_target() {
@ -154,18 +141,13 @@ impl Router {
}
/// send_token generates an HTTPResponse with the new token
pub fn send_token(token: &str) -> HTTPResponse {
let mut message = JSONMessage::default();
message.put("token", token);
let json = {
match message.try_into() {
Ok(m) => m,
Err(_e) => json::parse(r#"{"token": "error.generation.token"}"#).unwrap(),
}
pub fn send_token(jwt_message: &str) -> HTTPResponse {
let message = if jwt_message != "" {
jwt_message
} else {
r#"{"token": "error.generation.token"}"#
};
HTTPResponse::as_200(Some(json))
HTTPResponse::as_200(Some(json::parse(message).unwrap()))
}
// this MUST be used like a Singleton
@ -179,9 +161,9 @@ async fn test_route() {
let config: Config = Config::default();
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!(
HTTPStatusCode::Http400,
response.status_line.get_status_code()
response.get_status_code()
);
}

View File

@ -3,7 +3,7 @@ use std::path::Path;
use super::store::{Credentials, Store};
/// references a credentials store file
/// FileStore references a credentials store file
pub struct FileStore {
path: String,
credentials: Vec<Credentials>,
@ -17,7 +17,7 @@ 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
async fn parse_contents(&mut self) {
let contents = tokio::fs::read_to_string(&self.path).await;
@ -79,13 +79,14 @@ async fn test_store() {
use std::env;
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 mut store = FileStore::new(store_path);
let data = json::parse(r#"{"email": "toto@toto.fr", "password": "tata"}"#).unwrap();
let credentials = store.is_auth(&data).await;
assert_eq!(false, credentials.is_none());
assert_eq!(credentials.unwrap().email, "toto@toto.fr");
let credentials = Credentials::from(&data);
assert_eq!(credentials.get_email(), "toto@toto.fr");
let is_auth = store.is_auth(&credentials).await;
assert_eq!(true, is_auth);
}

View File

@ -16,15 +16,31 @@ fi
for i in {0..10}
do
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" ]
then
echo "bad http status code : ${http_response}, expect 200"
echo "bad http status code : ${http_response}, expect 403"
exit 1
fi
if [ "$(cat response.txt | jq -r '.error')" != "url forbidden" ]
then
echo "bad data returned, expect : invalid credentials"
echo "bad data returned, expect : url forbidden"
exit 1
fi
done
@ -35,7 +51,7 @@ do
http_response=$(curl -s -o response.txt -w "%{http_code}" ${URL}/ge/ -d '{"username":"toto", "password":"tutu"}')
if [ $http_response != "404" ]
then
echo "bad http status code : ${http_response}, expect 400"
echo "bad http status code : ${http_response}, expect 404"
exit 1
fi
done
@ -46,7 +62,7 @@ do
http_response=$(curl -s -o response.txt -w "%{http_code}" ${URL}/pubkey/)
if [ $http_response != "200" ]
then
echo "bad http status code : ${http_response}, expect 400"
echo "bad http status code : ${http_response}, expect 200"
exit 1
fi
done

View File

@ -22,7 +22,7 @@ class TestResponse(TestCase):
self.assertEqual(resp.status_code, 200, "bad status code returned")
self.assertIsNotNone(resp.json(), "response data can't be empty")
token = resp.json()["token"]
token = resp.json()["access_token"]
jwt_decoded = jwt.decode(
token,
pubkey or self.pub_key,