+ Jwt tables - SEE NOTE

! wat - because have to do maintain permissions on a per request level we have
to do this check for permissions at what is basically every level, this does
mean we have to hit the database for a lot of routes however there is a check
that requests go through in order to avoid hitting the database whenever possible

+ rng field in claims now has real purpose
It's purpose is to act as a validator field in the jwt table. By verifying rng
fields we no  longer have to store whole jwt's
This commit is contained in:
shockrah 2021-05-08 01:29:44 -07:00
parent 9a22713080
commit c850d42ce1
6 changed files with 73 additions and 22 deletions

19
json-api/db/src/jwt.rs Normal file
View File

@ -0,0 +1,19 @@
use mysql_async::{Pool, params, Result, prelude::Queryable};
pub async fn listed(p: &Pool, id: u64, given_rng_value: &str) -> Result<bool> {
let mut conn = p.get_conn().await?;
let q = "SELECT rng FROM jwt WHERE id = :id";
let row: Option<String> = conn.exec_first(q, params!{"id" => id}).await?;
if let Some(value) = row {
Ok(value == given_rng_value)
} else{
Ok(false)
}
}
pub async fn insert(p: &Pool, id: u64, given_rng_value: &str) -> Result<()> {
let mut conn = p.get_conn().await?;
let q = "INSERT INTO jwt (id, rng) VALUES (:id, :rng)";
conn.exec_drop(q, params!{"id" => id, "rng" => given_rng_value}).await?;
Ok(())
}

View File

@ -6,6 +6,7 @@ pub mod invites;
pub mod channels; pub mod channels;
pub mod messages; pub mod messages;
pub mod neighbors; pub mod neighbors;
pub mod jwt;
use std::vec::Vec; use std::vec::Vec;

View File

@ -0,0 +1 @@
DROP TABLE `jwt`;

View File

@ -0,0 +1,5 @@
CREATE TABLE IF NOT EXISTS `jwt` (
`id` BIGINT UNSIGNED NOT NULL,
`rng` VARCHAR(48) NOT NULL,
PRIMARY KEY(`id`)
);

View File

