
+ Auth module now uses std::time for time based calculations ! All time notations are i64 and rounded to the Millisecond * Moving db pool to a lazy static to avoid constructing a whole pool on every request + Adding more logging per request, even if its lazy logging * Content-Types are now correctly written per type
287 lines
8.0 KiB
Rust
287 lines
8.0 KiB
Rust
use serde::{Serialize, Deserialize};
|
|
use bcrypt::{self, BcryptResult};
|
|
use mysql_async::Pool;
|
|
|
|
use std::collections::HashMap;
|
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
|
|
use crate::routes;
|
|
use crate::qs_param;
|
|
|
|
use db::{Response, Member};
|
|
|
|
use jsonwebtoken::EncodingKey;
|
|
lazy_static! {
|
|
static ref HMAC_SECRET: Vec<u8> = {
|
|
std::fs::read("hmac.secret").expect("Couldn't get HMAC secret")
|
|
};
|
|
|
|
static ref ENCODING_KEY: EncodingKey = {
|
|
EncodingKey::from_secret(&HMAC_SECRET)
|
|
};
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct Claim {
|
|
sub: db::UBigInt, // user id
|
|
exp: db::BigInt, // expiry date
|
|
cookie: String, // unique cookie value
|
|
}
|
|
|
|
impl Claim {
|
|
pub fn new(id: db::UBigInt) -> Claim {
|
|
|
|
// JWT's expire every 48 hours
|
|
let now = (SystemTime::now() + Duration::from_secs(60 * 60 * 48))
|
|
.duration_since(UNIX_EPOCH)
|
|
.expect("System time fetch failed")
|
|
.as_millis() as i64;
|
|
|
|
Claim {
|
|
sub: id,
|
|
exp: now,
|
|
cookie: generate_cookie()
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// used when we create a new users for the first time
|
|
#[derive(Debug)]
|
|
pub enum AuthReason {
|
|
Good, //passed regular check
|
|
OpenAuth, // route does not require auth
|
|
NoKey, // key missing
|
|
BadKey, // key is bad
|
|
LoginValid, // used only to access the login route which is also our refresh
|
|
ServerIssue(String) // for well 500's
|
|
}
|
|
|
|
|
|
fn valid_secret(given_pass: &str, hash: &str) -> bool {
|
|
let result = bcrypt::verify(given_pass, hash);
|
|
return match result {
|
|
Ok(result) => result,
|
|
Err(e) => {
|
|
eprintln!("{}", e);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn valid_perms(member: Member, path: &str) -> bool {
|
|
use crate::perms;
|
|
// if there are perms on the current path make sure the user has them
|
|
if let Some(p) = perms::get_perm_mask(path) {
|
|
return (p & member.permissions) == p;
|
|
}
|
|
// if no perms then we don't care
|
|
else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
fn rng_secret(length: usize) -> String {
|
|
use getrandom::getrandom;
|
|
use base64::{encode_config, URL_SAFE};
|
|
|
|
let mut buf: Vec<u8> = vec![0;length];
|
|
getrandom(&mut buf).unwrap();
|
|
encode_config(buf,URL_SAFE)
|
|
|
|
}
|
|
|
|
pub fn generate_secret() -> String {
|
|
/*
|
|
* Generates a url-safe-plaintext secret for our db
|
|
* */
|
|
return rng_secret(64);
|
|
}
|
|
|
|
pub fn generate_cookie() -> String {
|
|
return rng_secret(32)
|
|
}
|
|
|
|
pub fn encrypt_secret(raw: &str) -> BcryptResult<String> {
|
|
const BCRYPT_COST: u32 = 14;
|
|
return bcrypt::hash(raw, BCRYPT_COST);
|
|
}
|
|
|
|
|
|
async fn valid_jwt(p: &Pool, token: &str) -> AuthReason {
|
|
use jsonwebtoken::{
|
|
decode, DecodingKey,
|
|
Validation, Algorithm
|
|
};
|
|
// crypto things that should prolly not fail assuming we're configured correctly
|
|
let algo = Algorithm::HS512;
|
|
let dk = DecodingKey::from_secret(&HMAC_SECRET);
|
|
if let Ok(decoded) = decode::<Claim>(token, &dk, &Validation::new(algo)) {
|
|
let now = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.expect("System time fetch failed")
|
|
.as_millis() as i64;
|
|
|
|
// subject used for querying speed NOT security
|
|
let listed = db::auth::listed_jwt(p, decoded.claims.sub, token).await.unwrap();
|
|
let active = now < decoded.claims.exp;
|
|
|
|
return match listed && active {
|
|
true => AuthReason::Good,
|
|
false => AuthReason::BadKey
|
|
};
|
|
}
|
|
else {
|
|
return AuthReason::BadKey;
|
|
}
|
|
}
|
|
|
|
fn login_params_from_qs(params: &HashMap<String, String>)
|
|
-> Option<(db::UBigInt, &str)> {
|
|
|
|
return match (params.get("id"), params.get("secret")) {
|
|
// partially accpept if both keys are present
|
|
(Some(id), Some(secret)) => {
|
|
let id_s: String = String::from(id);
|
|
|
|
match id_s.parse::<db::UBigInt>() {
|
|
// full accpet if id can parse + secret is present
|
|
Ok(id) => Some((id, secret)),
|
|
_ => None
|
|
}
|
|
},
|
|
|
|
_ => None
|
|
}
|
|
}
|
|
|
|
|
|
pub async fn wall_entry<'path, 'pool, 'params>(
|
|
path: &'path str,
|
|
pool: &'pool Pool,
|
|
params: &'params HashMap<String, String>)
|
|
-> AuthReason {
|
|
|
|
// Dont need to auth if it's not required
|
|
let open_path = routes::is_open(path);
|
|
let jwt = params.get("jwt");
|
|
|
|
if open_path { // ignore the parameters since they're irelevant
|
|
return AuthReason::OpenAuth;
|
|
}
|
|
|
|
if let Some(jwt) = jwt {
|
|
// get the headers here
|
|
return valid_jwt(pool, jwt).await;
|
|
}
|
|
if let Some((id, secret)) = login_params_from_qs(params) {
|
|
// Last chance we might be hitting the /login route so we have to do the heavy auth flow
|
|
|
|
if path != routes::AUTH_LOGIN {
|
|
return AuthReason::BadKey;
|
|
}
|
|
else {
|
|
return match Member::get(pool, id).await {
|
|
Response::Row(user) => {
|
|
if valid_secret(secret, &user.secret) && valid_perms(user, path){
|
|
AuthReason::LoginValid
|
|
}
|
|
else {
|
|
AuthReason::BadKey
|
|
}
|
|
},
|
|
Response::Empty => AuthReason::BadKey,
|
|
Response::Other(err) => AuthReason::ServerIssue(err),
|
|
_ => AuthReason::ServerIssue("db-lib returned garbage".into())
|
|
}
|
|
}
|
|
}
|
|
return AuthReason::NoKey;
|
|
}
|
|
|
|
pub async fn login_get_jwt(p: &Pool, response: &mut hyper::Response<hyper::Body>, params: HashMap<String, String>) {
|
|
// Login data has already been validated at this point
|
|
// Required data such as 'id' and 'secret' are there and validated
|
|
use jsonwebtoken::{
|
|
Header, Algorithm,
|
|
encode
|
|
};
|
|
use hyper::header::HeaderValue;
|
|
use crate::http;
|
|
|
|
let id = qs_param!(params, "id", u64).unwrap();
|
|
|
|
|
|
let claim = Claim::new(id);
|
|
let header = Header::new(Algorithm::HS512);
|
|
let encoded = encode(
|
|
&header,
|
|
&claim,
|
|
&ENCODING_KEY).unwrap();
|
|
|
|
match db::auth::add_jwt(p, id, &encoded).await {
|
|
Ok(_) => {
|
|
response.headers_mut().insert("Content-Type",
|
|
HeaderValue::from_static("application/json"));
|
|
|
|
http::set_json_body(response, serde_json::json!({"jwt": encoded}));
|
|
},
|
|
Err(e) => {
|
|
eprintln!("{}", e);
|
|
*response.status_mut() = hyper::StatusCode::INTERNAL_SERVER_ERROR;
|
|
}
|
|
};
|
|
}
|
|
|
|
|
|
#[cfg(test)]
|
|
mod auth_tests {
|
|
use jsonwebtoken::{
|
|
Header, encode, Algorithm
|
|
};
|
|
#[test]
|
|
fn validity_check() {
|
|
use bcrypt::{hash, DEFAULT_COST};
|
|
let plain = super::generate_secret();
|
|
match hash(&plain, DEFAULT_COST) {
|
|
Ok(hash) => assert_eq!(super::valid_secret(&plain, &hash), true),
|
|
Err(err) => panic!("{}", err)
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn verify_jwt() {
|
|
let claim = super::Claim::new(123); // example claim that we send out
|
|
let header = Header::new(Algorithm::HS512); // header that basically all clients get
|
|
let encoded = encode(
|
|
&header,
|
|
&claim,
|
|
&super::ENCODING_KEY).unwrap();
|
|
|
|
use jsonwebtoken::{decode, DecodingKey, Validation};
|
|
let dc = decode::<super::Claim>(
|
|
&encoded,
|
|
&DecodingKey::from_secret(&super::HMAC_SECRET),
|
|
&Validation::new(Algorithm::HS512)
|
|
); // decoding works yet fails on the debugger
|
|
assert_eq!(dc.is_ok(), true);
|
|
println!("{:?}", dc);
|
|
println!("{}", encoded);
|
|
|
|
let mut parts:Vec<String> = Vec::new();
|
|
for i in encoded.split('.') {
|
|
parts.push(i.to_string());
|
|
}
|
|
let head = &parts[0];
|
|
let claim = &parts[1];
|
|
let sig = &parts[2];
|
|
let res = jsonwebtoken::crypto::verify(
|
|
sig,
|
|
&format!("{}.{}", head, claim),
|
|
&DecodingKey::from_secret(&super::HMAC_SECRET),
|
|
Algorithm::HS512);
|
|
|
|
println!("{:?}", res);
|
|
}
|
|
}
|