freechat/json-api/src/auth.rs
shockrah cdb956a85c Minor changes lumped together
* claim.rng is now seeded from 16 bytes
+ passing Member to auth::login_get_jwt
* Better error loggin on jwt::insert call

+ Moar meta config-caching fixes
Basically lazy static sucks and we have to start accessing meta::basic_config
though a proxy function which forces the initialization on startup
! We should probably init this prior to listening for connections to avoid connection issues

+ New /neighbors/list endpoint which has already passed required checks
2021-05-08 02:03:58 -07:00

317 lines
9.1 KiB
Rust

use serde::{Serialize, Deserialize};
use bcrypt::{self, BcryptResult};
use mysql_async::Pool;
use hyper::StatusCode;
use std::collections::HashMap;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use crate::routes;
use db::{Response, Member};
use jsonwebtoken::EncodingKey;
lazy_static! {
static ref HMAC_SECRET: Vec<u8> = {
let path = match std::env::var("HMAC_PATH") {
Ok(p) => p,
Err(_) => "hmac.secret".into()
};
std::fs::read(path).expect("Couldn't get HMAC secret")
};
static ref ENCODING_KEY: EncodingKey = {
EncodingKey::from_secret(&HMAC_SECRET)
};
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Claim {
sub: u64,// user id
exp: i64, // expiry date
nbf: i64, // "valid-start" time for the claim
prm: u64, // user permissions
rng: String, // Cheesy way of helping reduce the amount
}
impl Claim {
pub fn new(id: db::UBigInt, prm: u64) -> Claim {
// JWT's expire every 48 hours
let now = SystemTime::now();
let exp = (now + Duration::from_secs(60 * 60 * 48))
.duration_since(UNIX_EPOCH)
.expect("System time conversion failed")
.as_secs() as i64;
let nbf = now
.duration_since(UNIX_EPOCH)
.expect("System time conversion failed")
.as_secs() as i64;
Claim {
sub: id,
exp,
nbf,
prm,
rng: generate_cookie()
}
}
}
// used when we create a new users for the first time
#[derive(Debug)]
pub enum AuthReason {
Good(Claim), //passed regular check
OpenAuth, // route does not require auth
NoKey, // key missing
BadKey, // key is bad
LoginValid(Member), // 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(16)
}
pub fn encrypt_secret(raw: &str) -> BcryptResult<String> {
const BCRYPT_COST: u32 = 14;
return bcrypt::hash(raw, BCRYPT_COST);
}
async fn valid_jwt(pool: &Pool, path: &str, token: &str) -> AuthReason {
/*
* This function does a database check because of the requirement of always
* enforcing permissions
* TODO: Not all routes actually require a permissions check so we should
* try to only do that DB call when absolutely required
*/
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_secs() as i64;
let active = now < decoded.claims.exp;
if active {
if routes::requires_perms(path) {
match db::jwt::listed(pool, decoded.claims.sub, decoded.claims.rng.as_str()).await {
Ok(listed) => if listed {
AuthReason::Good(decoded.claims)
} else {
AuthReason::BadKey
}
_ => AuthReason::BadKey
}
} else {
AuthReason::Good(decoded.claims)
}
} else {
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 {
return valid_jwt(pool, path, 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 {
match Member::get(pool, id).await {
Ok(response) => match response {
Response::Row(user) => {
if valid_secret(secret, &user.secret) && valid_perms(&user, path){
AuthReason::LoginValid(user)
}
else {
AuthReason::BadKey
}
},
Response::Empty => AuthReason::BadKey,
Response::Other(err) => AuthReason::ServerIssue(err),
_ => AuthReason::ServerIssue("db-lib returned garbage".into())
}
Err(err) => {
AuthReason::ServerIssue(format!("{}", err))
}
}
}
} else {
return AuthReason::NoKey;
}
}
pub async fn login_get_jwt(p: &Pool, response: &mut hyper::Response<hyper::Body>, user: Member) {
// 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 = user.id;
let claim = Claim::new(id, user.permissions);
let header = Header::new(Algorithm::HS512);
let encoded = encode(
&header,
&claim,
&ENCODING_KEY).unwrap();
response.headers_mut().insert("Content-Type",
HeaderValue::from_static("application/json"));
match db::jwt::insert(p, id, &claim.rng).await {
Ok(_) => http::set_json_body(response, serde_json::json!({"jwt": encoded})),
Err(e) => {
eprintln!("/login 500 {}", e);
*response.status_mut() = 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, 456); // 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);
}
}