@ -6,7 +6,6 @@ use std::collections::HashMap;
use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::time::{Duration, SystemTime, UNIX_EPOCH};
use crate::routes; use crate::routes;
use crate::qs_param;
use db::{Response, Member}; use db::{Response, Member};
@ -26,15 +25,16 @@ lazy_static! {
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
struct Claim { pub struct Claim {
sub: db::UBigInt, // user id sub: u64,// user id
exp: db::BigInt, // expiry date exp: i64, // expiry date
nbf: i64, nbf: i64, // "valid-start" time for the claim
cookie: String, // unique cookie value prm: u64, // user permissions
rng: String, // Cheesy way of helping reduce the amount
} }
impl Claim { impl Claim {
pub fn new(id: db::UBigInt) -> Claim { pub fn new(id: db::UBigInt, prm: u64) -> Claim {
// JWT's expire every 48 hours // JWT's expire every 48 hours
let now = SystemTime::now(); let now = SystemTime::now();
@ -52,20 +52,20 @@ impl Claim {
sub: id, sub: id,
exp, exp,
nbf, nbf,
cookie: generate_cookie() prm,
rng: generate_cookie()
} }
} }
} }
// used when we create a new users for the first time // used when we create a new users for the first time
#[derive(Debug)] #[derive(Debug)]
pub enum AuthReason { pub enum AuthReason {
Good, //passed regular check Good(Claim), //passed regular check
OpenAuth, // route does not require auth OpenAuth, // route does not require auth
NoKey, // key missing NoKey, // key missing
BadKey, // key is bad BadKey, // key is bad
LoginValid, // used only to access the login route which is also our refresh LoginValid(Member), // used only to access the login route which is also our refresh
ServerIssue(String) // for well 500's ServerIssue(String) // for well 500's
} }
@ -81,7 +81,7 @@ fn valid_secret(given_pass: &str, hash: &str) -> bool {
} }
} }
fn valid_perms(member: Member, path: &str) -> bool { fn valid_perms(member: &Member, path: &str) -> bool {
use crate::perms; use crate::perms;
// if there are perms on the current path make sure the user has them // if there are perms on the current path make sure the user has them
if let Some(p) = perms::get_perm_mask(path) { if let Some(p) = perms::get_perm_mask(path) {
@ -120,7 +120,13 @@ pub fn encrypt_secret(raw: &str) -> BcryptResult<String> {
} }
async fn valid_jwt(token: &str) -> AuthReason { 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::{ use jsonwebtoken::{
decode, DecodingKey, decode, DecodingKey,
Validation, Algorithm Validation, Algorithm
@ -136,7 +142,18 @@ async fn valid_jwt(token: &str) -> AuthReason {
let active = now < decoded.claims.exp; let active = now < decoded.claims.exp;
if active { if active {
AuthReason::Good 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 { } else {
AuthReason::BadKey AuthReason::BadKey
} }
@ -182,7 +199,7 @@ pub async fn wall_entry<'path, 'pool, 'params>(
if let Some(jwt) = jwt { if let Some(jwt) = jwt {
// get the headers here // get the headers here
return valid_jwt(jwt).await; return valid_jwt(pool, path, jwt).await;
} }
if let Some((id, secret)) = login_params_from_qs(params) { 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 // Last chance we might be hitting the /login route so we have to do the heavy auth flow
@ -194,8 +211,8 @@ pub async fn wall_entry<'path, 'pool, 'params>(
match Member::get(pool, id).await { match Member::get(pool, id).await {
Ok(response) => match response { Ok(response) => match response {
Response::Row(user) => { Response::Row(user) => {
if valid_secret(secret, &user.secret) && valid_perms(user, path){ if valid_secret(secret, &user.secret) && valid_perms(&user, path){
AuthReason::LoginValid AuthReason::LoginValid(user)
} }
else { else {
AuthReason::BadKey AuthReason::BadKey
@ -215,7 +232,7 @@ pub async fn wall_entry<'path, 'pool, 'params>(
} }
} }
pub async fn login_get_jwt(response: &mut hyper::Response<hyper::Body>, params: HashMap<String, String>) { pub async fn login_get_jwt(response: &mut hyper::Response<hyper::Body>, user: Member) {
// Login data has already been validated at this point // Login data has already been validated at this point
// Required data such as 'id' and 'secret' are there and validated // Required data such as 'id' and 'secret' are there and validated
use jsonwebtoken::{ use jsonwebtoken::{
@ -225,10 +242,9 @@ pub async fn login_get_jwt(response: &mut hyper::Response<hyper::Body>, params:
use hyper::header::HeaderValue; use hyper::header::HeaderValue;
use crate::http; use crate::http;
let id = qs_param!(params, "id", u64).unwrap(); let id = user.id;
let claim = Claim::new(id, user.permissions);
let claim = Claim::new(id);
let header = Header::new(Algorithm::HS512); let header = Header::new(Algorithm::HS512);
let encoded = encode( let encoded = encode(
&header, &header,
@ -259,7 +275,7 @@ mod auth_tests {
#[test] #[test]
fn verify_jwt() { fn verify_jwt() {
let claim = super::Claim::new(123); // example claim that we send out 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 header = Header::new(Algorithm::HS512); // header that basically all clients get
let encoded = encode( let encoded = encode(
&header, &header,

View File

@ -24,7 +24,16 @@ pub const SELF_UPDATE_NICKNAME: Rstr= "/members/me/nickname";
pub const SET_PERMS_BY_ADMIN: Rstr = "/admin/setpermisions"; // @requires perms::ADMIN pub const SET_PERMS_BY_ADMIN: Rstr = "/admin/setpermisions"; // @requires perms::ADMIN
pub const SET_NEW_ADMIN: Rstr = "/owner/newadmin"; // @requiers: owner perms pub const SET_NEW_ADMIN: Rstr = "/owner/newadmin"; // @requiers: owner perms
// Server -> Server Routes
pub const GET_NEIGHBORS: Rstr = "/neighbors/list"; // @requires: none @note must be a member for this list
pub fn is_open(path: &str) -> bool { pub fn is_open(path: &str) -> bool {
return path.starts_with("/join") || path.starts_with("/meta"); return path.starts_with("/join") || path.starts_with("/meta");
} }
pub fn requires_perms(path: &str) -> bool {
return match path {
AUTH_LOGIN | META | CHANNELS_LIST | GET_MYSELF | GET_NEIGHBORS => false,
_ => true
}
}