diff --git a/services/identity/src/client_service.rs b/services/identity/src/client_service.rs --- a/services/identity/src/client_service.rs +++ b/services/identity/src/client_service.rs @@ -116,9 +116,10 @@ self.check_username_taken(&message.username).await?; let username_in_reserved_usernames_table = self .client - .username_in_reserved_usernames_table(&message.username) + .get_user_id_from_reserved_usernames_table(&message.username) .await - .map_err(handle_db_error)?; + .map_err(handle_db_error)? + .is_some(); if username_in_reserved_usernames_table { return Err(tonic::Status::already_exists( @@ -187,9 +188,10 @@ let username_in_reserved_usernames_table = self .client - .username_in_reserved_usernames_table(&message.username) + .get_user_id_from_reserved_usernames_table(&message.username) .await - .map_err(handle_db_error)?; + .map_err(handle_db_error)? + .is_some(); if !username_in_reserved_usernames_table { return Err(tonic::Status::permission_denied( tonic_status_messages::USERNAME_NOT_RESERVED, @@ -326,9 +328,10 @@ // service. let username_in_reserved_usernames_table = self .client - .username_in_reserved_usernames_table(&message.username) + .get_user_id_from_reserved_usernames_table(&message.username) .await - .map_err(handle_db_error)?; + .map_err(handle_db_error)? + .is_some(); if username_in_reserved_usernames_table { return Err(tonic::Status::permission_denied( @@ -484,48 +487,77 @@ construct_flattened_device_key_upload(&message)?; let login_time = chrono::Utc::now(); - let Some(user_id) = self + + let user_id = if let Some(user_id) = self .client - .get_user_id_from_user_info(wallet_address.clone(), &AuthType::Wallet) + .get_user_id_from_reserved_usernames_table(&wallet_address) .await .map_err(handle_db_error)? - else { + { // It's possible that the user attempting login is already registered - // on Ashoat's keyserver. If they are, we should send back a gRPC status - // code instructing them to get a signed message from Ashoat's keyserver - // in order to claim their wallet address and register with the Identity - // service. - let username_in_reserved_usernames_table = self + // on Ashoat's keyserver. If they are, we should try to register them if + // they're on a mobile device, otherwise we should send back a gRPC status + // code instructing them to try logging in from a mobile device first. + if platform_metadata.device_type.to_uppercase() != "ANDROID" + && platform_metadata.device_type.to_uppercase() != "IOS" + { + return Err(tonic::Status::permission_denied( + tonic_status_messages::RETRY_FROM_NATIVE, + )); + }; + + let social_proof = + SocialProof::new(message.siwe_message, message.siwe_signature); + + self + .check_device_id_taken(&flattened_device_key_upload, Some(&user_id)) + .await?; + + self .client - .username_in_reserved_usernames_table(&wallet_address) + .add_wallet_user_to_users_table( + flattened_device_key_upload.clone(), + wallet_address.clone(), + social_proof, + Some(user_id.clone()), + platform_metadata, + login_time, + message.farcaster_id, + None, + ) .await .map_err(handle_db_error)?; - if username_in_reserved_usernames_table { - return Err(tonic::Status::permission_denied( - tonic_status_messages::NEED_KEYSERVER_MESSAGE_TO_CLAIM_USERNAME, + user_id + } else { + let Some(user_id) = self + .client + .get_user_id_from_user_info(wallet_address.clone(), &AuthType::Wallet) + .await + .map_err(handle_db_error)? + else { + return Err(tonic::Status::not_found( + tonic_status_messages::USER_NOT_FOUND, )); - } + }; - return Err(tonic::Status::not_found( - tonic_status_messages::USER_NOT_FOUND, - )); - }; + self + .check_device_id_taken(&flattened_device_key_upload, Some(&user_id)) + .await?; - self - .check_device_id_taken(&flattened_device_key_upload, Some(&user_id)) - .await?; + self + .client + .add_user_device( + user_id.clone(), + flattened_device_key_upload.clone(), + platform_metadata, + chrono::Utc::now(), + ) + .await + .map_err(handle_db_error)?; - self - .client - .add_user_device( - user_id.clone(), - flattened_device_key_upload.clone(), - platform_metadata, - chrono::Utc::now(), - ) - .await - .map_err(handle_db_error)?; + user_id + }; // Create access token let token = AccessTokenData::with_created_time( @@ -565,39 +597,17 @@ &message.siwe_signature, )?; - match self - .client - .get_nonce_from_nonces_table(&parsed_message.nonce) - .await - .map_err(handle_db_error)? - { - None => { - return Err(tonic::Status::invalid_argument( - tonic_status_messages::INVALID_NONCE, - )) - } - Some(nonce) if nonce.is_expired() => { - // we don't need to remove the nonce from the table here - // because the DynamoDB TTL will take care of it - return Err(tonic::Status::aborted( - tonic_status_messages::NONCE_EXPIRED, - )); - } - Some(_) => self - .client - .remove_nonce_from_nonces_table(&parsed_message.nonce) - .await - .map_err(handle_db_error)?, - }; + self.verify_and_remove_nonce(&parsed_message.nonce).await?; let wallet_address = eip55(&parsed_message.address); self.check_wallet_address_taken(&wallet_address).await?; let username_in_reserved_usernames_table = self .client - .username_in_reserved_usernames_table(&wallet_address) + .get_user_id_from_reserved_usernames_table(&wallet_address) .await - .map_err(handle_db_error)?; + .map_err(handle_db_error)? + .is_some(); if username_in_reserved_usernames_table { return Err(tonic::Status::already_exists( @@ -680,22 +690,16 @@ self.check_wallet_address_taken(&wallet_address).await?; - let wallet_address_in_reserved_usernames_table = self + let maybe_user_id = self .client - .username_in_reserved_usernames_table(&wallet_address) + .get_user_id_from_reserved_usernames_table(&wallet_address) .await .map_err(handle_db_error)?; - if !wallet_address_in_reserved_usernames_table { + let Some(user_id) = maybe_user_id else { return Err(tonic::Status::permission_denied( tonic_status_messages::WALLET_ADDRESS_NOT_RESERVED, )); - } - - let user_id = validate_account_ownership_message_and_get_user_id( - &wallet_address, - &message.keyserver_message, - &message.keyserver_signature, - )?; + }; let flattened_device_key_upload = construct_flattened_device_key_upload(&message)?; @@ -1015,15 +1019,17 @@ Some(Identifier::WalletAddress(address)) => (address, AuthType::Wallet), }; - let (is_reserved_result, user_id_result) = tokio::join!( + let (get_user_id_from_reserved_usernames_table_result, user_id_result) = tokio::join!( self .client - .username_in_reserved_usernames_table(&user_ident), + .get_user_id_from_reserved_usernames_table(&user_ident), self .client .get_user_id_from_user_info(user_ident.clone(), &auth_type), ); - let is_reserved = is_reserved_result.map_err(handle_db_error)?; + let is_reserved = get_user_id_from_reserved_usernames_table_result + .map_err(handle_db_error)? + .is_some(); let user_id = user_id_result.map_err(handle_db_error)?; Ok(Response::new(FindUserIdResponse { diff --git a/services/identity/src/constants.rs b/services/identity/src/constants.rs --- a/services/identity/src/constants.rs +++ b/services/identity/src/constants.rs @@ -266,6 +266,7 @@ "missing_platform_or_code_version_metadata"; pub const MISSING_KEY: &str = "missing_key"; pub const MESSAGE_NOT_AUTHENTICATED: &str = "message_not_authenticated"; + pub const RETRY_FROM_NATIVE: &str = "retry_from_native"; } // Tunnelbroker diff --git a/services/identity/src/database.rs b/services/identity/src/database.rs --- a/services/identity/src/database.rs +++ b/services/identity/src/database.rs @@ -4,7 +4,9 @@ put_item::PutItemOutput, query::QueryOutput, }, primitives::Blob, - types::{AttributeValue, PutRequest, WriteRequest}, + types::{ + AttributeValue, Delete, Put, PutRequest, TransactWriteItem, WriteRequest, + }, }; use comm_lib::aws::{AwsConfig, DynamoDBClient}; use comm_lib::database::{ @@ -223,7 +225,7 @@ initial_device_list: Option, ) -> Result { let wallet_identity = EthereumIdentity { - wallet_address, + wallet_address: wallet_address.clone(), social_proof, }; let user_id = self @@ -284,7 +286,8 @@ AttributeValue::S(user_id.clone()), )]); - if let Some((username, password_file)) = username_and_password_file { + if let Some((username, password_file)) = username_and_password_file.clone() + { user.insert( USERS_TABLE_USERNAME_ATTRIBUTE.to_string(), AttributeValue::S(username), @@ -295,7 +298,7 @@ ); } - if let Some(eth_identity) = wallet_identity { + if let Some(eth_identity) = wallet_identity.clone() { user.insert( USERS_TABLE_WALLET_ADDRESS_ATTRIBUTE.to_string(), AttributeValue::S(eth_identity.wallet_address), @@ -313,17 +316,54 @@ ); } - self - .client - .put_item() + let put_user = Put::builder() .table_name(USERS_TABLE) .set_item(Some(user)) - // make sure we don't accidentaly overwrite existing row + // make sure we don't accidentally overwrite existing row .condition_expression("attribute_not_exists(#pk)") .expression_attribute_names("#pk", USERS_TABLE_PARTITION_KEY) + .build(); + + let put_user_operation = TransactWriteItem::builder().put(put_user).build(); + + let partition_key_value = + match (username_and_password_file, wallet_identity) { + (Some((username, _)), _) => username, + (_, Some(ethereum_identity)) => ethereum_identity.wallet_address, + _ => return Err(Error::MalformedItem), + }; + + // We make sure to delete the user from the reserved usernames table when we + // add them to the users table + let delete_user_from_reserved_usernames = Delete::builder() + .table_name(RESERVED_USERNAMES_TABLE) + .key( + RESERVED_USERNAMES_TABLE_PARTITION_KEY, + AttributeValue::S(partition_key_value), + ) + .build(); + + let delete_user_from_reserved_usernames_operation = + TransactWriteItem::builder() + .delete(delete_user_from_reserved_usernames) + .build(); + + self + .client + .transact_write_items() + .set_transact_items(Some(vec![ + put_user_operation, + delete_user_from_reserved_usernames_operation, + ])) .send() .await - .map_err(|e| Error::AwsSdk(e.into()))?; + .map_err(|e| { + error!( + errorType = error_types::GENERIC_DB_LOG, + "Add user transaction failed: {:?}", e + ); + Error::AwsSdk(e.into()) + })?; Ok(user_id) } @@ -1078,11 +1118,11 @@ } } - pub async fn username_in_reserved_usernames_table( + pub async fn get_user_id_from_reserved_usernames_table( &self, username: &str, - ) -> Result { - match self + ) -> Result, Error> { + let response = self .client .get_item() .table_name(RESERVED_USERNAMES_TABLE) @@ -1093,11 +1133,19 @@ .consistent_read(true) .send() .await - { - Ok(GetItemOutput { item: Some(_), .. }) => Ok(true), - Ok(_) => Ok(false), - Err(e) => Err(Error::AwsSdk(e.into())), - } + .map_err(|e| Error::AwsSdk(e.into()))?; + + let GetItemOutput { + item: Some(mut item), + .. + } = response + else { + return Ok(None); + }; + + // We should not return `Ok(None)` if `userID` is missing from the item + let user_id = item.take_attr(RESERVED_USERNAMES_TABLE_USER_ID_ATTRIBUTE)?; + Ok(Some(user_id)) } } diff --git a/services/identity/src/ddb_utils.rs b/services/identity/src/ddb_utils.rs --- a/services/identity/src/ddb_utils.rs +++ b/services/identity/src/ddb_utils.rs @@ -179,6 +179,7 @@ } } +#[derive(Clone)] pub struct EthereumIdentity { pub wallet_address: String, pub social_proof: SocialProof, diff --git a/services/identity/src/siwe.rs b/services/identity/src/siwe.rs --- a/services/identity/src/siwe.rs +++ b/services/identity/src/siwe.rs @@ -62,7 +62,7 @@ ethereum_address_regex.is_match(candidate) } -#[derive(derive_more::Constructor)] +#[derive(derive_more::Constructor, Clone)] pub struct SocialProof { pub message: String, pub signature: String,