+ 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 messages;
pub mod neighbors;
pub mod jwt;
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 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<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::{
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<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
// 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<hyper::Body>, 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,

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