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]]
|
||||
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",
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user