diff --git a/api-server/src/auth_2fa.rs b/api-server/src/auth_2fa.rs index d41b5768..3b2d6844 100644 --- a/api-server/src/auth_2fa.rs +++ b/api-server/src/auth_2fa.rs @@ -76,6 +76,8 @@ pub struct UseBackupCodeRequest { pub struct Verify2faResponse { pub success: bool, pub message: String, + /// Opaque token for the session created after successful verification. + pub session_token: String, } /// In-process 2FA store (for demo/testing) diff --git a/api-server/src/handlers.rs b/api-server/src/handlers.rs index 82ec42fc..d3773bb5 100644 --- a/api-server/src/handlers.rs +++ b/api-server/src/handlers.rs @@ -2429,14 +2429,14 @@ pub async fn enable_2fa( } /// #984: Verify 2FA code during login -/// Returns success if TOTP code is valid and marks 2FA as verified +/// Returns success if TOTP code is valid, marks 2FA as verified, and creates a session. #[utoipa::path( post, path = "/auth/2fa/verify", tag = "Authentication", request_body = Verify2faRequest, responses( - (status = 200, description = "2FA code verified successfully", body = Verify2faResponse), + (status = 200, description = "2FA code verified and session created", body = Verify2faResponse), (status = 401, description = "Invalid 2FA code", body = ErrorResponse), (status = 400, description = "Invalid request", body = ErrorResponse), (status = 404, description = "2FA not configured for user", body = ErrorResponse), @@ -2486,6 +2486,7 @@ pub async fn verify_2fa( Ok(Json(Verify2faResponse { success: true, message: "2FA verified successfully".to_string(), + session_token, })) } Some(false) => Err(( @@ -2511,7 +2512,7 @@ pub async fn verify_2fa( tag = "Authentication", request_body = UseBackupCodeRequest, responses( - (status = 200, description = "Backup code accepted, account recovered", body = Verify2faResponse), + (status = 200, description = "Backup code accepted, account recovered, and session created", body = Verify2faResponse), (status = 401, description = "Invalid or already used backup code", body = ErrorResponse), (status = 404, description = "User not found", body = ErrorResponse), ) @@ -2557,9 +2558,11 @@ pub async fn use_backup_code( )); } + let session_token = issue_session_token(&body.user_id)?; Ok(Json(Verify2faResponse { success: true, message: "Account recovered with backup code. Please update your 2FA settings.".to_string(), + session_token, })) } @@ -2568,6 +2571,32 @@ pub async fn use_backup_code( pub(crate) static SESSION_STORE: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| crate::session::SessionStore::new(crate::session::SessionConfig::default())); +fn issue_session_token( + user_id: &str, +) -> Result)> { + let token = uuid::Uuid::new_v4().to_string(); + SESSION_STORE + .create_session(user_id, &token) + .map_err(|error| (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error })))?; + Ok(token) +} + +pub fn spawn_session_cleanup_task() { + tokio::spawn(async { + let mut interval = tokio::time::interval(std::time::Duration::from_secs(60)); + loop { + interval.tick().await; + match SESSION_STORE.cleanup_expired_sessions() { + Ok(removed) if removed > 0 => { + tracing::debug!(removed, "cleaned up expired sessions"); + } + Ok(_) => {} + Err(error) => tracing::error!(%error, "failed to clean up expired sessions"), + } + } + }); +} + /// #985: Check current session status /// Returns session status including timeout warnings and grace period info #[utoipa::path( diff --git a/api-server/src/main.rs b/api-server/src/main.rs index 374fe334..2be37340 100644 --- a/api-server/src/main.rs +++ b/api-server/src/main.rs @@ -384,6 +384,7 @@ async fn main() { metrics::init(); // #1064/#1065: periodic lifecycle anomaly detection + alert escalation. commitment_monitoring::spawn_background_evaluator(); + handlers::spawn_session_cleanup_task(); let subscription_broadcaster = init_subscription_broadcaster().await; let rpc_client: Arc = Arc::new(graphql::MockSorobanRpcClient::default()); @@ -469,6 +470,11 @@ async fn main() { .route("/v1/auth/recovery/verify-token", post(account_recovery::verify_recovery_token)) .route("/v1/auth/recovery/questions", get(account_recovery::get_security_questions)) .route("/v1/auth/recovery/verify-question", post(account_recovery::verify_security_question)) + .route("/auth/2fa/enable", post(handlers::enable_2fa)) + .route("/auth/2fa/verify", post(handlers::verify_2fa)) + .route("/auth/2fa/backup-code", post(handlers::use_backup_code)) + .route("/auth/session/status", post(handlers::check_session)) + .route("/auth/extend-session", post(handlers::extend_session)) .route("/ip/{ip_id}", get(handlers::get_ip)) .route("/ip/verify", post(handlers::verify_commitment)) .route("/ip/reveal-batch", post(handlers::batch_reveal_commitments)) @@ -488,12 +494,6 @@ async fn main() { .route("/swap/marketplace/seller/{seller}", get(handlers::marketplace_by_seller)) .route("/swap/marketplace/buyer/{buyer}", get(handlers::marketplace_by_buyer)) .route("/swap/marketplace/ip/{ip_id}", get(handlers::marketplace_by_ip)) - .route("/swap/initiate", post(handlers::initiate_swap).layer(signed.clone())) - .route("/swap/batch-initiate", post(handlers::batch_initiate_swap)) - .route("/swap/{swap_id}/accept", post(handlers::accept_swap).layer(signed.clone())) - .route("/swap/{swap_id}/reveal", post(handlers::reveal_key).layer(signed.clone())) - .route("/swap/{swap_id}/cancel", post(handlers::cancel_swap).layer(signed.clone())) - .route("/swap/{swap_id}/cancel-expired", post(handlers::cancel_expired_swap)) .route("/swap/{swap_id}", get(handlers::get_swap)) .route("/openapi.json", get(openapi_handler)) .route("/docs", get(docs::docs_handler)) @@ -662,8 +662,6 @@ fn build_app() -> Router { .route("/v1/swap/{swap_id}/history", get(handlers::get_swap_history)) .route("/v1/swap/{swap_id}/disputes", get(handlers::get_swap_disputes)) .route("/v1/swaps", get(handlers::list_swaps)) - .route("/v1/webhooks", post(handlers::register_webhook)) - .route("/v1/webhooks/{id}", axum::routing::delete(handlers::unregister_webhook)) .route("/v1/bulk/commit-ip", post(handlers::bulk_commit_ip)) .route("/v1/bulk/initiate-swap", post(handlers::bulk_initiate_swap)) .route("/v1/swaps/execute-batch", post(handlers::execute_batch_swaps)) diff --git a/api-server/src/schemas.rs b/api-server/src/schemas.rs index 734d7503..92d54bd1 100644 --- a/api-server/src/schemas.rs +++ b/api-server/src/schemas.rs @@ -502,6 +502,8 @@ pub struct Verify2faRequest { pub struct Verify2faResponse { pub success: bool, pub message: String, + /// Opaque token used with the session status and extension endpoints. + pub session_token: String, } /// #984: Request to use backup code for account recovery