use crate::config::CONFIG; use crate::error::{APIError, APIErrorsResponse}; use crate::AppState; use actix_web::web::{Data, Query}; use actix_web::{get, HttpResponse}; 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 starkingdoms_api_entities::entity; use std::collections::BTreeMap; use std::time::{SystemTime, UNIX_EPOCH}; use ulid::Ulid; #[derive(Deserialize)] pub struct CallbackQueryParams { pub token: String, pub realm: String, } #[get("/callback")] pub async fn callback(query: Query, state: Data) -> HttpResponse { // verify token from auth provider API // 1. load the public key let realm = match CONFIG.realms.get(&query.realm) { Some(r) => r, None => { error!("realm not found"); return HttpResponse::Unauthorized().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_UNKNOWN_REALM".to_string(), message: "Unknown realm".to_string(), path: None, }], }); } }; let rs256_pub_key = PKeyWithDigest { digest: MessageDigest::sha256(), key: PKey::public_key_from_pem(realm.public_key.as_bytes()).unwrap(), }; let token: BTreeMap = match query.token.verify_with_key(&rs256_pub_key) { Ok(tkn) => tkn, Err(e) => { error!("[callback] token verify error: {}", e); return generic_unauthorized(); } }; let realm = match token.get("realm").ok_or(generic_unauthorized()) { Ok(r) => r, Err(e) => return e, }; let realm_local_id = match token.get("realm_native_id").ok_or(generic_unauthorized()) { Ok(r) => r, Err(e) => return e, }; debug!( "got authenticated realm native authorization: authenticated as {}:{}", realm, realm_local_id ); // 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()); claims.insert("nonce", Ulid::new().to_string()); 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()); claims.insert("nonce", Ulid::new().to_string()); 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 { HttpResponse::Unauthorized().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_INVALID_STATE".to_string(), message: "Unknown/invalid login state".to_string(), path: None, }], }) }