From 3bab215b9b7d99c45839d82435fcf304d7e93625 Mon Sep 17 00:00:00 2001 From: tusharshah21 Date: Sat, 6 Dec 2025 18:17:31 +0000 Subject: [PATCH 1/2] feat(#106): Implement JWT authentication for profile API --- backend/.env.example | 4 +- backend/Cargo.lock | 50 +++++++++++++++++--- backend/Cargo.toml | 1 + backend/src/application/commands/login.rs | 6 +++ backend/src/application/commands/mod.rs | 1 + backend/src/application/dtos/auth_dtos.rs | 6 +++ backend/src/infrastructure/jwt.rs | 57 +++++++++++++++++++++++ backend/src/infrastructure/mod.rs | 1 + backend/src/presentation/api.rs | 4 +- backend/src/presentation/handlers.rs | 21 ++++++++- backend/src/presentation/middlewares.rs | 16 +++++++ 11 files changed, 157 insertions(+), 10 deletions(-) create mode 100644 backend/src/application/commands/login.rs create mode 100644 backend/src/infrastructure/jwt.rs diff --git a/backend/.env.example b/backend/.env.example index 0f858a3..f7b941c 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -4,4 +4,6 @@ DATABASE_URL=postgres://guild_user:guild_password@localhost:5432/guild_genesis # Optional: allow SQLx offline mode when building # SQLX_OFFLINE=true -# Other env vars your app uses... \ No newline at end of file +# JWT Configuration +JWT_SECRET=your-super-secret-jwt-key-change-in-production +JWT_EXPIRATION=86400 \ No newline at end of file diff --git a/backend/Cargo.lock b/backend/Cargo.lock index e550a55..a14472f 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1140,7 +1140,7 @@ dependencies = [ "hashers", "http 0.2.12", "instant", - "jsonwebtoken", + "jsonwebtoken 8.3.0", "once_cell", "pin-project", "reqwest", @@ -1490,8 +1490,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1553,6 +1555,7 @@ dependencies = [ "dotenvy", "ethers", "hyper 0.14.32", + "jsonwebtoken 9.3.1", "regex", "reqwest", "serde", @@ -2089,13 +2092,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" dependencies = [ "base64 0.21.7", - "pem", + "pem 1.1.1", "ring 0.16.20", "serde", "serde_json", "simple_asn1", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64 0.22.1", + "js-sys", + "pem 3.0.6", + "ring 0.17.14", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "k256" version = "0.13.4" @@ -2605,6 +2623,16 @@ dependencies = [ "base64 0.13.1", ] +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -3372,18 +3400,28 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 162f730..99a93d5 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -26,6 +26,7 @@ serde_json = "1.0" siwe = "0.6" ethers = { version = "2.0", features = ["rustls"] } sha3 = "0.10" +jsonwebtoken = "9.3" # Pin problematic dependencies to avoid edition 2024 base64ct = "1.7.3" diff --git a/backend/src/application/commands/login.rs b/backend/src/application/commands/login.rs new file mode 100644 index 0000000..906f9b4 --- /dev/null +++ b/backend/src/application/commands/login.rs @@ -0,0 +1,6 @@ +use crate::infrastructure::jwt::JwtManager; + +pub async fn login(address: String) -> Result { + let jwt_manager = JwtManager::new(); + jwt_manager.generate_token(&address) +} diff --git a/backend/src/application/commands/mod.rs b/backend/src/application/commands/mod.rs index d5e1704..2da8f3f 100644 --- a/backend/src/application/commands/mod.rs +++ b/backend/src/application/commands/mod.rs @@ -1,2 +1,3 @@ pub mod create_profile; +pub mod login; pub mod update_profile; diff --git a/backend/src/application/dtos/auth_dtos.rs b/backend/src/application/dtos/auth_dtos.rs index c94f36c..52c3d79 100644 --- a/backend/src/application/dtos/auth_dtos.rs +++ b/backend/src/application/dtos/auth_dtos.rs @@ -18,3 +18,9 @@ pub struct NonceResponse { pub nonce: i64, pub address: String, } + +#[derive(Debug, Serialize, Deserialize)] +pub struct AuthTokenResponse { + pub token: String, + pub address: String, +} diff --git a/backend/src/infrastructure/jwt.rs b/backend/src/infrastructure/jwt.rs new file mode 100644 index 0000000..22f38af --- /dev/null +++ b/backend/src/infrastructure/jwt.rs @@ -0,0 +1,57 @@ +use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; +use serde::{Deserialize, Serialize}; +use std::env; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct JwtClaims { + pub address: String, + pub exp: usize, +} + +pub struct JwtManager { + secret: String, + expiration: usize, +} + +impl JwtManager { + pub fn new() -> Self { + let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set"); + let expiration: usize = env::var("JWT_EXPIRATION") + .unwrap_or_else(|_| "86400".to_string()) + .parse() + .expect("JWT_EXPIRATION must be a valid number (seconds)"); + + JwtManager { secret, expiration } + } + + pub fn generate_token(&self, address: &str) -> Result { + let now = chrono::Utc::now().timestamp() as usize; + let claims = JwtClaims { + address: address.to_string(), + exp: now + self.expiration, + }; + + encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(self.secret.as_bytes()), + ) + .map_err(|e| format!("Failed to generate token: {}", e)) + } + + pub fn validate_token(&self, token: &str) -> Result { + decode::( + token, + &DecodingKey::from_secret(self.secret.as_bytes()), + &Validation::default(), + ) + .map(|data| data.claims) + .map_err(|e| format!("Invalid token: {}", e)) + } +} + +impl Default for JwtManager { + fn default() -> Self { + Self::new() + } +} diff --git a/backend/src/infrastructure/mod.rs b/backend/src/infrastructure/mod.rs index 4fc67ea..f97739b 100644 --- a/backend/src/infrastructure/mod.rs +++ b/backend/src/infrastructure/mod.rs @@ -1,2 +1,3 @@ +pub mod jwt; pub mod repositories; pub mod services; diff --git a/backend/src/presentation/api.rs b/backend/src/presentation/api.rs index 587a993..093bfaf 100644 --- a/backend/src/presentation/api.rs +++ b/backend/src/presentation/api.rs @@ -21,7 +21,7 @@ use tower_http::{ use super::handlers::{ create_profile_handler, delete_profile_handler, get_all_profiles_handler, get_nonce_handler, - get_profile_handler, update_profile_handler, + get_profile_handler, login_handler, update_profile_handler, }; use super::middlewares::{eth_auth_layer, test_auth_layer}; @@ -40,6 +40,7 @@ pub async fn create_app(pool: sqlx::PgPool) -> Router { .route("/profiles/", post(create_profile_handler)) .route("/profiles/:address", put(update_profile_handler)) .route("/profiles/:address", delete(delete_profile_handler)) + .route("/auth/login", post(login_handler)) .with_state(state.clone()); let protected_with_auth = if std::env::var("TEST_MODE").is_ok() { @@ -82,6 +83,7 @@ pub fn test_api(state: AppState) -> Router { .route("/profiles", post(create_profile_handler)) .route("/profiles/:address", put(update_profile_handler)) .route("/profiles/:address", delete(delete_profile_handler)) + .route("/auth/login", post(login_handler)) .with_state(state.clone()) .layer(from_fn(test_auth_layer)); diff --git a/backend/src/presentation/handlers.rs b/backend/src/presentation/handlers.rs index 79ebd72..08c1742 100644 --- a/backend/src/presentation/handlers.rs +++ b/backend/src/presentation/handlers.rs @@ -7,8 +7,8 @@ use axum::{ use crate::{ application::{ - commands::{create_profile::create_profile, update_profile::update_profile}, - dtos::{CreateProfileRequest, NonceResponse, ProfileResponse, UpdateProfileRequest}, + commands::{create_profile::create_profile, login::login, update_profile::update_profile}, + dtos::{AuthTokenResponse, CreateProfileRequest, NonceResponse, ProfileResponse, UpdateProfileRequest}, queries::{ get_all_profiles::get_all_profiles, get_login_nonce::get_login_nonce, get_profile::get_profile, @@ -87,3 +87,20 @@ pub async fn get_nonce_handler( Err(e) => (StatusCode::NOT_FOUND, Json(serde_json::json!({"error": e}))).into_response(), } } + +pub async fn login_handler( + Extension(VerifiedWallet(address)): Extension, +) -> impl IntoResponse { + match login(address.clone()).await { + Ok(token) => ( + StatusCode::OK, + Json(AuthTokenResponse { token, address }), + ) + .into_response(), + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": e})), + ) + .into_response(), + } +} diff --git a/backend/src/presentation/middlewares.rs b/backend/src/presentation/middlewares.rs index 8cfd10b..f3fcb53 100644 --- a/backend/src/presentation/middlewares.rs +++ b/backend/src/presentation/middlewares.rs @@ -7,6 +7,7 @@ use axum::{ }; use crate::domain::services::auth_service::AuthChallenge; +use crate::infrastructure::jwt::JwtManager; use super::api::AppState; @@ -31,6 +32,21 @@ pub async fn eth_auth_layer( return Ok(next.run(req).await); } + // Try JWT token first + if let Some(auth_header) = headers.get("authorization") { + if let Ok(auth_str) = auth_header.to_str() { + if let Some(token) = auth_str.strip_prefix("Bearer ") { + let jwt_manager = JwtManager::new(); + if let Ok(claims) = jwt_manager.validate_token(token) { + req.extensions_mut() + .insert(VerifiedWallet(claims.address)); + return Ok(next.run(req).await); + } + } + } + } + + // Fall back to signature verification let address = headers .get("x-eth-address") .and_then(|v| v.to_str().ok()) From 6122274e17aec8802b1ccddba81a8a06fd6a5216 Mon Sep 17 00:00:00 2001 From: tusharshah21 Date: Sun, 7 Dec 2025 06:42:07 +0000 Subject: [PATCH 2/2] style: fix cargo fmt formatting issues --- backend/src/presentation/handlers.rs | 11 +++++------ backend/src/presentation/middlewares.rs | 3 +-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/backend/src/presentation/handlers.rs b/backend/src/presentation/handlers.rs index 08c1742..18239ad 100644 --- a/backend/src/presentation/handlers.rs +++ b/backend/src/presentation/handlers.rs @@ -8,7 +8,10 @@ use axum::{ use crate::{ application::{ commands::{create_profile::create_profile, login::login, update_profile::update_profile}, - dtos::{AuthTokenResponse, CreateProfileRequest, NonceResponse, ProfileResponse, UpdateProfileRequest}, + dtos::{ + AuthTokenResponse, CreateProfileRequest, NonceResponse, ProfileResponse, + UpdateProfileRequest, + }, queries::{ get_all_profiles::get_all_profiles, get_login_nonce::get_login_nonce, get_profile::get_profile, @@ -92,11 +95,7 @@ pub async fn login_handler( Extension(VerifiedWallet(address)): Extension, ) -> impl IntoResponse { match login(address.clone()).await { - Ok(token) => ( - StatusCode::OK, - Json(AuthTokenResponse { token, address }), - ) - .into_response(), + Ok(token) => (StatusCode::OK, Json(AuthTokenResponse { token, address })).into_response(), Err(e) => ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e})), diff --git a/backend/src/presentation/middlewares.rs b/backend/src/presentation/middlewares.rs index f3fcb53..d4c2146 100644 --- a/backend/src/presentation/middlewares.rs +++ b/backend/src/presentation/middlewares.rs @@ -38,8 +38,7 @@ pub async fn eth_auth_layer( if let Some(token) = auth_str.strip_prefix("Bearer ") { let jwt_manager = JwtManager::new(); if let Ok(claims) = jwt_manager.validate_token(token) { - req.extensions_mut() - .insert(VerifiedWallet(claims.address)); + req.extensions_mut().insert(VerifiedWallet(claims.address)); return Ok(next.run(req).await); } }