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

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

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

21
Cargo.lock generated
View File

@@ -1495,6 +1495,7 @@ dependencies = [
"serde",
"serde_json",
"thiserror 1.0.69",
"walkdir",
]
[[package]]
@@ -2595,7 +2596,6 @@ dependencies = [
"jsonwebtoken",
"libfj",
"log",
"oj_rc_core",
"rocket",
"rocket_dyn_templates",
"serde",
@@ -3346,6 +3346,25 @@ dependencies = [
"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]]
name = "rc_multiplayer"
version = "0.3.0"

View File

@@ -14,6 +14,7 @@ readme = "README.md"
[workspace]
members = [
"auth", "cdn",
"rc_auth",
"polariton_auth",
"rc_services", "rc_services_room",
"rc_microtransactions",

View File

@@ -9,9 +9,8 @@ authors.workspace = true
[features]
steam = ["steamworks"]
robocraft = ["oj_rc_core"]
cardlife = []
default = ["robocraft", "cardlife"]
default = ["cardlife"]
[dependencies]
rocket.workspace = true
@@ -24,6 +23,5 @@ steamworks = { version = "0.11", optional = true }
hex = "0.4"
jsonwebtoken = "9"
clap.workspace = true
oj_rc_core = { version = "*", optional = true, path = "../rc_core" }
serde.workspace = true
git-version.workspace = true

View File

@@ -3,10 +3,6 @@ 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,
/// Robocraft asset data root
#[arg(long, default_value_t = {"../assets/robocraft".to_string()})]
pub assets_robocraft: String,
@@ -19,13 +15,9 @@ impl CliArgs {
pub async fn preloaded(self) -> Config {
Config {
#[cfg(feature = "robocraft")]
robocraft: crate::robocraft::RcConfig::from_args(&self).await,
}
}
}
pub struct Config {
#[cfg(feature = "robocraft")]
pub robocraft: crate::robocraft::RcConfig,
}

View File

@@ -30,9 +30,6 @@ fn get_steam_id_from_ticket(ticket: &[u8]) -> u64 {
#[cfg(all(feature = "steam", feature = "cardlife"))]
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")]
static STEAM_SERVER: std::sync::OnceLock<steamworks::Server> = std::sync::OnceLock::new();

View File

@@ -3,8 +3,6 @@ mod common;
#[cfg(feature = "cardlife")]
mod cardlife;
#[cfg(feature = "robocraft")]
mod robocraft;
#[rocket::get("/")]
fn index() -> String {
@@ -27,12 +25,7 @@ async fn rocket() -> _ {
#[cfg(feature = "cardlife")]
{builder = builder.attach(cardlife::stage());}
#[cfg(feature = "robocraft")]
{builder = builder.attach(robocraft::stage());}
builder
}
#[cfg(all(feature = "steam", feature = "robocraft", feature = "cardlife"))]
compile_error!("Feature \"steam\" cannot work with features \"cardlife\" and \"robocraft\" at the same time");

View File

@@ -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])
})
}

View File

@@ -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(),
})
}

View File

@@ -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
View 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
View File

@@ -0,0 +1,3 @@
#!/bin/bash
RUST_LOG=debug cargo run

31
rc_auth/src/cli.rs Normal file
View 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
View 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
}

View File

@@ -1,8 +1,8 @@
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>")]
pub async fn email_password_auth(body: Json<libfj::robocraft::EmailUserAuthenticationPayload>, config: &State<crate::common::cli::Config>) -> Result<Json<libfj::robocraft::AuthenticationResponseInfo>, Status> {
#[post("/authenticate/email/game")]
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);
let payload = libfj::robocraft::TokenPayload {
public_id: body.display_name.clone(),
@@ -16,16 +16,10 @@ pub async fn email_password_auth(body: Json<libfj::robocraft::EmailUserAuthentic
payload,
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| {
log::error!("Failed to authenticate {} user {}: {}", body.target, body.display_name, e);
Status { code: 401 }
log::error!("Failed to authenticate {} user {}: {}", body.target, body.display_name, e.message);
super::ErrorTy::from_err(e)
})?;
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])
})
}

View 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()
}
}

View File

