From fc22ed413f1e7e45b2dead5623064c7153002262 Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 6 Mar 2023 18:52:23 +0100 Subject: [PATCH] feat: improved access token security (JWT sorta) --- Cargo.toml | 4 ++++ src/main.rs | 13 ++++++++---- src/persistence.rs | 52 ++++++++++++++++++++++++++++++++++++++++++++++ src/routes/auth.rs | 24 ++++++++++++++++----- 4 files changed, 84 insertions(+), 9 deletions(-) create mode 100644 src/persistence.rs diff --git a/Cargo.toml b/Cargo.toml index ef0c485..de5ab66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,10 @@ reqwest = { version = "0.11", features = ["json"] } serde = { version = "1", features = ["derive"] } serde_json = "1" tracing-subscriber = "0.3" +rand = { version = "0.8", features = ["getrandom"]} uuid = "1" chrono = "0.4" tracing = "0.1" +jwt = "0.16" +sha2 = "0.10" +hmac = "0.12" diff --git a/src/main.rs b/src/main.rs index bd04cea..ed9a102 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod proto; mod entities; mod routes; +mod persistence; use std::{collections::HashMap, sync::Arc}; @@ -14,10 +15,10 @@ use uuid::Uuid; use tracing_subscriber::prelude::*; use tracing::{info, metadata::LevelFilter}; -use crate::routes::{ +use crate::{routes::{ auth::{authenticate, validate, refresh}, session::{join, has_joined_wrapper, profile}, register::register_unmigrated, -}; +}, persistence::load_secret}; /// Reimplementation of legacy auth server for minecraft #[derive(Parser, Debug, Clone)] @@ -56,6 +57,7 @@ pub struct AppState { store: Arc>>, db: DatabaseConnection, cfg: ConfigArgs, + secret: String, } #[tokio::main] @@ -69,7 +71,10 @@ async fn main() -> Result<(), Box> { let cfg = ConfigArgs::parse(); let db = Database::connect(cfg.database.clone()).await?; - let store = Arc::new(Mutex::new(HashMap::new())); + + let secret = load_secret(&db).await?; + + let store = Arc::new(Mutex::new(HashMap::new())); // TODO do this as an Actor let addr = cfg.bind_addr.parse()?; @@ -85,7 +90,7 @@ async fn main() -> Result<(), Box> { // CUSTOM .route("/register/unmigrated", post(register_unmigrated)) .fallback(fallback_route) - .with_state(AppState { store, db, cfg }); + .with_state(AppState { store, db, cfg, secret }); info!(target: "MAIN", "serving Yggdrasil on {}", &addr); diff --git a/src/persistence.rs b/src/persistence.rs new file mode 100644 index 0000000..603e8a2 --- /dev/null +++ b/src/persistence.rs @@ -0,0 +1,52 @@ +use chrono::Utc; +use hmac::{Hmac, Mac}; +use jwt::SignWithKey; +use rand::{rngs::OsRng, Rng, distributions::Alphanumeric}; +use sea_orm::{EntityTrait, DatabaseConnection, ActiveValue::NotSet, Set, DbErr}; +use sha2::Sha384; +use tracing::info; +use std::collections::BTreeMap; + +use crate::entities; + +/// Since we check against our db for each validate request, there's no real use for a JWT here. It +/// is still used to allow easier eventual future migration, and to make tokens look like true old +/// Mojang tokens +pub fn new_auth_token(secret: &[u8], fields: Vec<(&str, &str)>) -> Result { + let key: Hmac = Hmac::new_from_slice(secret)?; + let mut claims : BTreeMap<&str, &str> = BTreeMap::new(); + for (key, value) in fields { + claims.insert(key, value); + } + let token = claims.sign_with_key(&key)?; + Ok(token) +} + +pub async fn load_secret(db: &DatabaseConnection) -> Result { + let secret; + + if let Some(state) = entities::persistence::Entity::find().one(db).await? { + secret = state.secret; + } else { + info!(target: "SECRET", "generating new secret"); + secret = OsRng::default() + .sample_iter(&Alphanumeric) + .take(64) + .map(char::from) + .collect(); + + entities::persistence::Entity::delete_many().exec(db).await?; + + entities::persistence::Entity::insert( + entities::persistence::ActiveModel { + id: NotSet, + secret: Set(secret.clone()), + last_edit: Set(Utc::now()), + } + ).exec(db).await?; + } + + Ok(secret) +} + + diff --git a/src/routes/auth.rs b/src/routes/auth.rs index 38a397b..7922c39 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -1,10 +1,10 @@ use axum::{extract::State, Json, http::StatusCode}; -use chrono::Utc; +use chrono::{Utc, Duration}; use sea_orm::{EntityTrait, QueryFilter, ColumnTrait, ActiveValue::NotSet, Set}; -use uuid::Uuid; use tracing::{info, warn}; +use uuid::Uuid; -use crate::{entities, AppState, proto}; +use crate::{entities, AppState, proto, persistence::new_auth_token}; pub async fn validate(State(state): State, Json(payload): Json) -> Result { @@ -33,9 +33,13 @@ pub async fn refresh(State(state): State, Json(payload): Json, Json(payload): Json, Json(payload): Json proto::Property::from(s), None => proto::Property::default_skin(), }; + // make new token - let access_token = Uuid::new_v4().to_string(); // TODO maybe use a JWT? + + let access_token = new_auth_token(state.secret.as_bytes(), vec![("uuid", &u.uuid.to_string())]) + .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, proto::Error::simple("invalid secret").json()))?; + entities::token::Entity::insert(entities::token::ActiveModel { id: NotSet, user_id: Set(u.id), @@ -83,11 +94,13 @@ pub async fn authenticate(State(state): State, Json(payload): Json, Json(payload): Json