fix: restrict password login to existing phone accounts
This commit is contained in:
@@ -1031,6 +1031,7 @@ pub fn build_router(state: AppState) -> Router {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use axum::{
|
||||
Router,
|
||||
body::Body,
|
||||
http::{Request, StatusCode},
|
||||
};
|
||||
@@ -1048,6 +1049,40 @@ mod tests {
|
||||
|
||||
use super::build_router;
|
||||
|
||||
const TEST_PASSWORD: &str = "secret123";
|
||||
async fn seed_phone_user_with_password(
|
||||
state: &AppState,
|
||||
phone_number: &str,
|
||||
password: &str,
|
||||
) -> module_auth::AuthUser {
|
||||
state
|
||||
.seed_test_phone_user_with_password(phone_number, password)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn password_login_request(
|
||||
app: Router,
|
||||
phone_number: &str,
|
||||
password: &str,
|
||||
) -> axum::response::Response {
|
||||
app.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/auth/entry")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"phone": phone_number,
|
||||
"password": password
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("password login request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("password login request should succeed")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn healthz_returns_legacy_compatible_payload_and_headers() {
|
||||
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
|
||||
@@ -1162,24 +1197,17 @@ mod tests {
|
||||
async fn internal_auth_claims_returns_verified_claims() {
|
||||
let config = AppConfig::default();
|
||||
let state = AppState::new(config.clone()).expect("state should build");
|
||||
state
|
||||
.password_entry_service()
|
||||
.execute(module_auth::PasswordEntryInput {
|
||||
username: "guest_auth_debug".to_string(),
|
||||
password: "secret123".to_string(),
|
||||
})
|
||||
.await
|
||||
.expect("seed login should succeed");
|
||||
let seed_user = seed_phone_user_with_password(&state, "13800138010", TEST_PASSWORD).await;
|
||||
let claims = AccessTokenClaims::from_input(
|
||||
AccessTokenClaimsInput {
|
||||
user_id: "user_00000001".to_string(),
|
||||
user_id: seed_user.id.clone(),
|
||||
session_id: "sess_auth_debug".to_string(),
|
||||
provider: AuthProvider::Password,
|
||||
roles: vec!["user".to_string()],
|
||||
token_version: 1,
|
||||
token_version: seed_user.token_version,
|
||||
phone_verified: true,
|
||||
binding_status: BindingStatus::Active,
|
||||
display_name: Some("测试用户".to_string()),
|
||||
display_name: Some(seed_user.display_name.clone()),
|
||||
},
|
||||
state.auth_jwt_config(),
|
||||
OffsetDateTime::now_utc(),
|
||||
@@ -1210,17 +1238,14 @@ mod tests {
|
||||
let payload: Value =
|
||||
serde_json::from_slice(&body).expect("response body should be valid json");
|
||||
|
||||
assert_eq!(
|
||||
payload["claims"]["sub"],
|
||||
Value::String("user_00000001".to_string())
|
||||
);
|
||||
assert_eq!(payload["claims"]["sub"], Value::String(seed_user.id));
|
||||
assert_eq!(
|
||||
payload["claims"]["sid"],
|
||||
Value::String("sess_auth_debug".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
payload["claims"]["ver"],
|
||||
Value::Number(serde_json::Number::from(1))
|
||||
Value::Number(serde_json::Number::from(seed_user.token_version))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1293,26 +1318,21 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn password_entry_creates_user_and_sets_refresh_cookie() {
|
||||
async fn password_entry_rejects_unknown_phone_without_registration() {
|
||||
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/auth/entry")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"username": "guest_001",
|
||||
"password": "secret123"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
let response = password_login_request(app, "13800138011", TEST_PASSWORD).await;
|
||||
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn password_entry_logs_in_existing_phone_user_and_sets_refresh_cookie() {
|
||||
let state = AppState::new(AppConfig::default()).expect("state should build");
|
||||
let seed_user = seed_phone_user_with_password(&state, "13800138012", TEST_PASSWORD).await;
|
||||
let app = build_router(state);
|
||||
|
||||
let response = password_login_request(app, "13800138012", TEST_PASSWORD).await;
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert!(
|
||||
@@ -1332,9 +1352,10 @@ mod tests {
|
||||
let payload: Value =
|
||||
serde_json::from_slice(&body).expect("response body should be valid json");
|
||||
|
||||
assert_eq!(payload["user"]["id"], Value::String(seed_user.id));
|
||||
assert_eq!(
|
||||
payload["user"]["username"],
|
||||
Value::String("guest_001".to_string())
|
||||
payload["user"]["loginMethod"],
|
||||
Value::String("password".to_string())
|
||||
);
|
||||
assert!(payload["token"].as_str().is_some());
|
||||
}
|
||||
@@ -1371,7 +1392,7 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
payload["availableLoginMethods"],
|
||||
serde_json::json!(["phone", "wechat"])
|
||||
serde_json::json!(["phone", "password", "wechat"])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2232,7 +2253,9 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn auth_sessions_returns_multi_device_session_fields() {
|
||||
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
|
||||
let state = AppState::new(AppConfig::default()).expect("state should build");
|
||||
seed_phone_user_with_password(&state, "13800138013", TEST_PASSWORD).await;
|
||||
let app = build_router(state);
|
||||
|
||||
let first_login_response = app
|
||||
.clone()
|
||||
@@ -2248,8 +2271,8 @@ mod tests {
|
||||
.header("x-client-instance-id", "chrome-instance-001")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"username": "guest_sessions_api",
|
||||
"password": "secret123"
|
||||
"phone": "13800138013",
|
||||
"password": TEST_PASSWORD
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
@@ -2292,8 +2315,8 @@ mod tests {
|
||||
.header("user-agent", "Mozilla/5.0 Chrome/123.0 MicroMessenger")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"username": "guest_sessions_api",
|
||||
"password": "secret123"
|
||||
"phone": "13800138013",
|
||||
"password": TEST_PASSWORD
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
@@ -2346,27 +2369,13 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn password_entry_reuses_same_user_for_same_credentials() {
|
||||
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
|
||||
async fn password_entry_reuses_same_user_for_same_phone() {
|
||||
let state = AppState::new(AppConfig::default()).expect("state should build");
|
||||
let seed_user = seed_phone_user_with_password(&state, "13800138014", TEST_PASSWORD).await;
|
||||
let app = build_router(state);
|
||||
|
||||
let first_response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/auth/entry")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"username": "guest_001",
|
||||
"password": "secret123"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("first request should succeed");
|
||||
let first_response =
|
||||
password_login_request(app.clone(), "13800138014", TEST_PASSWORD).await;
|
||||
let first_body = first_response
|
||||
.into_body()
|
||||
.collect()
|
||||
@@ -2376,23 +2385,7 @@ mod tests {
|
||||
let first_payload: Value =
|
||||
serde_json::from_slice(&first_body).expect("first payload should be json");
|
||||
|
||||
let second_response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/auth/entry")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"username": "guest_001",
|
||||
"password": "secret123"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("second request should succeed");
|
||||
let second_response = password_login_request(app, "13800138014", TEST_PASSWORD).await;
|
||||
let second_body = second_response
|
||||
.into_body()
|
||||
.collect()
|
||||
@@ -2402,54 +2395,23 @@ mod tests {
|
||||
let second_payload: Value =
|
||||
serde_json::from_slice(&second_body).expect("second payload should be json");
|
||||
|
||||
assert_eq!(first_payload["user"]["id"], Value::String(seed_user.id));
|
||||
assert_eq!(first_payload["user"]["id"], second_payload["user"]["id"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn password_entry_rejects_wrong_password() {
|
||||
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
|
||||
let state = AppState::new(AppConfig::default()).expect("state should build");
|
||||
seed_phone_user_with_password(&state, "13800138015", TEST_PASSWORD).await;
|
||||
let app = build_router(state);
|
||||
|
||||
app.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/auth/entry")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"username": "guest_001",
|
||||
"password": "secret123"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("seed request should succeed");
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/auth/entry")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"username": "guest_001",
|
||||
"password": "secret999"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
let response = password_login_request(app, "13800138015", "secret999").await;
|
||||
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn password_entry_rejects_invalid_username() {
|
||||
async fn password_entry_rejects_email_or_username_identifier() {
|
||||
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
|
||||
|
||||
let response = app
|
||||
@@ -2460,8 +2422,8 @@ mod tests {
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"username": "无效用户",
|
||||
"password": "secret123"
|
||||
"phone": "user@example.com",
|
||||
"password": TEST_PASSWORD
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
@@ -2481,24 +2443,17 @@ mod tests {
|
||||
..AppConfig::default()
|
||||
};
|
||||
let state = AppState::new(config).expect("state should build");
|
||||
state
|
||||
.password_entry_service()
|
||||
.execute(module_auth::PasswordEntryInput {
|
||||
username: "guest_001".to_string(),
|
||||
password: "secret123".to_string(),
|
||||
})
|
||||
.await
|
||||
.expect("seed login should succeed");
|
||||
let seed_user = seed_phone_user_with_password(&state, "13800138016", TEST_PASSWORD).await;
|
||||
let claims = AccessTokenClaims::from_input(
|
||||
AccessTokenClaimsInput {
|
||||
user_id: "user_00000001".to_string(),
|
||||
user_id: seed_user.id.clone(),
|
||||
session_id: "sess_me_query".to_string(),
|
||||
provider: AuthProvider::Password,
|
||||
roles: vec!["user".to_string()],
|
||||
token_version: 1,
|
||||
token_version: seed_user.token_version,
|
||||
phone_verified: false,
|
||||
binding_status: BindingStatus::Active,
|
||||
display_name: Some("guest_001".to_string()),
|
||||
display_name: Some(seed_user.display_name.clone()),
|
||||
},
|
||||
state.auth_jwt_config(),
|
||||
OffsetDateTime::now_utc(),
|
||||
@@ -2529,13 +2484,10 @@ mod tests {
|
||||
let payload: Value =
|
||||
serde_json::from_slice(&body).expect("response body should be valid json");
|
||||
|
||||
assert_eq!(
|
||||
payload["user"]["id"],
|
||||
Value::String("user_00000001".to_string())
|
||||
);
|
||||
assert_eq!(payload["user"]["id"], Value::String(seed_user.id));
|
||||
assert_eq!(
|
||||
payload["availableLoginMethods"],
|
||||
serde_json::json!(["phone", "wechat"])
|
||||
serde_json::json!(["phone", "password", "wechat"])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2577,26 +2529,12 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn refresh_session_rotates_cookie_and_returns_new_access_token() {
|
||||
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
|
||||
let state = AppState::new(AppConfig::default()).expect("state should build");
|
||||
seed_phone_user_with_password(&state, "13800138017", TEST_PASSWORD).await;
|
||||
let app = build_router(state);
|
||||
|
||||
let login_response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/auth/entry")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"username": "guest_refresh",
|
||||
"password": "secret123"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("login request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("login request should succeed");
|
||||
let login_response =
|
||||
password_login_request(app.clone(), "13800138017", TEST_PASSWORD).await;
|
||||
let first_cookie = login_response
|
||||
.headers()
|
||||
.get("set-cookie")
|
||||
@@ -2685,26 +2623,12 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn logout_clears_cookie_and_invalidates_current_access_token() {
|
||||
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
|
||||
let state = AppState::new(AppConfig::default()).expect("state should build");
|
||||
seed_phone_user_with_password(&state, "13800138018", TEST_PASSWORD).await;
|
||||
let app = build_router(state);
|
||||
|
||||
let login_response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/auth/entry")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"username": "guest_logout_api",
|
||||
"password": "secret123"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("login request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("login request should succeed");
|
||||
let login_response =
|
||||
password_login_request(app.clone(), "13800138018", TEST_PASSWORD).await;
|
||||
let refresh_cookie = login_response
|
||||
.headers()
|
||||
.get("set-cookie")
|
||||
@@ -2773,26 +2697,12 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn logout_succeeds_without_refresh_cookie_when_bearer_token_is_valid() {
|
||||
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
|
||||
let state = AppState::new(AppConfig::default()).expect("state should build");
|
||||
seed_phone_user_with_password(&state, "13800138019", TEST_PASSWORD).await;
|
||||
let app = build_router(state);
|
||||
|
||||
let login_response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/auth/entry")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"username": "guest_logout_no_cookie",
|
||||
"password": "secret123"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("login request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("login request should succeed");
|
||||
let login_response =
|
||||
password_login_request(app.clone(), "13800138019", TEST_PASSWORD).await;
|
||||
let login_body = login_response
|
||||
.into_body()
|
||||
.collect()
|
||||
@@ -2830,7 +2740,9 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn logout_all_clears_cookie_and_invalidates_all_sessions() {
|
||||
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
|
||||
let state = AppState::new(AppConfig::default()).expect("state should build");
|
||||
seed_phone_user_with_password(&state, "13800138020", TEST_PASSWORD).await;
|
||||
let app = build_router(state);
|
||||
|
||||
let first_login_response = app
|
||||
.clone()
|
||||
@@ -2845,8 +2757,8 @@ mod tests {
|
||||
)
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"username": "guest_logout_all_api",
|
||||
"password": "secret123"
|
||||
"phone": "13800138020",
|
||||
"password": TEST_PASSWORD
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
@@ -2884,8 +2796,8 @@ mod tests {
|
||||
.header("x-client-instance-id", "logout-all-instance-002")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"username": "guest_logout_all_api",
|
||||
"password": "secret123"
|
||||
"phone": "13800138020",
|
||||
"password": TEST_PASSWORD
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
@@ -2976,26 +2888,12 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn logout_all_succeeds_without_refresh_cookie_when_bearer_token_is_valid() {
|
||||
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
|
||||
let state = AppState::new(AppConfig::default()).expect("state should build");
|
||||
seed_phone_user_with_password(&state, "13800138021", TEST_PASSWORD).await;
|
||||
let app = build_router(state);
|
||||
|
||||
let login_response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/auth/entry")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"username": "guest_logout_all_nc",
|
||||
"password": "secret123"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("login request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("login request should succeed");
|
||||
let login_response =
|
||||
password_login_request(app.clone(), "13800138021", TEST_PASSWORD).await;
|
||||
let login_body = login_response
|
||||
.into_body()
|
||||
.collect()
|
||||
@@ -3079,26 +2977,11 @@ mod tests {
|
||||
config.admin_username = Some("root".to_string());
|
||||
config.admin_password = Some("secret123".to_string());
|
||||
let state = AppState::new(config).expect("state should build");
|
||||
seed_phone_user_with_password(&state, "13800138022", TEST_PASSWORD).await;
|
||||
let app = build_router(state.clone());
|
||||
|
||||
let login_response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/auth/entry")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"username": "guest_admin_forbidden",
|
||||
"password": "secret123"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("login request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("login should succeed");
|
||||
let login_response =
|
||||
password_login_request(app.clone(), "13800138022", TEST_PASSWORD).await;
|
||||
let login_body = login_response
|
||||
.into_body()
|
||||
.collect()
|
||||
|
||||
Reference in New Issue
Block a user