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 @@ -11,7 +11,6 @@ AttributeExtractor, AttributeMap, DBItemAttributeError, DBItemError, TryFromAttribute, }; -use constant_time_eq::constant_time_eq; use std::collections::{HashMap, HashSet}; use std::str::FromStr; use std::sync::Arc; @@ -34,11 +33,8 @@ use crate::client_service::{FlattenedDeviceKeyUpload, UserRegistrationInfo}; use crate::config::CONFIG; use crate::constants::{ - ACCESS_TOKEN_SORT_KEY, ACCESS_TOKEN_TABLE, - ACCESS_TOKEN_TABLE_AUTH_TYPE_ATTRIBUTE, ACCESS_TOKEN_TABLE_CREATED_ATTRIBUTE, - ACCESS_TOKEN_TABLE_PARTITION_KEY, ACCESS_TOKEN_TABLE_TOKEN_ATTRIBUTE, - ACCESS_TOKEN_TABLE_VALID_ATTRIBUTE, NONCE_TABLE, - NONCE_TABLE_CREATED_ATTRIBUTE, NONCE_TABLE_EXPIRATION_TIME_ATTRIBUTE, + NONCE_TABLE, NONCE_TABLE_CREATED_ATTRIBUTE, + NONCE_TABLE_EXPIRATION_TIME_ATTRIBUTE, NONCE_TABLE_EXPIRATION_TIME_UNIX_ATTRIBUTE, NONCE_TABLE_PARTITION_KEY, RESERVED_USERNAMES_TABLE, RESERVED_USERNAMES_TABLE_PARTITION_KEY, RESERVED_USERNAMES_TABLE_USER_ID_ATTRIBUTE, USERS_TABLE, @@ -50,12 +46,13 @@ }; use crate::id::generate_uuid; use crate::nonce::NonceData; -use crate::token::{AccessTokenData, AuthType}; +use crate::token::AuthType; pub use grpc_clients::identity::DeviceType; mod device_list; mod farcaster; mod one_time_keys; +mod token; mod workflows; pub use device_list::{DeviceListRow, DeviceListUpdate, DeviceRow}; @@ -479,160 +476,6 @@ } } - pub async fn get_access_token_data( - &self, - user_id: String, - signing_public_key: String, - ) -> Result, Error> { - let primary_key = create_composite_primary_key( - ( - ACCESS_TOKEN_TABLE_PARTITION_KEY.to_string(), - user_id.clone(), - ), - ( - ACCESS_TOKEN_SORT_KEY.to_string(), - signing_public_key.clone(), - ), - ); - let get_item_result = self - .client - .get_item() - .table_name(ACCESS_TOKEN_TABLE) - .set_key(Some(primary_key)) - .consistent_read(true) - .send() - .await; - match get_item_result { - Ok(GetItemOutput { - item: Some(mut item), - .. - }) => { - let created = DateTime::::try_from_attr( - ACCESS_TOKEN_TABLE_CREATED_ATTRIBUTE, - item.remove(ACCESS_TOKEN_TABLE_CREATED_ATTRIBUTE), - )?; - let auth_type = parse_auth_type_attribute( - item.remove(ACCESS_TOKEN_TABLE_AUTH_TYPE_ATTRIBUTE), - )?; - let valid = parse_valid_attribute( - item.remove(ACCESS_TOKEN_TABLE_VALID_ATTRIBUTE), - )?; - let access_token = parse_token_attribute( - item.remove(ACCESS_TOKEN_TABLE_TOKEN_ATTRIBUTE), - )?; - Ok(Some(AccessTokenData { - user_id, - signing_public_key, - access_token, - created, - auth_type, - valid, - })) - } - Ok(_) => { - info!( - "No item found for user {} and signing public key {} in token table", - user_id, signing_public_key - ); - Ok(None) - } - Err(e) => { - error!( - "DynamoDB client failed to get token for user {} with signing public key {}: {}", - user_id, signing_public_key, e - ); - Err(Error::AwsSdk(e.into())) - } - } - } - - pub async fn verify_access_token( - &self, - user_id: String, - signing_public_key: String, - access_token_to_verify: String, - ) -> Result { - let is_valid = self - .get_access_token_data(user_id, signing_public_key) - .await? - .map(|access_token_data| { - constant_time_eq( - access_token_data.access_token.as_bytes(), - access_token_to_verify.as_bytes(), - ) && access_token_data.is_valid() - }) - .unwrap_or(false); - - Ok(is_valid) - } - - pub async fn put_access_token_data( - &self, - access_token_data: AccessTokenData, - ) -> Result { - let item = HashMap::from([ - ( - ACCESS_TOKEN_TABLE_PARTITION_KEY.to_string(), - AttributeValue::S(access_token_data.user_id), - ), - ( - ACCESS_TOKEN_SORT_KEY.to_string(), - AttributeValue::S(access_token_data.signing_public_key), - ), - ( - ACCESS_TOKEN_TABLE_TOKEN_ATTRIBUTE.to_string(), - AttributeValue::S(access_token_data.access_token), - ), - ( - ACCESS_TOKEN_TABLE_CREATED_ATTRIBUTE.to_string(), - AttributeValue::S(access_token_data.created.to_rfc3339()), - ), - ( - ACCESS_TOKEN_TABLE_AUTH_TYPE_ATTRIBUTE.to_string(), - AttributeValue::S(match access_token_data.auth_type { - AuthType::Password => "password".to_string(), - AuthType::Wallet => "wallet".to_string(), - }), - ), - ( - ACCESS_TOKEN_TABLE_VALID_ATTRIBUTE.to_string(), - AttributeValue::Bool(access_token_data.valid), - ), - ]); - self - .client - .put_item() - .table_name(ACCESS_TOKEN_TABLE) - .set_item(Some(item)) - .send() - .await - .map_err(|e| Error::AwsSdk(e.into())) - } - - pub async fn delete_access_token_data( - &self, - user_id: String, - device_id_key: String, - ) -> Result<(), Error> { - self - .client - .delete_item() - .table_name(ACCESS_TOKEN_TABLE) - .key( - ACCESS_TOKEN_TABLE_PARTITION_KEY.to_string(), - AttributeValue::S(user_id), - ) - .key( - ACCESS_TOKEN_SORT_KEY.to_string(), - AttributeValue::S(device_id_key), - ) - .send() - .await - .map_err(|e| Error::AwsSdk(e.into()))?; - - Ok(()) - } - pub async fn wallet_address_taken( &self, wallet_address: String, @@ -1157,64 +1000,6 @@ primary_key } -fn parse_auth_type_attribute( - attribute: Option, -) -> Result { - if let Some(AttributeValue::S(auth_type)) = &attribute { - match auth_type.as_str() { - "password" => Ok(AuthType::Password), - "wallet" => Ok(AuthType::Wallet), - _ => Err(DBItemError::new( - ACCESS_TOKEN_TABLE_AUTH_TYPE_ATTRIBUTE.to_string(), - attribute.into(), - DBItemAttributeError::IncorrectType, - )), - } - } else { - Err(DBItemError::new( - ACCESS_TOKEN_TABLE_AUTH_TYPE_ATTRIBUTE.to_string(), - attribute.into(), - DBItemAttributeError::Missing, - )) - } -} - -fn parse_valid_attribute( - attribute: Option, -) -> Result { - match attribute { - Some(AttributeValue::Bool(valid)) => Ok(valid), - Some(_) => Err(DBItemError::new( - ACCESS_TOKEN_TABLE_VALID_ATTRIBUTE.to_string(), - attribute.into(), - DBItemAttributeError::IncorrectType, - )), - None => Err(DBItemError::new( - ACCESS_TOKEN_TABLE_VALID_ATTRIBUTE.to_string(), - attribute.into(), - DBItemAttributeError::Missing, - )), - } -} - -fn parse_token_attribute( - attribute: Option, -) -> Result { - match attribute { - Some(AttributeValue::S(token)) => Ok(token), - Some(_) => Err(DBItemError::new( - ACCESS_TOKEN_TABLE_TOKEN_ATTRIBUTE.to_string(), - attribute.into(), - DBItemAttributeError::IncorrectType, - )), - None => Err(DBItemError::new( - ACCESS_TOKEN_TABLE_TOKEN_ATTRIBUTE.to_string(), - attribute.into(), - DBItemAttributeError::Missing, - )), - } -} - fn parse_registration_data_attribute( attribute: Option, ) -> Result, DBItemError> { diff --git a/services/identity/src/database/token.rs b/services/identity/src/database/token.rs new file mode 100644 --- /dev/null +++ b/services/identity/src/database/token.rs @@ -0,0 +1,239 @@ +use std::collections::HashMap; + +use chrono::{DateTime, Utc}; +use comm_lib::{ + aws::ddb::{ + operation::{get_item::GetItemOutput, put_item::PutItemOutput}, + types::AttributeValue, + }, + database::{DBItemAttributeError, DBItemError, TryFromAttribute}, +}; +use constant_time_eq::constant_time_eq; +use tracing::{error, info}; + +use crate::{ + constants::{ + ACCESS_TOKEN_SORT_KEY, ACCESS_TOKEN_TABLE, + ACCESS_TOKEN_TABLE_AUTH_TYPE_ATTRIBUTE, + ACCESS_TOKEN_TABLE_CREATED_ATTRIBUTE, ACCESS_TOKEN_TABLE_PARTITION_KEY, + ACCESS_TOKEN_TABLE_TOKEN_ATTRIBUTE, ACCESS_TOKEN_TABLE_VALID_ATTRIBUTE, + }, + error::Error, + token::{AccessTokenData, AuthType}, +}; + +use super::{create_composite_primary_key, DatabaseClient}; + +impl DatabaseClient { + pub async fn get_access_token_data( + &self, + user_id: String, + signing_public_key: String, + ) -> Result, Error> { + let primary_key = create_composite_primary_key( + ( + ACCESS_TOKEN_TABLE_PARTITION_KEY.to_string(), + user_id.clone(), + ), + ( + ACCESS_TOKEN_SORT_KEY.to_string(), + signing_public_key.clone(), + ), + ); + let get_item_result = self + .client + .get_item() + .table_name(ACCESS_TOKEN_TABLE) + .set_key(Some(primary_key)) + .consistent_read(true) + .send() + .await; + match get_item_result { + Ok(GetItemOutput { + item: Some(mut item), + .. + }) => { + let created = DateTime::::try_from_attr( + ACCESS_TOKEN_TABLE_CREATED_ATTRIBUTE, + item.remove(ACCESS_TOKEN_TABLE_CREATED_ATTRIBUTE), + )?; + let auth_type = parse_auth_type_attribute( + item.remove(ACCESS_TOKEN_TABLE_AUTH_TYPE_ATTRIBUTE), + )?; + let valid = parse_valid_attribute( + item.remove(ACCESS_TOKEN_TABLE_VALID_ATTRIBUTE), + )?; + let access_token = parse_token_attribute( + item.remove(ACCESS_TOKEN_TABLE_TOKEN_ATTRIBUTE), + )?; + Ok(Some(AccessTokenData { + user_id, + signing_public_key, + access_token, + created, + auth_type, + valid, + })) + } + Ok(_) => { + info!( + "No item found for user {} and signing public key {} in token table", + user_id, signing_public_key + ); + Ok(None) + } + Err(e) => { + error!( + "DynamoDB client failed to get token for user {} with signing public key {}: {}", + user_id, signing_public_key, e + ); + Err(Error::AwsSdk(e.into())) + } + } + } + + pub async fn verify_access_token( + &self, + user_id: String, + signing_public_key: String, + access_token_to_verify: String, + ) -> Result { + let is_valid = self + .get_access_token_data(user_id, signing_public_key) + .await? + .map(|access_token_data| { + constant_time_eq( + access_token_data.access_token.as_bytes(), + access_token_to_verify.as_bytes(), + ) && access_token_data.is_valid() + }) + .unwrap_or(false); + + Ok(is_valid) + } + + pub async fn put_access_token_data( + &self, + access_token_data: AccessTokenData, + ) -> Result { + let item = HashMap::from([ + ( + ACCESS_TOKEN_TABLE_PARTITION_KEY.to_string(), + AttributeValue::S(access_token_data.user_id), + ), + ( + ACCESS_TOKEN_SORT_KEY.to_string(), + AttributeValue::S(access_token_data.signing_public_key), + ), + ( + ACCESS_TOKEN_TABLE_TOKEN_ATTRIBUTE.to_string(), + AttributeValue::S(access_token_data.access_token), + ), + ( + ACCESS_TOKEN_TABLE_CREATED_ATTRIBUTE.to_string(), + AttributeValue::S(access_token_data.created.to_rfc3339()), + ), + ( + ACCESS_TOKEN_TABLE_AUTH_TYPE_ATTRIBUTE.to_string(), + AttributeValue::S(match access_token_data.auth_type { + AuthType::Password => "password".to_string(), + AuthType::Wallet => "wallet".to_string(), + }), + ), + ( + ACCESS_TOKEN_TABLE_VALID_ATTRIBUTE.to_string(), + AttributeValue::Bool(access_token_data.valid), + ), + ]); + self + .client + .put_item() + .table_name(ACCESS_TOKEN_TABLE) + .set_item(Some(item)) + .send() + .await + .map_err(|e| Error::AwsSdk(e.into())) + } + + pub async fn delete_access_token_data( + &self, + user_id: String, + device_id_key: String, + ) -> Result<(), Error> { + self + .client + .delete_item() + .table_name(ACCESS_TOKEN_TABLE) + .key( + ACCESS_TOKEN_TABLE_PARTITION_KEY.to_string(), + AttributeValue::S(user_id), + ) + .key( + ACCESS_TOKEN_SORT_KEY.to_string(), + AttributeValue::S(device_id_key), + ) + .send() + .await + .map_err(|e| Error::AwsSdk(e.into()))?; + + Ok(()) + } +} + +fn parse_auth_type_attribute( + attribute: Option, +) -> Result { + if let Some(AttributeValue::S(auth_type)) = &attribute { + match auth_type.as_str() { + "password" => Ok(AuthType::Password), + "wallet" => Ok(AuthType::Wallet), + _ => Err(DBItemError::new( + ACCESS_TOKEN_TABLE_AUTH_TYPE_ATTRIBUTE.to_string(), + attribute.into(), + DBItemAttributeError::IncorrectType, + )), + } + } else { + Err(DBItemError::new( + ACCESS_TOKEN_TABLE_AUTH_TYPE_ATTRIBUTE.to_string(), + attribute.into(), + DBItemAttributeError::Missing, + )) + } +} + +fn parse_valid_attribute( + attribute: Option, +) -> Result { + match attribute { + Some(AttributeValue::Bool(valid)) => Ok(valid), + Some(_) => Err(DBItemError::new( + ACCESS_TOKEN_TABLE_VALID_ATTRIBUTE.to_string(), + attribute.into(), + DBItemAttributeError::IncorrectType, + )), + None => Err(DBItemError::new( + ACCESS_TOKEN_TABLE_VALID_ATTRIBUTE.to_string(), + attribute.into(), + DBItemAttributeError::Missing, + )), + } +} + +fn parse_token_attribute( + attribute: Option, +) -> Result { + match attribute { + Some(AttributeValue::S(token)) => Ok(token), + Some(_) => Err(DBItemError::new( + ACCESS_TOKEN_TABLE_TOKEN_ATTRIBUTE.to_string(), + attribute.into(), + DBItemAttributeError::IncorrectType, + )), + None => Err(DBItemError::new( + ACCESS_TOKEN_TABLE_TOKEN_ATTRIBUTE.to_string(), + attribute.into(), + DBItemAttributeError::Missing, + )), + } +}