diff --git a/server-api/src/auth.rs b/server-api/src/auth.rs index 1dd2fa2..63fe2cd 100644 --- a/server-api/src/auth.rs +++ b/server-api/src/auth.rs @@ -1,16 +1,40 @@ +use serde::{Serialize, Deserialize}; use bcrypt::{self, BcryptResult}; use mysql_async::Pool; -use mysql_async::error::Error as SqlError; - -use hyper::{ - HeaderMap, header::HeaderValue -}; +use chrono::{Utc, Duration}; use crate::routes; use db::{member::Member, common::FromDB}; 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 #[derive(Debug)] pub enum AuthReason { @@ -18,6 +42,7 @@ pub enum AuthReason { OpenAuth, // route does not require auth NoKey, // key missing BadKey, // key is bad + ServerIssue(String) // for well 500's } @@ -61,15 +86,48 @@ pub fn encrypt_secret(raw: &str) -> BcryptResult { 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 // option -> some(value) -> string return params.get("token")?.as_str(); } -fn valid_jwt(token: &str) -> AuthReason { - // TODO : take the token string and check if we have the jwt somewhere - return AuthReason::Good; // Good|Bad +async fn valid_jwt(token: &str) -> AuthReason { + use jsonwebtoken::{ + 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::(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)> { @@ -88,85 +146,82 @@ fn get_login(params: &serde_json::Value) -> Option<(db::UBigInt, &str)> { pub async fn wall_entry<'path, 'pool, 'params>( - headers: HeaderMap, path: &'path str, pool: &'pool Pool, params: &'params serde_json::Value) - -> Result { - use std::borrow::Cow; + -> AuthReason { // Dont need to auth if it's not required let open_path = routes::is_open(path); - let jwt = get_jwt(params); - - let head = headers.get("Authorization"); - println!("Auth header: {:?}", head); + let jwt = get_jwt_json(params); if open_path { // ignore the parameters since they're irelevant - return Ok(AuthReason::OpenAuth); + return AuthReason::OpenAuth; } if let Some(jwt) = jwt { // get the headers here - return Ok(valid_jwt(jwt)); + return valid_jwt(jwt).await; } 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 if path != routes::AUTH_LOGIN { - return Ok(AuthReason::BadKey); + return AuthReason::BadKey; } else { return match Member::get(pool, id).await { Response::Row(user) => { if valid_secret(secret, &user.secret) && valid_perms(user, path){ - Ok(AuthReason::Good) + AuthReason::Good } else { - Ok(AuthReason::BadKey) + AuthReason::BadKey } }, - Response::Empty => Ok(AuthReason::BadKey), - Response::Other(err) => Err(SqlError::Other(Cow::from(err))), - _ => Err(SqlError::Other(Cow::from("Undefined result"))) + Response::Empty => AuthReason::BadKey, + Response::Other(err) => AuthReason::ServerIssue(err), + _ => AuthReason::ServerIssue("db-lib returned garbage".into()) } } } - return Ok(AuthReason::BadKey); + return AuthReason::NoKey; } pub async fn login_get_jwt(response: &mut hyper::Response, 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)] 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] fn validity_check() { use bcrypt::{hash, DEFAULT_COST};