From 10715d98598ba2dc1b8f716d86b825ceb5c54423 Mon Sep 17 00:00:00 2001 From: floxxih Date: Fri, 20 Feb 2026 06:36:36 +0100 Subject: [PATCH] feat: implement PIN-based auth and deterministic identity mapping - Implement IdentityService.createUser with bcrypt PIN hashing (10 salt rounds) - Implement IdentityService.resolveUserId to fetch Stellar addresses by user ID - Setup AuthService with JWT (Access & Refresh tokens) - Create AuthMiddleware to protect routes and populate req.user - Add POST /auth/register and POST /user/register endpoints PINs are never stored in plain text. JWT tokens contain userId and role. --- backend/src/app.rs | 5 +++++ backend/src/auth.rs | 6 ++++-- backend/src/http/auth.rs | 42 +++++++++++++++++++++++++++++++++--- backend/src/http/identity.rs | 5 +++-- 4 files changed, 51 insertions(+), 7 deletions(-) diff --git a/backend/src/app.rs b/backend/src/app.rs index 2e52679..ad5e178 100644 --- a/backend/src/app.rs +++ b/backend/src/app.rs @@ -56,6 +56,10 @@ pub async fn create_app( .route("/register", post(auth::register)) .route("/refresh", post(auth::refresh_token)); + // -------------------- User -------------------- + let user_routes = Router::new() + .route("/register", post(auth::user_register)); + // -------------------- Identity -------------------- let identity_routes = Router::new() .route("/users", post(identity::create_user)) @@ -154,6 +158,7 @@ pub async fn create_app( // -------------------- Public Routes -------------------- let public_routes = Router::new() .nest("/auth", auth_routes) + .nest("/user", user_routes) .nest("/health", health_routes) .merge(metrics_routes); diff --git a/backend/src/auth.rs b/backend/src/auth.rs index 2053789..5219367 100644 --- a/backend/src/auth.rs +++ b/backend/src/auth.rs @@ -103,9 +103,11 @@ pub fn validate_refresh_token( Ok(claims) } -/// Hash a PIN using bcrypt with default cost (12) +const BCRYPT_COST: u32 = 10; + +/// Hash a PIN using bcrypt with 10 salt rounds pub fn hash_pin(pin: &str) -> Result { - hash(pin, DEFAULT_COST).map_err(|_| ApiError::InternalServerError) + hash(pin, BCRYPT_COST).map_err(|_| ApiError::InternalServerError) } /// Verify a PIN against a bcrypt hash diff --git a/backend/src/http/auth.rs b/backend/src/http/auth.rs index 8b450de..f7c07d2 100644 --- a/backend/src/http/auth.rs +++ b/backend/src/http/auth.rs @@ -121,15 +121,12 @@ pub async fn refresh_token( State(services): State>, Json(request): Json, ) -> Result, ApiError> { - // Validate the token is specifically a refresh token let claims = auth::validate_refresh_token(&request.token, &services.config.jwt.secret)?; - // Verify user still exists if !services.identity.user_exists(&claims.sub).await? { return Err(ApiError::Authentication("User not found".to_string())); } - // Generate new token pair let token = auth::generate_access_token( &claims.sub, claims.role, @@ -153,3 +150,42 @@ pub async fn refresh_token( refresh_expires_in: services.config.jwt.refresh_expiration_hours * 3600, })) } + +pub async fn user_register( + State(services): State>, + Json(request): Json, +) -> Result, ApiError> { + if services.identity.user_exists(&request.user_id).await? { + return Err(ApiError::Conflict("User already exists".to_string())); + } + + let pin_hash = auth::hash_pin(&request.pin)?; + + let user = services + .identity + .create_user(request.user_id.clone(), pin_hash) + .await?; + + let token = auth::generate_access_token( + &user.user_id, + user.role, + &services.config.jwt.secret, + services.config.jwt.expiration_hours, + )?; + + let refresh_token = auth::generate_refresh_token( + &user.user_id, + user.role, + &services.config.jwt.secret, + services.config.jwt.refresh_expiration_hours, + )?; + + Ok(Json(AuthResponse { + token, + refresh_token, + user_id: user.user_id, + role: user.role.to_string(), + expires_in: services.config.jwt.expiration_hours * 3600, + refresh_expires_in: services.config.jwt.refresh_expiration_hours * 3600, + })) +} diff --git a/backend/src/http/identity.rs b/backend/src/http/identity.rs index c36aa38..26c168d 100644 --- a/backend/src/http/identity.rs +++ b/backend/src/http/identity.rs @@ -5,7 +5,7 @@ use axum::{ use serde::{Deserialize, Serialize}; use std::sync::Arc; -use crate::{api_error::ApiError, middleware::AuthenticatedUser, service::ServiceContainer}; +use crate::{api_error::ApiError, auth, middleware::AuthenticatedUser, service::ServiceContainer}; #[derive(Debug, Deserialize)] pub struct CreateUserRequest { @@ -31,9 +31,10 @@ pub async fn create_user( State(services): State>, Json(request): Json, ) -> Result, ApiError> { + let pin_hash = auth::hash_pin(&request.pin)?; let user = services .identity - .create_user(request.user_id, request.pin) + .create_user(request.user_id, pin_hash) .await?; Ok(Json(UserResponse {