//! Giriş / kayıt güvenliği: IP başına üst üste 3 hatada 15 dk kilit (giriş + kayıt birlikte), //! sahte X-Forwarded-For ile aşılamaz, bot doğrulaması (Turnstile, testte ağsız sabit sonuç). mod common; use axum::{ body::Body, http::{Method, Request, StatusCode, header}, }; use common::TestApp; use kakeibo::config::RegistrationMode; use serde_json::{Value, json}; /// Belirli bir IP'den (vekilin eklediği en sağdaki X-Forwarded-For) JSON POST async fn post_from( app: &TestApp, xff: &str, path: &str, body: Value, ) -> (StatusCode, Value, Option) { let req = Request::builder() .method(Method::POST) .uri(format!("/api{path}")) .header(header::CONTENT_TYPE, "application/json") .header("x-forwarded-for", xff) .body(Body::from(body.to_string())) .unwrap(); app.send_with(req).await } async fn guard_from(app: &TestApp, xff: &str) -> Value { let req = Request::builder() .uri("/api/auth/guard") .header("x-forwarded-for", xff) .body(Body::empty()) .unwrap(); app.send_with(req).await.1 } fn login(email: &str, password: &str) -> Value { json!({ "email": email, "password": password, "captcha_token": "jeton" }) } #[tokio::test] async fn three_wrong_logins_lock_login_and_register_for_that_ip() { let app = TestApp::new().await; let (st, _, _) = post_from( &app, "10.0.0.1", "/auth/register", json!({ "name": "Sahip", "email": "sahip@ornek.com", "password": "dogru-sifre-1" }), ) .await; assert_eq!(st, StatusCode::CREATED); let ip = "203.0.113.5"; for left in [2, 1] { let (st, b, _) = post_from(&app, ip, "/auth/login", login("sahip@ornek.com", "yanlis")).await; assert_eq!(st, StatusCode::UNAUTHORIZED); assert_eq!( b["error"]["message"], format!("E-posta veya şifre hatalı. Kalan deneme hakkı: {left}") ); } // 3. hata → kilit let req = Request::builder() .method(Method::POST) .uri("/api/auth/login") .header(header::CONTENT_TYPE, "application/json") .header("x-forwarded-for", ip) .body(Body::from(login("sahip@ornek.com", "yanlis").to_string())) .unwrap(); let res = tower::ServiceExt::oneshot(app_router(&app), req) .await .unwrap(); assert_eq!(res.status(), StatusCode::TOO_MANY_REQUESTS); assert_eq!(res.headers()[header::RETRY_AFTER], "900"); // Kilitliyken doğru şifre de, kayıt da reddedilir let (st, b, _) = post_from( &app, ip, "/auth/login", login("sahip@ornek.com", "dogru-sifre-1"), ) .await; assert_eq!(st, StatusCode::TOO_MANY_REQUESTS); assert_eq!(b["error"]["code"], "locked"); assert!( b["error"]["message"] .as_str() .unwrap() .contains("15 dakika") ); let (st, _, _) = post_from( &app, ip, "/auth/register", json!({ "name": "Yeni", "email": "yeni@ornek.com", "password": "sifre-12345" }), ) .await; assert_eq!(st, StatusCode::TOO_MANY_REQUESTS, "kayıt da kilitli"); let g = guard_from(&app, ip).await; assert!(g["locked_seconds"].as_u64().unwrap() > 890, "{g}"); // Sahte sol girdi kilidi aşamaz (en sağdaki = vekilin eklediği) let (st, _, _) = post_from( &app, &format!("1.1.1.1, {ip}"), "/auth/login", login("sahip@ornek.com", "dogru-sifre-1"), ) .await; assert_eq!(st, StatusCode::TOO_MANY_REQUESTS); // Hane sahibi başka bağlantıdan girebilir (e-postaya göre kilit yok) let (st, _, _) = post_from( &app, "198.51.100.9", "/auth/login", login("sahip@ornek.com", "dogru-sifre-1"), ) .await; assert_eq!(st, StatusCode::OK); assert_eq!(guard_from(&app, "198.51.100.9").await["locked_seconds"], 0); app.cleanup().await; } fn app_router(app: &TestApp) -> axum::Router { app.router_clone() } #[tokio::test] async fn success_resets_the_counter() { let app = TestApp::new().await; post_from( &app, "10.0.0.1", "/auth/register", json!({ "name": "A", "email": "a@ornek.com", "password": "dogru-sifre-1" }), ) .await; let ip = "203.0.113.6"; for _ in 0..2 { post_from(&app, ip, "/auth/login", login("a@ornek.com", "yanlis")).await; } let (st, _, _) = post_from( &app, ip, "/auth/login", login("a@ornek.com", "dogru-sifre-1"), ) .await; assert_eq!(st, StatusCode::OK); for _ in 0..2 { let (st, _, _) = post_from(&app, ip, "/auth/login", login("a@ornek.com", "yanlis")).await; assert_eq!( st, StatusCode::UNAUTHORIZED, "sayaç sıfırlandı, henüz kilit yok" ); } app.cleanup().await; } #[tokio::test] async fn invite_code_guessing_is_locked() { let app = TestApp::with_registration(RegistrationMode::InviteOnly).await; // İlk hane serbest let (st, _, _) = post_from( &app, "10.0.0.1", "/auth/register", json!({ "name": "Sahip", "email": "sahip@ornek.com", "password": "sifre-12345" }), ) .await; assert_eq!(st, StatusCode::CREATED); let ip = "203.0.113.7"; let guess = |code: &str, n: u32| json!({ "name": "Tahmin", "email": format!("t{n}@ornek.com"), "password": "sifre-12345", "invite_code": code }); for (n, code) in ["AAAA-AAAA", "BBBB-BBBB"].iter().enumerate() { let (st, _, _) = post_from(&app, ip, "/auth/register", guess(code, n as u32)).await; assert_eq!(st, StatusCode::BAD_REQUEST); } // Davetsiz yeni hane denemesi (kapalı kayıt) da sayılır → 3. hata kilit let (st, b, _) = post_from( &app, ip, "/auth/register", json!({ "name": "X", "email": "x@ornek.com", "password": "sifre-12345" }), ) .await; assert_eq!(st, StatusCode::TOO_MANY_REQUESTS, "{b}"); // Form doğrulama hataları sayılmaz (başka IP'de 5 kez kısa şifre) for _ in 0..5 { let (st, _, _) = post_from( &app, "203.0.113.8", "/auth/register", json!({ "name": "Y", "email": "y@ornek.com", "password": "kisa" }), ) .await; assert_eq!(st, StatusCode::BAD_REQUEST); } assert_eq!(guard_from(&app, "203.0.113.8").await["locked_seconds"], 0); app.cleanup().await; } #[tokio::test] async fn bot_check_is_required_and_failures_count() { // Doğrulama her zaman başarısız let app = TestApp::with_captcha(false).await; let g = guard_from(&app, "203.0.113.9").await; assert_eq!(g["captcha_site_key"], "test-site-key"); for _ in 0..2 { let (st, b, _) = post_from( &app, "203.0.113.9", "/auth/login", login("kim@ornek.com", "x"), ) .await; assert_eq!(st, StatusCode::BAD_REQUEST); assert_eq!(b["error"]["code"], "captcha"); } let (st, _, _) = post_from( &app, "203.0.113.9", "/auth/login", login("kim@ornek.com", "x"), ) .await; assert_eq!( st, StatusCode::TOO_MANY_REQUESTS, "bot denemeleri de sayılır" ); app.cleanup().await; // Doğrulama geçer, ama jeton gönderilmezse red let app = TestApp::with_captcha(true).await; let body = json!({ "name": "A", "email": "a@ornek.com", "password": "dogru-sifre-1" }); let (st, b, _) = post_from(&app, "10.0.0.2", "/auth/register", body.clone()).await; assert_eq!( (st, b["error"]["code"].as_str()), (StatusCode::BAD_REQUEST, Some("captcha")) ); let mut with_token = body; with_token["captcha_token"] = json!("jeton"); let (st, _, _) = post_from(&app, "10.0.0.2", "/auth/register", with_token).await; assert_eq!(st, StatusCode::CREATED); let (st, _, _) = post_from( &app, "10.0.0.2", "/auth/login", login("a@ornek.com", "dogru-sifre-1"), ) .await; assert_eq!(st, StatusCode::OK); app.cleanup().await; }