+ 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:
parent
9a22713080
commit
c850d42ce1
19
json-api/db/src/jwt.rs
Normal file
19
json-api/db/src/jwt.rs
Normal 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(())
|
||||||
|
}
|
@ -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;
|
||||||
|
|
||||||
|
1
json-api/migrations/2021-05-07-201858_jwt/down.sql
Normal file
1
json-api/migrations/2021-05-07-201858_jwt/down.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
DROP TABLE `jwt`;
|
5
json-api/migrations/2021-05-07-201858_jwt/up.sql
Normal file
5
json-api/migrations/2021-05-07-201858_jwt/up.sql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `jwt` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL,
|
||||||
|
`rng` VARCHAR(48) NOT NULL,
|
||||||
|
PRIMARY KEY(`id`)
|
||||||
|
);
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user