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

Actually verify user credentials

This commit is contained in:
NGnius (Graham)
2025-04-01 19:38:03 -04:00
parent a540cf6d8f
commit 69d1bfaee0
14 changed files with 319 additions and 63 deletions

View File

@@ -11,4 +11,9 @@ serde.workspace = true
serde_json.workspace = true
chrono = "0.4"
polariton_server.workspace = true
tokio = { version = "1.43", features = [ "net", "macros", "rt-multi-thread", "io-util", "time"] }
tokio = { version = "1.43", features = [ "net", "macros", "rt-multi-thread", "io-util", "time" ] }
# auth
libfj.workspace = true
jsonwebtoken = "9"
argon2 = { version = "0.5", features = [ "std" ] }

View File

@@ -4,5 +4,5 @@ mod state;
pub use state::UserState;
pub mod persist;
pub use persist::user::{UserImpl, UserProvider};
pub use persist::user::{UserImpl, UserProvider, UserAuthenticator};
pub use persist::config::{ConfigImpl, ConfigProvider};

View File

@@ -1,3 +1,4 @@
use argon2::PasswordVerifier;
use serde::{Serialize, Deserialize};
use crate::persist::config::ConfigProvider;
@@ -5,15 +6,29 @@ use crate::persist::config::ConfigProvider;
pub struct AccountProvider {
root: std::path::PathBuf,
cubes: std::sync::Arc<Vec<u32>>,
secret: Vec<u8>,
}
impl AccountProvider {
pub fn load(root: impl AsRef<std::path::Path>, cubes: &crate::persist::config::ConfigImpl) -> std::io::Result<Self> {
let token_path = root.as_ref().join(super::TOKEN_SECRET_FILENAME);
let root = root.as_ref().join(super::USERS_DIR);
std::fs::create_dir_all(&root)?;
Ok(Self {
root,
cubes: std::sync::Arc::new(<crate::persist::config::ConfigImpl as ConfigProvider<()>>::ids(cubes)),
secret: std::fs::read(&token_path)?,
})
}
pub fn load_for_auth(root: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
let token_path = root.as_ref().join(super::TOKEN_SECRET_FILENAME);
let root = root.as_ref().join(super::USERS_DIR);
std::fs::create_dir_all(&root)?;
Ok(Self {
root,
cubes: std::sync::Arc::new(Vec::default()),
secret: std::fs::read(&token_path)?,
})
}
}
@@ -21,11 +36,10 @@ impl AccountProvider {
impl <C: Clone> super::UserProvider<C> for AccountProvider {
fn authenticate(&self, token: super::UserToken) -> Result<Box<dyn super::User<C> + Send + Sync>, String> {
let new_root = self.root.join(&token.uuid);
if !new_root.exists() {
std::fs::create_dir(&new_root).map_err(|e| e.to_string())?;
log::info!("New user {}", token.uuid);
super::setup_directory(&new_root).map_err(|e| e.to_string())?;
}
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 account_info = AccountInfo::load(&new_root).map_err(|e| e.to_string())?;
Ok(Box::new(UserData {
root: new_root,
@@ -36,6 +50,73 @@ impl <C: Clone> super::UserProvider<C> for AccountProvider {
//Err("Unable to authenticate".to_string())
}
}
impl super::UserAuthenticator for AccountProvider {
fn login(&self, info: super::UserInfo) -> Result<super::UserLoginInfo, String> {
let new_root = self.root.join(&info.payload.public_id);
let is_new_user = !new_root.exists();
if is_new_user {
std::fs::create_dir(&new_root).map_err(|e| e.to_string())?;
log::info!("New user {}", info.payload.public_id);
super::setup_directory(&new_root).map_err(|e| e.to_string())?;
}
let mut account_info = AccountInfo::load(&new_root).map_err(|e| e.to_string())?;
let is_new_user = is_new_user || (account_info.password.is_none() && account_info.steam_id.is_none()); // migration
match info.extra {
super::ExtraUserInfo::Steam { id } => {
if is_new_user {
account_info.steam_id = Some(id);
}
if let Some(expected_steam_id) = account_info.steam_id {
if expected_steam_id != id {
return Err("SteamID does not match".to_owned())
}
} else {
return Err("SteamID not supported for this user".to_owned());
}
},
super::ExtraUserInfo::Standalone { password } => {
use argon2::password_hash::PasswordHasher;
let argon2_algo = argon2::Argon2::default();
if is_new_user {
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();
account_info.password = Some(password_hash);
}
if let Some(expected_password) = &account_info.password {
let expected = argon2::password_hash::PasswordHash::new(expected_password).map_err(|e| e.to_string())?;
argon2_algo.verify_password(password.as_bytes(), &expected).map_err(|e| e.to_string())?;
} else {
return Err("Password not supported for this user".to_owned())
}
}
}
// authentication has now definitely succeeded
if is_new_user {
account_info.save(new_root).map_err(|e| e.to_string())?;
}
// build token
let header = jsonwebtoken::Header {
typ: Some("JWT".to_string()),
alg: jsonwebtoken::Algorithm::HS256,
..Default::default()
};
let secret = jsonwebtoken::EncodingKey::from_secret(&self.secret);
let token = jsonwebtoken::encode(&header, &info.payload, &secret)
.unwrap_or_else(|e| {
log::error!("Failed to encode JWT: {}", e);
libfj::robocraft::DEFAULT_TOKEN.to_owned()
});
Ok(super::UserLoginInfo {
response: libfj::robocraft::AuthenticationResponseInfo {
token,
refresh_token: "qwertyuiop".to_string(), // TODO
refresh_token_expiry: "0".to_string(), // TODO (seems like this isn't actually considered by the client)
},
is_new: is_new_user,
})
}
}
#[allow(dead_code)]
struct UserData {
@@ -239,6 +320,8 @@ pub struct AccountInfo {
pub is_mod: bool,
pub is_admin: bool,
pub is_dev: bool,
pub password: Option<String>,
pub steam_id: Option<u64>,
pub inventory: super::UnlockedParts,
pub garage: super::SelectedGarage,
}

View File

@@ -28,6 +28,8 @@ fn default_user_data() -> super::AccountInfo {
is_mod: false,
is_admin: false,
is_dev: false,
steam_id: None,
password: None,
inventory: super::UnlockedParts {
unlocked: vec![],
override_: super::inventory::UnlockOverride::Normal,

View File

@@ -11,7 +11,9 @@ mod inventory;
pub use inventory::UnlockedParts;
mod traits;
pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData};
pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserInfo, UserLoginInfo, ExtraUserInfo, UserAuthenticator};
pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key";
pub const USERS_DIR: &str = "accounts";
pub const USER_FILE: &str = "user.json";

View File

@@ -6,10 +6,33 @@ pub struct UserToken {
pub refresh_token: String,
}
pub struct UserInfo {
pub payload: libfj::robocraft::TokenPayload,
pub extra: ExtraUserInfo,
}
pub enum ExtraUserInfo {
Steam {
id: u64,
},
Standalone {
password: String,
}
}
pub struct UserLoginInfo {
pub response: libfj::robocraft::AuthenticationResponseInfo,
pub is_new: bool,
}
pub trait UserProvider<C> {
fn authenticate(&self, user: UserToken) -> Result<Box<dyn User<C> + Send + Sync>, String>;
}
pub trait UserAuthenticator {
fn login(&self, info: UserInfo) -> Result<UserLoginInfo, String>;
}
pub trait User<C> {
fn token(&self) -> &'_ super::UserToken;
fn is_mod(&self) -> bool;