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 @@ -147,6 +147,12 @@ message.username.clone(), message.farcaster_id.clone(), )?; + self + .check_device_id_taken( + ®istration_state.flattened_device_key_upload, + None, + ) + .await?; let server_registration = comm_opaque2::server::Registration::new(); let server_message = server_registration .start( @@ -207,6 +213,12 @@ message.username.clone(), None, )?; + self + .check_device_id_taken( + ®istration_state.flattened_device_key_upload, + None, + ) + .await?; let server_registration = comm_opaque2::server::Registration::new(); let server_message = server_registration .start( @@ -336,6 +348,9 @@ let flattened_device_key_upload = construct_flattened_device_key_upload(&message)?; + self + .check_device_id_taken(&flattened_device_key_upload, Some(&user_id)) + .await?; let maybe_device_to_remove = self .get_keyserver_device_to_remove( @@ -502,6 +517,10 @@ )); }; + self + .check_device_id_taken(&flattened_device_key_upload, Some(&user_id)) + .await?; + self .client .add_user_device( @@ -597,6 +616,9 @@ let flattened_device_key_upload = construct_flattened_device_key_upload(&message)?; + self + .check_device_id_taken(&flattened_device_key_upload, None) + .await?; let login_time = chrono::Utc::now(); @@ -682,6 +704,9 @@ let flattened_device_key_upload = construct_flattened_device_key_upload(&message)?; + self + .check_device_id_taken(&flattened_device_key_upload, None) + .await?; let initial_device_list = message.get_and_verify_initial_device_list()?; let social_proof = @@ -746,6 +771,10 @@ let nonce = challenge_response.verify_and_get_nonce(&device_id)?; self.verify_and_remove_nonce(&nonce).await?; + self + .check_device_id_taken(&flattened_device_key_upload, Some(&user_id)) + .await?; + let user_identity = self .client .get_user_identity(&user_id) @@ -1081,6 +1110,40 @@ Ok(()) } + async fn check_device_id_taken( + &self, + key_upload: &FlattenedDeviceKeyUpload, + requesting_user_id: Option<&str>, + ) -> Result<(), tonic::Status> { + let device_id = key_upload.device_id_key.as_str(); + let Some(existing_device_user_id) = self + .client + .find_user_id_for_device(device_id) + .await + .map_err(handle_db_error)? + else { + // device ID doesn't exist + return Ok(()); + }; + + // allow already-existing device ID for the same user + match requesting_user_id { + Some(user_id) if user_id == existing_device_user_id => { + debug!( + "Found already-existing device {} for user {}", + device_id, user_id + ); + Ok(()) + } + _ => { + warn!("Device ID already exists: {device_id}"); + Err(tonic::Status::already_exists( + tonic_status_messages::DEVICE_ID_ALREADY_EXISTS, + )) + } + } + } + async fn verify_and_remove_nonce( &self, nonce: &str, 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 @@ -230,6 +230,7 @@ pub const USERNAME_RESERVED: &str = "username_reserved"; pub const WALLET_ADDRESS_TAKEN: &str = "wallet_address_taken"; pub const WALLET_ADDRESS_NOT_RESERVED: &str = "wallet_address_not_reserved"; + pub const DEVICE_ID_ALREADY_EXISTS: &str = "device_id_already_exists"; pub const USER_NOT_FOUND: &str = "user_not_found"; pub const INVALID_NONCE: &str = "invalid_nonce"; pub const NONCE_EXPIRED: &str = "nonce_expired"; diff --git a/services/identity/src/database/device_list.rs b/services/identity/src/database/device_list.rs --- a/services/identity/src/database/device_list.rs +++ b/services/identity/src/database/device_list.rs @@ -746,11 +746,13 @@ Ok(user_devices_keys) } + /// Find owner's user ID for given device ID. Useful for finding + /// devices table partition key. #[tracing::instrument(skip_all)] - pub async fn find_device_by_id( + pub async fn find_user_id_for_device( &self, device_id: &str, - ) -> Result, Error> { + ) -> Result, Error> { let response = self .client .query() @@ -785,15 +787,23 @@ return Err(Error::IllegalState); } - let Some(user_id) = results + let user_id = results .pop() .map(|mut attrs| attrs.take_attr::(devices_table::ATTR_USER_ID)) - .transpose()? - else { + .transpose()?; + + Ok(user_id) + } + + #[tracing::instrument(skip_all)] + pub async fn find_device_by_id( + &self, + device_id: &str, + ) -> Result, Error> { + let Some(user_id) = self.find_user_id_for_device(device_id).await? else { debug!("No device found with ID: {}", device_id); return Ok(None); }; - self.get_device_data(user_id, device_id).await }