a2eff1a984
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
264 lines
8.1 KiB
Rust
264 lines
8.1 KiB
Rust
//! 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<String>) {
|
||
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;
|
||
}
|