From c850d42ce150baad71209c761fba503abc3127a8 Mon Sep 17 00:00:00 2001 From: shockrah Date: Sat, 8 May 2021 01:29:44 -0700 Subject: [PATCH] + 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 --- json-api/db/src/jwt.rs | 19 ++++++ json-api/db/src/lib.rs | 1 + .../migrations/2021-05-07-201858_jwt/down.sql | 1 + .../migrations/2021-05-07-201858_jwt/up.sql | 5 ++ json-api/src/auth.rs | 60 ++++++++++++------- json-api/src/routes.rs | 9 +++ 6 files changed, 73 insertions(+), 22 deletions(-) create mode 100644 json-api/db/src/jwt.rs create mode 100644 json-api/migrations/2021-05-07-201858_jwt/down.sql create mode 100644 json-api/migrations/2021-05-07-201858_jwt/up.sql diff --git a/json-api/db/src/jwt.rs b/json-api/db/src/jwt.rs new file mode 100644 index 0000000..26efeaa --- /dev/null +++ b/json-api/db/src/jwt.rs @@ -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 { + let mut conn = p.get_conn().await?; + let q = "SELECT rng FROM jwt WHERE id = :id"; + let row: Option = 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(()) +} diff --git a/json-api/db/src/lib.rs b/json-api/db/src/lib.rs index aef5d38..564d421 100644 --- a/json-api/db/src/lib.rs +++ b/json-api/db/src/lib.rs @@ -6,6 +6,7 @@ pub mod invites; pub mod channels; pub mod messages; pub mod neighbors; +pub mod jwt; use std::vec::Vec; diff --git a/json-api/migrations/2021-05-07-201858_jwt/down.sql b/json-api/migrations/2021-05-07-201858_jwt/down.sql new file mode 100644 index 0000000..df15255 --- /dev/null +++ b/json-api/migrations/2021-05-07-201858_jwt/down.sql @@ -0,0 +1 @@ +DROP TABLE `jwt`; diff --git a/json-api/migrations/2021-05-07-201858_jwt/up.sql b/json-api/migrations/2021-05-07-201858_jwt/up.sql new file mode 100644 index 0000000..e91e054 --- /dev/null +++ b/json-api/migrations/2021-05-07-201858_jwt/up.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS `jwt` ( + `id` BIGINT UNSIGNED NOT NULL, + `rng` VARCHAR(48) NOT NULL, + PRIMARY KEY(`id`) +); diff --git a/json-api/src/auth.rs b/json-api/src/auth.rs index 3060b92..40fda73 100644 --- a/json-api/src/auth.rs +++ b/json-api/src/auth.rs @@ -6,7 +6,6 @@ use std::collections::HashMap; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use crate::routes; -use crate::qs_param; use db::{Response, Member}; @@ -26,15 +25,16 @@ lazy_static! { } #[derive(Debug, Serialize, Deserialize)] -struct Claim { - sub: db::UBigInt, // user id - exp: db::BigInt, // expiry date - nbf: i64, - cookie: String, // unique cookie value +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) -> Claim { + pub fn new(id: db::UBigInt, prm: u64) -> Claim { // JWT's expire every 48 hours let now = SystemTime::now(); @@ -52,20 +52,20 @@ impl Claim { sub: id, exp, nbf, - cookie: generate_cookie() + prm, + rng: generate_cookie() } } } - // used when we create a new users for the first time #[derive(Debug)] pub enum AuthReason { - Good, //passed regular check + Good(Claim), //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 + LoginValid(Member), // used only to access the login route which is also our refresh 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; // if there are perms on the current path make sure the user has them if let Some(p) = perms::get_perm_mask(path) { @@ -120,7 +120,13 @@ pub fn encrypt_secret(raw: &str) -> BcryptResult { } -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::{ decode, DecodingKey, Validation, Algorithm @@ -136,7 +142,18 @@ async fn valid_jwt(token: &str) -> AuthReason { let active = now < decoded.claims.exp; 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 { AuthReason::BadKey } @@ -182,7 +199,7 @@ pub async fn wall_entry<'path, 'pool, 'params>( if let Some(jwt) = jwt { // 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) { // 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 { Ok(response) => match response { Response::Row(user) => { - if valid_secret(secret, &user.secret) && valid_perms(user, path){ - AuthReason::LoginValid + if valid_secret(secret, &user.secret) && valid_perms(&user, path){ + AuthReason::LoginValid(user) } else { AuthReason::BadKey @@ -215,7 +232,7 @@ pub async fn wall_entry<'path, 'pool, 'params>( } } -pub async fn login_get_jwt(response: &mut hyper::Response, params: HashMap) { +pub async fn login_get_jwt(response: &mut hyper::Response, user: Member) { // Login data has already been validated at this point // Required data such as 'id' and 'secret' are there and validated use jsonwebtoken::{ @@ -225,10 +242,9 @@ pub async fn login_get_jwt(response: &mut hyper::Response, params: use hyper::header::HeaderValue; use crate::http; - let id = qs_param!(params, "id", u64).unwrap(); + let id = user.id; - - let claim = Claim::new(id); + let claim = Claim::new(id, user.permissions); let header = Header::new(Algorithm::HS512); let encoded = encode( &header, @@ -259,7 +275,7 @@ mod auth_tests { #[test] 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 encoded = encode( &header, diff --git a/json-api/src/routes.rs b/json-api/src/routes.rs index e2bfb6e..9f8f672 100644 --- a/json-api/src/routes.rs +++ b/json-api/src/routes.rs @@ -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_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 { 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 + } +}