new create user account (WIP)

This commit is contained in:
Brandon Thomas committed 2024-09-20 03:32:00 -04:00
1 parent 47ffa5696a
commit bf538b55d1
8 files changed
+382 -18

No files matched your search

Generated
+1
View File
@@ -7162,6 +7162,7 @@ dependencies = [
"futures 0.3.29",
"futures-core",
"futures-util",
"google_sign_in",
"hashing",
"hostname 0.4.0",
"http_server_common",
@@ -1,3 +1,4 @@
use std::collections::HashSet;
use crate::certs::key_map::KeyMap;
use crate::claims::claims::Claims;
use crate::claims::google_custom_claims::GoogleCustomClaims;
@@ -6,27 +7,10 @@ use errors::{anyhow, AnyhowResult};
use jwt_simple::algorithms::RS256PublicKey;
use jwt_simple::algorithms::RSAPublicKeyLike;
use jwt_simple::common::VerificationOptions;
/*
Example payload:
iss https://accounts.google.com
azp 788843034237-uqcg8tbgofrcf1to37e1bqphd924jaf6.apps.googleusercontent.com
aud 788843034237-uqcg8tbgofrcf1to37e1bqphd924jaf6.apps.googleusercontent.com
sub 113101967612396793777
email vocodes2020@gmail.com
email_verified true
nbf 1726786100
name Vocodes Vocodes
picture https://lh3.googleusercontent.com/a/ACg8ocLz2-2OaAm0MQxR6j8CNr-Po8_Xr-aryATiCn4c0i_TuDmL_g=s96-c
given_name Vocodes
family_name Vocodes
iat 1726786400
exp 1726790000
jti 4d44eeac06ce79fc0ab2270cfeea30d8acf77613
*/
/// Decode a Google Sign In JWT.
/// Verification options can be supplied to increase clock skew tolerance, etc.
pub fn decode_and_verify_token_claims(keys: &KeyMap, token: &str, options: Option<VerificationOptions>) -> AnyhowResult<Claims> {
pub fn decode_and_verify_token_claims(keys: &KeyMap, token: &str, mut options: Option<VerificationOptions>) -> AnyhowResult<Claims> {
let header = decode_jwt_header(token)?;
let key_id = header.kid.as_deref()
@@ -36,6 +20,16 @@ pub fn decode_and_verify_token_claims(keys: &KeyMap, token: &str, options: Optio
let key = keys.get(key_id)
.ok_or_else(|| anyhow!("Key not found"))?;
if options.is_none() {
options = Some(VerificationOptions {
allowed_issuers: Some(HashSet::from([
"https://accounts.google.com".to_string(),
"accounts.google.com".to_string(),
])),
..Default::default()
});
}
decode_and_verify_token_claims_with_key(key, token, options)
}
@@ -64,6 +58,24 @@ mod tests {
use std::fs::read_to_string;
use testing::test_file_path::test_file_path;
/*
Example payload:
iss https://accounts.google.com
azp 788843034237-uqcg8tbgofrcf1to37e1bqphd924jaf6.apps.googleusercontent.com
aud 788843034237-uqcg8tbgofrcf1to37e1bqphd924jaf6.apps.googleusercontent.com
sub 113101967612396793777
email vocodes2020@gmail.com
email_verified true
nbf 1726786100
name Vocodes Vocodes
picture https://lh3.googleusercontent.com/a/ACg8ocLz2-2OaAm0MQxR6j8CNr-Po8_Xr-aryATiCn4c0i_TuDmL_g=s96-c
given_name Vocodes
family_name Vocodes
iat 1726786400
exp 1726790000
jti 4d44eeac06ce79fc0ab2270cfeea30d8acf77613
*/
#[test]
fn test_decode() {
let jwk_file = test_file_path("test_data/crypto/google_signin.jwk").unwrap();
@@ -25,3 +25,5 @@ pub mod download_certs;
pub(crate) mod jwt;
// Export for external users
pub use jwt_simple::common::VerificationOptions;
@@ -45,6 +45,7 @@ enums = { path = "../../../_types/enums" }
enums_public = { path = "../../../_types/enums_public" }
errors = { path = "../../../lib/errors" }
filesys = { path = "../../../lib/files/filesys" }
google_sign_in = { path = "../../../lib/api_clients/google_sign_in" }
hashing = { path = "../../../lib/files/hashing" }
http_server_common = { path = "../../../lib/deprecated/http_server_common" }
idempotency = { path = "../../../lib/idempotency" }
@@ -132,6 +132,7 @@ use crate::http_server::endpoints::users::get_profile_handler::*;
use crate::http_server::endpoints::users::login_handler::*;
use crate::http_server::endpoints::users::logout_handler::*;
use crate::http_server::endpoints::users::session_info_handler::*;
use crate::http_server::endpoints::users::create_account_from_google_sign_in_handler::*;
use crate::http_server::endpoints::users::session_token_info_handler::*;
use crate::http_server::endpoints::voice_conversion::enqueue_voice_conversion_inference_handler::*;
use crate::http_server::endpoints::voice_designer::inference::enqueue_tts_request::*;
@@ -188,6 +189,7 @@ use crate::http_server::web_utils::response_success_helpers::*;
crate::http_server::endpoints::media_files::get::get_media_file_handler::get_media_file_handler,
crate::http_server::endpoints::media_files::list::list_featured_media_files_handler::list_featured_media_files_handler,
crate::http_server::endpoints::media_files::list::list_media_files_by_batch_token_handler::list_media_files_by_batch_token_handler,
crate::http_server::endpoints::users::create_account_from_google_sign_in_handler::create_account_from_google_sign_in_handler,
crate::http_server::endpoints::media_files::list::list_media_files_for_user_handler::list_media_files_for_user_handler,
crate::http_server::endpoints::media_files::list::list_media_files_handler::list_media_files_handler,
crate::http_server::endpoints::media_files::upload::upload_studio_shot::upload_studio_shot_media_file_handler::upload_studio_shot_media_file_handler,
@@ -348,6 +350,9 @@ use crate::http_server::web_utils::response_success_helpers::*;
ByQueueStats,
ChangeMediaFileAnimationTypeError,
ChangeMediaFileAnimationTypeRequest,
GoogleCreateAccountRequest,
GoogleCreateAccountErrorResponse,
GoogleCreateAccountSuccessResponse,
ChangeMediaFileEngineCategoryError,
ChangeMediaFileEngineCategoryRequest,
ChangeMediaFileVisibilityError,
@@ -0,0 +1,336 @@
// NB: Incrementally getting rid of build warnings...
//#![forbid(unused_imports)]
//#![forbid(unused_mut)]
//#![forbid(unused_variables)]
use std::collections::HashMap;
use std::fmt;
use std::fmt::Formatter;
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::{web, HttpRequest, HttpResponse};
use log::{info, warn};
use sqlx::MySqlPool;
use utoipa::ToSchema;
use actix_helpers::extractors::get_request_origin_uri::get_request_origin_uri;
use google_sign_in::decode_and_verify_token_claims::decode_and_verify_token_claims;
use google_sign_in::download_certs::download_cert_key_set;
use google_sign_in::VerificationOptions;
use http_server_common::request::get_request_ip::get_request_ip;
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
use mysql_queries::mediators::firehose_publisher::FirehosePublisher;
use mysql_queries::queries::users::user::create_account::{create_account, CreateAccountArgs, CreateAccountError};
use mysql_queries::queries::users::user_sessions::create_user_session::create_user_session;
use password::bcrypt_hash_password::bcrypt_hash_password;
use tokens::tokens::user_sessions::UserSessionToken;
use user_input_common::check_for_slurs::contains_slurs;
use crate::http_server::session::http::http_user_session_manager::HttpUserSessionManager;
use crate::http_server::validations::is_reserved_username::is_reserved_username;
use crate::http_server::validations::validate_passwords::validate_passwords;
use crate::http_server::validations::validate_username::validate_username;
use crate::util::email_to_gravatar::email_to_gravatar;
use crate::util::enroll_in_studio::enroll_in_studio;
use crate::util::generate_random_username::generate_random_username;
#[derive(ToSchema, Deserialize)]
pub struct GoogleCreateAccountRequest {
pub google_credential: String,
}
#[derive(ToSchema, Serialize)]
pub struct GoogleCreateAccountSuccessResponse {
pub success: bool,
/// A signed session that can be sent as a header, bypassing cookies.
/// This is useful for API clients that don't support cookies or Google
/// browsers killing cross-domain cookies.
pub signed_session: String,
/// Tell the user what the automatically created display name was in case
/// they want to rename their account.
pub automatic_user_display_name: String,
}
#[derive(ToSchema, Serialize, Debug)]
pub struct GoogleCreateAccountErrorResponse {
pub success: bool,
pub error_type: GoogleCreateAccountErrorType,
pub error_fields: HashMap<String, String>,
}
#[derive(ToSchema, Copy, Clone, Debug, Serialize)]
pub enum GoogleCreateAccountErrorType {
BadRequest, // Other request malformed errors, eg. bad Origin header
BadInput,
EmailTaken,
ServerError,
UsernameReserved,
UsernameTaken,
}
impl GoogleCreateAccountErrorResponse {
fn server_error() -> Self {
Self {
success: false,
error_type: GoogleCreateAccountErrorType::ServerError,
error_fields: HashMap::new(),
}
}
fn bad_request() -> Self {
Self {
success: false,
error_type: GoogleCreateAccountErrorType::BadRequest,
error_fields: HashMap::new(),
}
}
}
// NB: Not using DeriveMore since Clion doesn't understand it.
impl fmt::Display for GoogleCreateAccountErrorResponse {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self.error_type)
}
}
impl ResponseError for GoogleCreateAccountErrorResponse {
fn status_code(&self) -> StatusCode {
match self.error_type {
GoogleCreateAccountErrorType::BadRequest => StatusCode::BAD_REQUEST,
GoogleCreateAccountErrorType::BadInput => StatusCode::BAD_REQUEST,
GoogleCreateAccountErrorType::EmailTaken => StatusCode::BAD_REQUEST,
GoogleCreateAccountErrorType::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
GoogleCreateAccountErrorType::UsernameReserved => StatusCode::BAD_REQUEST,
GoogleCreateAccountErrorType::UsernameTaken => StatusCode::BAD_REQUEST,
}
}
fn error_response(&self) -> HttpResponse {
serialize_as_json_error(self)
}
}
/// Create a new account from "Sign in with Google" credentials.
#[utoipa::path(
post,
tag = "Users",
path = "/v1/accounts/google_create",
responses(
(status = 200, description = "Success", body = GoogleCreateAccountSuccessResponse),
(status = 400, description = "Bad input", body = GoogleCreateAccountErrorResponse),
(status = 401, description = "Not authorized", body = GoogleCreateAccountErrorResponse),
(status = 500, description = "Server error", body = GoogleCreateAccountErrorResponse),
),
params(
("request" = GoogleCreateAccountRequest, description = "Payload for Request"),
)
)]
pub async fn create_account_from_google_sign_in_handler(
http_request: HttpRequest,
request: web::Json<GoogleCreateAccountRequest>,
mysql_pool: web::Data<MySqlPool>,
session_cookie_manager: web::Data<HttpUserSessionManager>,
) -> Result<HttpResponse, GoogleCreateAccountErrorResponse>
{
// TODO(bt,2024-09-20): Cache certs in shared server state
let keys = download_cert_key_set()
.await
.map_err(|e| {
warn!("error downloading google certs: {:?}", e);
GoogleCreateAccountErrorResponse::server_error()
})?;
let claims = decode_and_verify_token_claims(&keys, &request.google_credential, None)
.map_err(|e| {
warn!("error decoding google token claims: {:?}", e);
GoogleCreateAccountErrorResponse::bad_request()
})?;
info!("Google JWT credential claims: email {:?}, verified: {}",
claims.email(),
claims.email_verified());
let display_name = generate_random_username();
let username = display_name.trim().to_lowercase();
info!("generated username: {}", username);
/*
let mut error_fields = HashMap::new();
if let Err(reason) = validate_username(&request.username) {
error_fields.insert("username".to_string(), reason);
}
if let Err(reason) = validate_passwords(&request.password, &request.password_confirmation) {
error_fields.insert("password".to_string(), reason);
}
if contains_slurs(&request.username) {
error_fields.insert("username".to_string(), "username contains slurs".to_string());
}
if !request.email_address.contains("@") {
error_fields.insert("email_address".to_string(), "invalid email address".to_string());
}
if is_reserved_username(&request.username) {
error_fields.insert("username".to_string(), "username is reserved".to_string());
return Err(GoogleCreateAccountErrorResponse {
success: false,
error_type: GoogleCreateAccountErrorType::UsernameReserved,
error_fields
});
}
if !error_fields.is_empty() {
return Err(GoogleCreateAccountErrorResponse {
success: false,
error_type: GoogleCreateAccountErrorType::BadInput,
error_fields
});
}
let password_hash = match bcrypt_hash_password(request.password.clone()) {
Ok(hash) => hash,
Err(err) => {
warn!("Bcrypt error: {:?}", err);
return Err(GoogleCreateAccountErrorResponse::server_error());
}
};
let username = request.username.trim().to_lowercase();
let display_name = request.username.trim().to_string();
let email_address = request.email_address.trim().to_lowercase();
let email_gravatar_hash = email_to_gravatar(&email_address);
let ip_address = get_request_ip(&http_request);
let maybe_origin = get_request_origin_uri(&http_request);
let mut maybe_source = None;
match maybe_origin {
Ok(Some(uri)) => {
if let Some(host) = uri.host() {
if host.contains("storyteller") {
maybe_source = Some("storyteller");
} else if host.contains("fakeyou") {
maybe_source = Some("fakeyou");
}
}
}
// Fail open for now.
Ok(None) => {}
Err(err) => {
warn!("Origin header error: {:?}", err);
}
}
let create_account_result = create_account(
&mysql_pool,
CreateAccountArgs {
username: &username,
display_name: &display_name,
email_address: &email_address,
email_gravatar_hash: &email_gravatar_hash,
password_hash: &password_hash,
ip_address: &ip_address,
maybe_source,
maybe_user_token: None, // NB: This parameter is for internal testing only
}
).await;
let new_user_data = match create_account_result {
Ok(success) => success,
Err(err) => {
let mut error_fields = HashMap::new();
let error_type;
match err {
CreateAccountError::EmailIsTaken => {
error_type = GoogleCreateAccountErrorType::EmailTaken;
error_fields.insert("email_address".to_string(), "email is taken".to_string());
}
CreateAccountError::UsernameIsTaken => {
error_type = GoogleCreateAccountErrorType::UsernameTaken;
error_fields.insert("username".to_string(), "username is taken".to_string());
}
CreateAccountError::DatabaseError | CreateAccountError::OtherError => {
error_type = GoogleCreateAccountErrorType::ServerError;
}
}
return Err(GoogleCreateAccountErrorResponse {
success: false,
error_type,
error_fields
});
}
};
info!("new user id: {}", new_user_data.user_id);
let create_session_result = create_user_session(
new_user_data.user_token.as_str(),
&ip_address,
&mysql_pool
).await;
let session_token = match create_session_result {
Ok(token) => token,
Err(e) => {
warn!("create account session creation error : {:?}", e);
return Err(GoogleCreateAccountErrorResponse::server_error());
}
};
info!("new user session created");
firehose_publisher.publish_user_sign_up(new_user_data.user_token.as_str())
.await
.map_err(|e| {
warn!("error publishing event: {:?}", e);
GoogleCreateAccountErrorResponse::server_error()
})?;
// NB: Enroll new users in studio for a while.
enroll_in_studio(&new_user_data.user_token, &ip_address, &mysql_pool, None).await
.map_err(|e| {
warn!("error enrolling in studio: {:?}", e);
}).ok();
let session_token = UserSessionToken::new_from_str(&session_token);
let session_cookie = match session_cookie_manager.create_cookie(&session_token, &new_user_data.user_token) {
Ok(cookie) => cookie,
Err(_) => return Err(GoogleCreateAccountErrorResponse::server_error()),
};
let signed_session = match session_cookie_manager.encode_session_payload(&session_token, &new_user_data.user_token) {
Ok(payload) => payload,
Err(_) => return Err(GoogleCreateAccountErrorResponse::server_error()),
};
*/
let response = GoogleCreateAccountSuccessResponse {
success: true,
signed_session: "todo".to_string(),
automatic_user_display_name: display_name,
};
let body = serde_json::to_string(&response)
.map_err(|_e| GoogleCreateAccountErrorResponse::server_error())?;
Ok(HttpResponse::Ok()
// .cookie(session_cookie) // TODO / FIXME
.content_type("application/json")
.body(body))
}
@@ -1,3 +1,4 @@
pub mod create_account_from_google_sign_in_handler;
pub mod create_account_handler;
pub mod edit_profile_handler;
pub mod get_profile_handler;
@@ -6,6 +6,7 @@ use actix_web::error::Error;
use crate::http_server::endpoints::tts::list_user_tts_inference_results::list_user_tts_inference_results_handler;
use crate::http_server::endpoints::tts::list_user_tts_models::list_user_tts_models_handler;
use crate::http_server::endpoints::users::create_account_from_google_sign_in_handler::create_account_from_google_sign_in_handler;
use crate::http_server::endpoints::users::create_account_handler::create_account_handler;
use crate::http_server::endpoints::users::edit_profile_handler::edit_profile_handler;
use crate::http_server::endpoints::users::get_profile_handler::get_profile_handler;
@@ -41,6 +42,11 @@ pub fn add_user_routes<T, B> (app: App<T>) -> App<T>
.route(web::post().to(create_account_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(
web::resource("/v1/accounts/google_create")
.route(web::post().to(create_account_from_google_sign_in_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(
// TODO(bt,2022-11-16): non-/v1/ endpoints are deprecated and subject for future removal
web::resource("/login")