fix: restrict password login to existing phone accounts
This commit is contained in:
@@ -18,8 +18,6 @@ use shared_kernel::{
|
||||
use time::{Duration, OffsetDateTime};
|
||||
use tracing::{info, warn};
|
||||
|
||||
const USERNAME_MIN_LENGTH: usize = 3;
|
||||
const USERNAME_MAX_LENGTH: usize = 24;
|
||||
const PASSWORD_MIN_LENGTH: usize = 6;
|
||||
const PASSWORD_MAX_LENGTH: usize = 128;
|
||||
const SMS_CODE_LENGTH: usize = 6;
|
||||
@@ -65,7 +63,7 @@ pub struct PublicUserSearchResult {
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PasswordEntryInput {
|
||||
pub username: String,
|
||||
pub phone_number: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
@@ -315,7 +313,7 @@ pub struct AuthStoreSnapshotProcedureResult {
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum PasswordEntryError {
|
||||
InvalidUsername,
|
||||
InvalidPhoneNumber,
|
||||
InvalidPasswordLength,
|
||||
InvalidPublicUserCode,
|
||||
InvalidCredentials,
|
||||
@@ -476,27 +474,16 @@ impl PasswordEntryService {
|
||||
input: PasswordEntryInput,
|
||||
) -> Result<PasswordEntryResult, PasswordEntryError> {
|
||||
validate_password(&input.password)?;
|
||||
let normalized_phone = normalize_mainland_china_phone_number(&input.phone_number)
|
||||
.map_err(|_| PasswordEntryError::InvalidPhoneNumber)?;
|
||||
let Some(existing_user) = self
|
||||
.store
|
||||
.find_by_phone_number_for_password(&normalized_phone.e164)?
|
||||
else {
|
||||
return Err(PasswordEntryError::InvalidCredentials);
|
||||
};
|
||||
|
||||
// 登录面板现在固定使用手机号作为密码登录标识;先走手机号索引,
|
||||
// 再保留历史用户名路径给开发游客和旧测试数据使用。
|
||||
if let Ok(normalized_phone) = normalize_mainland_china_phone_number(&input.username) {
|
||||
let Some(existing_user) = self
|
||||
.store
|
||||
.find_by_phone_number_for_password(&normalized_phone.e164)?
|
||||
else {
|
||||
return Err(PasswordEntryError::InvalidCredentials);
|
||||
};
|
||||
|
||||
return verify_stored_password_user(existing_user, &input.password).await;
|
||||
}
|
||||
|
||||
let username = normalize_username(&input.username)?;
|
||||
|
||||
if let Some(existing_user) = self.store.find_by_username(&username)? {
|
||||
return verify_stored_password_user(existing_user, &input.password).await;
|
||||
}
|
||||
|
||||
Err(PasswordEntryError::InvalidCredentials)
|
||||
verify_stored_password_user(existing_user, &input.password).await
|
||||
}
|
||||
|
||||
pub fn get_user_by_id(
|
||||
@@ -1232,17 +1219,6 @@ impl InMemoryAuthStore {
|
||||
.map_err(RefreshSessionError::Store)
|
||||
}
|
||||
|
||||
fn find_by_username(
|
||||
&self,
|
||||
username: &str,
|
||||
) -> Result<Option<StoredPasswordUser>, PasswordEntryError> {
|
||||
let state = self
|
||||
.inner
|
||||
.lock()
|
||||
.map_err(|_| PasswordEntryError::Store("用户仓储锁已中毒".to_string()))?;
|
||||
Ok(state.users_by_username.get(username).cloned())
|
||||
}
|
||||
|
||||
fn find_by_user_id(
|
||||
&self,
|
||||
user_id: &str,
|
||||
@@ -2087,10 +2063,10 @@ impl AuthBindingStatus {
|
||||
impl fmt::Display for PasswordEntryError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::InvalidUsername => f.write_str("用户名只允许 3 到 24 位字母、数字、下划线"),
|
||||
Self::InvalidPhoneNumber => f.write_str("手机号格式不正确"),
|
||||
Self::InvalidPasswordLength => f.write_str("密码长度需要在 6 到 128 位之间"),
|
||||
Self::InvalidPublicUserCode => f.write_str("叙世号格式不正确"),
|
||||
Self::InvalidCredentials => f.write_str("用户名或密码错误"),
|
||||
Self::InvalidCredentials => f.write_str("手机号或密码错误"),
|
||||
Self::UserNotFound => f.write_str("用户不存在"),
|
||||
Self::Store(message) | Self::PasswordHash(message) => f.write_str(message),
|
||||
}
|
||||
@@ -2161,7 +2137,7 @@ impl Error for LogoutError {}
|
||||
fn map_password_store_error(error: PasswordEntryError) -> RefreshSessionError {
|
||||
match error {
|
||||
PasswordEntryError::Store(message) => RefreshSessionError::Store(message),
|
||||
PasswordEntryError::InvalidUsername
|
||||
PasswordEntryError::InvalidPhoneNumber
|
||||
| PasswordEntryError::InvalidPasswordLength
|
||||
| PasswordEntryError::InvalidPublicUserCode
|
||||
| PasswordEntryError::InvalidCredentials
|
||||
@@ -2176,7 +2152,7 @@ fn map_password_error_to_phone_error(error: PasswordEntryError) -> PhoneAuthErro
|
||||
match error {
|
||||
PasswordEntryError::Store(message) => PhoneAuthError::Store(message),
|
||||
PasswordEntryError::PasswordHash(message) => PhoneAuthError::PasswordHash(message),
|
||||
PasswordEntryError::InvalidUsername
|
||||
PasswordEntryError::InvalidPhoneNumber
|
||||
| PasswordEntryError::InvalidPasswordLength
|
||||
| PasswordEntryError::InvalidPublicUserCode
|
||||
| PasswordEntryError::InvalidCredentials
|
||||
@@ -2187,7 +2163,7 @@ fn map_password_error_to_phone_error(error: PasswordEntryError) -> PhoneAuthErro
|
||||
fn map_password_error_to_logout_error(error: PasswordEntryError) -> LogoutError {
|
||||
match error {
|
||||
PasswordEntryError::Store(message) => LogoutError::Store(message),
|
||||
PasswordEntryError::InvalidUsername
|
||||
PasswordEntryError::InvalidPhoneNumber
|
||||
| PasswordEntryError::InvalidPasswordLength
|
||||
| PasswordEntryError::InvalidPublicUserCode
|
||||
| PasswordEntryError::InvalidCredentials
|
||||
@@ -2215,21 +2191,6 @@ fn map_refresh_error_to_logout_error(error: RefreshSessionError) -> LogoutError
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_username(raw_username: &str) -> Result<String, PasswordEntryError> {
|
||||
let username = normalize_required_string(raw_username).unwrap_or_default();
|
||||
let valid_length =
|
||||
(USERNAME_MIN_LENGTH..=USERNAME_MAX_LENGTH).contains(&username.chars().count());
|
||||
let valid_chars = username
|
||||
.chars()
|
||||
.all(|character| character.is_ascii_alphanumeric() || character == '_');
|
||||
|
||||
if !valid_length || !valid_chars {
|
||||
return Err(PasswordEntryError::InvalidUsername);
|
||||
}
|
||||
|
||||
Ok(username)
|
||||
}
|
||||
|
||||
fn validate_password(password: &str) -> Result<(), PasswordEntryError> {
|
||||
let length = password.chars().count();
|
||||
if !(PASSWORD_MIN_LENGTH..=PASSWORD_MAX_LENGTH).contains(&length) {
|
||||
@@ -2255,7 +2216,10 @@ async fn verify_stored_password_user(
|
||||
}
|
||||
|
||||
Ok(PasswordEntryResult {
|
||||
user: existing_user.user,
|
||||
user: AuthUser {
|
||||
login_method: AuthLoginMethod::Password,
|
||||
..existing_user.user
|
||||
},
|
||||
created: false,
|
||||
})
|
||||
}
|
||||
@@ -2501,7 +2465,7 @@ mod tests {
|
||||
|
||||
let error = service
|
||||
.execute(PasswordEntryInput {
|
||||
username: "guest_001".to_string(),
|
||||
phone_number: "13800138000".to_string(),
|
||||
password: "secret123".to_string(),
|
||||
})
|
||||
.await
|
||||
@@ -2516,7 +2480,7 @@ mod tests {
|
||||
let user = create_phone_login_user(store.clone(), "13800138000").await;
|
||||
let service = build_password_service(store);
|
||||
|
||||
let changed = service
|
||||
service
|
||||
.change_password(ChangePasswordInput {
|
||||
user_id: user.id.clone(),
|
||||
current_password: None,
|
||||
@@ -2526,7 +2490,7 @@ mod tests {
|
||||
.expect("phone user should set first password");
|
||||
let result = service
|
||||
.execute(PasswordEntryInput {
|
||||
username: changed.user.username.clone(),
|
||||
phone_number: "13800138000".to_string(),
|
||||
password: "secret123".to_string(),
|
||||
})
|
||||
.await
|
||||
@@ -2534,7 +2498,7 @@ mod tests {
|
||||
|
||||
assert!(!result.created);
|
||||
assert_eq!(result.user.id, user.id);
|
||||
assert_eq!(result.user.login_method, AuthLoginMethod::Phone);
|
||||
assert_eq!(result.user.login_method, AuthLoginMethod::Password);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -2553,7 +2517,7 @@ mod tests {
|
||||
|
||||
let error = service
|
||||
.execute(PasswordEntryInput {
|
||||
username: user.username,
|
||||
phone_number: "13800138001".to_string(),
|
||||
password: "secret999".to_string(),
|
||||
})
|
||||
.await
|
||||
@@ -2651,18 +2615,18 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn invalid_username_returns_bad_request_error() {
|
||||
async fn password_entry_rejects_email_or_username_identifier() {
|
||||
let service = build_password_service(build_store());
|
||||
|
||||
let error = service
|
||||
.execute(PasswordEntryInput {
|
||||
username: "坏用户名".to_string(),
|
||||
phone_number: "user@example.com".to_string(),
|
||||
password: "secret123".to_string(),
|
||||
})
|
||||
.await
|
||||
.expect_err("invalid username should fail");
|
||||
.expect_err("email should fail");
|
||||
|
||||
assert_eq!(error, PasswordEntryError::InvalidUsername);
|
||||
assert_eq!(error, PasswordEntryError::InvalidPhoneNumber);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user