use serde_json to build http response with JWT json body
This commit is contained in:
parent
3b6e208004
commit
e992f7a3ce
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -635,8 +635,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.1.4"
|
version = "0.1.5"
|
||||||
source = "git+https://gitea.thegux.fr/rmanach/http#7d4aabad2c6d2cc07359b64214ba6e61f42ed80f"
|
source = "git+https://gitea.thegux.fr/rmanach/http#72bf34127b185f5a52895c08a3d8b229c0588dc0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"json",
|
"json",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
|||||||
@ -17,7 +17,7 @@ log = "0.4.17"
|
|||||||
base64 = "0.13.1"
|
base64 = "0.13.1"
|
||||||
serde_json = "1.0"
|
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 ?)
|
# useful for tests (embedded files should be delete in release ?)
|
||||||
#rust-embed="6.4.1"
|
#rust-embed="6.4.1"
|
||||||
|
|||||||
@ -7,6 +7,35 @@ use tokio::fs;
|
|||||||
|
|
||||||
use crate::stores::Credentials;
|
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)]
|
#[derive(Serialize, Deserialize)]
|
||||||
struct JWTCustomClaims {
|
struct JWTCustomClaims {
|
||||||
email: String,
|
email: String,
|
||||||
@ -60,7 +89,7 @@ impl JWTSigner {
|
|||||||
verification_options
|
verification_options
|
||||||
}
|
}
|
||||||
|
|
||||||
/// builds and signs the token
|
/// sign builds and signs the token
|
||||||
pub fn sign(&self, credentials: Credentials) -> 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) {
|
||||||
@ -71,13 +100,18 @@ impl JWTSigner {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut claims = Claims::with_custom_claims(
|
let mut claims = Claims::with_custom_claims(
|
||||||
JWTCustomClaims { email: credentials.get_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, sign/validate the token and have useful functions to
|
||||||
|
//! manage HTTP response message
|
||||||
mod jwt;
|
mod jwt;
|
||||||
|
|
||||||
pub use jwt::JWTSigner;
|
pub use jwt::{JWTMessage, JWTSigner};
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
//! 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 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::{JWTMessage, JWTSigner};
|
||||||
use crate::stores::{Credentials, FileStore, Store};
|
use crate::stores::{Credentials, FileStore, Store};
|
||||||
|
|
||||||
// TODO: must be mapped with corresponding handler
|
// 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() {
|
match request.get_body() {
|
||||||
Some(d) => {
|
Some(d) => {
|
||||||
|
|
||||||
let credentials = Credentials::from(d);
|
let credentials = Credentials::from(d);
|
||||||
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");
|
||||||
@ -59,11 +57,7 @@ async fn handle_get(request: HTTPRequest<'_>, config: Config, method: &str) -> H
|
|||||||
/// validates the token by checking:
|
/// 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();
|
||||||
}
|
}
|
||||||
@ -109,11 +103,7 @@ async fn handle_validate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// returns the JWT public key in base64 encoded
|
/// 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();
|
||||||
}
|
}
|
||||||
@ -130,18 +120,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 handling method
|
||||||
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() {
|
||||||
@ -154,18 +141,13 @@ 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
|
||||||
@ -179,9 +161,9 @@ 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,
|
HTTPStatusCode::Http400,
|
||||||
response.status_line.get_status_code()
|
response.get_status_code()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ 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>,
|
||||||
@ -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
|
/// 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;
|
||||||
@ -79,13 +79,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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user