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:
21
Cargo.lock
generated
21
Cargo.lock
generated
@@ -1495,6 +1495,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2595,7 +2596,6 @@ dependencies = [
|
|||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"libfj",
|
"libfj",
|
||||||
"log",
|
"log",
|
||||||
"oj_rc_core",
|
|
||||||
"rocket",
|
"rocket",
|
||||||
"rocket_dyn_templates",
|
"rocket_dyn_templates",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -3346,6 +3346,25 @@ dependencies = [
|
|||||||
"zerocopy 0.8.14",
|
"zerocopy 0.8.14",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rc_auth"
|
||||||
|
version = "0.3.0"
|
||||||
|
dependencies = [
|
||||||
|
"actix-files",
|
||||||
|
"actix-web",
|
||||||
|
"clap",
|
||||||
|
"env_logger",
|
||||||
|
"git-version",
|
||||||
|
"handlebars",
|
||||||
|
"hex",
|
||||||
|
"libfj",
|
||||||
|
"log",
|
||||||
|
"oj_rc_core",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rc_multiplayer"
|
name = "rc_multiplayer"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ readme = "README.md"
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"auth", "cdn",
|
"auth", "cdn",
|
||||||
|
"rc_auth",
|
||||||
"polariton_auth",
|
"polariton_auth",
|
||||||
"rc_services", "rc_services_room",
|
"rc_services", "rc_services_room",
|
||||||
"rc_microtransactions",
|
"rc_microtransactions",
|
||||||
|
|||||||
@@ -9,9 +9,8 @@ authors.workspace = true
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
steam = ["steamworks"]
|
steam = ["steamworks"]
|
||||||
robocraft = ["oj_rc_core"]
|
|
||||||
cardlife = []
|
cardlife = []
|
||||||
default = ["robocraft", "cardlife"]
|
default = ["cardlife"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rocket.workspace = true
|
rocket.workspace = true
|
||||||
@@ -24,6 +23,5 @@ steamworks = { version = "0.11", optional = true }
|
|||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
jsonwebtoken = "9"
|
jsonwebtoken = "9"
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
oj_rc_core = { version = "*", optional = true, path = "../rc_core" }
|
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
git-version.workspace = true
|
git-version.workspace = true
|
||||||
|
|||||||
@@ -3,10 +3,6 @@ use clap::Parser;
|
|||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(version, about, long_about = None)]
|
#[command(version, about, long_about = None)]
|
||||||
pub struct CliArgs {
|
pub struct CliArgs {
|
||||||
#[cfg(feature = "robocraft")]
|
|
||||||
/// Robocraft user data root
|
|
||||||
#[arg(long, default_value_t = {"../data/robocraft".to_string()})]
|
|
||||||
pub data_robocraft: String,
|
|
||||||
/// Robocraft asset data root
|
/// Robocraft asset data root
|
||||||
#[arg(long, default_value_t = {"../assets/robocraft".to_string()})]
|
#[arg(long, default_value_t = {"../assets/robocraft".to_string()})]
|
||||||
pub assets_robocraft: String,
|
pub assets_robocraft: String,
|
||||||
@@ -19,13 +15,9 @@ impl CliArgs {
|
|||||||
|
|
||||||
pub async fn preloaded(self) -> Config {
|
pub async fn preloaded(self) -> Config {
|
||||||
Config {
|
Config {
|
||||||
#[cfg(feature = "robocraft")]
|
|
||||||
robocraft: crate::robocraft::RcConfig::from_args(&self).await,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
#[cfg(feature = "robocraft")]
|
|
||||||
pub robocraft: crate::robocraft::RcConfig,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,9 +30,6 @@ fn get_steam_id_from_ticket(ticket: &[u8]) -> u64 {
|
|||||||
#[cfg(all(feature = "steam", feature = "cardlife"))]
|
#[cfg(all(feature = "steam", feature = "cardlife"))]
|
||||||
const STEAM_ID: &str = "920690"; // Cardlife steam app id
|
const STEAM_ID: &str = "920690"; // Cardlife steam app id
|
||||||
|
|
||||||
#[cfg(all(feature = "steam", feature = "robocraft"))]
|
|
||||||
const STEAM_ID: &str = "301520"; // Robocraft steam app id
|
|
||||||
|
|
||||||
#[cfg(feature = "steam")]
|
#[cfg(feature = "steam")]
|
||||||
static STEAM_SERVER: std::sync::OnceLock<steamworks::Server> = std::sync::OnceLock::new();
|
static STEAM_SERVER: std::sync::OnceLock<steamworks::Server> = std::sync::OnceLock::new();
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ mod common;
|
|||||||
|
|
||||||
#[cfg(feature = "cardlife")]
|
#[cfg(feature = "cardlife")]
|
||||||
mod cardlife;
|
mod cardlife;
|
||||||
#[cfg(feature = "robocraft")]
|
|
||||||
mod robocraft;
|
|
||||||
|
|
||||||
#[rocket::get("/")]
|
#[rocket::get("/")]
|
||||||
fn index() -> String {
|
fn index() -> String {
|
||||||
@@ -27,12 +25,7 @@ async fn rocket() -> _ {
|
|||||||
|
|
||||||
#[cfg(feature = "cardlife")]
|
#[cfg(feature = "cardlife")]
|
||||||
{builder = builder.attach(cardlife::stage());}
|
{builder = builder.attach(cardlife::stage());}
|
||||||
#[cfg(feature = "robocraft")]
|
|
||||||
{builder = builder.attach(robocraft::stage());}
|
|
||||||
|
|
||||||
builder
|
builder
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(feature = "steam", feature = "robocraft", feature = "cardlife"))]
|
|
||||||
compile_error!("Feature \"steam\" cannot work with features \"cardlife\" and \"robocraft\" at the same time");
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
use rocket::{post, routes};
|
|
||||||
|
|
||||||
#[post("/", data = "<body>")]
|
|
||||||
pub fn debug_endpoint(body: String) -> String {
|
|
||||||
log::info!("got body: `{}`", body);
|
|
||||||
body.to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn stage() -> rocket::fairing::AdHoc {
|
|
||||||
rocket::fairing::AdHoc::on_ignite("Robocraft debug", |rocket| async {
|
|
||||||
rocket.mount("/", routes![debug_endpoint])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
mod debug;
|
|
||||||
mod username;
|
|
||||||
mod steam;
|
|
||||||
mod email;
|
|
||||||
mod registration;
|
|
||||||
|
|
||||||
pub fn stage() -> rocket::fairing::AdHoc {
|
|
||||||
rocket::fairing::AdHoc::on_ignite("robocraft", |rocket| async {
|
|
||||||
rocket.attach(username::stage())
|
|
||||||
.attach(email::stage())
|
|
||||||
.attach(steam::stage())
|
|
||||||
.attach(debug::stage())
|
|
||||||
.attach(registration::stage())
|
|
||||||
.register("/", rocket::catchers![unauthorized])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct RcConfig {
|
|
||||||
pub data: std::path::PathBuf,
|
|
||||||
pub account_provider: oj_rc_core::UserImpl,
|
|
||||||
pub assets: std::path::PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RcConfig {
|
|
||||||
pub async fn from_args(args: &crate::common::cli::CliArgs) -> Self {
|
|
||||||
let conf = oj_rc_core::persist::config::ConfigImpl::load(&args.assets_robocraft).expect("Bad config data");
|
|
||||||
Self {
|
|
||||||
account_provider: oj_rc_core::UserImpl::load(&args.data_robocraft, &conf).await.expect("Invalid Robocraft user data"),
|
|
||||||
data: args.data_robocraft.clone().into(),
|
|
||||||
assets: args.assets_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,33 +0,0 @@
|
|||||||
use oj_rc_core::UserAuthenticator;
|
|
||||||
use rocket::{http::Status, post, routes, serde::json::Json, State};
|
|
||||||
|
|
||||||
#[post("/authenticate/steam/game", data = "<body>")]
|
|
||||||
pub async fn steam_auth(body: Json<libfj::robocraft::SteamAuthenticationPayload>, config: &State<crate::common::cli::Config>) -> Result<Json<libfj::robocraft::AuthenticationResponseInfo>, Status> {
|
|
||||||
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);
|
|
||||||
let payload = libfj::robocraft::TokenPayload {
|
|
||||||
public_id: steam_id.to_string(),
|
|
||||||
display_name: steam_id.to_string(),
|
|
||||||
robocraft_name: steam_id.to_string(),
|
|
||||||
email_address: format!("{}.rc.steam@ngram.ca", steam_id),
|
|
||||||
email_verified: true,
|
|
||||||
flags: Vec::new(),
|
|
||||||
};
|
|
||||||
let user_info = oj_rc_core::persist::user::UserInfo {
|
|
||||||
payload,
|
|
||||||
extra: oj_rc_core::persist::user::ExtraUserInfo::Steam { id: steam_id },
|
|
||||||
};
|
|
||||||
let response = config.robocraft.account_provider.login(user_info).await
|
|
||||||
.map_err(|e| {
|
|
||||||
log::error!("Failed to authenticate {} steam user {}: {}", body.target, steam_id, e);
|
|
||||||
Status { code: 401 }
|
|
||||||
})?;
|
|
||||||
Ok(Json(response.response))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn stage() -> rocket::fairing::AdHoc {
|
|
||||||
rocket::fairing::AdHoc::on_ignite("Robocraft Steam", |rocket| async {
|
|
||||||
rocket.mount("/", routes![steam_auth])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
24
rc_auth/Cargo.toml
Normal file
24
rc_auth/Cargo.toml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[package]
|
||||||
|
name = "rc_auth"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
readme.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix-web.workspace = true
|
||||||
|
actix-files.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
env_logger.workspace = true
|
||||||
|
tokio = { version = "1.43", features = [ "rt-multi-thread" ] }
|
||||||
|
clap.workspace = true
|
||||||
|
oj_rc_core = { version = "*", path = "../rc_core" }
|
||||||
|
libfj.workspace = true
|
||||||
|
git-version.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
hex = "0.4"
|
||||||
|
|
||||||
|
handlebars = { version = "5", features = ["dir_source"] }
|
||||||
3
rc_auth/run_debug.sh
Executable file
3
rc_auth/run_debug.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
RUST_LOG=debug cargo run
|
||||||
31
rc_auth/src/cli.rs
Normal file
31
rc_auth/src/cli.rs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug, Clone)]
|
||||||
|
#[command(version, about, long_about = None)]
|
||||||
|
pub struct CliArgs {
|
||||||
|
/// TCP port on which to accept connections
|
||||||
|
#[arg(short, long, default_value_t = 8001)]
|
||||||
|
pub port: u16,
|
||||||
|
|
||||||
|
/// IP Address on which to accept connections
|
||||||
|
#[arg(long, default_value_t = {"127.0.0.1".to_string()})]
|
||||||
|
pub ip: String,
|
||||||
|
|
||||||
|
/// Assets root
|
||||||
|
#[arg(long, default_value_t = {"../assets/robocraft".to_string()})]
|
||||||
|
pub assets_robocraft: String,
|
||||||
|
|
||||||
|
/// 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 async fn preloaded(self) -> crate::robocraft::RcConfig {
|
||||||
|
crate::robocraft::RcConfig::from_args(&self).await
|
||||||
|
}
|
||||||
|
}
|
||||||
60
rc_auth/src/main.rs
Normal file
60
rc_auth/src/main.rs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
mod cli;
|
||||||
|
mod robocraft;
|
||||||
|
|
||||||
|
use actix_web::{App, HttpServer, Responder};
|
||||||
|
|
||||||
|
#[actix_web::get("/")]
|
||||||
|
async fn index() -> impl Responder {
|
||||||
|
let name = env!("CARGO_PKG_NAME");
|
||||||
|
let version = env!("CARGO_PKG_VERSION");
|
||||||
|
let git_version = git_version::git_version!(args = ["--always", "--dirty=+"]);
|
||||||
|
let authors = env!("CARGO_PKG_AUTHORS");
|
||||||
|
let license = env!("CARGO_PKG_LICENSE");
|
||||||
|
let repo = env!("CARGO_PKG_REPOSITORY");
|
||||||
|
format!("{} {}:{} by [{}]\n{}\n{}", name, version, git_version, authors, license, repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
env_logger::init();
|
||||||
|
let cli_args = cli::CliArgs::get();
|
||||||
|
let cli_args2 = actix_web::web::Data::new(cli_args.clone());
|
||||||
|
let rc_preloaded = actix_web::web::Data::new(cli_args.clone().preloaded().await);
|
||||||
|
|
||||||
|
let mut handlebars = handlebars::Handlebars::new();
|
||||||
|
handlebars
|
||||||
|
.register_templates_directory(
|
||||||
|
std::path::PathBuf::from(&cli_args.assets_robocraft).parent().expect("Bad robocraft asset path").join("templates"),
|
||||||
|
handlebars::DirectorySourceOptions {
|
||||||
|
tpl_extension: ".html.hbs".to_owned(),
|
||||||
|
hidden: false,
|
||||||
|
temporary: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let handlebars_ref = actix_web::web::Data::new(handlebars);
|
||||||
|
|
||||||
|
HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
.app_data(cli_args2.clone())
|
||||||
|
.app_data(rc_preloaded.clone())
|
||||||
|
.app_data(handlebars_ref.clone())
|
||||||
|
.service(index)
|
||||||
|
.service(robocraft::registration::form_submit)
|
||||||
|
.service(robocraft::registration::form_load)
|
||||||
|
.service(robocraft::registration::favicon)
|
||||||
|
.service(robocraft::email::email_password_auth)
|
||||||
|
.service(robocraft::steam::steam_auth)
|
||||||
|
.service(robocraft::username::user_password_auth)
|
||||||
|
/*.service(robocraft::live_data::live_data_json)
|
||||||
|
.service(robocraft::user_avatar::get)
|
||||||
|
.service(robocraft::clan_avatar::get)
|
||||||
|
.service(robocraft::brawl_data::get)
|
||||||
|
.service(robocraft::campaign_data::get)
|
||||||
|
.service(robocraft::factory::arc::get)
|
||||||
|
.service(robocraft::favicon::get)*/
|
||||||
|
})
|
||||||
|
.bind((cli_args.ip, cli_args.port))?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
use oj_rc_core::UserAuthenticator;
|
use oj_rc_core::UserAuthenticator;
|
||||||
use rocket::{post, routes, serde::json::Json, http::Status, State};
|
use actix_web::{post, web::{Data, Json}};
|
||||||
|
|
||||||
#[post("/authenticate/email/game", data = "<body>")]
|
#[post("/authenticate/email/game")]
|
||||||
pub async fn email_password_auth(body: Json<libfj::robocraft::EmailUserAuthenticationPayload>, config: &State<crate::common::cli::Config>) -> Result<Json<libfj::robocraft::AuthenticationResponseInfo>, Status> {
|
pub async fn email_password_auth(body: Json<libfj::robocraft::EmailUserAuthenticationPayload>, config: Data<super::RcConfig>) -> Result<Json<libfj::robocraft::AuthenticationResponseInfo>, super::ErrorTy> {
|
||||||
log::info!("Authenticating {} user {}", body.target, body.display_name);
|
log::info!("Authenticating {} user {}", body.target, body.display_name);
|
||||||
let payload = libfj::robocraft::TokenPayload {
|
let payload = libfj::robocraft::TokenPayload {
|
||||||
public_id: body.display_name.clone(),
|
public_id: body.display_name.clone(),
|
||||||
@@ -16,16 +16,10 @@ pub async fn email_password_auth(body: Json<libfj::robocraft::EmailUserAuthentic
|
|||||||
payload,
|
payload,
|
||||||
extra: oj_rc_core::persist::user::ExtraUserInfo::Email { password: body.password.clone() },
|
extra: oj_rc_core::persist::user::ExtraUserInfo::Email { password: body.password.clone() },
|
||||||
};
|
};
|
||||||
let response = config.robocraft.account_provider.login(user_info).await
|
let response = config.account_provider.login(user_info).await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
log::error!("Failed to authenticate {} user {}: {}", body.target, body.display_name, e);
|
log::error!("Failed to authenticate {} user {}: {}", body.target, body.display_name, e.message);
|
||||||
Status { code: 401 }
|
super::ErrorTy::from_err(e)
|
||||||
})?;
|
})?;
|
||||||
Ok(Json(response.response))
|
Ok(Json(response.response))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stage() -> rocket::fairing::AdHoc {
|
|
||||||
rocket::fairing::AdHoc::on_ignite("Robocraft Username/Password", |rocket| async {
|
|
||||||
rocket.mount("/", routes![email_password_auth])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
63
rc_auth/src/robocraft/mod.rs
Normal file
63
rc_auth/src/robocraft/mod.rs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
pub mod email;
|
||||||
|
pub mod registration;
|
||||||
|
pub mod steam;
|
||||||
|
pub mod username;
|
||||||
|
|
||||||
|
pub struct RcConfig {
|
||||||
|
//pub data: std::path::PathBuf,
|
||||||
|
pub account_provider: oj_rc_core::UserImpl,
|
||||||
|
pub assets: std::path::PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RcConfig {
|
||||||
|
pub async fn from_args(args: &crate::cli::CliArgs) -> Self {
|
||||||
|
let conf = oj_rc_core::persist::config::ConfigImpl::load(&args.assets_robocraft).expect("Bad config data");
|
||||||
|
Self {
|
||||||
|
account_provider: oj_rc_core::UserImpl::load(&args.data_robocraft, &conf).await.expect("Invalid Robocraft user data"),
|
||||||
|
//data: args.data_robocraft.clone().into(),
|
||||||
|
assets: args.assets_robocraft.clone().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(self) struct ErrorTy {
|
||||||
|
json: libfj::robocraft::ErrorInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErrorTy {
|
||||||
|
pub fn from_err(error: oj_rc_core::persist::user::AuthError) -> Self {
|
||||||
|
Self {
|
||||||
|
json: libfj::robocraft::ErrorInfo {
|
||||||
|
error_code: error.code.to_str(),
|
||||||
|
error_message: error.message,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl actix_web::error::ResponseError for ErrorTy {
|
||||||
|
fn status_code(&self) -> actix_web::http::StatusCode {
|
||||||
|
actix_web::http::StatusCode::UNAUTHORIZED
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error_response(&self) -> actix_web::HttpResponse<actix_web::body::BoxBody> {
|
||||||
|
actix_web::HttpResponse::with_body(
|
||||||
|
self.status_code(),
|
||||||
|
serde_json::to_string(&self.json).unwrap(),
|
||||||
|
).map_into_boxed_body()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Display for ErrorTy {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
//use core::fmt::Write;
|
||||||
|
write!(f, "({}) {}", self.json.error_code, self.json.error_message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Debug for ErrorTy {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("ErrorTy")
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use oj_rc_core::UserAuthenticator;
|
use oj_rc_core::UserAuthenticator;
|
||||||
use rocket::{post, get, form::{Form, FromForm}, routes, http::Status, State};
|
use actix_web::{get, post, web::{Data, Form, Html}, Responder};
|
||||||
use rocket_dyn_templates::{Template, context};
|
//use rocket_dyn_templates::{Template, context};
|
||||||
use serde::Serialize;
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
const FORM_NAME: &str = "rc_register";
|
const FORM_NAME: &str = "rc_register";
|
||||||
const FORM_NAME_SUCCESS: &str = "rc_register_success";
|
const FORM_NAME_SUCCESS: &str = "rc_register_success";
|
||||||
@@ -12,7 +12,7 @@ const VALID_CHARS: &[char] = &[
|
|||||||
'_',
|
'_',
|
||||||
];
|
];
|
||||||
|
|
||||||
#[derive(FromForm, Serialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
struct RegisterForm {
|
struct RegisterForm {
|
||||||
display_name: String,
|
display_name: String,
|
||||||
password: String,
|
password: String,
|
||||||
@@ -29,6 +29,14 @@ struct Context {
|
|||||||
source_url: String,
|
source_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ContextSuccess {
|
||||||
|
display_name: String,
|
||||||
|
id: i32,
|
||||||
|
version: String,
|
||||||
|
source_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
fn version_string() -> String {
|
fn version_string() -> String {
|
||||||
let name = env!("CARGO_PKG_NAME");
|
let name = env!("CARGO_PKG_NAME");
|
||||||
let version = env!("CARGO_PKG_VERSION");
|
let version = env!("CARGO_PKG_VERSION");
|
||||||
@@ -46,35 +54,37 @@ fn all_valid_chars(s: &str) -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn registration_ok(form: RegisterForm) -> Template {
|
fn registration_ok(form: RegisterForm, renderer: &handlebars::Handlebars<'_>) -> Html {
|
||||||
Template::render(FORM_NAME, Context {
|
let rendered = renderer.render(FORM_NAME, &Context {
|
||||||
form,
|
form,
|
||||||
error: None,
|
error: None,
|
||||||
version: version_string(),
|
version: version_string(),
|
||||||
source_url: env!("CARGO_PKG_REPOSITORY").to_owned(),
|
source_url: env!("CARGO_PKG_REPOSITORY").to_owned(),
|
||||||
})
|
}).unwrap();
|
||||||
|
Html::new(rendered)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn registration_err(form: RegisterForm, error: String) -> Template {
|
fn registration_err(form: RegisterForm, error: String , renderer: &handlebars::Handlebars<'_>) -> Html {
|
||||||
Template::render(FORM_NAME, Context {
|
let rendered = renderer.render(FORM_NAME, &Context {
|
||||||
form,
|
form,
|
||||||
error: Some(error),
|
error: Some(error),
|
||||||
version: version_string(),
|
version: version_string(),
|
||||||
source_url: env!("CARGO_PKG_REPOSITORY").to_owned(),
|
source_url: env!("CARGO_PKG_REPOSITORY").to_owned(),
|
||||||
})
|
}).unwrap();
|
||||||
|
Html::new(rendered)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/register", data = "<form>")]
|
#[post("/register")]
|
||||||
async fn form_submit(form: Form<RegisterForm>, config: &State<crate::common::cli::Config>) -> Result<Template, Status> {
|
pub async fn form_submit(form: Form<RegisterForm>, config: Data<super::RcConfig>, handlebars_ref: Data<handlebars::Handlebars<'_>>) -> Result<Html, actix_web::error::Error> {
|
||||||
// password confirmation validation
|
// password confirmation validation
|
||||||
if form.password != form.password_c {
|
if form.password != form.password_c {
|
||||||
return Ok(registration_err(form.into_inner(), "Passwords do not match".to_owned()));
|
return Ok(registration_err(form.into_inner(), "Passwords do not match".to_owned(), &*handlebars_ref));
|
||||||
}
|
}
|
||||||
if form.password.len() < 8 {
|
if form.password.len() < 8 {
|
||||||
return Ok(registration_err(form.into_inner(), "Password too short (minimum 8 characters)".to_owned()));
|
return Ok(registration_err(form.into_inner(), "Password too short (minimum 8 characters)".to_owned(), &*handlebars_ref));
|
||||||
}
|
}
|
||||||
if form.password.len() > 128 {
|
if form.password.len() > 128 {
|
||||||
return Ok(registration_err(form.into_inner(), "Password too long (maximum 128 characters)".to_owned()));
|
return Ok(registration_err(form.into_inner(), "Password too long (maximum 128 characters)".to_owned(), &*handlebars_ref));
|
||||||
}
|
}
|
||||||
|
|
||||||
// email validation
|
// email validation
|
||||||
@@ -84,16 +94,16 @@ async fn form_submit(form: Form<RegisterForm>, config: &State<crate::common::cli
|
|||||||
actual_email = None;
|
actual_email = None;
|
||||||
} else {
|
} else {
|
||||||
if !email.contains('@') {
|
if !email.contains('@') {
|
||||||
return Ok(registration_err(form.into_inner(), "Email must contain @".to_owned()));
|
return Ok(registration_err(form.into_inner(), "Email must contain @".to_owned(), &*handlebars_ref));
|
||||||
}
|
}
|
||||||
let email_exists = config.robocraft.account_provider.user_exists(oj_rc_core::persist::user::UserId::Email(email.to_owned()))
|
let email_exists = config.account_provider.user_exists(oj_rc_core::persist::user::UserId::Email(email.to_owned()))
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
log::error!("Failed to check if user email {} exists: {}", email, e);
|
log::error!("Failed to check if user email {} exists: {}", email, e);
|
||||||
Status { code: 500 }
|
actix_web::error::ErrorInternalServerError(e)
|
||||||
})?;
|
})?;
|
||||||
if email_exists {
|
if email_exists {
|
||||||
return Ok(registration_err(form.into_inner(), "Email already registered".to_owned()));
|
return Ok(registration_err(form.into_inner(), "Email already registered".to_owned(), &*handlebars_ref));
|
||||||
}
|
}
|
||||||
actual_email = Some(email.to_owned());
|
actual_email = Some(email.to_owned());
|
||||||
}
|
}
|
||||||
@@ -109,19 +119,19 @@ async fn form_submit(form: Form<RegisterForm>, config: &State<crate::common::cli
|
|||||||
} else {
|
} else {
|
||||||
let steam_id = match steam_id.parse() {
|
let steam_id = match steam_id.parse() {
|
||||||
Ok(id) => id,
|
Ok(id) => id,
|
||||||
Err(_e) => return Ok(registration_err(form.into_inner(), "Invalid SteamID (not an integer)".to_owned())),
|
Err(_e) => return Ok(registration_err(form.into_inner(), "Invalid SteamID (not an integer)".to_owned(), &*handlebars_ref)),
|
||||||
};
|
};
|
||||||
if steam_id >= 7656120_0000000000 || steam_id < 7656119_0000000000 {
|
if steam_id >= 7656120_0000000000 || steam_id < 7656119_0000000000 {
|
||||||
return Ok(registration_err(form.into_inner(), "Invalid SteamID (should be like 7656119XXXXXXXXXX)".to_owned()));
|
return Ok(registration_err(form.into_inner(), "Invalid SteamID (should be like 7656119XXXXXXXXXX)".to_owned(), &*handlebars_ref));
|
||||||
}
|
}
|
||||||
let steam_exists = config.robocraft.account_provider.user_exists(oj_rc_core::persist::user::UserId::SteamId(steam_id))
|
let steam_exists = config.account_provider.user_exists(oj_rc_core::persist::user::UserId::SteamId(steam_id))
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
log::error!("Failed to check if user steam id {} exists: {}", steam_id, e);
|
log::error!("Failed to check if user steam id {} exists: {}", steam_id, e);
|
||||||
Status { code: 500 }
|
actix_web::error::ErrorInternalServerError(e)
|
||||||
})?;
|
})?;
|
||||||
if steam_exists {
|
if steam_exists {
|
||||||
return Ok(registration_err(form.into_inner(), "SteamID already registered".to_owned()));
|
return Ok(registration_err(form.into_inner(), "SteamID already registered".to_owned(), &*handlebars_ref));
|
||||||
}
|
}
|
||||||
actual_steam_id = Some(steam_id);
|
actual_steam_id = Some(steam_id);
|
||||||
}
|
}
|
||||||
@@ -131,25 +141,25 @@ async fn form_submit(form: Form<RegisterForm>, config: &State<crate::common::cli
|
|||||||
|
|
||||||
// username validation
|
// username validation
|
||||||
if form.display_name.len() < 4 {
|
if form.display_name.len() < 4 {
|
||||||
return Ok(registration_err(form.into_inner(), "Username too short (minimum 4 characters)".to_owned()));
|
return Ok(registration_err(form.into_inner(), "Username too short (minimum 4 characters)".to_owned(), &*handlebars_ref));
|
||||||
}
|
}
|
||||||
if form.display_name.len() > 32 {
|
if form.display_name.len() > 32 {
|
||||||
return Ok(registration_err(form.into_inner(), "Username too long (maximum 32 characters)".to_owned()));
|
return Ok(registration_err(form.into_inner(), "Username too long (maximum 32 characters)".to_owned(), &*handlebars_ref));
|
||||||
}
|
}
|
||||||
if !all_valid_chars(&form.display_name.to_lowercase()) {
|
if !all_valid_chars(&form.display_name.to_lowercase()) {
|
||||||
return Ok(registration_err(form.into_inner(), "Invalid username (only alphanumerics and _ allowed)".to_owned()));
|
return Ok(registration_err(form.into_inner(), "Invalid username (only alphanumerics and _ allowed)".to_owned(), &*handlebars_ref));
|
||||||
}
|
}
|
||||||
let username_exists = config.robocraft.account_provider.user_exists(oj_rc_core::persist::user::UserId::Username(form.display_name.to_owned()))
|
let username_exists = config.account_provider.user_exists(oj_rc_core::persist::user::UserId::Username(form.display_name.to_owned()))
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
log::error!("Failed to check if user name {} exists: {}", form.display_name, e);
|
log::error!("Failed to check if user name {} exists: {}", form.display_name, e);
|
||||||
Status { code: 500 }
|
actix_web::error::ErrorInternalServerError(e)
|
||||||
})?;
|
})?;
|
||||||
if username_exists {
|
if username_exists {
|
||||||
return Ok(registration_err(form.into_inner(), "Username already registered".to_owned()));
|
return Ok(registration_err(form.into_inner(), "Username already registered".to_owned(), &*handlebars_ref));
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_id = match config.robocraft.account_provider.register(oj_rc_core::persist::user::RegistrationInfo {
|
let user_id = match config.account_provider.register(oj_rc_core::persist::user::RegistrationInfo {
|
||||||
display_name: form.display_name.clone(),
|
display_name: form.display_name.clone(),
|
||||||
password: form.password.clone(),
|
password: form.password.clone(),
|
||||||
email: actual_email,
|
email: actual_email,
|
||||||
@@ -157,40 +167,31 @@ async fn form_submit(form: Form<RegisterForm>, config: &State<crate::common::cli
|
|||||||
}).await {
|
}).await {
|
||||||
Ok(id) => id,
|
Ok(id) => id,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return Ok(registration_err(form.into_inner(), format!("Registration failed: {}", e)));
|
return Ok(registration_err(form.into_inner(), format!("Registration failed: {}", e), &*handlebars_ref));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Template::render(FORM_NAME_SUCCESS, context! {
|
Ok(Html::new(handlebars_ref.render(FORM_NAME_SUCCESS, &ContextSuccess {
|
||||||
display_name: form.display_name.clone(),
|
display_name: form.display_name.clone(),
|
||||||
id: user_id,
|
id: user_id,
|
||||||
version: version_string(),
|
version: version_string(),
|
||||||
source_url: env!("CARGO_PKG_REPOSITORY").to_owned(),
|
source_url: env!("CARGO_PKG_REPOSITORY").to_owned(),
|
||||||
}))
|
}).unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/register")]
|
#[get("/register")]
|
||||||
async fn form_load() -> Template {
|
pub async fn form_load(handlebars_ref: Data<handlebars::Handlebars<'_>>) -> Html {
|
||||||
registration_ok(RegisterForm {
|
registration_ok(RegisterForm {
|
||||||
display_name: "".to_owned(),
|
display_name: "".to_owned(),
|
||||||
password: "".to_owned(),
|
password: "".to_owned(),
|
||||||
password_c: "".to_owned(),
|
password_c: "".to_owned(),
|
||||||
email: None,
|
email: None,
|
||||||
steam_id: None,
|
steam_id: None,
|
||||||
})
|
}, &*handlebars_ref)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/robocraft/favicon")]
|
#[get("/robocraft/favicon")]
|
||||||
pub async fn favicon(config: &State<crate::common::cli::Config>) -> Result<rocket::fs::NamedFile, std::io::Error> {
|
pub async fn favicon(config: Data<super::RcConfig>) -> impl Responder {
|
||||||
let path = config.robocraft.assets.join("favicon.jpg");
|
let path = config.assets.join("favicon.jpg");
|
||||||
rocket::fs::NamedFile::open(path).await
|
actix_files::NamedFile::open_async(path).await
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pub fn stage() -> rocket::fairing::AdHoc {
|
|
||||||
rocket::fairing::AdHoc::on_ignite("Robocraft Steam", |rocket| async {
|
|
||||||
rocket.mount("/", routes![form_load, form_submit, favicon])
|
|
||||||
.attach(Template::fairing())
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
65
rc_auth/src/robocraft/steam.rs
Normal file
65
rc_auth/src/robocraft/steam.rs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
use oj_rc_core::UserAuthenticator;
|
||||||
|
use actix_web::{post, web::{Data, Json}};
|
||||||
|
|
||||||
|
fn authenticate_steam_ticket(hex_ticket: &str) -> Result<u64, ()> {
|
||||||
|
get_steam_id_from_ticket_hex(hex_ticket)
|
||||||
|
.map_err(|e| {
|
||||||
|
log::error!("Failed to parse steamId: {}", e);
|
||||||
|
()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_steam_id_from_ticket_hex(hex_ticket: &str) -> Result<u64, hex::FromHexError> {
|
||||||
|
let decoded_ticket = hex::decode(hex_ticket)?;
|
||||||
|
if decoded_ticket.len() < 72 {
|
||||||
|
Err(hex::FromHexError::InvalidStringLength)
|
||||||
|
} else {
|
||||||
|
Ok(get_steam_id_from_ticket(&decoded_ticket))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_steam_id_from_ticket(ticket: &[u8]) -> u64 {
|
||||||
|
get_u64_with_offset(&ticket, 12 /* also at 64 ??? */) // should be 76600000000000000 > number > 76500000000000000
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_u64_with_offset(arr: &[u8], start: usize) -> u64 {
|
||||||
|
let arr_actual: [u8; 8] = [
|
||||||
|
arr[start],
|
||||||
|
arr[start+1],
|
||||||
|
arr[start+2],
|
||||||
|
arr[start+3],
|
||||||
|
arr[start+4],
|
||||||
|
arr[start+5],
|
||||||
|
arr[start+6],
|
||||||
|
arr[start+7],
|
||||||
|
];
|
||||||
|
u64::from_le_bytes(arr_actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/authenticate/steam/game")]
|
||||||
|
pub async fn steam_auth(body: Json<libfj::robocraft::SteamAuthenticationPayload>, config: Data<super::RcConfig>) -> Result<Json<libfj::robocraft::AuthenticationResponseInfo>, super::ErrorTy> {
|
||||||
|
let steam_id = authenticate_steam_ticket(&body.steam_ticket)
|
||||||
|
.map_err(|_| super::ErrorTy::from_err(oj_rc_core::persist::user::AuthError {
|
||||||
|
message: "Bad SteamId".to_owned(),
|
||||||
|
code: oj_rc_core::data::error_codes::AuthErrorCode::BadCredentials,
|
||||||
|
}))?;
|
||||||
|
log::info!("Authenticating {} steam user {}", body.target, steam_id);
|
||||||
|
let payload = libfj::robocraft::TokenPayload {
|
||||||
|
public_id: steam_id.to_string(),
|
||||||
|
display_name: steam_id.to_string(),
|
||||||
|
robocraft_name: steam_id.to_string(),
|
||||||
|
email_address: format!("{}.rc.steam@ngram.ca", steam_id),
|
||||||
|
email_verified: true,
|
||||||
|
flags: Vec::new(),
|
||||||
|
};
|
||||||
|
let user_info = oj_rc_core::persist::user::UserInfo {
|
||||||
|
payload,
|
||||||
|
extra: oj_rc_core::persist::user::ExtraUserInfo::Steam { id: steam_id },
|
||||||
|
};
|
||||||
|
let response = config.account_provider.login(user_info).await
|
||||||
|
.map_err(|e| {
|
||||||
|
log::error!("Failed to authenticate {} steam user {}: {}", body.target, steam_id, e.message);
|
||||||
|
super::ErrorTy::from_err(e)
|
||||||
|
})?;
|
||||||
|
Ok(Json(response.response))
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
use oj_rc_core::UserAuthenticator;
|
use oj_rc_core::UserAuthenticator;
|
||||||
use rocket::{post, routes, serde::json::Json, http::Status, State};
|
use actix_web::{post, web::{Data, Json}};
|
||||||
|
|
||||||
#[post("/authenticate/robocraft/game", data = "<body>")]
|
#[post("/authenticate/robocraft/game")]
|
||||||
pub async fn user_password_auth(body: Json<libfj::robocraft::EmailUserAuthenticationPayload>, config: &State<crate::common::cli::Config>) -> Result<Json<libfj::robocraft::AuthenticationResponseInfo>, Status> {
|
pub async fn user_password_auth(body: Json<libfj::robocraft::EmailUserAuthenticationPayload>, config: Data<super::RcConfig>) -> Result<Json<libfj::robocraft::AuthenticationResponseInfo>, super::ErrorTy> {
|
||||||
log::info!("Authenticating {} user {}", body.target, body.display_name);
|
log::info!("Authenticating {} user {}", body.target, body.display_name);
|
||||||
let payload = libfj::robocraft::TokenPayload {
|
let payload = libfj::robocraft::TokenPayload {
|
||||||
public_id: body.display_name.clone(),
|
public_id: body.display_name.clone(),
|
||||||
@@ -16,16 +16,10 @@ pub async fn user_password_auth(body: Json<libfj::robocraft::EmailUserAuthentica
|
|||||||
payload,
|
payload,
|
||||||
extra: oj_rc_core::persist::user::ExtraUserInfo::Username { password: body.password.clone() },
|
extra: oj_rc_core::persist::user::ExtraUserInfo::Username { password: body.password.clone() },
|
||||||
};
|
};
|
||||||
let response = config.robocraft.account_provider.login(user_info).await
|
let response = config.account_provider.login(user_info).await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
log::error!("Failed to authenticate {} user {}: {}", body.target, body.display_name, e);
|
log::error!("Failed to authenticate {} user {}: {}", body.target, body.display_name, e.message);
|
||||||
Status { code: 401 }
|
super::ErrorTy::from_err(e)
|
||||||
})?;
|
})?;
|
||||||
Ok(Json(response.response))
|
Ok(Json(response.response))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stage() -> rocket::fairing::AdHoc {
|
|
||||||
rocket::fairing::AdHoc::on_ignite("Robocraft Username/Password", |rocket| async {
|
|
||||||
rocket.mount("/", routes![user_password_auth])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -63,21 +63,36 @@ impl AccountProvider {
|
|||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl <C: Clone> super::UserProvider<C> for AccountProvider {
|
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 new_root = self.root.join(&token.uuid);
|
||||||
let secret = jsonwebtoken::DecodingKey::from_secret(&self.secret);
|
let secret = jsonwebtoken::DecodingKey::from_secret(&self.secret);
|
||||||
let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256);
|
let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256);
|
||||||
validation.set_required_spec_claims::<&str>(&[]);
|
validation.set_required_spec_claims::<&str>(&[]);
|
||||||
jsonwebtoken::decode::<libfj::robocraft::TokenPayload>(&token.token, &secret, &validation).map_err(|e| e.to_string())?;
|
jsonwebtoken::decode::<libfj::robocraft::TokenPayload>(&token.token, &secret, &validation).map_err(|e| super::AuthError {
|
||||||
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())? {
|
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
|
user_info
|
||||||
} else {
|
} 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
|
user_perms
|
||||||
} else {
|
} 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())?;
|
//let account_info = AccountInfo::load(&new_root).map_err(|e| e.to_string())?;
|
||||||
Ok(Box::new(UserData {
|
Ok(Box::new(UserData {
|
||||||
@@ -90,16 +105,28 @@ impl <C: Clone> super::UserProvider<C> for AccountProvider {
|
|||||||
//Err("Unable to authenticate".to_string())
|
//Err("Unable to authenticate".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn multiplayer_authenticate(&self, user: String) -> Result<Box<dyn super::User<C> + Send + Sync>, 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| e.to_string())? {
|
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
|
user_info
|
||||||
} else {
|
} 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
|
user_perms
|
||||||
} else {
|
} 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 {
|
Ok(Box::new(UserData {
|
||||||
account: user_info,
|
account: user_info,
|
||||||
@@ -113,14 +140,17 @@ impl <C: Clone> super::UserProvider<C> for AccountProvider {
|
|||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl super::UserAuthenticator for AccountProvider {
|
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 new_root = self.root.join(&info.payload.public_id);
|
||||||
let is_new_user;
|
let is_new_user;
|
||||||
let user_opt = match &info.extra {
|
let user_opt = match &info.extra {
|
||||||
super::ExtraUserInfo::Steam { id } => self.db.user_by_steam_id(*id).await,
|
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::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,
|
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 {
|
let mut user_info = if let Some(user_info) = user_opt {
|
||||||
is_new_user = false;
|
is_new_user = false;
|
||||||
user_info
|
user_info
|
||||||
@@ -128,11 +158,20 @@ impl super::UserAuthenticator for AccountProvider {
|
|||||||
is_new_user = true;
|
is_new_user = true;
|
||||||
if self.auto_signups {
|
if self.auto_signups {
|
||||||
log::info!("New user {}", info.payload.public_id);
|
log::info!("New user {}", info.payload.public_id);
|
||||||
super::setup_new_user(&info, &self.db).await.map_err(|e| e.to_string())?;
|
super::setup_new_user(&info, &self.db).await.map_err(|e| super::AuthError {
|
||||||
self.db.user_by_display_name(info.payload.display_name.clone()).await.map_err(|e| e.to_string())?.unwrap()
|
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 {
|
} else {
|
||||||
log::info!("Rejecting user sign-in for `{}` (set settings.server.auto_signup=true to disable this behaviour)", info.payload.public_id);
|
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();
|
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();
|
let id_str = id.to_string();
|
||||||
if let Some(expected_steam_id) = user_info.steam_id {
|
if let Some(expected_steam_id) = user_info.steam_id {
|
||||||
if expected_steam_id != id_str {
|
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 {
|
} 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 }
|
super::ExtraUserInfo::Email { password }
|
||||||
@@ -153,17 +198,46 @@ impl super::UserAuthenticator for AccountProvider {
|
|||||||
let argon2_algo = argon2::Argon2::default();
|
let argon2_algo = argon2::Argon2::default();
|
||||||
if override_password {
|
if override_password {
|
||||||
let salt = argon2::password_hash::SaltString::generate(&mut argon2::password_hash::rand_core::OsRng);
|
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;
|
user_info.password = password_hash;
|
||||||
}
|
}
|
||||||
if !user_info.password.is_empty() {
|
if !user_info.password.is_empty() {
|
||||||
let expected = argon2::password_hash::PasswordHash::new(&user_info.password).map_err(|e| e.to_string())?;
|
let expected = argon2::password_hash::PasswordHash::new(&user_info.password).map_err(|e| super::AuthError {
|
||||||
argon2_algo.verify_password(password.as_bytes(), &expected).map_err(|e| e.to_string())?;
|
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 {
|
} 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
|
// authentication has now definitely succeeded
|
||||||
// build token
|
// build token
|
||||||
let header = jsonwebtoken::Header {
|
let header = jsonwebtoken::Header {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ mod inventory;
|
|||||||
pub use inventory::UnlockedParts;
|
pub use inventory::UnlockedParts;
|
||||||
|
|
||||||
mod traits;
|
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";
|
pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key";
|
||||||
|
|
||||||
|
|||||||
@@ -41,16 +41,21 @@ pub struct RegistrationInfo {
|
|||||||
pub steam_id: Option<u64>,
|
pub steam_id: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct AuthError {
|
||||||
|
pub message: String,
|
||||||
|
pub code: crate::data::error_codes::AuthErrorCode,
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
pub trait UserProvider<C> {
|
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]
|
#[async_trait::async_trait]
|
||||||
pub trait UserAuthenticator {
|
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 user_exists(&self, user: UserId) -> Result<bool, String>;
|
||||||
async fn register(&self, info: RegistrationInfo) -> Result<i32, String>;
|
async fn register(&self, info: RegistrationInfo) -> Result<i32, String>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ impl <C: Clone + Send + 'static> UserState<C> {
|
|||||||
true
|
true
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to authenticate {}: {}", splits[0], e);
|
log::error!("Failed to authenticate {}: ({:?}) {}", splits[0], e.code, e.message);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user