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:
@@ -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" ] }
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user