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
@@ -23,6 +23,7 @@
   database::{DatabaseClient, Error as DBError, KeyPayload},
   id::generate_uuid,
   nonce::generate_nonce_data,
+  token::AccessTokenData,
 };
 use aws_sdk_dynamodb::Error as DynamoDBError;
 pub use client_proto::identity_client_service_server::{
@@ -40,16 +41,16 @@
 
 #[derive(Clone)]
 pub struct UserRegistrationInfo {
-  username: String,
-  device_id_key: String,
-  key_payload: String,
-  key_payload_signature: String,
-  identity_prekey: String,
-  identity_prekey_signature: String,
-  identity_onetime_keys: Vec<String>,
-  notif_prekey: String,
-  notif_prekey_signature: String,
-  notif_onetime_keys: Vec<String>,
+  pub username: String,
+  pub device_id_key: String,
+  pub key_payload: String,
+  pub key_payload_signature: String,
+  pub identity_prekey: String,
+  pub identity_prekey_signature: String,
+  pub identity_onetime_keys: Vec<String>,
+  pub notif_prekey: String,
+  pub notif_prekey_signature: String,
+  pub notif_onetime_keys: Vec<String>,
 }
 
 #[derive(derive_more::Constructor)]
@@ -144,9 +145,51 @@
 
   async fn register_password_user_finish(
     &self,
-    _request: tonic::Request<RegistrationFinishRequest>,
+    request: tonic::Request<RegistrationFinishRequest>,
   ) -> Result<tonic::Response<RegistrationFinishResponse>, tonic::Status> {
-    unimplemented!();
+    let message = request.into_inner();
+
+    if let Some(WorkflowInProgress::Registration(state)) =
+      self.cache.get(&message.session_id)
+    {
+      self.cache.invalidate(&message.session_id).await;
+
+      let server_registration = comm_opaque2::server::Registration::new();
+      let password_file = server_registration
+        .finish(&message.opaque_registration_upload)
+        .map_err(comm_opaque2::grpc::protocol_error_to_grpc_status)?;
+
+      let device_id = state.device_id_key.clone();
+      let user_id = self
+        .client
+        .add_user_to_users_table(state, password_file)
+        .await
+        .map_err(handle_db_error)?;
+
+      // Create access token
+      let token = AccessTokenData::new(
+        message.session_id,
+        device_id,
+        crate::token::AuthType::Password,
+        &mut OsRng,
+      );
+
+      let access_token = token.access_token.clone();
+
+      self
+        .client
+        .put_access_token_data(token)
+        .await
+        .map_err(handle_db_error)?;
+
+      let response = RegistrationFinishResponse {
+        user_id,
+        access_token,
+      };
+      Ok(Response::new(response))
+    } else {
+      Err(tonic::Status::not_found("session not found"))
+    }
   }
 
   async fn update_user_password_start(
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
@@ -8,74 +8,65 @@
 // DynamoDB
 
 // User table information, supporting opaque_ke 2.0 and X3DH information
-pub mod opaque2 {
-  // Users can sign in either through username+password or Eth wallet.
-  //
-  // This structure should be aligned with the messages defined in
-  // shared/protos/identity_client.proto
-  //
-  // Structure for a user should be:
-  // {
-  //   userID: String,
-  //   opaqueRegistrationData: Option<String>,
-  //   username: Option<String>,
-  //   walletAddress: Option<String>,
-  //   devices: HashMap<String, Device>
-  // }
-  //
-  // A device is defined as:
-  // {
-  //     deviceType: String, # client or keyserver
-  //     keyPayload: String,
-  //     identityPreKey: String,
-  //     identityPreKeySignature: String,
-  //     identityOneTimeKeys: Vec<String>,
-  //     notifPreKey: String,
-  //     notifPreKeySignature: String,
-  //     notifOneTimeKeys: Vec<String>,
-  //   }
-  // }
-  //
-  // Additional context:
-  // "devices" uses the signing public identity key of the device as a key for the devices map
-  // "keyPayload" is a JSON encoded string containing identity and notif keys (both signature and verification)
-  // if "deviceType" == "keyserver", then the device will not have any notif key information
-
-  pub const USERS_TABLE: &str = "identity-users-opaque2";
-  pub const USERS_TABLE_PARTITION_KEY: &str = "userID";
-  pub const USERS_TABLE_REGISTRATION_ATTRIBUTE: &str = "opaqueRegistrationData";
-  pub const USERS_TABLE_USERNAME_ATTRIBUTE: &str = "username";
-  pub const USERS_TABLE_DEVICES_ATTRIBUTE: &str = "devices";
-  pub const USERS_TABLE_DEVICES_MAP_KEY_PAYLOAD_ATTRIBUTE_NAME: &str =
-    "keyPayload";
-  pub const USERS_TABLE_DEVICES_MAP_IDENTITY_PREKEY_ATTRIBUTE_NAME: &str =
-    "identityPreKey";
-  pub const USERS_TABLE_DEVICES_MAP_IDENTITY_PREKEY_SIGNATURE_ATTRIBUTE_NAME:
-    &str = "identityPreKeySignature";
-  pub const USERS_TABLE_DEVICES_MAP_IDENTITY_ONETIME_KEYS_ATTRIBUTE_NAME: &str =
-    "identityOneTimeKeys";
-  pub const USERS_TABLE_DEVICES_MAP_NOTIF_PREKEY_ATTRIBUTE_NAME: &str =
-    "preKey";
-  pub const USERS_TABLE_DEVICES_MAP_NOTIF_PREKEY_SIGNATURE_ATTRIBUTE_NAME:
-    &str = "preKeySignature";
-  pub const USERS_TABLE_DEVICES_MAP_NOTIF_ONETIME_KEYS_ATTRIBUTE_NAME: &str =
-    "notifOneTimeKeys";
-  pub const USERS_TABLE_WALLET_ADDRESS_ATTRIBUTE: &str = "walletAddress";
-  pub const USERS_TABLE_USERNAME_INDEX: &str = "username-index";
-  pub const USERS_TABLE_WALLET_ADDRESS_INDEX: &str = "walletAddress-index";
-}
+
+// Users can sign in either through username+password or Eth wallet.
+//
+// This structure should be aligned with the messages defined in
+// shared/protos/identity_client.proto
+//
+// Structure for a user should be:
+// {
+//   userID: String,
+//   opaqueRegistrationData: Option<String>,
+//   username: Option<String>,
+//   walletAddress: Option<String>,
+//   devices: HashMap<String, Device>
+// }
+//
+// A device is defined as:
+// {
+//     deviceType: String, # client or keyserver
+//     keyPayload: String,
+//     keyPayloadSignature: String,
+//     identityPreKey: String,
+//     identityPreKeySignature: String,
+//     identityOneTimeKeys: Vec<String>,
+//     notifPreKey: String,
+//     notifPreKeySignature: String,
+//     notifOneTimeKeys: Vec<String>,
+//   }
+// }
+//
+// Additional context:
+// "devices" uses the signing public identity key of the device as a key for the devices map
+// "keyPayload" is a JSON encoded string containing identity and notif keys (both signature and verification)
+// if "deviceType" == "keyserver", then the device will not have any notif key information
 
 pub const USERS_TABLE: &str = "identity-users";
 pub const USERS_TABLE_PARTITION_KEY: &str = "userID";
-pub const USERS_TABLE_REGISTRATION_ATTRIBUTE: &str = "pakeRegistrationData";
+pub const USERS_TABLE_REGISTRATION_ATTRIBUTE: &str = "opaqueRegistrationData";
 pub const USERS_TABLE_USERNAME_ATTRIBUTE: &str = "username";
 pub const USERS_TABLE_DEVICES_ATTRIBUTE: &str = "devices";
-pub const USERS_TABLE_DEVICE_ATTRIBUTE_NAME: &str = "device";
-pub const USERS_TABLE_DEVICES_MAP_ATTRIBUTE_NAME: &str = "signingPublicKey";
+pub const USERS_TABLE_DEVICES_MAP_DEVICE_TYPE_ATTRIBUTE_NAME: &str =
+  "deviceType";
+pub const USERS_TABLE_DEVICES_MAP_KEY_PAYLOAD_ATTRIBUTE_NAME: &str =
+  "keyPayload";
+pub const USERS_TABLE_DEVICES_MAP_KEY_PAYLOAD_SIGNATURE_ATTRIBUTE_NAME: &str =
+  "keyPayloadSignature";
+pub const USERS_TABLE_DEVICES_MAP_IDENTITY_PREKEY_ATTRIBUTE_NAME: &str =
+  "identityPreKey";
+pub const USERS_TABLE_DEVICES_MAP_IDENTITY_PREKEY_SIGNATURE_ATTRIBUTE_NAME:
+  &str = "identityPreKeySignature";
+pub const USERS_TABLE_DEVICES_MAP_IDENTITY_ONETIME_KEYS_ATTRIBUTE_NAME: &str =
+  "identityOneTimeKeys";
+pub const USERS_TABLE_DEVICES_MAP_NOTIF_PREKEY_ATTRIBUTE_NAME: &str = "preKey";
+pub const USERS_TABLE_DEVICES_MAP_NOTIF_PREKEY_SIGNATURE_ATTRIBUTE_NAME: &str =
+  "preKeySignature";
+pub const USERS_TABLE_DEVICES_MAP_NOTIF_ONETIME_KEYS_ATTRIBUTE_NAME: &str =
+  "notifOneTimeKeys";
 pub const USERS_TABLE_WALLET_ADDRESS_ATTRIBUTE: &str = "walletAddress";
 pub const USERS_TABLE_USERNAME_INDEX: &str = "username-index";
 pub const USERS_TABLE_WALLET_ADDRESS_INDEX: &str = "walletAddress-index";
-pub const USERS_TABLE_INITIALIZATION_INFO: &str = "initializationInfo";
 
 pub const ACCESS_TOKEN_TABLE: &str = "identity-tokens";
 pub const ACCESS_TOKEN_TABLE_PARTITION_KEY: &str = "userID";
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
@@ -6,7 +6,7 @@
 use aws_config::SdkConfig;
 use aws_sdk_dynamodb::model::AttributeValue;
 use aws_sdk_dynamodb::output::{
-  DeleteItemOutput, GetItemOutput, PutItemOutput, QueryOutput, UpdateItemOutput,
+  DeleteItemOutput, GetItemOutput, PutItemOutput, QueryOutput,
 };
 use aws_sdk_dynamodb::types::Blob;
 use aws_sdk_dynamodb::{Client, Error as DynamoDBError};
@@ -15,6 +15,7 @@
 use serde::{Deserialize, Serialize};
 use tracing::{debug, error, info, warn};
 
+use crate::client_service::UserRegistrationInfo;
 use crate::config::CONFIG;
 use crate::constants::{
   ACCESS_TOKEN_SORT_KEY, ACCESS_TOKEN_TABLE,
@@ -22,12 +23,21 @@
   ACCESS_TOKEN_TABLE_PARTITION_KEY, ACCESS_TOKEN_TABLE_TOKEN_ATTRIBUTE,
   ACCESS_TOKEN_TABLE_VALID_ATTRIBUTE, NONCE_TABLE,
   NONCE_TABLE_CREATED_ATTRIBUTE, NONCE_TABLE_PARTITION_KEY, USERS_TABLE,
-  USERS_TABLE_DEVICES_ATTRIBUTE, USERS_TABLE_DEVICES_MAP_ATTRIBUTE_NAME,
-  USERS_TABLE_DEVICE_ATTRIBUTE_NAME, USERS_TABLE_INITIALIZATION_INFO,
+  USERS_TABLE_DEVICES_ATTRIBUTE,
+  USERS_TABLE_DEVICES_MAP_DEVICE_TYPE_ATTRIBUTE_NAME,
+  USERS_TABLE_DEVICES_MAP_IDENTITY_ONETIME_KEYS_ATTRIBUTE_NAME,
+  USERS_TABLE_DEVICES_MAP_IDENTITY_PREKEY_ATTRIBUTE_NAME,
+  USERS_TABLE_DEVICES_MAP_IDENTITY_PREKEY_SIGNATURE_ATTRIBUTE_NAME,
+  USERS_TABLE_DEVICES_MAP_KEY_PAYLOAD_ATTRIBUTE_NAME,
+  USERS_TABLE_DEVICES_MAP_KEY_PAYLOAD_SIGNATURE_ATTRIBUTE_NAME,
+  USERS_TABLE_DEVICES_MAP_NOTIF_ONETIME_KEYS_ATTRIBUTE_NAME,
+  USERS_TABLE_DEVICES_MAP_NOTIF_PREKEY_ATTRIBUTE_NAME,
+  USERS_TABLE_DEVICES_MAP_NOTIF_PREKEY_SIGNATURE_ATTRIBUTE_NAME,
   USERS_TABLE_PARTITION_KEY, USERS_TABLE_REGISTRATION_ATTRIBUTE,
   USERS_TABLE_USERNAME_ATTRIBUTE, USERS_TABLE_USERNAME_INDEX,
   USERS_TABLE_WALLET_ADDRESS_ATTRIBUTE, USERS_TABLE_WALLET_ADDRESS_INDEX,
 };
+use crate::id::generate_uuid;
 use crate::nonce::NonceData;
 use crate::token::{AccessTokenData, AuthType};
 use comm_opaque::Cipher;
@@ -55,6 +65,20 @@
   }
 }
 
