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:
98
Cargo.lock
generated
98
Cargo.lock
generated
@@ -106,6 +106,18 @@ dependencies = [
|
|||||||
"num-traits 0.2.19",
|
"num-traits 0.2.19",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "argon2"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"blake2",
|
||||||
|
"cpufeatures",
|
||||||
|
"password-hash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-stream"
|
name = "async-stream"
|
||||||
version = "0.3.6"
|
version = "0.3.6"
|
||||||
@@ -158,11 +170,13 @@ dependencies = [
|
|||||||
name = "auth"
|
name = "auth"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"clap",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"hex",
|
"hex",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"libfj",
|
"libfj",
|
||||||
"log",
|
"log",
|
||||||
|
"rc_core",
|
||||||
"rocket",
|
"rocket",
|
||||||
"steamworks",
|
"steamworks",
|
||||||
"uuid",
|
"uuid",
|
||||||
@@ -201,6 +215,12 @@ version = "0.22.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64ct"
|
||||||
|
version = "1.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "binascii"
|
name = "binascii"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
@@ -219,6 +239,24 @@ version = "2.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
|
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blake2"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-buffer"
|
||||||
|
version = "0.10.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.16.0"
|
version = "3.16.0"
|
||||||
@@ -363,6 +401,15 @@ version = "0.8.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
version = "1.4.2"
|
version = "1.4.2"
|
||||||
@@ -378,6 +425,16 @@ version = "0.2.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
|
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-common"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.3.11"
|
version = "0.3.11"
|
||||||
@@ -420,6 +477,17 @@ dependencies = [
|
|||||||
"syn 2.0.96",
|
"syn 2.0.96",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "digest"
|
||||||
|
version = "0.10.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
|
dependencies = [
|
||||||
|
"block-buffer",
|
||||||
|
"crypto-common",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "displaydoc"
|
name = "displaydoc"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
@@ -614,6 +682,16 @@ dependencies = [
|
|||||||
"windows",
|
"windows",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generic-array"
|
||||||
|
version = "0.14.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "genmesh"
|
name = "genmesh"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
@@ -1444,6 +1522,17 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "password-hash"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pear"
|
name = "pear"
|
||||||
version = "0.2.9"
|
version = "0.2.9"
|
||||||
@@ -1751,8 +1840,11 @@ dependencies = [
|
|||||||
name = "rc_core"
|
name = "rc_core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"argon2",
|
||||||
"chrono",
|
"chrono",
|
||||||
"hex",
|
"hex",
|
||||||
|
"jsonwebtoken",
|
||||||
|
"libfj",
|
||||||
"log",
|
"log",
|
||||||
"polariton",
|
"polariton",
|
||||||
"polariton_server",
|
"polariton_server",
|
||||||
@@ -2730,6 +2822,12 @@ version = "0.2.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ubyte"
|
name = "ubyte"
|
||||||
version = "0.10.4"
|
version = "0.10.4"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ edition = "2021"
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
steam = ["steamworks"]
|
steam = ["steamworks"]
|
||||||
robocraft = []
|
robocraft = ["rc_core"]
|
||||||
cardlife = []
|
cardlife = []
|
||||||
default = ["robocraft", "cardlife"]
|
default = ["robocraft", "cardlife"]
|
||||||
|
|
||||||
@@ -18,3 +18,5 @@ uuid = { version = "1.12", features = [ "v4", "fast-rng" ] }
|
|||||||
steamworks = { version = "0.11", optional = true }
|
steamworks = { version = "0.11", optional = true }
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
jsonwebtoken = "9"
|
jsonwebtoken = "9"
|
||||||
|
clap.workspace = true
|
||||||
|
rc_core = { version = "*", optional = true, path = "../rc_core" }
|
||||||
|
|||||||
28
auth/src/common/cli.rs
Normal file
28
auth/src/common/cli.rs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(version, about, long_about = None)]
|
||||||
|
pub struct CliArgs {
|
||||||
|
#[cfg(feature = "robocraft")]
|
||||||
|
/// Robocraft user data root
|
||||||
|
#[arg(long, default_value_t = {"../data/robocraft".to_string()})]
|
||||||
|
pub data_robocraft: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CliArgs {
|
||||||
|
pub fn get() -> Self {
|
||||||
|
Self::parse()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn preloaded(self) -> Config {
|
||||||
|
Config {
|
||||||
|
#[cfg(feature = "robocraft")]
|
||||||
|
robocraft: crate::robocraft::RcConfig::from_args(&self),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Config {
|
||||||
|
#[cfg(feature = "robocraft")]
|
||||||
|
pub robocraft: crate::robocraft::RcConfig,
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
pub(crate) mod steam_utils;
|
pub(crate) mod steam_utils;
|
||||||
|
pub(crate) mod cli;
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ fn index() -> &'static str {
|
|||||||
#[rocket::launch]
|
#[rocket::launch]
|
||||||
fn rocket() -> _ {
|
fn rocket() -> _ {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
let args = common::cli::CliArgs::get();
|
||||||
#[allow(unused_mut)]
|
#[allow(unused_mut)]
|
||||||
let mut builder = rocket::build().mount("/", rocket::routes![index]);
|
let mut builder = rocket::build().mount("/", rocket::routes![index])
|
||||||
|
.manage(args.preloaded());
|
||||||
|
|
||||||
#[cfg(feature = "cardlife")]
|
#[cfg(feature = "cardlife")]
|
||||||
{builder = builder.attach(cardlife::stage());}
|
{builder = builder.attach(cardlife::stage());}
|
||||||
|
|||||||
@@ -1,35 +1,27 @@
|
|||||||
use rocket::{post, routes, serde::json::Json, http::Status};
|
use rc_core::UserAuthenticator;
|
||||||
|
use rocket::{post, routes, serde::json::Json, http::Status, State};
|
||||||
|
|
||||||
fn generate_token(user_auth: &libfj::robocraft::EmailUserAuthenticationPayload) -> String {
|
#[post("/authenticate/robocraft/game", data = "<body>")]
|
||||||
let header = jsonwebtoken::Header {
|
pub fn email_password_auth(body: Json<libfj::robocraft::EmailUserAuthenticationPayload>, config: &State<crate::common::cli::Config>) -> Result<Json<libfj::robocraft::AuthenticationResponseInfo>, Status> {
|
||||||
typ: Some("JWT".to_string()),
|
log::info!("Authenticating {} user {}", body.target, body.display_name);
|
||||||
alg: jsonwebtoken::Algorithm::HS256,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let payload = libfj::robocraft::TokenPayload {
|
let payload = libfj::robocraft::TokenPayload {
|
||||||
public_id: user_auth.display_name.to_owned(),
|
public_id: body.display_name.clone(),
|
||||||
display_name: user_auth.display_name.to_owned(),
|
display_name: body.display_name.clone(),
|
||||||
robocraft_name: user_auth.display_name.to_owned(),
|
robocraft_name: body.display_name.clone(),
|
||||||
email_address: user_auth.email_address.to_owned(),
|
email_address: body.email_address.clone(),
|
||||||
email_verified: true,
|
email_verified: true,
|
||||||
flags: Vec::new(),
|
flags: Vec::new(),
|
||||||
};
|
};
|
||||||
let secret = jsonwebtoken::EncodingKey::from_secret(user_auth.password.as_bytes()); // FIXME use an actually secret secret
|
let user_info = rc_core::persist::user::UserInfo {
|
||||||
jsonwebtoken::encode(&header, &payload, &secret)
|
payload,
|
||||||
.unwrap_or_else(|e| {
|
extra: rc_core::persist::user::ExtraUserInfo::Standalone { password: body.password.clone() },
|
||||||
log::error!("Failed to encode JWT: {}", e);
|
};
|
||||||
libfj::robocraft::DEFAULT_TOKEN.to_owned()
|
let response = config.robocraft.account_provider.login(user_info)
|
||||||
})
|
.map_err(|e| {
|
||||||
}
|
log::error!("Failed to authenticate {} user {}: {}", body.target, body.display_name, e);
|
||||||
|
Status { code: 401 }
|
||||||
#[post("/authenticate/robocraft/game", data = "<body>")]
|
})?;
|
||||||
pub fn email_password_auth(body: Json<libfj::robocraft::EmailUserAuthenticationPayload>) -> Result<Json<libfj::robocraft::AuthenticationResponseInfo>, Status> {
|
Ok(Json(response.response))
|
||||||
log::info!("Authenticating {} user {}", body.target, body.display_name);
|
|
||||||
Ok(Json(libfj::robocraft::AuthenticationResponseInfo {
|
|
||||||
token: generate_token(&body),
|
|
||||||
refresh_token: "qwertyuiop".to_string(), // TODO
|
|
||||||
refresh_token_expiry: "0".to_string(), // TODO (seems like this isn't actually considered by the client)
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stage() -> rocket::fairing::AdHoc {
|
pub fn stage() -> rocket::fairing::AdHoc {
|
||||||
|
|||||||
@@ -7,5 +7,31 @@ pub fn stage() -> rocket::fairing::AdHoc {
|
|||||||
rocket.attach(email::stage())
|
rocket.attach(email::stage())
|
||||||
.attach(steam::stage())
|
.attach(steam::stage())
|
||||||
.attach(debug::stage())
|
.attach(debug::stage())
|
||||||
|
.register("/", rocket::catchers![unauthorized])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct RcConfig {
|
||||||
|
pub root: std::path::PathBuf,
|
||||||
|
pub account_provider: rc_core::UserImpl,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RcConfig {
|
||||||
|
pub fn from_args(args: &crate::common::cli::CliArgs) -> Self {
|
||||||
|
Self {
|
||||||
|
account_provider: rc_core::UserImpl::load_for_auth(&args.data_robocraft).expect("Invalid Robocraft user data"),
|
||||||
|
root: args.data_robocraft.clone().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use rocket::{catch, serde::json::Json};
|
||||||
|
|
||||||
|
#[catch(401)]
|
||||||
|
fn unauthorized() -> Json<libfj::robocraft::ErrorInfo> {
|
||||||
|
Json(libfj::robocraft::ErrorInfo {
|
||||||
|
error_code: "204".to_owned(),
|
||||||
|
error_message: "Invalid username, password, or SteamID".to_owned(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,29 @@
|
|||||||
use rocket::{post, routes, serde::json::Json, http::Status};
|
use rc_core::UserAuthenticator;
|
||||||
|
use rocket::{http::Status, post, routes, serde::json::Json, State};
|
||||||
|
|
||||||
fn generate_token(user_auth: &libfj::robocraft::SteamAuthenticationPayload, steam_id: u64) -> String {
|
#[post("/authenticate/steam/game", data = "<body>")]
|
||||||
let header = jsonwebtoken::Header {
|
pub fn steam_auth(body: Json<libfj::robocraft::SteamAuthenticationPayload>, config: &State<crate::common::cli::Config>) -> Result<Json<libfj::robocraft::AuthenticationResponseInfo>, Status> {
|
||||||
typ: Some("JWT".to_string()),
|
let steam_id = crate::common::steam_utils::authenticate_steam_ticket(&body.steam_ticket)
|
||||||
alg: jsonwebtoken::Algorithm::HS256,
|
.map_err(|_| Status { code: 401 })?;
|
||||||
..Default::default()
|
log::info!("Authenticating {} steam user {}", body.target, steam_id);
|
||||||
};
|
|
||||||
let payload = libfj::robocraft::TokenPayload {
|
let payload = libfj::robocraft::TokenPayload {
|
||||||
public_id: steam_id.to_string(),
|
public_id: steam_id.to_string(),
|
||||||
display_name: steam_id.to_string(),
|
display_name: steam_id.to_string(),
|
||||||
robocraft_name: steam_id.to_string(),
|
robocraft_name: steam_id.to_string(),
|
||||||
email_address: format!("{}.rc.steam@ngni.us", steam_id),
|
email_address: format!("{}.rc.steam@ngram.ca", steam_id),
|
||||||
email_verified: true,
|
email_verified: true,
|
||||||
flags: Vec::new(),
|
flags: Vec::new(),
|
||||||
};
|
};
|
||||||
let secret = jsonwebtoken::EncodingKey::from_secret(user_auth.steam_ticket.as_ref()); // FIXME use an actually secret secret
|
let user_info = rc_core::persist::user::UserInfo {
|
||||||
jsonwebtoken::encode(&header, &payload, &secret)
|
payload,
|
||||||
.unwrap_or_else(|e| {
|
extra: rc_core::persist::user::ExtraUserInfo::Steam { id: steam_id },
|
||||||
log::error!("Failed to encode JWT: {}", e);
|
};
|
||||||
libfj::robocraft::DEFAULT_TOKEN.to_owned()
|
let response = config.robocraft.account_provider.login(user_info)
|
||||||
})
|
.map_err(|e| {
|
||||||
}
|
log::error!("Failed to authenticate {} steam user {}: {}", body.target, steam_id, e);
|
||||||
|
Status { code: 401 }
|
||||||
#[post("/authenticate/steam/game", data = "<body>")]
|
})?;
|
||||||
pub fn steam_auth(body: Json<libfj::robocraft::SteamAuthenticationPayload>) -> Result<Json<libfj::robocraft::AuthenticationResponseInfo>, Status> {
|
Ok(Json(response.response))
|
||||||
let steam_id = crate::common::steam_utils::authenticate_steam_ticket(&body.steam_ticket)
|
|
||||||
.map_err(|_| Status { code: 401 })?;
|
|
||||||
log::info!("Authenticating {} steam user {}", body.target, steam_id);
|
|
||||||
Ok(Json(libfj::robocraft::AuthenticationResponseInfo {
|
|
||||||
token: generate_token(&body, steam_id),
|
|
||||||
refresh_token: "qwertyuiop".to_string(), // TODO
|
|
||||||
refresh_token_expiry: "0".to_string(), // TODO (seems like this isn't actually considered by the client)
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stage() -> rocket::fairing::AdHoc {
|
pub fn stage() -> rocket::fairing::AdHoc {
|
||||||
|
|||||||
@@ -11,4 +11,9 @@ serde.workspace = true
|
|||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
polariton_server.workspace = true
|
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 use state::UserState;
|
||||||
|
|
||||||
pub mod persist;
|
pub mod persist;
|
||||||
pub use persist::user::{UserImpl, UserProvider};
|
pub use persist::user::{UserImpl, UserProvider, UserAuthenticator};
|
||||||
pub use persist::config::{ConfigImpl, ConfigProvider};
|
pub use persist::config::{ConfigImpl, ConfigProvider};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use argon2::PasswordVerifier;
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
use crate::persist::config::ConfigProvider;
|
use crate::persist::config::ConfigProvider;
|
||||||
@@ -5,15 +6,29 @@ use crate::persist::config::ConfigProvider;
|
|||||||
pub struct AccountProvider {
|
pub struct AccountProvider {
|
||||||
root: std::path::PathBuf,
|
root: std::path::PathBuf,
|
||||||
cubes: std::sync::Arc<Vec<u32>>,
|
cubes: std::sync::Arc<Vec<u32>>,
|
||||||
|
secret: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AccountProvider {
|
impl AccountProvider {
|
||||||
pub fn load(root: impl AsRef<std::path::Path>, cubes: &crate::persist::config::ConfigImpl) -> std::io::Result<Self> {
|
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);
|
let root = root.as_ref().join(super::USERS_DIR);
|
||||||
std::fs::create_dir_all(&root)?;
|
std::fs::create_dir_all(&root)?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
root,
|
root,
|
||||||
cubes: std::sync::Arc::new(<crate::persist::config::ConfigImpl as ConfigProvider<()>>::ids(cubes)),
|
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 {
|
impl <C: Clone> super::UserProvider<C> for AccountProvider {
|
||||||
fn authenticate(&self, token: super::UserToken) -> Result<Box<dyn super::User<C> + Send + Sync>, String> {
|
fn authenticate(&self, token: super::UserToken) -> Result<Box<dyn super::User<C> + Send + Sync>, String> {
|
||||||
let new_root = self.root.join(&token.uuid);
|
let new_root = self.root.join(&token.uuid);
|
||||||
if !new_root.exists() {
|
let secret = jsonwebtoken::DecodingKey::from_secret(&self.secret);
|
||||||
std::fs::create_dir(&new_root).map_err(|e| e.to_string())?;
|
let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256);
|
||||||
log::info!("New user {}", token.uuid);
|
validation.set_required_spec_claims::<&str>(&[]);
|
||||||
super::setup_directory(&new_root).map_err(|e| e.to_string())?;
|
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())?;
|
let account_info = AccountInfo::load(&new_root).map_err(|e| e.to_string())?;
|
||||||
Ok(Box::new(UserData {
|
Ok(Box::new(UserData {
|
||||||
root: new_root,
|
root: new_root,
|
||||||
@@ -36,6 +50,73 @@ impl <C: Clone> super::UserProvider<C> for AccountProvider {
|
|||||||
//Err("Unable to authenticate".to_string())
|
//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)]
|
#[allow(dead_code)]
|
||||||
struct UserData {
|
struct UserData {
|
||||||
@@ -239,6 +320,8 @@ pub struct AccountInfo {
|
|||||||
pub is_mod: bool,
|
pub is_mod: bool,
|
||||||
pub is_admin: bool,
|
pub is_admin: bool,
|
||||||
pub is_dev: bool,
|
pub is_dev: bool,
|
||||||
|
pub password: Option<String>,
|
||||||
|
pub steam_id: Option<u64>,
|
||||||
pub inventory: super::UnlockedParts,
|
pub inventory: super::UnlockedParts,
|
||||||
pub garage: super::SelectedGarage,
|
pub garage: super::SelectedGarage,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ fn default_user_data() -> super::AccountInfo {
|
|||||||
is_mod: false,
|
is_mod: false,
|
||||||
is_admin: false,
|
is_admin: false,
|
||||||
is_dev: false,
|
is_dev: false,
|
||||||
|
steam_id: None,
|
||||||
|
password: None,
|
||||||
inventory: super::UnlockedParts {
|
inventory: super::UnlockedParts {
|
||||||
unlocked: vec![],
|
unlocked: vec![],
|
||||||
override_: super::inventory::UnlockOverride::Normal,
|
override_: super::inventory::UnlockOverride::Normal,
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ mod inventory;
|
|||||||
pub use inventory::UnlockedParts;
|
pub use inventory::UnlockedParts;
|
||||||
|
|
||||||
mod traits;
|
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 USERS_DIR: &str = "accounts";
|
||||||
pub const USER_FILE: &str = "user.json";
|
pub const USER_FILE: &str = "user.json";
|
||||||
|
|||||||
@@ -6,10 +6,33 @@ pub struct UserToken {
|
|||||||
pub refresh_token: String,
|
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> {
|
pub trait UserProvider<C> {
|
||||||
fn authenticate(&self, user: UserToken) -> Result<Box<dyn User<C> + Send + Sync>, String>;
|
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> {
|
pub trait User<C> {
|
||||||
fn token(&self) -> &'_ super::UserToken;
|
fn token(&self) -> &'_ super::UserToken;
|
||||||
fn is_mod(&self) -> bool;
|
fn is_mod(&self) -> bool;
|
||||||
|
|||||||
Reference in New Issue
Block a user