Files
kakeibo/backend/tests/security.rs
T

264 lines
8.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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;
}