+pub enum Device {
+  Client,
+  Keyserver,
+}
+
+impl Display for Device {
+  fn fmt(&self, f: &mut Formatter) -> FmtResult {
+    match self {
+      Device::Client => write!(f, "client"),
+      Device::Keyserver => write!(f, "keyserver"),
+    }
+  }
+}
+
 #[derive(Clone)]
 pub struct DatabaseClient {
   client: Arc<Client>,
@@ -111,148 +135,87 @@
     }
   }
 
-  pub async fn get_session_initialization_info(
-    &self,
-    user_id: &str,
-  ) -> Result<Option<HashMap<String, HashMap<String, String>>>, Error> {
-    match self.get_item_from_users_table(user_id).await {
-      Ok(GetItemOutput {
-        item: Some(mut item),
-        ..
-      }) => parse_devices_attribute(item.remove(USERS_TABLE_DEVICES_ATTRIBUTE))
-        .map(Some)
-        .map_err(Error::Attribute),
-      Ok(_) => {
-        info!("No item found for user {} in users table", user_id);
-        Ok(None)
-      }
-      Err(e) => {
-        error!(
-          "DynamoDB client failed to get session initialization info for user {}: {}",
-          user_id, e
-        );
-        Err(e)
-      }
-    }
-  }
-
-  pub async fn update_users_table(
-    &self,
-    user_id: String,
-    signing_public_key: Option<String>,
-    registration: Option<ServerRegistration<Cipher>>,
-    username: Option<String>,
-    session_initialization_info: Option<&HashMap<String, String>>,
-  ) -> Result<UpdateItemOutput, Error> {
-    let mut update_expression_parts = Vec::new();
-    let mut expression_attribute_names = HashMap::new();
-    let mut expression_attribute_values = HashMap::new();
-    if let Some(reg) = registration {
-      update_expression_parts
-        .push(format!("{} = :r", USERS_TABLE_REGISTRATION_ATTRIBUTE));
-      expression_attribute_values.insert(
-        ":r".to_string(),
-        AttributeValue::B(Blob::new(reg.serialize())),
-      );
-    };
-    if let Some(username) = username {
-      update_expression_parts
-        .push(format!("{} = :u", USERS_TABLE_USERNAME_ATTRIBUTE));
-      expression_attribute_values
-        .insert(":u".to_string(), AttributeValue::S(username));
-    };
-    if let Some(public_key) = signing_public_key {
-      let device_info = match session_initialization_info {
-        Some(info) => info
-          .iter()
-          .map(|(k, v)| (k.to_string(), AttributeValue::S(v.to_string())))
-          .collect(),
-        None => HashMap::new(),
-      };
-
-      // How we construct the update expression will depend on whether the user
-      // already exists or not
-      if let GetItemOutput { item: Some(_), .. } =
-        self.get_item_from_users_table(&user_id).await?
-      {
-        update_expression_parts.push(format!(
-          "{}.#{} = :k",
-          USERS_TABLE_DEVICES_ATTRIBUTE, USERS_TABLE_DEVICES_MAP_ATTRIBUTE_NAME,
-        ));
-        expression_attribute_names.insert(
-          format!("#{}", USERS_TABLE_DEVICES_MAP_ATTRIBUTE_NAME),
-          public_key,
-        );
-        expression_attribute_values
-          .insert(":k".to_string(), AttributeValue::M(device_info));
-      } else {
-        update_expression_parts
-          .push(format!("{} = :k", USERS_TABLE_DEVICES_ATTRIBUTE));
-        let mut devices = HashMap::new();
-        devices.insert(public_key, AttributeValue::M(device_info));
-        expression_attribute_values
-          .insert(":k".to_string(), AttributeValue::M(devices));
-      };
-    };
-
-    self
-      .client
-      .update_item()
-      .table_name(USERS_TABLE)
-      .key(USERS_TABLE_PARTITION_KEY, AttributeValue::S(user_id))
-      .update_expression(format!("SET {}", update_expression_parts.join(",")))
-      .set_expression_attribute_names(
-        if expression_attribute_names.is_empty() {
-          None
-        } else {
-          Some(expression_attribute_names)
-        },
-      )
-      .set_expression_attribute_values(
-        if expression_attribute_values.is_empty() {
-          None
-        } else {
-          Some(expression_attribute_values)
-        },
-      )
-      .send()
-      .await
-      .map_err(|e| Error::AwsSdk(e.into()))
-  }
-
   pub async fn add_user_to_users_table(
     &self,
-    user_id: String,
-    registration: ServerRegistration<Cipher>,
-    username: String,
-    signing_public_key: String,
-    session_initialization_info: &HashMap<String, String>,
-  ) -> Result<PutItemOutput, Error> {
-    let device_info: HashMap<String, AttributeValue> =
-      session_initialization_info
-        .iter()
-        .map(|(k, v)| (k.to_string(), AttributeValue::S(v.to_string())))
-        .collect();
+    registration_state: UserRegistrationInfo,
+    password_file: Vec<u8>,
+  ) -> Result<String, Error> {
+    let user_id = generate_uuid();
+    let device_info = HashMap::from([
+      (
+        USERS_TABLE_DEVICES_MAP_DEVICE_TYPE_ATTRIBUTE_NAME.to_string(),
+        AttributeValue::S(Device::Client.to_string()),
+      ),
+      (
+        USERS_TABLE_DEVICES_MAP_KEY_PAYLOAD_ATTRIBUTE_NAME.to_string(),
+        AttributeValue::S(registration_state.key_payload),
+      ),
+      (
+        USERS_TABLE_DEVICES_MAP_KEY_PAYLOAD_SIGNATURE_ATTRIBUTE_NAME
+          .to_string(),
+        AttributeValue::S(registration_state.key_payload_signature),
+      ),
+      (
+        USERS_TABLE_DEVICES_MAP_IDENTITY_PREKEY_ATTRIBUTE_NAME.to_string(),
+        AttributeValue::S(registration_state.identity_prekey),
+      ),
+      (
+        USERS_TABLE_DEVICES_MAP_IDENTITY_PREKEY_SIGNATURE_ATTRIBUTE_NAME
+          .to_string(),
+        AttributeValue::S(registration_state.identity_prekey_signature),
+      ),
+      (
+        USERS_TABLE_DEVICES_MAP_IDENTITY_ONETIME_KEYS_ATTRIBUTE_NAME
+          .to_string(),
+        AttributeValue::L(
+          registration_state
+            .identity_onetime_keys
+            .into_iter()
+            .map(AttributeValue::S)
+            .collect(),
+        ),
+      ),
+      (
+        USERS_TABLE_DEVICES_MAP_NOTIF_PREKEY_ATTRIBUTE_NAME.to_string(),
+        AttributeValue::S(registration_state.notif_prekey),
+      ),
+      (
+        USERS_TABLE_DEVICES_MAP_NOTIF_PREKEY_SIGNATURE_ATTRIBUTE_NAME
+          .to_string(),
+        AttributeValue::S(registration_state.notif_prekey_signature),
+      ),
+      (
+        USERS_TABLE_DEVICES_MAP_NOTIF_ONETIME_KEYS_ATTRIBUTE_NAME.to_string(),
+        AttributeValue::L(
+          registration_state
+            .notif_onetime_keys
+            .into_iter()
+            .map(AttributeValue::S)
+            .collect(),
+        ),
+      ),
+    ]);
+    let devices = HashMap::from([(
+      registration_state.device_id_key,
+      AttributeValue::M(device_info),
+    )]);
 
-    let item = HashMap::from([
+    let user = HashMap::from([
       (
         USERS_TABLE_PARTITION_KEY.to_string(),
-        AttributeValue::S(user_id),
+        AttributeValue::S(user_id.clone()),
       ),
       (
         USERS_TABLE_USERNAME_ATTRIBUTE.to_string(),
-        AttributeValue::S(username),
+        AttributeValue::S(registration_state.username),
       ),
       (
-        USERS_TABLE_REGISTRATION_ATTRIBUTE.to_string(),
-        AttributeValue::B(Blob::new(registration.serialize())),
+        USERS_TABLE_DEVICES_ATTRIBUTE.to_string(),
+        AttributeValue::M(devices),
       ),
       (
-        USERS_TABLE_DEVICES_ATTRIBUTE.to_string(),
-        AttributeValue::M(HashMap::from([(
-          signing_public_key,
-          AttributeValue::M(device_info),
-        )])),
+        USERS_TABLE_REGISTRATION_ATTRIBUTE.to_string(),
+        AttributeValue::B(Blob::new(password_file)),
       ),
     ]);
 
@@ -260,12 +223,13 @@
       .client
       .put_item()
       .table_name(USERS_TABLE)
-      .set_item(Some(item))
+      .set_item(Some(user))
       .send()
       .await
-      .map_err(|e| Error::AwsSdk(e.into()))
-  }
+      .map_err(|e| Error::AwsSdk(e.into()))?;
 
+    Ok(user_id)
+  }
   pub async fn delete_user(
     &self,
     user_id: String,
@@ -714,36 +678,6 @@
   }
 }
 
