diff --git a/services/identity/src/config.rs b/services/identity/src/config.rs index 5bdf613ba..2c7a35be9 100644 --- a/services/identity/src/config.rs +++ b/services/identity/src/config.rs @@ -1,162 +1,187 @@ +use std::{env, fmt, fs, io, path}; + use base64::{engine::general_purpose, DecodeError, Engine as _}; use clap::{Parser, Subcommand}; +use http::HeaderValue; use once_cell::sync::Lazy; -use std::{env, fmt, fs, io, path}; +use tower_http::cors::AllowOrigin; use tracing::{error, info}; use crate::constants::{ - DEFAULT_OPENSEARCH_ENDPOINT, DEFAULT_TUNNELBROKER_ENDPOINT, - KEYSERVER_PUBLIC_KEY, LOCALSTACK_ENDPOINT, OPAQUE_SERVER_SETUP, - OPENSEARCH_ENDPOINT, SECRETS_DIRECTORY, SECRETS_SETUP_FILE, - TUNNELBROKER_GRPC_ENDPOINT, + cors::ALLOW_ORIGIN_LIST, DEFAULT_OPENSEARCH_ENDPOINT, + DEFAULT_TUNNELBROKER_ENDPOINT, KEYSERVER_PUBLIC_KEY, LOCALSTACK_ENDPOINT, + OPAQUE_SERVER_SETUP, OPENSEARCH_ENDPOINT, SECRETS_DIRECTORY, + SECRETS_SETUP_FILE, TUNNELBROKER_GRPC_ENDPOINT, }; /// Raw CLI arguments, should be only used internally to create ServerConfig static CLI: Lazy = Lazy::new(Cli::parse); pub static CONFIG: Lazy = Lazy::new(|| { ServerConfig::from_cli(&CLI).expect("Failed to load server config") }); pub(super) fn parse_cli_command() -> &'static Command { &Lazy::force(&CLI).command } pub(super) fn load_server_config() -> &'static ServerConfig { Lazy::force(&CONFIG) } #[derive(Parser)] #[clap(author, version, about, long_about = None)] #[clap(propagate_version = true)] struct Cli { #[clap(subcommand)] command: Command, /// AWS Localstack service URL #[arg(long, global = true)] #[arg(env = LOCALSTACK_ENDPOINT)] localstack_endpoint: Option, /// Tunnelbroker gRPC endpoint #[arg(long, global = true)] #[arg(env = TUNNELBROKER_GRPC_ENDPOINT)] #[arg(default_value = DEFAULT_TUNNELBROKER_ENDPOINT)] tunnelbroker_endpoint: String, /// OpenSearch domain endpoint #[arg(long, global = true)] #[arg(env = OPENSEARCH_ENDPOINT)] #[arg(default_value = DEFAULT_OPENSEARCH_ENDPOINT)] opensearch_endpoint: String, + + /// Allowed origins + #[arg(long, global = true)] + #[arg(env = ALLOW_ORIGIN_LIST)] + allow_origin_list: Option, } #[derive(Subcommand)] pub enum Command { /// Runs the server Server, /// Generates and persists a keypair to use for PAKE registration and login Keygen { #[arg(short, long)] #[arg(default_value = SECRETS_DIRECTORY)] dir: String, }, /// Syncs DynamoDB users with identity-search search index SyncIdentitySearch, } #[derive(Clone)] pub struct ServerConfig { pub localstack_endpoint: Option, // Opaque 2.0 server secrets pub server_setup: comm_opaque2::ServerSetup, pub keyserver_public_key: Option, pub tunnelbroker_endpoint: String, pub opensearch_endpoint: String, + pub allow_origin: Option, } impl ServerConfig { fn from_cli(cli: &Cli) -> Result { if !matches!(cli.command, Command::Server | Command::SyncIdentitySearch) { panic!("ServerConfig is only available for the `server` or `sync-identity-search` command"); } info!("Tunnelbroker endpoint: {}", &cli.tunnelbroker_endpoint); if let Some(endpoint) = &cli.localstack_endpoint { info!("Using Localstack endpoint: {}", endpoint); } - info!("Using OpenSearch endpoint: {}", cli.opensearch_endpoint); let mut path_buf = path::PathBuf::new(); path_buf.push(SECRETS_DIRECTORY); path_buf.push(SECRETS_SETUP_FILE); - let server_setup = get_server_setup(path_buf.as_path())?; + let keyserver_public_key = env::var(KEYSERVER_PUBLIC_KEY).ok(); + let allow_origin = cli + .allow_origin_list + .clone() + .map(|s| slice_to_allow_origin(&s)) + .transpose()?; + Ok(Self { localstack_endpoint: cli.localstack_endpoint.clone(), tunnelbroker_endpoint: cli.tunnelbroker_endpoint.clone(), opensearch_endpoint: cli.opensearch_endpoint.clone(), server_setup, keyserver_public_key, + allow_origin, }) } - - pub fn is_dev(&self) -> bool { - self.localstack_endpoint.is_some() - } } impl fmt::Debug for ServerConfig { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("ServerConfig") - .field("server_keypair", &"** redacted **") - .field("keyserver_auth_token", &"** redacted **") .field("localstack_endpoint", &self.localstack_endpoint) + .field("server_setup", &"** redacted **") + .field("keyserver_public_key", &self.keyserver_public_key) + .field("tunnelbroker_endpoint", &self.tunnelbroker_endpoint) + .field("opensearch_endpoint", &self.opensearch_endpoint) + .field("allow_origin_list", &"** redacted **") .finish() } } #[derive(Debug, derive_more::Display, derive_more::From)] pub enum Error { #[display(...)] Opaque(comm_opaque2::ProtocolError), #[display(...)] Io(io::Error), #[display(...)] Env(env::VarError), #[display(...)] Json(serde_json::Error), #[display(...)] Decode(DecodeError), + #[display(...)] + InvalidHeaderValue(http::header::InvalidHeaderValue), } fn get_server_setup( path: &path::Path, ) -> Result, Error> { let encoded_server_setup = if let Ok(env_setup) = env::var(OPAQUE_SERVER_SETUP) { info!( "Using OPAQUE server setup from env var: {}", OPAQUE_SERVER_SETUP ); env_setup } else if let Ok(file_setup) = fs::read_to_string(path) { info!("Using OPAQUE server setup from file: {}", path.display()); file_setup } else { error!("Unable to locate OPAQUE server setup. Please run `keygen` command and run Identity service again."); return Err(Error::Io(io::Error::new( io::ErrorKind::NotFound, "Missing server credentials", ))); }; let decoded_server_setup = general_purpose::STANDARD_NO_PAD.decode(encoded_server_setup)?; comm_opaque2::ServerSetup::deserialize(&decoded_server_setup) .map_err(Error::Opaque) } + +fn slice_to_allow_origin(origins: &str) -> Result { + let allow_origin_result: Result, Error> = origins + .split(',') + .map(|s| HeaderValue::from_str(s.trim()).map_err(Error::InvalidHeaderValue)) + .collect(); + let allow_origin_list = allow_origin_result?; + Ok(AllowOrigin::list(allow_origin_list)) +} diff --git a/services/identity/src/constants.rs b/services/identity/src/constants.rs index cae4a3dce..2c82573ac 100644 --- a/services/identity/src/constants.rs +++ b/services/identity/src/constants.rs @@ -1,221 +1,220 @@ use tokio::time::Duration; // Secrets pub const SECRETS_DIRECTORY: &str = "secrets"; pub const SECRETS_SETUP_FILE: &str = "server_setup.txt"; // DynamoDB // User table information, supporting opaque_ke 2.0 and X3DH information // Users can sign in either through username+password or Eth wallet. // // This structure should be aligned with the messages defined in // shared/protos/identity_unauthenticated.proto // // Structure for a user should be: // { // userID: String, // opaqueRegistrationData: Option, // username: Option, // walletAddress: Option, // devices: HashMap // } // // A device is defined as: // { // deviceType: String, # client or keyserver // keyPayload: String, // keyPayloadSignature: String, // identityPreKey: String, // identityPreKeySignature: String, // identityOneTimeKeys: Vec, // notifPreKey: String, // notifPreKeySignature: String, // notifOneTimeKeys: Vec, // socialProof: Option // } // } // // 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 = "opaqueRegistrationData"; pub const USERS_TABLE_USERNAME_ATTRIBUTE: &str = "username"; pub const USERS_TABLE_DEVICES_MAP_DEVICE_TYPE_ATTRIBUTE_NAME: &str = "deviceType"; pub const USERS_TABLE_WALLET_ADDRESS_ATTRIBUTE: &str = "walletAddress"; pub const USERS_TABLE_SOCIAL_PROOF_ATTRIBUTE_NAME: &str = "socialProof"; pub const USERS_TABLE_DEVICELIST_TIMESTAMP_ATTRIBUTE_NAME: &str = "deviceListTimestamp"; pub const USERS_TABLE_USERNAME_INDEX: &str = "username-index"; pub const USERS_TABLE_WALLET_ADDRESS_INDEX: &str = "walletAddress-index"; pub const ACCESS_TOKEN_TABLE: &str = "identity-tokens"; pub const ACCESS_TOKEN_TABLE_PARTITION_KEY: &str = "userID"; pub const ACCESS_TOKEN_SORT_KEY: &str = "signingPublicKey"; pub const ACCESS_TOKEN_TABLE_CREATED_ATTRIBUTE: &str = "created"; pub const ACCESS_TOKEN_TABLE_AUTH_TYPE_ATTRIBUTE: &str = "authType"; pub const ACCESS_TOKEN_TABLE_VALID_ATTRIBUTE: &str = "valid"; pub const ACCESS_TOKEN_TABLE_TOKEN_ATTRIBUTE: &str = "token"; pub const NONCE_TABLE: &str = "identity-nonces"; pub const NONCE_TABLE_PARTITION_KEY: &str = "nonce"; pub const NONCE_TABLE_CREATED_ATTRIBUTE: &str = "created"; pub const NONCE_TABLE_EXPIRATION_TIME_ATTRIBUTE: &str = "expirationTime"; pub const NONCE_TABLE_EXPIRATION_TIME_UNIX_ATTRIBUTE: &str = "expirationTimeUnix"; // Usernames reserved because they exist in Ashoat's keyserver already pub const RESERVED_USERNAMES_TABLE: &str = "identity-reserved-usernames"; pub const RESERVED_USERNAMES_TABLE_PARTITION_KEY: &str = "username"; pub const RESERVED_USERNAMES_TABLE_USER_ID_ATTRIBUTE: &str = "userID"; pub mod devices_table { /// table name pub const NAME: &str = "identity-devices"; pub const TIMESTAMP_INDEX_NAME: &str = "deviceList-timestamp-index"; /// partition key pub const ATTR_USER_ID: &str = "userID"; /// sort key pub const ATTR_ITEM_ID: &str = "itemID"; // itemID prefixes (one shouldn't be a prefix of the other) pub const DEVICE_ITEM_KEY_PREFIX: &str = "device-"; pub const DEVICE_LIST_KEY_PREFIX: &str = "devicelist-"; // device-specific attrs pub const ATTR_DEVICE_TYPE: &str = "deviceType"; pub const ATTR_DEVICE_KEY_INFO: &str = "deviceKeyInfo"; pub const ATTR_CONTENT_PREKEY: &str = "contentPreKey"; pub const ATTR_NOTIF_PREKEY: &str = "notifPreKey"; // IdentityKeyInfo constants pub const ATTR_KEY_PAYLOAD: &str = "keyPayload"; pub const ATTR_KEY_PAYLOAD_SIGNATURE: &str = "keyPayloadSignature"; // PreKey constants pub const ATTR_PREKEY: &str = "preKey"; pub const ATTR_PREKEY_SIGNATURE: &str = "preKeySignature"; // device-list-specific attrs pub const ATTR_TIMESTAMP: &str = "timestamp"; pub const ATTR_DEVICE_IDS: &str = "deviceIDs"; // migration-specific attrs pub const ATTR_CODE_VERSION: &str = "codeVersion"; pub const ATTR_LOGIN_TIME: &str = "loginTime"; } // One time keys table, which need to exist in their own table to ensure // atomicity of additions and removals pub mod one_time_keys_table { // The `PARTITION_KEY` will contain "notification_${deviceID}" or // "content_${deviceID}" to allow for both key sets to coexist in the same table pub const NAME: &str = "identity-one-time-keys"; pub const PARTITION_KEY: &str = "deviceID"; pub const DEVICE_ID: &str = PARTITION_KEY; pub const SORT_KEY: &str = "oneTimeKey"; pub const ONE_TIME_KEY: &str = SORT_KEY; } // One-time key constants for device info map pub const CONTENT_ONE_TIME_KEY: &str = "contentOneTimeKey"; pub const NOTIF_ONE_TIME_KEY: &str = "notifOneTimeKey"; // Tokio pub const MPSC_CHANNEL_BUFFER_CAPACITY: usize = 1; pub const IDENTITY_SERVICE_SOCKET_ADDR: &str = "[::]:50054"; pub const IDENTITY_SERVICE_WEBSOCKET_ADDR: &str = "[::]:51004"; pub const SOCKET_HEARTBEAT_TIMEOUT: Duration = Duration::from_secs(3); // Token pub const ACCESS_TOKEN_LENGTH: usize = 512; // Temporary config pub const AUTH_TOKEN: &str = "COMM_IDENTITY_SERVICE_AUTH_TOKEN"; pub const KEYSERVER_PUBLIC_KEY: &str = "KEYSERVER_PUBLIC_KEY"; // Nonce pub const NONCE_LENGTH: usize = 17; pub const NONCE_TTL_DURATION: i64 = 120; // seconds // Identity pub const DEFAULT_IDENTITY_ENDPOINT: &str = "http://localhost:50054"; // LocalStack pub const LOCALSTACK_ENDPOINT: &str = "LOCALSTACK_ENDPOINT"; // OPAQUE Server Setup pub const OPAQUE_SERVER_SETUP: &str = "OPAQUE_SERVER_SETUP"; // Identity Search pub const OPENSEARCH_ENDPOINT: &str = "OPENSEARCH_ENDPOINT"; pub const DEFAULT_OPENSEARCH_ENDPOINT: &str = "identity-search-domain.us-east-2.opensearch.localhost.local stack.cloud:4566"; pub const IDENTITY_SEARCH_INDEX: &str = "users"; pub const IDENTITY_SEARCH_RESULT_SIZE: u32 = 20; // Tunnelbroker pub const TUNNELBROKER_GRPC_ENDPOINT: &str = "TUNNELBROKER_GRPC_ENDPOINT"; pub const DEFAULT_TUNNELBROKER_ENDPOINT: &str = "http://localhost:50051"; // X3DH key management // Threshold for requesting more one_time keys pub const ONE_TIME_KEY_MINIMUM_THRESHOLD: usize = 5; // Number of keys to be refreshed when below the threshold pub const ONE_TIME_KEY_REFRESH_NUMBER: u32 = 5; // Minimum supported code versions pub const MIN_SUPPORTED_NATIVE_VERSION: u64 = 270; // Request metadata pub mod request_metadata { pub const CODE_VERSION: &str = "code_version"; pub const DEVICE_TYPE: &str = "device_type"; pub const USER_ID: &str = "user_id"; pub const DEVICE_ID: &str = "device_id"; pub const ACCESS_TOKEN: &str = "access_token"; } // CORS pub mod cors { use std::time::Duration; pub const DEFAULT_MAX_AGE: Duration = Duration::from_secs(24 * 60 * 60); pub const DEFAULT_EXPOSED_HEADERS: [&str; 3] = ["grpc-status", "grpc-message", "grpc-status-details-bin"]; pub const DEFAULT_ALLOW_HEADERS: [&str; 9] = [ "x-grpc-web", "content-type", "x-user-agent", "grpc-timeout", super::request_metadata::CODE_VERSION, super::request_metadata::DEVICE_TYPE, super::request_metadata::USER_ID, super::request_metadata::DEVICE_ID, super::request_metadata::ACCESS_TOKEN, ]; - pub const DEFAULT_ALLOW_ORIGIN: [&str; 2] = - ["https://web.comm.app", "http://localhost:3000"]; + pub const ALLOW_ORIGIN_LIST: &str = "ALLOW_ORIGIN_LIST"; } diff --git a/services/identity/src/cors.rs b/services/identity/src/cors.rs index 03dfc48a4..51667e1e0 100644 --- a/services/identity/src/cors.rs +++ b/services/identity/src/cors.rs @@ -1,35 +1,30 @@ -use http::{HeaderName, HeaderValue}; +use http::HeaderName; use tower_http::cors::{AllowOrigin, CorsLayer}; use crate::{config::CONFIG, constants::cors}; pub fn cors_layer() -> CorsLayer { - let allow_origin = if CONFIG.is_dev() { - AllowOrigin::mirror_request() - } else { - AllowOrigin::list( - cors::DEFAULT_ALLOW_ORIGIN - .iter() - .cloned() - .map(HeaderValue::from_static), - ) - }; + let allow_origin = CONFIG + .allow_origin + .clone() + .unwrap_or_else(AllowOrigin::mirror_request); + CorsLayer::new() .allow_origin(allow_origin) .allow_credentials(true) .max_age(cors::DEFAULT_MAX_AGE) .expose_headers( cors::DEFAULT_EXPOSED_HEADERS .iter() .cloned() .map(HeaderName::from_static) .collect::>(), ) .allow_headers( cors::DEFAULT_ALLOW_HEADERS .iter() .cloned() .map(HeaderName::from_static) .collect::>(), ) }