From b34a06387fa473e05ea7530b3cbb706c7af4dcf1 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Fri, 22 Dec 2023 22:00:49 +0100 Subject: [PATCH 1/3] Integrated the new authentication into Actix --- server/src/jwt.rs | 51 +++--------------------------------- server/src/main.rs | 5 +++- server/src/routes/comment.rs | 5 ++-- server/src/routes/post.rs | 10 ++++--- server/src/routes/users.rs | 14 +++++++--- 5 files changed, 28 insertions(+), 57 deletions(-) diff --git a/server/src/jwt.rs b/server/src/jwt.rs index 5e61fd1..63cb21a 100755 --- a/server/src/jwt.rs +++ b/server/src/jwt.rs @@ -1,12 +1,8 @@ use jsonwebtoken::{ decode, encode, errors::Result as JwtResult, DecodingKey, EncodingKey, Header, Validation, }; -use log::*; use serde::{Deserialize, Serialize}; -const DAYS_VALID: i64 = 7; -const JWT_SECRET: &[u8] = "secret".as_bytes(); - /// Claims holds the data that will be encoded into the JWT token. #[derive(Debug, Serialize, Deserialize)] pub struct Claims { @@ -33,6 +29,7 @@ impl Claims { /// Authentication holds the data needed to encode and decode JWT tokens. // This is then passed to the AuthenticationMiddleware +#[derive(Clone)] pub struct Authentication { encoding_key: EncodingKey, decoding_key: DecodingKey, @@ -42,7 +39,7 @@ pub struct Authentication { impl Authentication { /// Create a new Authentication struct - fn new(secret: &[u8]) -> Self { + pub fn new(secret: &[u8]) -> Self { Authentication { encoding_key: EncodingKey::from_secret(secret), decoding_key: DecodingKey::from_secret(secret), @@ -57,58 +54,18 @@ impl Authentication { } /// Wrapper for encode_raw that takes a username (sub) and creates a Claims struct - fn encode(&self, sub: &str) -> JwtResult { + pub fn encode(&self, sub: &str) -> JwtResult { let claims = Claims::new(sub, self.days_valid); self.encode_raw(claims) } /// Decode a JWT token into a Claims struct // If this faie, it means the token is invalid - fn decode(&self, token: &str) -> JwtResult { + pub fn decode(&self, token: &str) -> JwtResult { decode::(token, &self.decoding_key, &self.validation).map(|data| data.claims) } } -// JwtResult is just a predefined error from the jsonwebtoken crate -pub fn token_factory(user: &str) -> JwtResult { - info!("Issuing JWT token for {}", user); - - let token = encode( - &Header::default(), - &Claims { - sub: user.to_string(), - iss: "frostbyte".to_string(), - iat: chrono::Utc::now().timestamp() as usize, - exp: (chrono::Utc::now() + chrono::Duration::days(DAYS_VALID)).timestamp() as usize, - }, - &EncodingKey::from_secret(JWT_SECRET), - ) - .unwrap(); - - Ok(token) -} - -// JwtResult is just a predefined error from the jsonwebtoken crate -// This function is incomplete and should be expanded to check for more things -pub fn validate_token(token: &str) -> JwtResult { - let token_data = decode::( - token, - &DecodingKey::from_secret(JWT_SECRET), - &Validation::default(), - ); - - match token_data { - Ok(token_data) => { - info!("Token validated for {}", token_data.claims.sub); - Ok(token_data.claims) - } - Err(e) => { - error!("Token validation failed: {}", e); - Err(e) - } - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/server/src/main.rs b/server/src/main.rs index 5e72109..d6792e8 100755 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -12,6 +12,7 @@ mod state; mod types; mod util; +use jwt::Authentication; use routes::{get_comments, get_posts, login, new_comment, new_post, post_by_id, register}; use state::CaptchaState; use state::ServerState; @@ -23,6 +24,7 @@ async fn main() -> std::io::Result<()> { let data = ServerState::new().await; let capt_db = CaptchaState::new(); + let auth = Authentication::new("secret".as_bytes()); #[cfg(debug_assertions)] { @@ -56,7 +58,8 @@ async fn main() -> std::io::Result<()> { .service(login) .service(register) .app_data(Data::new(data.clone())) - .app_data(Data::new(capt_db.clone())), + .app_data(Data::new(capt_db.clone())) + .app_data(Data::new(auth.clone())), ) .service( Files::new("/", "./public") diff --git a/server/src/routes/comment.rs b/server/src/routes/comment.rs index 3df7e00..dcb1afe 100644 --- a/server/src/routes/comment.rs +++ b/server/src/routes/comment.rs @@ -1,5 +1,5 @@ use crate::db::{db_get_comments, db_new_comment}; -use crate::jwt::validate_token; +use crate::jwt::Authentication; use crate::types::{CommentQueryParams, NewComment}; use crate::ServerState; @@ -31,8 +31,9 @@ pub async fn get_comments( pub async fn new_comment( data: Json, state: Data, + auth: Data, ) -> Result { - let user_claims = validate_token(&data.user_token); + let user_claims = auth.decode(&data.user_token); // Bail if the token is invalid if let Err(e) = user_claims { diff --git a/server/src/routes/post.rs b/server/src/routes/post.rs index 0eb4ec5..b54f49c 100755 --- a/server/src/routes/post.rs +++ b/server/src/routes/post.rs @@ -1,5 +1,5 @@ use crate::db::{db_get_latest_posts, db_get_post, db_new_post}; -use crate::jwt::validate_token; +use crate::jwt::Authentication; use crate::types::{NewPost, PostQueryParams}; use crate::ServerState; @@ -23,8 +23,12 @@ pub async fn get_posts( /// Creates a new post, requires a token in release mode #[post("/posts")] -pub async fn new_post(new_post: Json, state: Data) -> Result { - let user_claims = validate_token(&new_post.token); +pub async fn new_post( + new_post: Json, + state: Data, + auth: Data, +) -> Result { + let user_claims = auth.decode(&new_post.token); if let Err(e) = user_claims { info!("Error validating token: {}", e); diff --git a/server/src/routes/users.rs b/server/src/routes/users.rs index 21013dd..99f4edf 100755 --- a/server/src/routes/users.rs +++ b/server/src/routes/users.rs @@ -1,5 +1,6 @@ use crate::db::{db_new_user, db_user_login}; -use crate::jwt::token_factory; +use crate::jwt::Authentication; +// use crate::jwt::token_factory; use crate::state::CaptchaState; use crate::types::{AuthResponse, LoginData, RegisterData}; use crate::ServerState; @@ -14,6 +15,7 @@ pub async fn register( data: Json, state: Data, captcha_state: Data, + auth: Data, ) -> Result { if !captcha_state .capthca_db @@ -30,7 +32,7 @@ pub async fn register( info!("User: {} registered", &user.username); Ok(HttpResponse::Ok().json(AuthResponse { username: user.username.clone(), - token: token_factory(&user.username).unwrap(), + token: auth.encode(&user.username).unwrap(), })) } None => { @@ -41,14 +43,18 @@ pub async fn register( } #[post("/login")] -pub async fn login(data: Json, state: Data) -> Result { +pub async fn login( + data: Json, + state: Data, + auth: Data, +) -> Result { let result = db_user_login(data.username.clone(), data.password.clone(), &state.pool).await; match result { Some(_) => { return Ok(HttpResponse::Ok().json(AuthResponse { username: data.username.clone(), - token: token_factory(&data.username).unwrap(), + token: auth.encode(&data.username).unwrap(), })); } None => { From 07b40105c22cedaeca6789ac9ceae188de261d9d Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Fri, 22 Dec 2023 22:01:32 +0100 Subject: [PATCH 2/3] Disallowed localhost cors in debug mode --- server/src/main.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/src/main.rs b/server/src/main.rs index d6792e8..b3d0788 100755 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -39,10 +39,13 @@ async fn main() -> std::io::Result<()> { HttpServer::new(move || { let cors = Cors::default() .allowed_origin("https://shitpost.se") - .allowed_origin("http://localhost:8080") .allowed_methods(vec!["GET", "POST"]) .max_age(3600); + // In debug mode, allow localhost + #[cfg(debug_assertions)] + let cors = cors.allowed_origin("http://localhost:8080"); + App::new() .wrap(cors) .wrap(middleware::Compress::default()) From efcb7a305be4a7e88cac8566a5d4f5f28206890b Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Fri, 22 Dec 2023 23:11:11 +0100 Subject: [PATCH 3/3] State testing --- server/src/state.rs | 53 +++++++++++++++++---------------------------- 1 file changed, 20 insertions(+), 33 deletions(-) diff --git a/server/src/state.rs b/server/src/state.rs index 01d1305..dbbd254 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -55,9 +55,12 @@ impl ServerState { None => error!("Failed to create default user..."), } + // We want dummy posts + lipsum_setup(&pool).await.unwrap(); + #[cfg(debug_assertions)] debug_setup(&pool).await.unwrap(); - lipsum_setup(&pool).await.unwrap(); + Self { pool } } } @@ -66,48 +69,18 @@ impl ServerState { // Mostly useful for debugging new posts, as we need to satisfy foreign key constraints. #[cfg(debug_assertions)] async fn debug_setup(pool: &PgPool) -> Result<(), sqlx::Error> { - use lipsum::lipsum; - use rand::prelude::*; - use sqlx::query; - use crate::db::db_new_user; - db_new_user("user".to_string(), "pass".to_string(), pool).await; - - // Check if the demo post already exists - let no_posts = query!("SELECT * FROM posts WHERE id = 1",) - .fetch_one(pool) - .await - .ok() - .is_none(); - - // If the demo user already has a post, don't insert another one - if no_posts { - let mut rng = rand::thread_rng(); - - // This requires that the user with id 1 exists in the user table - for _ in 0..100 { - query!( - "INSERT INTO posts (user_id, content) VALUES (1, $1)", - lipsum(rng.gen_range(10..100)) - ) - .execute(pool) - .await?; - } - - query!("INSERT INTO posts (user_id, content) VALUES (1, 'Hello world! The demo username is user and the password is pass.')",) - .execute(pool) - .await?; - } - Ok(()) } +/// Inserts a bunch of dummy posts into the database async fn lipsum_setup(pool: &PgPool) -> Result<(), sqlx::Error> { use lipsum::lipsum; use rand::prelude::*; use sqlx::query; + // Fetch any user let user_exist = query!("SELECT * FROM users",) .fetch_one(pool) .await @@ -117,6 +90,7 @@ async fn lipsum_setup(pool: &PgPool) -> Result<(), sqlx::Error> { if user_exist { let mut rng = rand::thread_rng(); + // Insert a 100 dummy posts. // This requires that the user with id 1 exists in the user table for _ in 0..100 { query!( @@ -130,3 +104,16 @@ async fn lipsum_setup(pool: &PgPool) -> Result<(), sqlx::Error> { Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_server_state() { + let state = CaptchaState::new(); + assert!(state.capthca_db.lock().unwrap().is_empty()); + state.capthca_db.lock().unwrap().insert("test".to_string()); + assert!(!state.capthca_db.lock().unwrap().is_empty()); + } +}