@@ -1,7 +1,7 @@
use oj_rc_core::UserAuthenticator;
use rocket::{post, get, form::{Form, FromForm}, routes, http::Status, State};
use rocket_dyn_templates::{Template, context};
use serde::Serialize;
use actix_web::{get, post, web::{Data, Form, Html}, Responder};
//use rocket_dyn_templates::{Template, context};
use serde::{Serialize, Deserialize};
const FORM_NAME: &str = "rc_register";
const FORM_NAME_SUCCESS: &str = "rc_register_success";
@@ -12,7 +12,7 @@ const VALID_CHARS: &[char] = &[
'_',
];
#[derive(FromForm, Serialize)]
#[derive(Serialize, Deserialize)]
struct RegisterForm {
display_name: String,
password: String,
@@ -29,6 +29,14 @@ struct Context {
source_url: String,
}
#[derive(Serialize)]
struct ContextSuccess {
display_name: String,
id: i32,
version: String,
source_url: String,
}
fn version_string() -> String {
let name = env!("CARGO_PKG_NAME");
let version = env!("CARGO_PKG_VERSION");
@@ -46,35 +54,37 @@ fn all_valid_chars(s: &str) -> bool {
true
}
fn registration_ok(form: RegisterForm) -> Template {
Template::render(FORM_NAME, Context {
fn registration_ok(form: RegisterForm, renderer: &handlebars::Handlebars<'_>) -> Html {
let rendered = renderer.render(FORM_NAME, &Context {
form,
error: None,
version: version_string(),
source_url: env!("CARGO_PKG_REPOSITORY").to_owned(),
})
}).unwrap();
Html::new(rendered)
}
fn registration_err(form: RegisterForm, error: String) -> Template {
Template::render(FORM_NAME, Context {
fn registration_err(form: RegisterForm, error: String , renderer: &handlebars::Handlebars<'_>) -> Html {
let rendered = renderer.render(FORM_NAME, &Context {
form,
error: Some(error),
version: version_string(),
source_url: env!("CARGO_PKG_REPOSITORY").to_owned(),
})
}).unwrap();
Html::new(rendered)
}
#[post("/register", data = "<form>")]
async fn form_submit(form: Form<RegisterForm>, config: &State<crate::common::cli::Config>) -> Result<Template, Status> {
#[post("/register")]
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
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 {
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 {
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
@@ -84,16 +94,16 @@ async fn form_submit(form: Form<RegisterForm>, config: &State<crate::common::cli
actual_email = None;
} else {
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
.map_err(|e| {
log::error!("Failed to check if user email {} exists: {}", email, e);
Status { code: 500 }
actix_web::error::ErrorInternalServerError(e)
})?;
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());
}
@@ -109,19 +119,19 @@ async fn form_submit(form: Form<RegisterForm>, config: &State<crate::common::cli
} else {
let steam_id = match steam_id.parse() {
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 {
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
.map_err(|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 {
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);
}
@@ -131,25 +141,25 @@ async fn form_submit(form: Form<RegisterForm>, config: &State<crate::common::cli
// username validation
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 {
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()) {
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
.map_err(|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 {
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(),
password: form.password.clone(),
email: actual_email,
@@ -157,40 +167,31 @@ async fn form_submit(form: Form<RegisterForm>, config: &State<crate::common::cli
}).await {
Ok(id) => id,
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(),
id: user_id,
version: version_string(),
source_url: env!("CARGO_PKG_REPOSITORY").to_owned(),
}))
}).unwrap()))
}
#[get("/register")]
async fn form_load() -> Template {
pub async fn form_load(handlebars_ref: Data<handlebars::Handlebars<'_>>) -> Html {
registration_ok(RegisterForm {
display_name: "".to_owned(),
password: "".to_owned(),
password_c: "".to_owned(),
email: None,
steam_id: None,
})
}, &*handlebars_ref)
}
#[get("/robocraft/favicon")]
pub async fn favicon(config: &State<crate::common::cli::Config>) -> Result<rocket::fs::NamedFile, std::io::Error> {
let path = config.robocraft.assets.join("favicon.jpg");
rocket::fs::NamedFile::open(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())
})
pub async fn favicon(config: Data<super::RcConfig>) -> impl Responder {
let path = config.assets.join("favicon.jpg");
actix_files::NamedFile::open_async(path).await
}

View 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))
}

View File

@@ -1,8 +1,8 @@
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>")]
pub async fn user_password_auth(body: Json<libfj::robocraft::EmailUserAuthenticationPayload>, config: &State<crate::common::cli::Config>) -> Result<Json<libfj::robocraft::AuthenticationResponseInfo>, Status> {
#[post("/authenticate/robocraft/game")]
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);
let payload = libfj::robocraft::TokenPayload {
public_id: body.display_name.clone(),
@@ -16,16 +16,10 @@ pub async fn user_password_auth(body: Json<libfj::robocraft::EmailUserAuthentica
payload,
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| {
log::error!("Failed to authenticate {} user {}: {}", body.target, body.display_name, e);
Status { code: 401 }
log::error!("Failed to authenticate {} user {}: {}", body.target, body.display_name, e.message);
super::ErrorTy::from_err(e)
})?;
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])
})
}

View File

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

View File

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

View File

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

View File

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

View File

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