1. adding hmac_secret as a private lazy_static variable
This is only initialized once and is read only after the fact so it should be fine 2. Adding a Claim struct that all JWT's will use for their structure. A helpful .new() function is provided 3. AuthReason::ServerIssue(String) is a now a thing Should help the auth module be more clear in what's going on 4. fn get_jwt_json -> renamed from get_jwt to indicate we're getting something from json payload. Nothing more 5. async fn valid_jwt: Attempts to decode the given token, if successful it then looks if that user has given us an active jwt budget-session-key if (returns boolean) 6. wall_entry now returns an AuthReason without the sql wrapping, since thats db-lib's problem anyway Also we're collecting all data upfront then verifying it as we go. 7. async fn login_get_jwt::ROUTE On login_auth here we simply create a JWT for the user, store it in cache, then give it back to the user to use. Tokens take a week to expire 8. Removed busted tests that weren't doing anything useful for the time being
This commit is contained in:
parent
c0f5908089
commit
11251fe6d3
@ -1,16 +1,40 @@
|
|||||||
|
use serde::{Serialize, Deserialize};
|
||||||
use bcrypt::{self, BcryptResult};
|
use bcrypt::{self, BcryptResult};
|
||||||
use mysql_async::Pool;
|
use mysql_async::Pool;
|
||||||
use mysql_async::error::Error as SqlError;
|
use chrono::{Utc, Duration};
|
||||||
|
|
||||||
use hyper::{
|
|
||||||
HeaderMap, header::HeaderValue
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::routes;
|
use crate::routes;
|
||||||
|
|
||||||
use db::{member::Member, common::FromDB};
|
use db::{member::Member, common::FromDB};
|
||||||
use db::Response;
|
use db::Response;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref HMAC_SECRET: String = {
|
||||||
|
std::fs::read_to_string("hmac.secret").expect("Couldn't get HMAC secret")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct Claim {
|
||||||
|
sub: db::UBigInt, // user id
|
||||||
|
exp: db::BigInt, // expiry date
|
||||||
|
cookie: String, // unique cookie value
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Claim {
|
||||||
|
pub fn new(id: db::UBigInt) -> Claim {
|
||||||
|
Claim {
|
||||||
|
sub: id,
|
||||||
|
exp: Utc::now()
|
||||||
|
.checked_add_signed(Duration::weeks(1))
|
||||||
|
.expect("Couldn't generate an expirey date")
|
||||||
|
.timestamp(),
|
||||||
|
cookie: generate_secret()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// 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 {
|
||||||
@ -18,6 +42,7 @@ pub enum AuthReason {
|
|||||||
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
|
||||||
|
ServerIssue(String) // for well 500's
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -61,15 +86,48 @@ pub fn encrypt_secret(raw: &str) -> BcryptResult<String> {
|
|||||||
return bcrypt::hash(raw, BCRYPT_COST);
|
return bcrypt::hash(raw, BCRYPT_COST);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_jwt(params: &serde_json::Value) -> Option<&str> {
|
fn get_jwt_json(params: &serde_json::Value) -> Option<&str> {
|
||||||
// gets the `token` from the parameters
|
// gets the `token` from the parameters
|
||||||
// option<value> -> some(value) -> string
|
// option<value> -> some(value) -> string
|
||||||
return params.get("token")?.as_str();
|
return params.get("token")?.as_str();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn valid_jwt(token: &str) -> AuthReason {
|
async fn valid_jwt(token: &str) -> AuthReason {
|
||||||
// TODO : take the token string and check if we have the jwt somewhere
|
use jsonwebtoken::{
|
||||||
return AuthReason::Good; // Good|Bad
|
decode, DecodingKey,
|
||||||
|
Validation, Algorithm
|
||||||
|
};
|
||||||
|
// TODO: add a blacklist in redis to make sure we don't ever accidently authenticate a bad
|
||||||
|
// token
|
||||||
|
// NOTE: for now we're doing purely stateless validation with a bs key
|
||||||
|
|
||||||
|
// crypto things that should prolly not fail assuming we're configured correctly
|
||||||
|
let algo = Algorithm::HS512;
|
||||||
|
let dk = DecodingKey::from_base64_secret(&HMAC_SECRET).unwrap();
|
||||||
|
let raw = decode::<Claim>(token, &dk, &Validation::new(algo));
|
||||||
|
|
||||||
|
// if the decoding worked then check on the redis cache for the jwt
|
||||||
|
// recall we have the id as a lookup but it is mapped to a session-id
|
||||||
|
// that mapping should be the same as the temporary usermapping
|
||||||
|
// Additionally: invalidating a session id is as easy as just making a new new for the user
|
||||||
|
if raw.is_err() {
|
||||||
|
return AuthReason::BadKey;
|
||||||
|
}
|
||||||
|
let raw = raw.unwrap();
|
||||||
|
let id = raw.claims.sub;
|
||||||
|
let sesh_id = raw.claims.cookie;
|
||||||
|
|
||||||
|
return match db::auth::active_jwt(id, &sesh_id).await {
|
||||||
|
Ok(active) => {
|
||||||
|
match active {
|
||||||
|
true => AuthReason::Good,
|
||||||
|
false => AuthReason::BadKey
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
AuthReason::ServerIssue(format!("{}", err))
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_login(params: &serde_json::Value) -> Option<(db::UBigInt, &str)> {
|
fn get_login(params: &serde_json::Value) -> Option<(db::UBigInt, &str)> {
|
||||||
@ -88,85 +146,82 @@ fn get_login(params: &serde_json::Value) -> Option<(db::UBigInt, &str)> {
|
|||||||
|
|
||||||
|
|
||||||
pub async fn wall_entry<'path, 'pool, 'params>(
|
pub async fn wall_entry<'path, 'pool, 'params>(
|
||||||
headers: HeaderMap<HeaderValue>,
|
|
||||||
path: &'path str,
|
path: &'path str,
|
||||||
pool: &'pool Pool,
|
pool: &'pool Pool,
|
||||||
params: &'params serde_json::Value)
|
params: &'params serde_json::Value)
|
||||||
-> Result<AuthReason, SqlError> {
|
-> AuthReason {
|
||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
// Dont need to auth if it's not required
|
// Dont need to auth if it's not required
|
||||||
let open_path = routes::is_open(path);
|
let open_path = routes::is_open(path);
|
||||||
let jwt = get_jwt(params);
|
let jwt = get_jwt_json(params);
|
||||||
|
|
||||||
let head = headers.get("Authorization");
|
|
||||||
println!("Auth header: {:?}", head);
|
|
||||||
|
|
||||||
if open_path { // ignore the parameters since they're irelevant
|
if open_path { // ignore the parameters since they're irelevant
|
||||||
return Ok(AuthReason::OpenAuth);
|
return AuthReason::OpenAuth;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(jwt) = jwt {
|
if let Some(jwt) = jwt {
|
||||||
// get the headers here
|
// get the headers here
|
||||||
return Ok(valid_jwt(jwt));
|
return valid_jwt(jwt).await;
|
||||||
}
|
}
|
||||||
if let Some((id, secret)) = get_login(params) {
|
if let Some((id, secret)) = get_login(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
|
||||||
|
|
||||||
if path != routes::AUTH_LOGIN {
|
if path != routes::AUTH_LOGIN {
|
||||||
return Ok(AuthReason::BadKey);
|
return AuthReason::BadKey;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return match Member::get(pool, id).await {
|
return match Member::get(pool, id).await {
|
||||||
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){
|
||||||
Ok(AuthReason::Good)
|
AuthReason::Good
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Ok(AuthReason::BadKey)
|
AuthReason::BadKey
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Response::Empty => Ok(AuthReason::BadKey),
|
Response::Empty => AuthReason::BadKey,
|
||||||
Response::Other(err) => Err(SqlError::Other(Cow::from(err))),
|
Response::Other(err) => AuthReason::ServerIssue(err),
|
||||||
_ => Err(SqlError::Other(Cow::from("Undefined result")))
|
_ => AuthReason::ServerIssue("db-lib returned garbage".into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Ok(AuthReason::BadKey);
|
return AuthReason::NoKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn login_get_jwt(response: &mut hyper::Response<hyper::Body>, params: serde_json::Value) {
|
pub async fn login_get_jwt(response: &mut hyper::Response<hyper::Body>, params: serde_json::Value) {
|
||||||
let (id_v, secret_v) = (params.get("id"), params.get("secret"));
|
// basically this route generates a jwt for the user and returns via the jwt key
|
||||||
|
// in the json response
|
||||||
|
use jsonwebtoken::{
|
||||||
|
Header, Algorithm,
|
||||||
|
encode, EncodingKey
|
||||||
|
};
|
||||||
|
use hyper::header::HeaderValue;
|
||||||
|
let id = params.get("id").unwrap().as_u64().unwrap(); // only route where we have the "id is there guarantee"
|
||||||
|
let claim = Claim::new(id);
|
||||||
|
let header = Header::new(Algorithm::HS512);
|
||||||
|
let encoded = encode(
|
||||||
|
&header,
|
||||||
|
&claim,
|
||||||
|
&EncodingKey::from_base64_secret(HMAC_SECRET.as_ref()).expect("Couldn't encode from secret"))
|
||||||
|
.expect("Could not encode JWT");
|
||||||
|
|
||||||
|
if let Ok(_) = db::auth::add_jwt(id, &encoded).await {
|
||||||
|
response.headers_mut().insert("Content-Type",
|
||||||
|
HeaderValue::from_static("application/json"));
|
||||||
|
|
||||||
|
let payload = serde_json::json!({
|
||||||
|
"jwt": encoded
|
||||||
|
});
|
||||||
|
*response.body_mut() = hyper::Body::from(payload.to_string());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
*response.status_mut() = hyper::StatusCode::INTERNAL_SERVER_ERROR;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod auth_tests {
|
mod auth_tests {
|
||||||
use crate::testing::get_pool;
|
|
||||||
use serde_json::Value;
|
|
||||||
use mysql_async::prelude::Queryable;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn missing_key() {
|
|
||||||
let pool = get_pool();
|
|
||||||
let conn = pool.get_conn().await.unwrap();
|
|
||||||
let conn = conn.drop_exec(
|
|
||||||
r#"INSERT INTO members (id, secret, name, joindate, status,permissions)
|
|
||||||
VALUES(1, "abc", "bsname", 1,0,0)
|
|
||||||
"#,
|
|
||||||
()).await.unwrap();
|
|
||||||
|
|
||||||
let params: Value = serde_json::from_str(r#"
|
|
||||||
{
|
|
||||||
"id": 1
|
|
||||||
}
|
|
||||||
"#).unwrap();
|
|
||||||
|
|
||||||
let result = super::wall_entry("/channels/list", &pool, ¶ms).await;
|
|
||||||
let _ = conn.drop_exec(r#"DELETE FROM members WHERE secret = "abc""#,()).await;
|
|
||||||
assert_eq!(true, result.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn validity_check() {
|
fn validity_check() {
|
||||||
use bcrypt::{hash, DEFAULT_COST};
|
use bcrypt::{hash, DEFAULT_COST};
|
||||||
|
Loading…
Reference in New Issue
Block a user