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<CallbackQueryParams>, state: Data<AppState>) -> 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<String, String> = 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<Sha256> = 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<Sha256> = 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,
}],
})
}