use std::collections::BTreeMap;
use std::time::{SystemTime, UNIX_EPOCH};
use actix_web::{get, HttpResponse};
use actix_web::web::{Data, Query};
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};
#[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());
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());
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,
}
],
})
}