From a53b7bca59edbffb01c8b3032adc456a9418fc59 Mon Sep 17 00:00:00 2001 From: c0repwn3r Date: Wed, 19 Apr 2023 13:23:41 -0400 Subject: [PATCH] client-side authentication --- Cargo.lock | 2 + api/Cargo.toml | 4 +- api/src/config.rs | 1 + api/src/routes/callback.rs | 97 ++++++++++++++++++++++++++++++++- api/templates/select_realm.tera | 2 +- client/index.html | 7 +++ client/src/index.ts | 11 +++- 7 files changed, 117 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e1eab354d8e5b54e6b86ec7ae8ee3826f07e7712..adc0a457df96acafaf6841329d4b3964bd66f52d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3325,6 +3325,7 @@ dependencies = [ "actix-files", "actix-request-identifier", "actix-web", + "hmac", "jwt", "log", "once_cell", @@ -3332,6 +3333,7 @@ dependencies = [ "reqwest", "sea-orm", "serde", + "sha2", "simple_logger", "starkingdoms_api_entities", "starkingdoms_api_migration", diff --git a/api/Cargo.toml b/api/Cargo.toml index a2f6e761431c393e35a4d81fc296ae2f404f91e0..9087beb92cb626c0d0c58d8b6726aa3c6e3f8c39 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -28,4 +28,6 @@ tera = "1" # Templates jwt = { version = "0.16", features = ["openssl"] } # Auth openssl = "0.10" # Auth -reqwest = "0.11" # Auth \ No newline at end of file +reqwest = "0.11" # Auth +hmac = "0.12.1" # Auth +sha2 = "0.10.6" # Auth \ No newline at end of file diff --git a/api/src/config.rs b/api/src/config.rs index 1fce88287c1d26c38934b65814b153129aee777d..33b7b61676e3a93bb32cfc6daf746dd7b3e63e75 100644 --- a/api/src/config.rs +++ b/api/src/config.rs @@ -28,6 +28,7 @@ pub struct StarkingdomsApiConfig { pub database: StarkingdomsApiConfigDatabase, pub server: StarkingdomsApiConfigServer, pub internal_tokens: Vec, + pub jwt_signing_secret: String, pub base: String, pub game: String, pub realms: HashMap diff --git a/api/src/routes/callback.rs b/api/src/routes/callback.rs index 82e595972b5a20a32faae21c2da935e2e72bedca..55191ca8e8347af4b3c38c6cfa7e7ed6b45fcc20 100644 --- a/api/src/routes/callback.rs +++ b/api/src/routes/callback.rs @@ -1,11 +1,18 @@ use std::collections::BTreeMap; +use std::time::{SystemTime, UNIX_EPOCH}; use actix_web::{get, HttpResponse}; use actix_web::web::{Data, Query}; -use jwt::{PKeyWithDigest, VerifyWithKey}; +use hmac::digest::KeyInit; +use hmac::Hmac; +use jwt::{PKeyWithDigest, SignWithKey, VerifyWithKey}; use log::{debug, error}; use openssl::hash::MessageDigest; use openssl::pkey::PKey; +use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter}; use serde::Deserialize; +use sha2::Sha256; +use ulid::Ulid; +use starkingdoms_api_entities::entity; use crate::AppState; use crate::config::CONFIG; use crate::error::{APIError, APIErrorsResponse}; @@ -61,7 +68,93 @@ pub async fn callback(query: Query, state: Data) debug!("got authenticated realm native authorization: authenticated as {}:{}", realm, realm_local_id); - HttpResponse::Ok().finish() + // see if a user on this realm already exists + let maybe_user = match entity::user_auth_realm::Entity::find() + .filter( + entity::user_auth_realm::Column::RealmNativeId.eq(realm_local_id.clone()) + ) + .filter( + entity::user_auth_realm::Column::Realm.eq(realm.clone()) + ).one(&state.conn).await { + Ok(r) => r, + Err(e) => { + error!("database error: {}", e); + return HttpResponse::InternalServerError().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_DB_ERROR".to_string(), + message: "Database error".to_string(), + path: None, + } + ], + }) + } + }; + + if let Some(user) = maybe_user { + let key: Hmac = Hmac::new_from_slice(CONFIG.jwt_signing_secret.as_bytes()).unwrap(); + let mut claims = BTreeMap::new(); + claims.insert("user", user.id.clone()); + let token_str = claims.sign_with_key(&key).unwrap(); + let auth_url = format!("{}/?token={}&user={}", CONFIG.game, token_str, user.id); + return HttpResponse::Found().append_header(("Location", auth_url)).finish(); + } + + // create the user + let new_user = entity::user::Model { + id: format!("user-{}", Ulid::new().to_string()), + username: Ulid::new().to_string(), + created_on: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64, + }; + let new_user_realm = entity::user_auth_realm::Model { + id: format!("{}-{}:{}", new_user.id.clone(), realm.clone(), realm_local_id.clone()), + realm: realm.clone(), + realm_native_id: realm_local_id.clone(), + user: new_user.id.clone(), + }; + + let key: Hmac = Hmac::new_from_slice(CONFIG.jwt_signing_secret.as_bytes()).unwrap(); + let mut claims = BTreeMap::new(); + claims.insert("user", new_user.id.clone()); + let token_str = claims.sign_with_key(&key).unwrap(); + let auth_url = format!("{}/?token={}&user={}", CONFIG.game, token_str, new_user.id.clone()); + + let new_user_active_model = new_user.into_active_model(); + let new_user_realm_active_model = new_user_realm.into_active_model(); + + match new_user_active_model.insert(&state.conn).await { + Ok(_) => (), + Err(e) => { + error!("database error: {}", e); + return HttpResponse::InternalServerError().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_DB_ERROR".to_string(), + message: "Database error".to_string(), + path: None, + } + ], + }) + } + } + + match new_user_realm_active_model.insert(&state.conn).await { + Ok(_) => (), + Err(e) => { + error!("database error: {}", e); + return HttpResponse::InternalServerError().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_DB_ERROR".to_string(), + message: "Database error".to_string(), + path: None, + } + ], + }) + } + } + + HttpResponse::Found().append_header(("Location", auth_url)).finish() } fn generic_unauthorized() -> HttpResponse { diff --git a/api/templates/select_realm.tera b/api/templates/select_realm.tera index 17d213b47006f61f9186404e0a4976a726e40454..c534cc1be99ea652500aa13a505beb33017dac79 100644 --- a/api/templates/select_realm.tera +++ b/api/templates/select_realm.tera @@ -7,5 +7,5 @@ {% for realm_id, realm in realms %} {{ realm_id }} {% endfor %} -

Selecting an authentication provider will redirect you to our trusted partner e3team's website for login. You will be returned to StarKingdoms when finished.

+

Selecting an authentication provider will redirect you to our parent e3team's website for login. You will be returned to StarKingdoms when finished.

{% endblock body %} \ No newline at end of file diff --git a/client/index.html b/client/index.html index 65c6ccadd6ac651a2d5fa5a45e3125eec0d46b57..17f1328b73a1e3010df3925bb1603a2bbc33f48c 100644 --- a/client/index.html +++ b/client/index.html @@ -53,6 +53,13 @@ for (let i = 0; i < servers.length; i++) { load_server(servers[i]); } + + let query = new URLSearchParams(window.location.search); + + if (query.has("token") && query.has("user")) { + window.localStorage.setItem("token", query.get("token")); + window.localStorage.setItem("user", query.get("user")); + } diff --git a/client/src/index.ts b/client/src/index.ts index ea2e1c60d552fbfc2ec79dae9be4b069b2a169f4..6582b53ff1c24b52c5b83809511a1d58ed2a5502 100644 --- a/client/src/index.ts +++ b/client/src/index.ts @@ -18,7 +18,8 @@ export interface GlobalData { spritesheet: object | null, context: CanvasRenderingContext2D, keys: Keys, - velocity: number + velocity: number, + can_beam_out: boolean } export interface Keys { @@ -43,12 +44,17 @@ export const global: GlobalData = { left: false, right: false }, - velocity: 0 + velocity: 0, + can_beam_out: false } async function client_main(server: string, username: string, texture_quality: string) { logger.info("StarKingdoms client - starting"); + if (window.localStorage.getItem("token") !== null && window.localStorage.getItem("user") !== null) { + global.can_beam_out = true; + } + logger.info("Loading textures"); let spritesheet_url = `/assets/dist/spritesheet-${texture_quality}.png`; let spritesheet_data_url = `/assets/dist/spritesheet-${texture_quality}.json`; @@ -224,7 +230,6 @@ if (!(query.has("server") || query.has("username") || query.has("textures"))) { window.location.href = "/index.html"; } - client_main(query.get("server")!, query.get("username")!, query.get("textures")!).then(() => {}); function planet_type_to_tex_id(ty: PlanetType): string {