-fn parse_devices_attribute(
-  attribute: Option<AttributeValue>,
-) -> Result<HashMap<String, HashMap<String, String>>, DBItemError> {
-  let mut devices = HashMap::new();
-  let ddb_devices =
-    parse_map_attribute(USERS_TABLE_DEVICES_ATTRIBUTE, attribute)?;
-
-  for (signing_public_key, session_initialization_info) in ddb_devices {
-    let session_initialization_info_map = parse_map_attribute(
-      USERS_TABLE_DEVICE_ATTRIBUTE_NAME,
-      Some(session_initialization_info),
-    )?;
-    let mut inner_hash_map = HashMap::new();
-    for (initialization_component_name, initialization_component_value) in
-      session_initialization_info_map
-    {
-      let initialization_piece_value_string = parse_string_attribute(
-        USERS_TABLE_INITIALIZATION_INFO,
-        Some(initialization_component_value),
-      )?;
-      inner_hash_map.insert(
-        initialization_component_name,
-        initialization_piece_value_string,
-      );
-    }
-    devices.insert(signing_public_key, inner_hash_map);
-  }
-  Ok(devices)
-}
-
 fn parse_map_attribute(
   attribute_name: &'static str,
   attribute_value: Option<AttributeValue>,
diff --git a/shared/protos/identity_client.proto b/shared/protos/identity_client.proto
--- a/shared/protos/identity_client.proto
+++ b/shared/protos/identity_client.proto
@@ -132,6 +132,8 @@
 }
 
 message RegistrationFinishResponse {
+  // Unique identifier for newly registered user
+  string userID = 1;
   // After successful unpacking of user credentials, return token
   string accessToken = 2;
 }