Merge branch 'feature/auth-refactor' into develop

This commit is contained in:
landrigun 2023-02-16 14:47:11 +00:00
commit bdd34a3490
16 changed files with 179 additions and 119 deletions

5
Cargo.lock generated
View File

@ -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",
] ]

View File

@ -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"

View File

@ -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
``` ```

View File

@ -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;

View File

@ -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));
} }

View File

@ -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;

View File

@ -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
View 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
View 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};

View File

@ -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;

View File

@ -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()
);
} }

View File

@ -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);
} }

View File

@ -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;

View File

@ -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
} }
} }

View File

@ -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

View File

@ -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):