Compare commits

..

No commits in common. "master" and "v0.2.0" have entirely different histories.

25 changed files with 1219 additions and 793 deletions

626
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package]
name = "simple-auth"
version = "0.3.2"
version = "0.2.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -12,12 +12,6 @@ regex = "1"
tokio = { version = "1.21.1", features = ["full"] }
async-trait = "0.1.57"
jwt-simple = "0.11.1"
simple_logger = "4.0.0"
log = "0.4.17"
base64 = "0.13.1"
serde_json = "1.0"
http = { git = "https://gitea.thegux.fr/rmanach/http", version = "0.1.6" }
# useful for tests (embedded files should be delete in release ?)
#rust-embed="6.4.1"
@ -33,7 +27,3 @@ features = ["derive"]
[dependencies.async-std]
version = "1.6"
features = ["attributes"]
[dependencies.serde]
version = "1.0"
features = ["derive"]

View File

@ -8,11 +8,12 @@ cargo build --release
```
## Configuration
### Store
The store represents the credentials. For now, this a `.txt` file with plain passwords. You have to create one like:
```txt
# acts as a comment (only on a start line)
<email>:<password>
<username>:<password>
```
**WARN**: the file should have a chmod to **600**.
@ -44,19 +45,13 @@ expiration_time = 2 # in hours
```bash
./simple-auth <ini_path>
# get a JWT
curl http://<ip>:<port>/get/ -d '{"email":"<email>", "password":"<password>"}'
curl http://<ip>:<port>/get/ -d '{"username":"<user>", "password":"<password>"}'
# should returned
{"token":"<header>.<payload>.<signature>"}
# validate a JWT
curl http://<ip>:<port>/validate/ -d '{"token":"<header>.<payload>.<signature>"}'
# should returned (if valid)
{"valid":"true"}
# get the public key for local validation
curl http://<ip>:<port>/pubkey/
{"pubkey":"<b64_encoded_public_key>"}
```
## Test
@ -71,7 +66,7 @@ cargo test
* set the following env variables:
```bash
export SIMPLE_AUTH_URL="http://<url>:<port>"
export SIMPLE_AUTH_PUB_KEY="<path_to_pem_pub_key>" # DO NOT USE THIS ONE IN PRODUCTION !
export SIMPLE_AUTH_PUB_KEY="<path_to_pem_pub_key>" # DO NOT USE THE ONE IN PRODUCTION !
```
* run the server (if no one is running remotly)
* run curl tests
@ -79,14 +74,14 @@ export SIMPLE_AUTH_PUB_KEY="<path_to_pem_pub_key>" # DO NOT USE THIS ONE IN PROD
cd tests/bash/
./curling.bash && echo "passed"
```
* run python tests
* run python requests tests
```bash
# create a python venv
cd tests/python
python3 -m venv venv
source venv/bin/activate
# install the requirements
# intall the requirements
pip install -r requirements
# launch the tests
@ -96,5 +91,5 @@ python -m unittest
## Documentation
```bash
# add the '--open' arg to open the doc on a browser
cargo doc -r --no-deps
cargo doc --no-deps
```

View File

@ -1,3 +1,5 @@
//! config module implements all the utilities to properly create and validate a router config
use configparser::ini::Ini;
use std::str::FromStr;
@ -32,10 +34,7 @@ impl TryFrom<Ini> for Config {
match u64::from_str(&exp_time) {
Ok(v) => v,
Err(e) => {
log::error!(
"unable to convert JWT expiration time into u64 details={}",
e
);
eprintln!("unable to convert JWT expiration time into u64 err={}", e);
0
}
}
@ -60,27 +59,27 @@ impl Config {
/// validates config ini file
fn validate(&self) -> bool {
if self.jwt_exp_time <= 0 {
log::error!("invalid config parameter: JWT expiration time is negative or equals to 0");
eprintln!("invalid config parameter: JWT expiration time is negative or equals to 0");
return false;
}
if self.jwt_issuer == "" {
log::error!("invalid config parameter: JWT issuer is empty");
eprintln!("invalid config parameter: JWT issuer is empty");
return false;
}
if self.jwt_pub_key == "" {
log::error!("invalid config parameter: JWT public key file path is empty");
eprintln!("invalid config parameter: JWT public key file path is empty");
return false;
}
if self.jwt_priv_key == "" {
log::error!("invalid config parameter: JWT private key file path is empty");
eprintln!("invalid config parameter: JWT private key file path is empty");
return false;
}
if self.filestore_path == "" {
log::error!("invalid config parameter: filestore path is empty");
eprintln!("invalid config parameter: filestore path is empty");
return false;
}

View File

@ -1,5 +1,3 @@
//! **config** module provides `Config` struct to load and validate `.ini` file.
mod config;
pub use config::Config;

93
src/http/message.rs Normal file
View File

@ -0,0 +1,93 @@
use json;
use std::collections::HashMap;
const JSON_DELIMITER: &'static str = ",";
/// `HashMap` wrapper, represents the JSON response body
pub struct HTTPMessage {
message: HashMap<String, String>,
}
impl Default for HTTPMessage {
fn default() -> Self {
HTTPMessage {
message: HashMap::new(),
}
}
}
/// try to convert `HTTPMessage` in `json::JsonValue`
impl TryInto<json::JsonValue> for HTTPMessage {
type Error = String;
fn try_into(self) -> Result<json::JsonValue, Self::Error> {
let message = format!(r#"{{{}}}"#, self.build_json());
match json::parse(&message) {
Ok(r) => Ok(r),
Err(e) => Err(format!(
"unable to parse the HTTPMessage correctly: {}, err={}",
message, e
)),
}
}
}
impl HTTPMessage {
pub fn put(&mut self, key: &str, value: &str) {
self.message.insert(key.to_string(), value.to_string());
}
/// associated function to build an HTTPMessage error
pub fn error(message: &str) -> Option<json::JsonValue> {
let mut http_message = HTTPMessage::default();
http_message.put("error", message);
match message.try_into() {
Ok(m) => Some(m),
Err(e) => {
eprintln!(
"unable to parse the message: {} into JSON, err={}",
message, e
);
return None;
}
}
}
/// loops over all the HashMap keys, builds a JSON key value for each one and join them with `JSON_DELIMITER`
fn build_json(self) -> String {
let unstruct: Vec<String> = self
.message
.keys()
.map(|k| format!(r#""{}":{:?}"#, k, self.message.get(k).unwrap()))
.collect();
let joined = unstruct.join(JSON_DELIMITER);
joined
}
}
#[test]
fn test_message() {
let mut http_message = HTTPMessage::default();
http_message.put("username", "toto");
http_message.put("password", "tata");
let mut json_result: Result<json::JsonValue, String> = http_message.try_into();
assert!(json_result.is_ok());
let mut json = json_result.unwrap();
assert!(json.has_key("username"));
assert!(json.has_key("password"));
let empty_http_message = HTTPMessage::default();
json_result = empty_http_message.try_into();
assert!(json_result.is_ok());
json = json_result.unwrap();
assert_eq!("{}", json.dump().to_string());
let mut bad_http_message = HTTPMessage::default();
bad_http_message.put("\"", "");
json_result = bad_http_message.try_into();
assert!(json_result.is_err());
}

11
src/http/mod.rs Normal file
View File

@ -0,0 +1,11 @@
//! http module includes tools to parse an HTTP request and build and HTTP response
pub mod message;
pub mod request;
pub mod response;
pub mod router;
pub use message::HTTPMessage;
pub use request::{HTTPRequest, HTTPVersion};
pub use response::{HTTPResponse, HTTPStatusCode};
pub use router::ROUTER;

408
src/http/request.rs Normal file
View File

@ -0,0 +1,408 @@
//! request handles properly the incoming request
//! it will parse the request according to the HTTP message specifications
//! see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages
//! NOTE: only few parts of the specification has been implemented
use json;
use lazy_static::lazy_static;
use regex::Regex;
use std::collections::VecDeque;
use crate::utils::extract_json_value;
type RequestParts = (String, VecDeque<String>, String);
const HTTP_REQUEST_SEPARATOR: &'static str = "\r\n";
const NULL_CHAR: &'static str = "\0";
lazy_static! {
static ref HTTP_VERSION_REGEX: Regex = Regex::new("^HTTP/(1.1|1.0|2)$").unwrap();
}
#[derive(Debug, Copy, Clone)]
pub enum HTTPVersion {
Http1_0,
Http1_1,
Http2,
Unknown,
}
impl Into<String> for HTTPVersion {
fn into(self) -> String {
match self {
Self::Http1_0 => "HTTP/1.0".to_string(),
Self::Http1_1 => "HTTP/1.1".to_string(),
Self::Http2 => "HTTP/2".to_string(),
Self::Unknown => "UNKNOWN".to_string(),
}
}
}
// TODO: not really satifying... could accept `String` too
impl From<&String> for HTTPVersion {
fn from(http_version: &String) -> Self {
match http_version.as_str() {
"HTTP/1.0" => Self::Http1_0,
"HTTP/1.1" => Self::Http1_1,
"HTTP/2" => Self::Http2,
_ => Self::Unknown,
}
}
}
#[derive(Debug)]
pub struct HTTPStartLine {
method: String,
target: String,
version: HTTPVersion,
}
impl HTTPStartLine {
fn new(method: String, target: String, version: HTTPVersion) -> Self {
HTTPStartLine {
method,
target,
version,
}
}
fn parse(start_line: &str) -> Result<Self, &str> {
// declare a new `start_line` var to borrow to &str `start_line`
let start_line = start_line.to_string();
let parts: Vec<&str> = start_line.split(" ").collect();
if parts.len() < 3 {
return Err("unable to parse the start correctly");
}
let method = parts[0].to_string();
let target = parts[1].to_string();
let version = parts[2].to_string();
if !Self::check_version(&version) {
return Err("http version validation failed, unknown version");
}
Ok(HTTPStartLine::new(
method,
target,
HTTPVersion::from(&version),
))
}
fn check_version(version: &String) -> bool {
HTTP_VERSION_REGEX.is_match(version)
}
pub fn is_valid(&self) -> bool {
return self.method != "" && self.target != "";
}
pub fn get_target(&self) -> String {
self.target.clone()
}
}
impl Default for HTTPStartLine {
fn default() -> Self {
HTTPStartLine {
method: "".to_string(),
target: "".to_string(),
version: HTTPVersion::Unknown,
}
}
}
impl Into<String> for HTTPStartLine {
fn into(self) -> String {
let version: String = self.version.into();
return format!("{} {} {}", self.method, self.target, version);
}
}
/// represents an HTTP request body
/// for simplicity, only json body is accepted
#[derive(Debug)]
pub struct HTTPBody {
data: json::JsonValue,
}
impl HTTPBody {
fn new(data: json::JsonValue) -> HTTPBody {
HTTPBody { data }
}
pub fn get_data(&self) -> &json::JsonValue {
&self.data
}
}
impl TryFrom<String> for HTTPBody {
type Error = String;
fn try_from(body: String) -> Result<HTTPBody, Self::Error> {
let body = body.replace(NULL_CHAR, "");
match json::parse(&body) {
Ok(v) => Ok(HTTPBody::new(v)),
Err(e) => Err(format!(
"error occurred during request body parsing err={}",
e
)),
}
}
}
/// Represents an HTTP request (headers are not parsed)
#[derive(Debug)]
pub struct HTTPRequest {
pub start_line: HTTPStartLine,
pub body: Option<HTTPBody>,
}
impl HTTPRequest {
/// split correctly the incoming request in order to get :
/// * start_line
/// * headers
/// * data (if exists)
fn get_request_parts(request: &str) -> Result<RequestParts, String> {
// separate the body part from the start_line and the headers
let mut request_parts: VecDeque<String> = request
.split(HTTP_REQUEST_SEPARATOR)
.map(|r| r.to_string())
.collect();
if request_parts.len() < 3 {
return Err("request has no enough informations to be correctly parsed".to_string());
}
let start_line = request_parts.pop_front().unwrap();
let body = request_parts.pop_back().unwrap();
Ok((start_line, request_parts, body))
}
/// parse the request by spliting the incoming request with the separator `\r\n`
fn parse(request: &str) -> Result<HTTPRequest, String> {
let request = request.to_string();
match HTTPRequest::get_request_parts(&request) {
Ok(rp) => {
let mut request = HTTPRequest::default();
let start_line = HTTPStartLine::parse(&rp.0);
match start_line {
Ok(v) => request.start_line = v,
Err(e) => eprintln!("error occurred while parsing start_line err={}", e),
}
let body = HTTPBody::try_from(rp.2);
match body {
Ok(v) => request.body = Some(v),
Err(e) => eprintln!("error occurred during body parsing err={}", e),
}
return Ok(request);
}
Err(e) => {
return Err(format!("error occurred getting request parts err={}", e));
}
}
}
/// retrieve value in `HTTPBody` (returns None if empty or does not exist)
pub fn get_body_value(&self, key: &str) -> Option<String> {
match self.body {
Some(ref b) => match &b.data {
json::JsonValue::Object(d) => extract_json_value(&d, key),
_ => None,
},
None => None,
}
}
#[allow(dead_code)]
pub fn is_valid(&self) -> bool {
return self.start_line.is_valid();
}
}
impl Default for HTTPRequest {
fn default() -> Self {
HTTPRequest {
start_line: HTTPStartLine::default(),
body: None,
}
}
}
impl From<&str> for HTTPRequest {
fn from(request: &str) -> Self {
match Self::parse(request) {
Ok(v) => v,
Err(v) => {
eprintln!("{}", format!("[ERR]: {v}"));
return HTTPRequest::default();
}
}
}
}
#[test]
fn test_request() {
struct Expect {
start_line: String,
body: Option<String>,
is_valid: bool,
has_token: bool,
}
let test_cases: [(String, Expect); 12] = [
(
"POST /get/ HTTP/1.1\r\n\r\n".to_string(),
Expect {
start_line: "POST /get/ HTTP/1.1".to_string(),
body: None,
is_valid: true,
has_token: false,
},
),
(
"POST /refresh/ HTTP/2\r\n\r\n".to_string(),
Expect {
start_line: "POST /refresh/ HTTP/2".to_string(),
body: None,
is_valid: true,
has_token: false,
},
),
(
"POST /validate/ HTTP/1.0\r\n\r\n".to_string(),
Expect {
start_line: "POST /validate/ HTTP/1.0".to_string(),
body: None,
is_valid: true,
has_token: false,
},
),
(
"GET / HTTP/1.1\r\n\r\n".to_string(),
Expect {
start_line: "GET / HTTP/1.1".to_string(),
body: None,
is_valid: true,
has_token: false,
},
),
// intentionally add HTTP with no version number
(
"OPTIONS /admin/2 HTTP/\r\nContent-Type: application/json\r\n".to_string(),
Expect {
start_line: " UNKNOWN".to_string(),
body: None,
is_valid: false,
has_token: false,
},
),
(
"POST HTTP".to_string(),
Expect {
start_line: " UNKNOWN".to_string(),
body: None,
is_valid: false,
has_token: false
}
),
(
"".to_string(),
Expect {
start_line: " UNKNOWN".to_string(),
body: None,
is_valid: false,
has_token: false
}
),
(
"fjlqskjd /oks?id=65 HTTP/2\r\n\r\n".to_string(),
Expect {
start_line: "fjlqskjd /oks?id=65 HTTP/2".to_string(),
body: None,
is_valid: true,
has_token: false
}
),
(
" ".to_string(),
Expect {
start_line: " UNKNOWN".to_string(),
body: None,
is_valid: false,
has_token: false,
}
),
(
r#"lm //// skkss\r\ndkldklkdl\r\n"{"access_token":"AAAAAAAAAAAA.BBBBBBBBBB.CCCCCCCCCC","refresh_token": "DDDDDDDDDDD.EEEEEEEEEEE.FFFFF"}""#.to_string(),
Expect {
start_line: " UNKNOWN".to_string(),
body: Some(r#"{"access_token":"AAAAAAAAAAAA.BBBBBBBBBB.CCCCCCCCCC","refresh_token": "DDDDDDDDDDD.EEEEEEEEEEE.FFFFF"}"#.to_string()),
is_valid: false,
has_token: false
}
),
(
format!("{}\r\nuselessheaders\r\n{}", "POST /refresh/ HTTP/1.1", r#"{"access_token": "toto", "refresh_token": "tutu"}"#),
Expect {
start_line: "POST /refresh/ HTTP/1.1".to_string(),
body: Some(r#"{"access_token":"toto","refresh_token":"tutu"}"#.to_string()),
is_valid: true,
has_token: false
}
),
(
format!("{}\r\nuselessheaders\r\n{}", "POST /get/ HTTP/1.1", r#"{"token": "toto", "refresh_token": "tutu"}"#),
Expect {
start_line: "POST /get/ HTTP/1.1".to_string(),
body: Some(r#"{"token":"toto","refresh_token":"tutu"}"#.to_string()),
is_valid: true,
has_token: true
}
),
];
for (request, expect) in test_cases {
let http_request = HTTPRequest::from(request.as_str());
assert_eq!(expect.is_valid, http_request.is_valid());
let token = http_request.get_body_value("token");
let start_line: String = http_request.start_line.into();
assert_eq!(expect.start_line, start_line);
match http_request.body {
Some(v) => {
assert_eq!(expect.body.unwrap(), v.data.dump())
}
None => continue,
}
match expect.has_token {
true => assert!(token.is_some()),
false => assert!(token.is_none()),
}
}
}
#[test]
fn test_http_body() {
let test_cases: [(&str, bool); 3] = [
("hello, how are you ?", false),
("qsdfqsdffqsdffsq", false),
(
r#"{"access_token":"AAAAAAAAAAAA.BBBBBBBBBB.CCCCCCCCCC","refresh_token": "DDDDDDDDDDD.EEEEEEEEEEE.FFFFF"}"#,
true,
),
];
for (body, is_valid) in test_cases {
match HTTPBody::try_from(body.to_string()) {
Ok(_) => assert!(is_valid),
Err(_) => assert!(!is_valid),
}
}
}

172
src/http/response.rs Normal file
View File

@ -0,0 +1,172 @@
//! response handles the incoming request parsed `HTTPRequest`
//! it will build an HTTPResponse corresponding to the HTTP message specs. see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages
//! NOTE: only few parts of the specification has been implemented
use super::{HTTPMessage, HTTPVersion};
use json;
#[derive(Debug, PartialEq, Clone)]
pub enum HTTPStatusCode {
Http200,
Http400,
Http403,
Http404,
Http500,
}
impl Into<String> for HTTPStatusCode {
fn into(self) -> String {
match self {
Self::Http200 => "200".to_string(),
Self::Http400 => "400".to_string(),
Self::Http404 => "404".to_string(),
Self::Http403 => "403".to_string(),
Self::Http500 => "500".to_string(),
}
}
}
pub struct HTTPStatusLine {
version: HTTPVersion,
status_code: HTTPStatusCode,
}
impl Default for HTTPStatusLine {
fn default() -> HTTPStatusLine {
HTTPStatusLine {
version: HTTPVersion::Http1_1,
status_code: HTTPStatusCode::Http400,
}
}
}
impl Into<String> for HTTPStatusLine {
fn into(self) -> String {
let version: String = self.version.into();
let status_code: String = self.status_code.into();
format! {"{} {}", version, status_code}
}
}
impl HTTPStatusLine {
pub fn set_status_code(&mut self, code: HTTPStatusCode) {
self.status_code = code;
}
#[allow(dead_code)]
pub fn get_status_code(&self) -> HTTPStatusCode {
self.status_code.clone()
}
}
/// represents an HTTP response (headers are not parsed)
/// NOTE: for simplicity, only JSON body are accepted
pub struct HTTPResponse {
pub status_line: HTTPStatusLine,
body: json::JsonValue,
}
impl Default for HTTPResponse {
fn default() -> Self {
HTTPResponse {
status_line: HTTPStatusLine::default(),
body: json::parse(r#"{"error": "the incoming request is not valid"}"#).unwrap(),
}
}
}
impl Into<String> for HTTPResponse {
fn into(self) -> String {
// move `self.body` into a new var
let b = self.body;
let body: String = json::stringify(b);
let status_line: String = self.status_line.into();
format!(
"{}\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
status_line,
body.len(),
body
)
}
}
impl HTTPResponse {
pub fn as_500(message: Option<json::JsonValue>) -> Self {
let mut response = Self::default();
response
.status_line
.set_status_code(HTTPStatusCode::Http500);
response.body = {
match message {
Some(m) => m,
None => json::parse(r#"{"error": "unexpected error occurred"}"#).unwrap(),
}
};
response
}
pub fn as_404() -> Self {
let mut response = Self::default();
response
.status_line
.set_status_code(HTTPStatusCode::Http404);
response.body = json::parse(r#"{"error": "the url requested does not exist"}"#).unwrap();
response
}
pub fn as_403() -> Self {
let mut response = HTTPResponse {
status_line: HTTPStatusLine::default(),
body: json::parse(r#"{"error": "invalid credentials"}"#).unwrap(),
};
response
.status_line
.set_status_code(HTTPStatusCode::Http403);
response
}
/// wrap the `Self::default()` associated func (not really clear)
pub fn as_400() -> Self {
Self::default()
}
pub fn as_200(message: Option<json::JsonValue>) -> Self {
let mut response = Self::default();
response
.status_line
.set_status_code(HTTPStatusCode::Http200);
response.body = {
match message {
Some(m) => m,
None => json::parse(r#"{"status": "ok"}"#).unwrap(),
}
};
response
}
/// builds an HTTP 200 response with the generated JWT
pub fn send_token(token: &str) -> Self {
let mut http_message = HTTPMessage::default();
http_message.put("token", token);
let message = {
match http_message.try_into() {
Ok(m) => m,
Err(_e) => json::parse(r#"{"token": "error.generation.token"}"#).unwrap(),
}
};
HTTPResponse::as_200(Some(message))
}
}

121
src/http/router.rs Normal file
View File

@ -0,0 +1,121 @@
//! router aims to handle correctly the request corresponding to the target
//! it implements all the logic to build an `HTTPResponse`
use json;
use super::{HTTPMessage, HTTPRequest, HTTPResponse};
use crate::config::Config;
use crate::jwt::JWTSigner;
use crate::stores::{FileStore, Store};
// TODO: must be mapped with corresponding handler
const GET_ROUTE: &'static str = "/get/";
const VALIDATE_ROUTE: &'static str = "/validate/";
async fn handle_get(request: HTTPRequest, config: Config) -> HTTPResponse {
let mut store = FileStore::new(config.filestore_path.clone());
match &request.body {
Some(ref b) => {
let is_auth = store.is_auth(&b.get_data()).await;
if !is_auth {
return HTTPResponse::as_403();
}
let jwt_signer = {
match JWTSigner::new(config).await {
Ok(s) => s,
Err(e) => {
let message = HTTPMessage::error(&e);
return HTTPResponse::as_500(message);
}
}
};
match jwt_signer.sign() {
Ok(t) => HTTPResponse::send_token(&t),
Err(e) => {
let message = HTTPMessage::error(&e);
return HTTPResponse::as_500(message);
}
}
}
None => HTTPResponse::as_400(),
}
}
/// validates the token by checking:
/// * expiration time
/// * signature
async fn handle_validate(request: HTTPRequest, config: Config) -> HTTPResponse {
let token = {
match request.get_body_value("token") {
Some(t) => t,
None => {
let mut message = HTTPMessage::default();
message.put("valid", "false");
message.put("reason", "no token provided in the request body");
let json = message.try_into().unwrap();
return HTTPResponse::as_200(Some(json));
}
}
};
let jwt_signer = {
match JWTSigner::new(config).await {
Ok(s) => s,
Err(e) => {
let message = HTTPMessage::error(&e);
let json = message.try_into().unwrap();
return HTTPResponse::as_500(Some(json));
}
}
};
let mut message = HTTPMessage::default();
match jwt_signer.validate(&token) {
Ok(()) => {
message.put("valid", "true");
}
Err(e) => {
message.put("valid", "false");
message.put("reason", &e);
}
}
let json: json::JsonValue = message.try_into().unwrap();
HTTPResponse::as_200(Some(json))
}
pub struct Router;
impl Router {
pub async fn route(&self, request_str: &str, config: Config) -> HTTPResponse {
let request = HTTPRequest::from(request_str);
let target = request.start_line.get_target();
match target.as_str() {
GET_ROUTE => handle_get(request, config).await,
VALIDATE_ROUTE => handle_validate(request, config).await,
_ => HTTPResponse::as_404(),
}
}
}
// this MUST be used like a Singleton
pub const ROUTER: Router = Router {};
#[tokio::test]
async fn test_route() {
use super::HTTPStatusCode;
let router: &Router = &ROUTER;
let config: Config = Config::default();
let request_str = "POST /get/ HTTP/1.1\r\n\r\n";
let response: HTTPResponse = router.route(request_str, config).await;
assert_eq!(
HTTPStatusCode::Http400,
response.status_line.get_status_code()
);
}

View File

@ -1,18 +1,11 @@
//! simple module to read `.pem` files and sign the token
use crate::config::Config;
use jwt_simple::common::VerificationOptions;
use jwt_simple::prelude::*;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use tokio::fs;
use crate::message::JWTMessage;
use crate::stores::Credentials;
#[derive(Serialize, Deserialize)]
struct JWTCustomClaims {
email: String,
}
pub struct JWTSigner {
private_key: String,
public_key: String,
@ -35,7 +28,7 @@ impl JWTSigner {
jwt_signer.private_key = c;
}
Err(e) => {
return Err(format!("unable to read the private key details={}", e));
return Err(format!("unable to read the private key err={}", e));
}
}
@ -44,7 +37,7 @@ impl JWTSigner {
jwt_signer.public_key = c;
}
Err(e) => {
return Err(format!("unable to read the public key details={}", e));
return Err(format!("unable to read the public key err={}", e));
}
}
@ -61,31 +54,23 @@ impl JWTSigner {
verification_options
}
/// sign builds and signs the token
pub fn sign(&self, credentials: Credentials) -> Result<String, String> {
/// builds and signs the token
pub fn sign(&self) -> Result<String, String> {
let jwt_key = {
match RS384KeyPair::from_pem(&self.private_key) {
Ok(k) => k,
Err(e) => {
return Err(format!("unable to load the private key details={}", e));
return Err(format!("unable to load the private key err={}", e));
}
}
};
let mut claims = Claims::with_custom_claims(
JWTCustomClaims {
email: credentials.get_email(),
},
Duration::from_hours(self.exp_time),
);
let mut claims = Claims::create(Duration::from_hours(self.exp_time));
claims.issuer = Some(self.issuer.clone());
match jwt_key.sign(claims) {
Ok(token) => {
// TODO: need to generate the refresh token
return Ok(serde_json::to_string(&JWTMessage::with_access(token)).unwrap());
}
Ok(token) => Ok(token),
Err(e) => {
return Err(format!("unable to sign the token details={}", e));
return Err(format!("unable to sign the token err={}", e));
}
}
}
@ -97,20 +82,16 @@ impl JWTSigner {
if let Err(e) =
key.verify_token::<NoCustomClaims>(token, Some(verification_options))
{
return Err(format!("token validation failed details={}", e));
return Err(format!("token validation failed err={}", e));
}
Ok(())
}
Err(e) => Err(format!(
"token validation failed, can't read the public key details={}",
"token validation failed can't read the public key err={}",
e
)),
}
}
pub fn get_public_key(&self) -> String {
self.public_key.clone()
}
}
#[tokio::test]

View File

@ -1,5 +1,3 @@
//! **jwt** module aims to read `.pem` files and sign/validate the token.
mod jwt;
pub use jwt::JWTSigner;

View File

@ -1,8 +1,8 @@
mod config;
mod http;
mod jwt;
mod message;
mod router;
mod stores;
mod utils;
use clap::Parser;
use configparser::ini::Ini;
@ -12,8 +12,8 @@ use tokio::{
time::{timeout, Duration},
};
use crate::router::ROUTER;
use config::Config;
use http::ROUTER;
#[derive(Parser)]
#[clap(author, version, about, long_about = None)]
@ -24,14 +24,13 @@ struct Cli {
#[tokio::main]
async fn main() {
simple_logger::init_with_level(log::Level::Info).unwrap();
let args = Cli::parse();
let mut config = Ini::new();
match config.load(args.config) {
Ok(c) => c,
Err(e) => {
log::error!("error while loading the config file details={}", e);
eprintln!("error while loading the config file, err={}", e);
std::process::exit(1);
}
};
@ -40,11 +39,11 @@ async fn main() {
let listener = {
match TcpListener::bind(&server_url).await {
Ok(t) => {
log::info!("server is listening on '{}'", server_url);
println!("server is listening on '{}'", server_url);
t
}
Err(e) => {
log::error!("while initializing tcp listener details={}", e);
eprintln!("error occurred while initializing tcp listener err={}", e);
std::process::exit(1);
}
}
@ -53,31 +52,28 @@ async fn main() {
let router_config: Config = {
match Config::try_from(config) {
Ok(c) => c,
Err(e) => {
log::error!("unable to load the configuration details={}", e);
Err(_e) => {
std::process::exit(1);
}
}
};
loop {
let (stream, addr) = listener.accept().await.unwrap();
let (stream, _) = listener.accept().await.unwrap();
let conf = router_config.clone();
tokio::spawn(handle_connection(stream, addr.to_string(), conf.clone()));
tokio::spawn(handle_connection(stream, conf.clone()));
}
}
/// handle_connection parses the incoming request and builds an HTTP response
async fn handle_connection(mut stream: TcpStream, addr: String, config: Config) {
log::info!("client connected: {}", addr);
/// parses the incoming request (partial spec implementation) and build an HTTP response
async fn handle_connection(mut stream: TcpStream, config: Config) {
let mut message = vec![];
let mut buffer: [u8; 1024] = [0; 1024];
let duration = Duration::from_millis(5);
let duration = Duration::from_micros(500);
// loop until the message is read
// the stream can be fragmented so, using a timeout (5ms should be far enough) for the future for completion
// the stream can be fragmented so, using a timeout (500um should be enough) for the future for completion
// after the timeout, the message is "considered" as entirely read
loop {
match timeout(duration, stream.read(&mut buffer)).await {
@ -95,6 +91,4 @@ async fn handle_connection(mut stream: TcpStream, addr: String, config: Config)
stream.write(response_str.as_bytes()).await.unwrap();
stream.flush().await.unwrap();
log::info!("connection closed: {}", addr);
}

View File

@ -1,48 +0,0 @@
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();
}
}

View File

@ -1,5 +0,0 @@
//! **message** module holds all structs to manage JSON response body for the authentication.
mod message;
pub use message::{JWTMessage, ValidationMessage};

View File

@ -1,4 +0,0 @@
//! **router** module includes all the handlers to get and validate JWT.
mod router;
pub use router::ROUTER;

View File

@ -1,162 +0,0 @@
use http::{HTTPRequest, HTTPResponse, JSONMessage};
use json::JsonValue;
use crate::config::Config;
use crate::jwt::JWTSigner;
use crate::message::{JWTMessage, ValidationMessage};
use crate::stores::{Credentials, FileStore, Store};
// TODO: must be mapped with corresponding handler
const GET_ROUTE: &'static str = "/get/";
const VALIDATE_ROUTE: &'static str = "/validate/";
const PUBKEY_ROUTE: &'static str = "/pubkey/";
async fn handle_get(request: HTTPRequest<'_>, config: Config, method: &str) -> HTTPResponse {
if method.trim().to_lowercase() != "post" {
return HTTPResponse::as_400();
}
let mut store = FileStore::new(config.filestore_path.clone());
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");
return HTTPResponse::as_400();
}
if !store.is_auth(&credentials).await {
return HTTPResponse::as_403();
}
let jwt_signer = {
match JWTSigner::new(config).await {
Ok(s) => s,
Err(e) => {
let message = JSONMessage::error(&e);
return HTTPResponse::as_500(message);
}
}
};
match jwt_signer.sign(credentials) {
Ok(t) => send_token(&t),
Err(e) => {
let message = JSONMessage::error(&e);
return HTTPResponse::as_500(message);
}
}
}
None => HTTPResponse::as_400(),
}
}
/// handle_validate validates the token by checking:
/// * expiration time
/// * signature
async fn handle_validate(request: HTTPRequest<'_>, config: Config, method: &str) -> HTTPResponse {
if request.get_method().trim().to_lowercase() != method {
return HTTPResponse::as_400();
}
let token = {
match request.get_body_value("token") {
Some(t) => t,
None => {
let mut message = ValidationMessage::default();
message.set_reason("no token provided in the request body");
let json = json::parse(&serde_json::to_string(&message).unwrap()).unwrap();
return HTTPResponse::as_200(Some(json));
}
}
};
let jwt_signer = {
match JWTSigner::new(config).await {
Ok(s) => s,
Err(e) => {
let message = JSONMessage::error(&e);
let json = message.try_into().unwrap();
return HTTPResponse::as_500(Some(json));
}
}
};
let mut message = ValidationMessage::default();
match jwt_signer.validate(&token) {
Ok(()) => {
message.set_valid(true);
}
Err(e) => {
message.set_reason(&e);
}
}
let json: JsonValue = json::parse(&serde_json::to_string(&message).unwrap()).unwrap();
HTTPResponse::as_200(Some(json))
}
/// handle_public_key returns the JWT public key in base64 encoded
async fn handle_public_key(request: HTTPRequest<'_>, config: Config, method: &str) -> HTTPResponse {
if request.get_method().trim().to_lowercase() != method {
return HTTPResponse::as_400();
}
let jwt_signer = {
match JWTSigner::new(config).await {
Ok(s) => s,
Err(e) => {
let message = JSONMessage::error(&e);
let json = message.try_into().unwrap();
return HTTPResponse::as_500(Some(json));
}
}
};
let public_key = jwt_signer.get_public_key();
let message = serde_json::to_string(&JWTMessage::with_pubkey(public_key)).unwrap();
HTTPResponse::as_200(Some(json::parse(&message).unwrap()))
}
pub struct Router;
impl Router {
/// route routes the request to the corresponding handler
pub async fn route(&self, request_str: &str, config: Config) -> HTTPResponse {
let request = HTTPRequest::from(request_str);
match request.get_target() {
GET_ROUTE => handle_get(request, config, "post").await,
VALIDATE_ROUTE => handle_validate(request, config, "post").await,
PUBKEY_ROUTE => handle_public_key(request, config, "get").await,
_ => HTTPResponse::as_404(),
}
}
}
/// send_token generates an HTTPResponse with the new token
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::parse(message).unwrap()))
}
// this **MUST** be used like a Singleton
pub const ROUTER: Router = Router {};
#[tokio::test]
async fn test_route() {
use http::HTTPStatusCode;
let router: &Router = &ROUTER;
let config: Config = Config::default();
let request_str = "POST /get/ HTTP/1.1\r\n\r\n";
let response: HTTPResponse = router.route(request_str, config).await;
assert_eq!(HTTPStatusCode::Http400, response.get_status_code());
}

View File

@ -1,9 +1,10 @@
use async_trait::async_trait;
use json;
use std::path::Path;
use super::store::{Credentials, Store};
/// FileStore references a credentials store file
/// references a credentials store file
pub struct FileStore {
path: String,
credentials: Vec<Credentials>,
@ -17,9 +18,8 @@ impl FileStore {
}
}
/// parse_contents loads and reads the file asynchonously
///
/// It parses the file line by line to retrieve the credentials
/// 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;
let mut credentials: Vec<Credentials> = vec![];
@ -41,18 +41,21 @@ impl FileStore {
}
}
Err(e) => {
log::error!("while reading store file: {}, details={:?}", self.path, e);
eprintln!(
"error occurred while reading store file: {}, err={:?}",
self.path, e
);
}
}
self.credentials = credentials;
}
/// auth checks if the credentials exist in the `FileStore`
fn auth(&self, email: String, password: String) -> bool {
/// checks if the credentials exist in the `FileStore`
fn auth(&self, username: String, password: String) -> bool {
let credentials: Vec<&Credentials> = self
.credentials
.iter()
.filter(|x| *x.get_email() == email && *x.get_password() == password)
.filter(|x| x.username == username && x.password == password)
.collect();
if credentials.len() == 1 {
return true;
@ -63,15 +66,21 @@ impl FileStore {
#[async_trait]
impl Store for FileStore {
async fn is_auth(&mut self, credentials: &Credentials) -> bool {
async fn is_auth(&mut self, data: &json::JsonValue) -> bool {
// ensure that the store file already exists even after its instanciation
if !Path::new(&self.path).is_file() {
log::error!("{} path referencing file store does not exist", self.path);
eprintln!("{} path referencing file store does not exist", self.path);
return false;
}
let credentials = Credentials::from(data);
if credentials.is_empty() {
eprintln!("unable to parse the credentials correctly from the incoming request");
return false;
}
self.parse_contents().await;
self.auth(credentials.get_email(), credentials.get_password())
self.auth(credentials.username, credentials.password)
}
}
@ -80,14 +89,11 @@ 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 = Credentials::from(&data);
assert_eq!(credentials.get_email(), "toto@toto.fr");
let is_auth = store.is_auth(&credentials).await;
assert_eq!(true, is_auth);
let data = json::parse(r#"{"username": "toto", "password": "tata"}"#).unwrap();
assert_eq!(store.is_auth(&data).await, true);
}

View File

@ -1,10 +1,11 @@
//! **store** module lists interfaces available to check credentials. Each store must implement the trait `is_auth`.
//!
//! For now one store is available:
//! store module lists interfaces available to check request credentials
//! each store must implement the trait `is_auth`
//! two stores are available :
//! * `FileStore`: credentials stored in a text file (like **/etc/passwd**)
//! * `DBStore`: credentials stored in a database (TODO)
mod file;
mod store;
pub use file::FileStore;
pub use store::{Credentials, Store};
pub use store::Store;

View File

@ -1,55 +1,47 @@
use async_trait::async_trait;
use json::JsonValue;
use serde::Deserialize;
use json;
use crate::utils::extract_json_value;
#[async_trait]
pub trait Store {
async fn is_auth(&mut self, data: &Credentials) -> bool;
async fn is_auth(&mut self, data: &json::JsonValue) -> bool;
}
#[derive(Default, Debug, Deserialize)]
#[derive(Default, Debug)]
pub struct Credentials {
email: String,
password: String,
pub username: String,
pub password: String,
}
/// Credentials represents the incoming user credentials for authentication checking
impl Credentials {
pub fn new(email: String, password: String) -> Self {
Credentials { email, password }
}
pub fn get_email(&self) -> String {
self.email.clone()
}
pub fn get_password(&self) -> String {
self.password.clone()
pub fn new(username: String, password: String) -> Self {
Credentials { username, password }
}
pub fn is_empty(&self) -> bool {
self.email == "" || self.password == ""
self.username == "" || self.password == ""
}
}
// TODO: could be less restrictive with `From<&str>`
impl From<&JsonValue> for Credentials {
fn from(data: &JsonValue) -> Self {
let res = serde_json::from_str(&data.dump());
match res {
Ok(c) => c,
Err(e) => {
log::warn!("unable to deserialize credentials: {}", e);
return Credentials::default();
impl From<&json::JsonValue> for Credentials {
fn from(data: &json::JsonValue) -> Self {
let mut credentials = Credentials::default();
match data {
json::JsonValue::Object(ref d) => {
credentials.username = extract_json_value(&d, "username").unwrap_or("".to_string());
credentials.password = extract_json_value(&d, "password").unwrap_or("".to_string());
}
_ => return credentials,
}
credentials
}
}
#[test]
fn test_credentials() {
struct Expect {
data: JsonValue,
data: json::JsonValue,
is_empty: bool,
}
let test_cases: [Expect; 2] = [
@ -58,7 +50,7 @@ fn test_credentials() {
is_empty: true
},
Expect {
data: json::parse(r#"{"email":"toto@toto.fr","password": "tata"}"#).unwrap(),
data: json::parse(r#"{"username":"toto","password": "tata"}"#).unwrap(),
is_empty: false
}
];

3
src/utils/mod.rs Normal file
View File

@ -0,0 +1,3 @@
mod utils;
pub use utils::extract_json_value;

30
src/utils/utils.rs Normal file
View File

@ -0,0 +1,30 @@
use json::object::Object;
/// extracts JSON value from a key
pub fn extract_json_value(data: &Object, key: &str) -> Option<String> {
match data.get(key) {
Some(u) => match u.as_str() {
Some(s) => return Some(s.to_string()),
None => None,
},
None => None,
}
}
#[test]
fn test_extract_json_value() {
let test_cases: [(json::JsonValue, bool, bool); 3] = [
(json::parse(r#"{"test": ""}"#).unwrap(), true, true),
(json::parse(r#"{}"#).unwrap(), true, false),
(json::parse(r#"[]"#).unwrap(), false, false),
];
for (value, is_valid, has_key) in test_cases {
match value {
json::JsonValue::Object(d) => {
assert_eq!(has_key, extract_json_value(&d, "test").is_some());
}
_ => assert!(!is_valid),
}
}
}

View File

@ -6,41 +6,24 @@
#
#######################################
URL=${SIMPLE_AUTH_URL}
if [ -z ${URL} ]
if [ -z ${SIMPLE_AUTH_URL} ]
then
echo "[WARN]: SIMPLE_AUTH_URL is empty, set to http://localhost:5555"
URL="http://localhost:5555"
echo "[WARN]: SIMPLE_AUTH_URL is empty, set to http://localhost:9001"
URL="http://localhost:9001"
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 403"
echo "bad http status code : ${http_response}, expect 200"
exit 1
fi
if [ "$(cat response.txt | jq -r '.error')" != "url forbidden" ]
if [ "$(cat response.txt | jq -r '.error')" != "invalid credentials" ]
then
echo "bad data returned, expect : url forbidden"
echo "bad data returned, expect : invalid credentials"
exit 1
fi
done
@ -51,18 +34,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 404"
exit 1
fi
done
for i in {0..10}
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 200"
echo "bad http status code : ${http_response}, expect 400"
exit 1
fi
done

View File

@ -1,4 +1,4 @@
# this a test password storage with password in clear
# need to be updated in the future to encrypt or hash the password
# <email>:<password>
toto@toto.fr:tata
# <username>:<password>
toto:tata

View File

@ -1,4 +1,3 @@
import base64
import jwt
import os
import requests
@ -6,7 +5,7 @@ import requests
from datetime import datetime
from unittest import TestCase
URL = os.getenv("SIMPLE_AUTH_URL", "http://127.0.0.1:5555")
URL = os.getenv("SIMPLE_AUTH_URL", "http://127.0.0.1:9001")
PUB_KEY_PATH = os.getenv("SIMPLE_AUTH_PUB_KEY", "")
@ -15,17 +14,17 @@ class TestResponse(TestCase):
with open(PUB_KEY_PATH, "r") as f:
self.pub_key = f.read()
def test_get_target(self, pubkey=None):
def test_get_target(self):
resp = requests.post(
URL + "/get/", json={"email": "toto@toto.fr", "password": "tata"}
URL + "/get/", json={"username": "toto", "password": "tata"}
)
self.assertEqual(resp.status_code, 200, "bad status code returned")
self.assertIsNotNone(resp.json(), "response data can't be empty")
token = resp.json()["access_token"]
token = resp.json()["token"]
jwt_decoded = jwt.decode(
token,
pubkey or self.pub_key,
self.pub_key,
algorithms=["RS384"],
options={
"verify_signature": True,
@ -34,7 +33,6 @@ class TestResponse(TestCase):
},
)
self.assertEqual("thegux.fr", jwt_decoded["iss"])
self.assertEqual("toto@toto.fr", jwt_decoded["email"])
jwt_exp = datetime.fromtimestamp(jwt_decoded["exp"])
jwt_iat = datetime.fromtimestamp(jwt_decoded["iat"])
@ -44,21 +42,21 @@ class TestResponse(TestCase):
def test_validate_target_no_token(self):
resp = requests.post(
URL + "/validate/", json={"username": "toto@toto.fr", "password": "tata"}
URL + "/validate/", json={"username": "toto", "password": "tata"}
)
self.assertEqual(resp.status_code, 200, "bad status code returned")
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")
def test_validate_target_empty_token(self):
resp = requests.post(URL + "/validate/", json={"tutu": "tutu", "token": ""})
self.assertEqual(resp.status_code, 200, "bad status code returned")
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"],
"token validation failed details=JWT compact encoding error",
"token validation failed err=JWT compact encoding error",
)
def test_validate_target(self):
@ -67,7 +65,7 @@ class TestResponse(TestCase):
resp = requests.post(URL + "/validate/", json={"token": token})
self.assertEqual(resp.status_code, 200, "bad status code returned")
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
def test_refresh_target(self):
@ -78,7 +76,7 @@ class TestResponse(TestCase):
self.assertIsNotNone(resp.json(), "response data can't be empty")
self.assertEqual(
resp.json()["error"],
"url not found",
"the url requested does not exist",
"bad status returned",
)
@ -88,17 +86,19 @@ class TestResponse(TestCase):
self.assertIsNotNone(resp.json(), "response data must not be empty")
self.assertEqual(
resp.json()["error"],
"bad request",
"the incoming request is not valid",
"invalid error message returned",
)
def test_bad_credentials(self):
resp = requests.post(URL + "/get/", json={"email": "tutu", "password": "titi"})
resp = requests.post(
URL + "/get/", json={"username": "tutu", "password": "titi"}
)
self.assertEqual(resp.status_code, 403, "bad status code returned")
self.assertIsNotNone(resp.json(), "response data must not be empty")
self.assertEqual(
resp.json()["error"],
"url forbidden",
"invalid credentials",
"invalid error message returned",
)
@ -110,29 +110,6 @@ class TestResponse(TestCase):
self.assertIsNotNone(resp.json(), "response data must not be empty")
self.assertEqual(
resp.json()["error"],
"url not found",
"invalid error message returned",
)
def test_get_pubkey(self):
resp = requests.get(URL + "/pubkey/")
self.assertEqual(resp.status_code, 200, "bad status code returned")
self.assertIsNotNone(resp.json(), "response data must not be empty")
self.assertIsNotNone(resp.json()["pubkey"], "invalid error message returned")
b64_pubkey = base64.b64decode(resp.json()["pubkey"])
self.assertIsNotNone(b64_pubkey, "public key b64 decoded can't be empty")
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):
resp = requests.post(URL + "/pubkey/", json={"tutu": "toto"})
self.assertEqual(resp.status_code, 400, "bad status code returned")
self.assertIsNotNone(resp.json(), "response data must not be empty")
self.assertEqual(
resp.json()["error"],
"bad request",
"the url requested does not exist",
"invalid error message returned",
)