From a351c442d3576a14502c2a210cf04adbb579bc46 Mon Sep 17 00:00:00 2001 From: "s0wlz (Matthias Puchstein)" Date: Tue, 28 Apr 2026 01:36:22 +0200 Subject: [PATCH] fix: secret() error propagation, exp as u64, bcrypt cost 4 in tests, skip empty auth header --- backend/src/auth.rs | 32 ++++++++++++++++++------------- backend/src/routes/auth_routes.rs | 4 ++-- backend/src/test_helpers.rs | 20 ++++++++++++------- 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/backend/src/auth.rs b/backend/src/auth.rs index db87274..a3a9031 100644 --- a/backend/src/auth.rs +++ b/backend/src/auth.rs @@ -4,24 +4,30 @@ use serde::{Deserialize, Serialize}; use crate::error::AppError; #[derive(Debug, Serialize, Deserialize, Clone)] -pub struct TutorClaims { pub sub: i64, pub email: String, pub exp: usize } +pub struct TutorClaims { pub sub: i64, pub email: String, pub exp: u64 } -fn secret() -> String { - std::env::var("JWT_SECRET").expect("JWT_SECRET not set") +fn secret() -> Result { + std::env::var("JWT_SECRET").map_err(|_| { + tracing::error!("JWT_SECRET environment variable is not set"); + AppError::Unauthorized + }) } pub fn encode_jwt(id: i64, email: &str) -> Result { - let exp = chrono::Utc::now().timestamp() as usize + 86400 * 7; + let exp = (chrono::Utc::now() + chrono::Duration::days(7)).timestamp() as u64; let claims = TutorClaims { sub: id, email: email.into(), exp }; - encode(&Header::default(), &claims, &EncodingKey::from_secret(secret().as_bytes())) + encode(&Header::default(), &claims, &EncodingKey::from_secret(secret()?.as_bytes())) .map_err(|_| AppError::Unauthorized) } pub fn decode_jwt(token: &str) -> Result { - decode::(token, &DecodingKey::from_secret(secret().as_bytes()), + decode::(token, &DecodingKey::from_secret(secret()?.as_bytes()), &Validation::default()) .map(|d| d.claims) - .map_err(|_| AppError::Unauthorized) + .map_err(|e| { + tracing::debug!(error = %e, "JWT decode failed"); + AppError::Unauthorized + }) } // Axum extractor: pulls JWT from Authorization: Bearer header @@ -41,16 +47,16 @@ mod tests { use super::*; #[test] - fn roundtrip_jwt() { - unsafe { std::env::set_var("JWT_SECRET", "testsecret"); } + fn jwt_roundtrip_and_rejection() { + // Set var inside the test; still unsafe in edition 2024 + unsafe { std::env::set_var("JWT_SECRET", "testsecret_auth"); } + + // roundtrip let token = encode_jwt(1, "test@example.com").unwrap(); let claims = decode_jwt(&token).unwrap(); assert_eq!(claims.sub, 1); - } - #[test] - fn invalid_jwt_rejected() { - unsafe { std::env::set_var("JWT_SECRET", "testsecret"); } + // rejection assert!(decode_jwt("not.a.token").is_err()); } } diff --git a/backend/src/routes/auth_routes.rs b/backend/src/routes/auth_routes.rs index 767fe48..87b5669 100644 --- a/backend/src/routes/auth_routes.rs +++ b/backend/src/routes/auth_routes.rs @@ -35,7 +35,7 @@ mod tests { #[sqlx::test(migrations = "./migrations")] async fn login_returns_token(pool: SqlitePool) { - let hash = bcrypt::hash("secret", bcrypt::DEFAULT_COST).unwrap(); + let hash = bcrypt::hash("secret", 4).unwrap(); sqlx::query("INSERT INTO tutors (name,email,password_hash) VALUES (?,?,?)") .bind("Test").bind("t@test.com").bind(&hash) .execute(&pool).await.unwrap(); @@ -49,7 +49,7 @@ mod tests { #[sqlx::test(migrations = "./migrations")] async fn login_wrong_password(pool: SqlitePool) { - let hash = bcrypt::hash("correct", bcrypt::DEFAULT_COST).unwrap(); + let hash = bcrypt::hash("correct", 4).unwrap(); sqlx::query("INSERT INTO tutors (name,email,password_hash) VALUES (?,?,?)") .bind("Test").bind("t@test.com").bind(&hash) .execute(&pool).await.unwrap(); diff --git a/backend/src/test_helpers.rs b/backend/src/test_helpers.rs index 7ff5136..ac3c018 100644 --- a/backend/src/test_helpers.rs +++ b/backend/src/test_helpers.rs @@ -7,7 +7,7 @@ use http_body_util::BodyExt; /// Insert a test tutor (if not exists), return a valid JWT for that tutor. pub async fn make_token(pool: &SqlitePool, email: &str) -> String { - let hash = bcrypt::hash("testpass", bcrypt::DEFAULT_COST).unwrap(); + let hash = bcrypt::hash("testpass", 4).unwrap(); sqlx::query("INSERT OR IGNORE INTO tutors (name,email,password_hash) VALUES (?,?,?)") .bind("Test Tutor").bind(email).bind(&hash) .execute(pool).await.unwrap(); @@ -29,10 +29,13 @@ pub async fn build_test_app(pool: SqlitePool) -> (Router, String) { pub async fn post_json(app: Router, path: &str, auth: &str, body: serde_json::Value) -> (StatusCode, bytes::Bytes) { - let req = Request::builder() + let mut builder = Request::builder() .method("POST").uri(path) - .header("Content-Type", "application/json") - .header("Authorization", auth) + .header("Content-Type", "application/json"); + if !auth.is_empty() { + builder = builder.header("Authorization", auth); + } + let req = builder .body(axum::body::Body::from(body.to_string())) .unwrap(); let res = app.oneshot(req).await.unwrap(); @@ -43,9 +46,12 @@ pub async fn post_json(app: Router, path: &str, auth: &str, body: serde_json::Va /// GET from the app (one-shot), returns (StatusCode, response body bytes). pub async fn get(app: Router, path: &str, auth: &str) -> (StatusCode, bytes::Bytes) { - let req = Request::builder() - .method("GET").uri(path) - .header("Authorization", auth) + let mut builder = Request::builder() + .method("GET").uri(path); + if !auth.is_empty() { + builder = builder.header("Authorization", auth); + } + let req = builder .body(axum::body::Body::empty()) .unwrap(); let res = app.oneshot(req).await.unwrap();