1
0
mirror of https://git.ngram.ca/OpenJam/rc-servers synced 2026-08-23 23:08:52 +00:00

Move Robocraft auth server to actix_web for better error responses #31

This commit is contained in:
NG (Graham)
2025-07-27 16:12:20 -04:00
parent 796304bc4a
commit 89b1db35ba
23 changed files with 455 additions and 211 deletions

View File

@@ -101,3 +101,23 @@ impl LobbyReasonCode {
}
}
}
#[repr(u16)] // doesn't really matter
#[derive(Debug)]
pub enum AuthErrorCode {
Unknown = 0,
InvalidDisplayName = 122,
AccountBlocked202 = 202,
PasswordInvalidated = 203,
BadCredentials = 204,
DisplayeNameAlreadyInUse = 210,
AccountBlocked301 = 301,
AccountUnconfirmed = 302,
UnlinkedSteamAccount = 303,
}
impl AuthErrorCode {
pub fn to_str(self) -> String {
(self as u16).to_string()
}
}

View File

@@ -63,21 +63,36 @@ impl AccountProvider {
#[async_trait::async_trait]
impl <C: Clone> super::UserProvider<C> for AccountProvider {
async fn authenticate(&self, token: super::UserToken) -> Result<Box<dyn super::User<C> + Send + Sync>, String> {
async fn authenticate(&self, token: super::UserToken) -> Result<Box<dyn super::User<C> + Send + Sync>, super::AuthError> {
//let new_root = self.root.join(&token.uuid);
let secret = jsonwebtoken::DecodingKey::from_secret(&self.secret);
let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256);
validation.set_required_spec_claims::<&str>(&[]);
jsonwebtoken::decode::<libfj::robocraft::TokenPayload>(&token.token, &secret, &validation).map_err(|e| e.to_string())?;
let user_info = if let Some(user_info) = self.db.user_by_any_unique_id(token.uuid.clone()).await.map_err(|e| e.to_string())? {
jsonwebtoken::decode::<libfj::robocraft::TokenPayload>(&token.token, &secret, &validation).map_err(|e| super::AuthError {
message: e.to_string(),
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
})?;
let user_info = if let Some(user_info) = self.db.user_by_any_unique_id(token.uuid.clone()).await.map_err(|e| super::AuthError {
message: e.to_string(),
code: crate::data::error_codes::AuthErrorCode::Unknown,
})? {
user_info
} else {
return Err("User not found".to_owned());
return Err(super::AuthError {
message: "User not found".to_owned(),
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
});
};
let user_perms = if let Some(user_perms) = self.db.perms_by_user_id(user_info.id).await.map_err(|e| e.to_string())? {
let user_perms = if let Some(user_perms) = self.db.perms_by_user_id(user_info.id).await.map_err(|e| super::AuthError {
message: e.to_string(),
code: crate::data::error_codes::AuthErrorCode::Unknown
})? {
user_perms
} else {
return Err("User permissions not found".to_owned());
return Err(super::AuthError {
message: "User permissions not found".to_owned(),
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
});
};
//let account_info = AccountInfo::load(&new_root).map_err(|e| e.to_string())?;
Ok(Box::new(UserData {
@@ -90,16 +105,28 @@ impl <C: Clone> super::UserProvider<C> for AccountProvider {
//Err("Unable to authenticate".to_string())
}
async fn multiplayer_authenticate(&self, user: String) -> Result<Box<dyn super::User<C> + Send + Sync>, String> {
let user_info = if let Some(user_info) = self.db.user_by_display_name(user).await.map_err(|e| e.to_string())? {
async fn multiplayer_authenticate(&self, user: String) -> Result<Box<dyn super::User<C> + Send + Sync>, super::AuthError> {
let user_info = if let Some(user_info) = self.db.user_by_display_name(user).await.map_err(|e| super::AuthError {
message: e.to_string(),
code: crate::data::error_codes::AuthErrorCode::Unknown,
})? {
user_info
} else {
return Err("User not found".to_owned());
return Err(super::AuthError {
message: "User not found".to_owned(),
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
});
};
let user_perms = if let Some(user_perms) = self.db.perms_by_user_id(user_info.id).await.map_err(|e| e.to_string())? {
let user_perms = if let Some(user_perms) = self.db.perms_by_user_id(user_info.id).await.map_err(|e| super::AuthError {
message: e.to_string(),
code: crate::data::error_codes::AuthErrorCode::Unknown,
})? {
user_perms
} else {
return Err("User permissions not found".to_owned());
return Err(super::AuthError {
message: "User permissions not found".to_owned(),
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
});
};
Ok(Box::new(UserData {
account: user_info,
@@ -113,14 +140,17 @@ impl <C: Clone> super::UserProvider<C> for AccountProvider {
#[async_trait::async_trait]
impl super::UserAuthenticator for AccountProvider {
async fn login(&self, info: super::UserInfo) -> Result<super::UserLoginInfo, String> {
async fn login(&self, info: super::UserInfo) -> Result<super::UserLoginInfo, super::AuthError> {
//let new_root = self.root.join(&info.payload.public_id);
let is_new_user;
let user_opt = match &info.extra {
super::ExtraUserInfo::Steam { id } => self.db.user_by_steam_id(*id).await,
super::ExtraUserInfo::Email { .. } => self.db.user_by_email(info.payload.email_address.clone()).await,
super::ExtraUserInfo::Username { .. } => self.db.user_by_display_name(info.payload.display_name.clone()).await,
}.map_err(|e| e.to_string())?;
}.map_err(|e| super::AuthError {
message: e.to_string(),
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
})?;
let mut user_info = if let Some(user_info) = user_opt {
is_new_user = false;
user_info
@@ -128,11 +158,20 @@ impl super::UserAuthenticator for AccountProvider {
is_new_user = true;
if self.auto_signups {
log::info!("New user {}", info.payload.public_id);
super::setup_new_user(&info, &self.db).await.map_err(|e| e.to_string())?;
self.db.user_by_display_name(info.payload.display_name.clone()).await.map_err(|e| e.to_string())?.unwrap()
super::setup_new_user(&info, &self.db).await.map_err(|e| super::AuthError {
message: e.to_string(),
code: crate::data::error_codes::AuthErrorCode::Unknown,
})?;
self.db.user_by_display_name(info.payload.display_name.clone()).await.map_err(|e| super::AuthError {
message: e.to_string(),
code: crate::data::error_codes::AuthErrorCode::Unknown,
})?.unwrap()
} else {
log::info!("Rejecting user sign-in for `{}` (set settings.server.auto_signup=true to disable this behaviour)", info.payload.public_id);
return Err(format!("User does not exist"));
return Err(super::AuthError {
message: "User not found".to_owned(),
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
});
}
};
let override_password = user_info.password.is_empty() && user_info.steam_id.is_none();
@@ -141,10 +180,16 @@ impl super::UserAuthenticator for AccountProvider {
let id_str = id.to_string();
if let Some(expected_steam_id) = user_info.steam_id {
if expected_steam_id != id_str {
return Err("SteamID does not match".to_owned())
return Err(super::AuthError {
message: "SteamID does not match".to_owned(),
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
});
}
} else {
return Err("SteamID not supported for this user".to_owned());
return Err(super::AuthError {
message: "SteamID not supported for this user".to_owned(),
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
});
}
},
super::ExtraUserInfo::Email { password }
@@ -153,17 +198,46 @@ impl super::UserAuthenticator for AccountProvider {
let argon2_algo = argon2::Argon2::default();
if override_password {
let salt = argon2::password_hash::SaltString::generate(&mut argon2::password_hash::rand_core::OsRng);
let password_hash = argon2_algo.hash_password(password.as_bytes(), &salt).map_err(|e| e.to_string())?.to_string();
let password_hash = argon2_algo.hash_password(password.as_bytes(), &salt).map_err(|e| super::AuthError {
message: e.to_string(),
code: crate::data::error_codes::AuthErrorCode::Unknown,
})?.to_string();
user_info.password = password_hash;
}
if !user_info.password.is_empty() {
let expected = argon2::password_hash::PasswordHash::new(&user_info.password).map_err(|e| e.to_string())?;
argon2_algo.verify_password(password.as_bytes(), &expected).map_err(|e| e.to_string())?;
let expected = argon2::password_hash::PasswordHash::new(&user_info.password).map_err(|e| super::AuthError {
message: e.to_string(),
code: crate::data::error_codes::AuthErrorCode::Unknown,
})?;
argon2_algo.verify_password(password.as_bytes(), &expected).map_err(|e| super::AuthError {
message: e.to_string(),
code: crate::data::error_codes::AuthErrorCode::Unknown,
})?;
} else {
return Err("Password not supported for this user".to_owned())
return Err(super::AuthError {
message: "Password not supported for this user".to_owned(),
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
});
}
}
}
// check if user is banned
if let Some(perms) = self.db.perms_by_user_id(user_info.id).await.map_err(|e| super::AuthError {
message: e.to_string(),
code: crate::data::error_codes::AuthErrorCode::Unknown,
})? {
if perms.banned {
return Err(super::AuthError {
message: "User is banned".to_owned(),
code: crate::data::error_codes::AuthErrorCode::AccountBlocked301,
});
}
} else {
return Err(super::AuthError {
message: "User has no permissions".to_owned(),
code: crate::data::error_codes::AuthErrorCode::BadCredentials,
});
}
// authentication has now definitely succeeded
// build token
let header = jsonwebtoken::Header {

View File

@@ -11,7 +11,7 @@ mod inventory;
pub use inventory::UnlockedParts;
mod traits;
pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserInfo, UserLoginInfo, ExtraUserInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent};
pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserInfo, UserLoginInfo, ExtraUserInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent, AuthError};
pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key";

View File

@@ -41,16 +41,21 @@ pub struct RegistrationInfo {
pub steam_id: Option<u64>,
}
pub struct AuthError {
pub message: String,
pub code: crate::data::error_codes::AuthErrorCode,
}
#[async_trait::async_trait]
pub trait UserProvider<C> {
async fn authenticate(&self, user: UserToken) -> Result<Box<dyn User<C> + Send + Sync>, String>;
async fn authenticate(&self, user: UserToken) -> Result<Box<dyn User<C> + Send + Sync>, AuthError>;
async fn multiplayer_authenticate(&self, user: String) -> Result<Box<dyn User<C> + Send + Sync>, String>;
async fn multiplayer_authenticate(&self, user: String) -> Result<Box<dyn User<C> + Send + Sync>, AuthError>;
}
#[async_trait::async_trait]
pub trait UserAuthenticator {
async fn login(&self, info: UserInfo) -> Result<UserLoginInfo, String>;
async fn login(&self, info: UserInfo) -> Result<UserLoginInfo, AuthError>;
async fn user_exists(&self, user: UserId) -> Result<bool, String>;
async fn register(&self, info: RegistrationInfo) -> Result<i32, String>;
}

View File

@@ -39,7 +39,7 @@ impl <C: Clone + Send + 'static> UserState<C> {
true
},
Err(e) => {
log::error!("Failed to authenticate {}: {}", splits[0], e);
log::error!("Failed to authenticate {}: ({:?}) {}", splits[0], e.code, e.message);
false
}
}