feat: improved access token security (JWT sorta)
This commit is contained in:
parent
3a815d8536
commit
fc22ed413f
4 changed files with 84 additions and 9 deletions
|
@ -14,6 +14,10 @@ reqwest = { version = "0.11", features = ["json"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
|
rand = { version = "0.8", features = ["getrandom"]}
|
||||||
uuid = "1"
|
uuid = "1"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
jwt = "0.16"
|
||||||
|
sha2 = "0.10"
|
||||||
|
hmac = "0.12"
|
||||||
|
|
13
src/main.rs
13
src/main.rs
|
@ -1,6 +1,7 @@
|
||||||
mod proto;
|
mod proto;
|
||||||
mod entities;
|
mod entities;
|
||||||
mod routes;
|
mod routes;
|
||||||
|
mod persistence;
|
||||||
|
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
|
@ -14,10 +15,10 @@ use uuid::Uuid;
|
||||||
use tracing_subscriber::prelude::*;
|
use tracing_subscriber::prelude::*;
|
||||||
use tracing::{info, metadata::LevelFilter};
|
use tracing::{info, metadata::LevelFilter};
|
||||||
|
|
||||||
use crate::routes::{
|
use crate::{routes::{
|
||||||
auth::{authenticate, validate, refresh},
|
auth::{authenticate, validate, refresh},
|
||||||
session::{join, has_joined_wrapper, profile}, register::register_unmigrated,
|
session::{join, has_joined_wrapper, profile}, register::register_unmigrated,
|
||||||
};
|
}, persistence::load_secret};
|
||||||
|
|
||||||
/// Reimplementation of legacy auth server for minecraft
|
/// Reimplementation of legacy auth server for minecraft
|
||||||
#[derive(Parser, Debug, Clone)]
|
#[derive(Parser, Debug, Clone)]
|
||||||
|
@ -56,6 +57,7 @@ pub struct AppState {
|
||||||
store: Arc<Mutex<HashMap<Uuid, JoinAttempt>>>,
|
store: Arc<Mutex<HashMap<Uuid, JoinAttempt>>>,
|
||||||
db: DatabaseConnection,
|
db: DatabaseConnection,
|
||||||
cfg: ConfigArgs,
|
cfg: ConfigArgs,
|
||||||
|
secret: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
@ -69,7 +71,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let cfg = ConfigArgs::parse();
|
let cfg = ConfigArgs::parse();
|
||||||
|
|
||||||
let db = Database::connect(cfg.database.clone()).await?;
|
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()?;
|
let addr = cfg.bind_addr.parse()?;
|
||||||
|
|
||||||
|
@ -85,7 +90,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// CUSTOM
|
// CUSTOM
|
||||||
.route("/register/unmigrated", post(register_unmigrated))
|
.route("/register/unmigrated", post(register_unmigrated))
|
||||||
.fallback(fallback_route)
|
.fallback(fallback_route)
|
||||||
.with_state(AppState { store, db, cfg });
|
.with_state(AppState { store, db, cfg, secret });
|
||||||
|
|
||||||
info!(target: "MAIN", "serving Yggdrasil on {}", &addr);
|
info!(target: "MAIN", "serving Yggdrasil on {}", &addr);
|
||||||
|
|
||||||
|
|
52
src/persistence.rs
Normal file
52
src/persistence.rs
Normal file
|
@ -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<String, jwt::Error> {
|
||||||
|
let key: Hmac<Sha384> = 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<String, DbErr> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
use axum::{extract::State, Json, http::StatusCode};
|
use axum::{extract::State, Json, http::StatusCode};
|
||||||
use chrono::Utc;
|
use chrono::{Utc, Duration};
|
||||||
use sea_orm::{EntityTrait, QueryFilter, ColumnTrait, ActiveValue::NotSet, Set};
|
use sea_orm::{EntityTrait, QueryFilter, ColumnTrait, ActiveValue::NotSet, Set};
|
||||||
use uuid::Uuid;
|
|
||||||
use tracing::{info, warn};
|
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<AppState>, Json(payload): Json<proto::ValidateRequest>) -> Result<StatusCode, StatusCode> {
|
pub async fn validate(State(state): State<AppState>, Json(payload): Json<proto::ValidateRequest>) -> Result<StatusCode, StatusCode> {
|
||||||
|
@ -33,9 +33,13 @@ pub async fn refresh(State(state): State<AppState>, Json(payload): Json<proto::R
|
||||||
let user = entities::user::Entity::find_by_id(t.user_id).one(&state.db).await
|
let user = entities::user::Entity::find_by_id(t.user_id).one(&state.db).await
|
||||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, proto::Error::simple("db error").json()))?
|
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, proto::Error::simple("db error").json()))?
|
||||||
.ok_or((StatusCode::NOT_FOUND, proto::Error::simple("no user owns this token").json()))?;
|
.ok_or((StatusCode::NOT_FOUND, proto::Error::simple("no user owns this token").json()))?;
|
||||||
|
|
||||||
entities::token::Entity::delete_by_id(t.id).exec(&state.db).await
|
entities::token::Entity::delete_by_id(t.id).exec(&state.db).await
|
||||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, proto::Error::simple("db error").json()))?;
|
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, proto::Error::simple("db error").json()))?;
|
||||||
let new_access_token = Uuid::new_v4(); // TODO same as with authenticate
|
|
||||||
|
let new_access_token = new_auth_token(state.secret.as_bytes(), vec![("uuid", &user.uuid.to_string())])
|
||||||
|
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, proto::Error::simple("invalid secret").json()))?;
|
||||||
|
|
||||||
entities::token::Entity::insert(
|
entities::token::Entity::insert(
|
||||||
entities::token::ActiveModel{
|
entities::token::ActiveModel{
|
||||||
id: NotSet,
|
id: NotSet,
|
||||||
|
@ -45,12 +49,14 @@ pub async fn refresh(State(state): State<AppState>, Json(payload): Json<proto::R
|
||||||
}
|
}
|
||||||
).exec(&state.db).await
|
).exec(&state.db).await
|
||||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, proto::Error::simple("db error").json()))?;
|
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, proto::Error::simple("db error").json()))?;
|
||||||
|
|
||||||
let response = proto::RefreshResponse {
|
let response = proto::RefreshResponse {
|
||||||
accessToken: new_access_token.to_string(),
|
accessToken: new_access_token.to_string(),
|
||||||
clientToken: payload.clientToken,
|
clientToken: payload.clientToken,
|
||||||
selectedProfile: proto::Profile { id: user.uuid, name: user.name },
|
selectedProfile: proto::Profile { id: user.uuid, name: user.name },
|
||||||
user: None,
|
user: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
info!(target: "AUTH", "[REFRESH] answering with {:?}", response);
|
info!(target: "AUTH", "[REFRESH] answering with {:?}", response);
|
||||||
Ok(Json(response))
|
Ok(Json(response))
|
||||||
} else {
|
} else {
|
||||||
|
@ -70,12 +76,17 @@ pub async fn authenticate(State(state): State<AppState>, Json(payload): Json<pro
|
||||||
let s = entities::property::Entity::find().filter(
|
let s = entities::property::Entity::find().filter(
|
||||||
entities::property::Column::UserId.eq(u.id)
|
entities::property::Column::UserId.eq(u.id)
|
||||||
).one(&state.db).await.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, proto::Error::simple("db error").json()))?;
|
).one(&state.db).await.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, proto::Error::simple("db error").json()))?;
|
||||||
|
|
||||||
let skin = match s {
|
let skin = match s {
|
||||||
Some(s) => proto::Property::from(s),
|
Some(s) => proto::Property::from(s),
|
||||||
None => proto::Property::default_skin(),
|
None => proto::Property::default_skin(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// make new token
|
// 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 {
|
entities::token::Entity::insert(entities::token::ActiveModel {
|
||||||
id: NotSet,
|
id: NotSet,
|
||||||
user_id: Set(u.id),
|
user_id: Set(u.id),
|
||||||
|
@ -83,11 +94,13 @@ pub async fn authenticate(State(state): State<AppState>, Json(payload): Json<pro
|
||||||
created_at: Set(Utc::now()),
|
created_at: Set(Utc::now()),
|
||||||
}).exec(&state.db).await
|
}).exec(&state.db).await
|
||||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, proto::Error::simple("db error").json()))?;
|
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, proto::Error::simple("db error").json()))?;
|
||||||
|
|
||||||
let client_token = payload.clientToken.unwrap_or(Uuid::new_v4().to_string());
|
let client_token = payload.clientToken.unwrap_or(Uuid::new_v4().to_string());
|
||||||
let profile = proto::Profile {
|
let profile = proto::Profile {
|
||||||
name: u.name.clone(),
|
name: u.name.clone(),
|
||||||
id: u.uuid,
|
id: u.uuid,
|
||||||
};
|
};
|
||||||
|
|
||||||
let response =proto::AuthenticateResponse {
|
let response =proto::AuthenticateResponse {
|
||||||
accessToken: access_token,
|
accessToken: access_token,
|
||||||
user: proto::User { id: u.uuid, username: u.name, properties: Some(vec![ skin ]) },
|
user: proto::User { id: u.uuid, username: u.name, properties: Some(vec![ skin ]) },
|
||||||
|
@ -95,6 +108,7 @@ pub async fn authenticate(State(state): State<AppState>, Json(payload): Json<pro
|
||||||
availableProfiles: vec![profile.clone()],
|
availableProfiles: vec![profile.clone()],
|
||||||
selectedProfile: profile,
|
selectedProfile: profile,
|
||||||
};
|
};
|
||||||
|
|
||||||
info!(target: "AUTH", "[AUTHENTICATE] answering with {:?}", response);
|
info!(target: "AUTH", "[AUTHENTICATE] answering with {:?}", response);
|
||||||
Ok(Json(response))
|
Ok(Json(response))
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Reference in a new issue