Skip to content

Commit b96357e

Browse files
authored
Move passkey configuration from hard-coded values to environment variables (#3818)
* refactor: add multi-platform origin validation for WebAuthn (Web/iOS/Android) Refactor WebAuthn origin verification to support multiple platforms using an origin pool (allowlist) approach instead of single origin validation. * add one more log * add some log * move key valuse into env variable. * update manifest file * fix rename issue
1 parent 4a07c15 commit b96357e

File tree

9 files changed

+130
-35
lines changed

9 files changed

+130
-35
lines changed

tee-worker/omni-executor/core/src/config.rs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,17 @@ pub struct OAuth2Config {
9090
pub client_secret: String,
9191
}
9292

93+
#[derive(Debug, Clone)]
94+
pub struct PasskeyConfig {
95+
pub rp_id: String,
96+
pub allowed_origins: Vec<String>,
97+
}
98+
9399
#[derive(Debug, Clone)]
94100
pub struct ConfigLoader {
95101
pub mailer_configs: HashMap<String, MailerConfig>,
96102
pub oauth2_configs: HashMap<String, HashMap<String, OAuth2Config>>, // client -> provider -> config
103+
pub passkey_configs: HashMap<String, PasskeyConfig>, // client -> passkey config
97104
pub ethereum_url: String,
98105
pub solana_url: String,
99106
pub bsc_url: String,
@@ -336,10 +343,12 @@ impl ConfigLoader {
336343

337344
let mailer_configs = Self::load_mailer_configs();
338345
let oauth2_configs = Self::load_oauth2_configs();
346+
let passkey_configs = Self::load_passkey_configs();
339347

340348
ConfigLoader {
341349
mailer_configs,
342350
oauth2_configs,
351+
passkey_configs,
343352
ethereum_url: append_key(&get("ethereum_url")),
344353
solana_url: append_key(&get("solana_url")),
345354
bsc_url: append_key(&get("bsc_url")),
@@ -530,4 +539,91 @@ impl ConfigLoader {
530539
let provider_key = provider.to_lowercase();
531540
self.oauth2_configs.get(&client_key)?.get(&provider_key).cloned()
532541
}
542+
543+
/// Load passkey configurations for all clients from environment variables
544+
/// Format: OE_PASSKEY_RP_ID_{CLIENT}, OE_PASSKEY_ALLOWED_ORIGINS_{CLIENT}
545+
/// ALLOWED_ORIGINS should be comma-separated list
546+
fn load_passkey_configs() -> HashMap<String, PasskeyConfig> {
547+
let mut configs = HashMap::new();
548+
549+
let env_vars: HashMap<String, String> = std::env::vars().collect();
550+
let mut clients = std::collections::HashSet::new();
551+
552+
// Find all unique client suffixes
553+
for key in env_vars.keys() {
554+
if key.starts_with("OE_PASSKEY_RP_ID_") {
555+
if let Some(client) = key.strip_prefix("OE_PASSKEY_RP_ID_") {
556+
info!("Found passkey configuration for client: {}", client);
557+
clients.insert(client.to_lowercase());
558+
}
559+
}
560+
}
561+
562+
info!("Total discovered passkey clients: {:?}", clients);
563+
564+
// If no clients configured, provide default localhost config
565+
if clients.is_empty() {
566+
warn!("No passkey configurations found in environment variables. Adding default localhost config.");
567+
let default_config = PasskeyConfig {
568+
rp_id: "localhost".to_string(),
569+
allowed_origins: vec![
570+
"http://localhost:3000".to_string(),
571+
"https://localhost:3000".to_string(),
572+
],
573+
};
574+
configs.insert("default".to_string(), default_config);
575+
return configs;
576+
}
577+
578+
// Load configuration for each client
579+
for client in clients {
580+
let client_upper = client.to_uppercase();
581+
582+
let rp_id = std::env::var(format!("OE_PASSKEY_RP_ID_{}", client_upper))
583+
.unwrap_or_else(|_| "localhost".to_string());
584+
585+
let allowed_origins_str =
586+
std::env::var(format!("OE_PASSKEY_ALLOWED_ORIGINS_{}", client_upper))
587+
.unwrap_or_else(|_| "http://localhost:3000,https://localhost:3000".to_string());
588+
589+
let allowed_origins: Vec<String> = allowed_origins_str
590+
.split(',')
591+
.map(|s| s.trim().to_string())
592+
.filter(|s| !s.is_empty())
593+
.collect();
594+
595+
if allowed_origins.is_empty() {
596+
warn!("No allowed origins configured for client '{}', skipping.", client);
597+
continue;
598+
}
599+
600+
let config = PasskeyConfig { rp_id, allowed_origins };
601+
602+
info!(
603+
"Loaded passkey config for client '{}': rp_id={}, origins={:?}",
604+
client, config.rp_id, config.allowed_origins
605+
);
606+
607+
configs.insert(client.clone(), config);
608+
}
609+
610+
configs
611+
}
612+
613+
/// Get passkey configuration for a specific client
614+
/// Falls back to "default" if client not found
615+
pub fn get_passkey_config(&self, client_id: &str) -> PasskeyConfig {
616+
let client_key = client_id.to_lowercase();
617+
self.passkey_configs
618+
.get(&client_key)
619+
.cloned()
620+
.or_else(|| self.passkey_configs.get("default").cloned())
621+
.unwrap_or_else(|| PasskeyConfig {
622+
rp_id: "localhost".to_string(),
623+
allowed_origins: vec![
624+
"http://localhost:3000".to_string(),
625+
"https://localhost:3000".to_string(),
626+
],
627+
})
628+
}
533629
}

tee-worker/omni-executor/crypto/src/passkey.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ impl PasskeyVerifier {
7878
pub fn verify_client_data_json<F>(
7979
client_data_json: &str,
8080
omni_account: &[u8; 32],
81-
expected_origin: &str,
81+
allowed_origins: &[&str],
8282
expected_type: &str,
8383
verify_and_consume_challenge: F,
8484
) -> Result<(), PasskeyError>
@@ -88,8 +88,8 @@ impl PasskeyVerifier {
8888
// Parse client data JSON
8989
let client_data = Self::parse_client_data_json(client_data_json)?;
9090

91-
// Verify origin
92-
if client_data.origin != expected_origin {
91+
// Verify origin against the allowed origins pool
92+
if !allowed_origins.iter().any(|&origin| origin == client_data.origin) {
9393
return Err(PasskeyError::OriginVerificationFailed);
9494
}
9595

tee-worker/omni-executor/omni-executor.manifest.template

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ loader.env.OE_GOOGLE_CLIENT_ID_WILDMETA = { passthrough = true }
7474
loader.env.OE_GOOGLE_CLIENT_SECRET_WILDMETA = { passthrough = true }
7575
loader.env.OE_APPLE_CLIENT_ID_WILDMETA = { passthrough = true }
7676
loader.env.OE_APPLE_CLIENT_SECRET_WILDMETA = { passthrough = true }
77+
loader.env.OE_PASSKEY_RP_ID_WILDMETA = { passthrough = true }
78+
loader.env.OE_PASSKEY_ALLOWED_ORIGINS_WILDMETA = { passthrough = true }
7779
loader.env.OE_ETHEREUM_URL = { passthrough = true }
7880
loader.env.OE_SOLANA_URL = { passthrough = true }
7981
loader.env.OE_BSC_URL = { passthrough = true }

tee-worker/omni-executor/rpc-server/src/methods/omni/attach_passkey.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,17 @@ pub fn register_attach_passkey<CrossChainIntentExecutor: IntentExecutor + Send +
5454
.user_id
5555
.to_omni_account(&params.client_id)
5656
.map_err_parse("Failed to convert to omni_account")?;
57-
let expected_origin = super::get_origin_for_client(&params.client_id);
57+
let allowed_origins =
58+
ctx.config_loader.get_passkey_config(&params.client_id).allowed_origins;
59+
let allowed_origins_refs: Vec<&str> =
60+
allowed_origins.iter().map(|s| s.as_str()).collect();
5861

5962
// Verify client data JSON and consume challenge
6063
let challenge_storage = PasskeyChallengeStorage::new(ctx.storage_db.clone());
6164
PasskeyVerifier::verify_client_data_json(
6265
&params.client_data_json,
6366
omni_account.as_ref(),
64-
expected_origin,
67+
&allowed_origins_refs,
6568
"webauthn.create", // For passkey registration/attachment
6669
|challenge, omni_account| {
6770
challenge_storage

tee-worker/omni-executor/rpc-server/src/methods/omni/get_hyperliquid_signature_data.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,15 +168,18 @@ pub fn register_get_hyperliquid_signature_data<
168168
e.to_detailed_error().to_rpc_error()
169169
})?;
170170

171-
// Determine expected origin based on client_id
172-
let expected_origin = super::get_origin_for_client(&params.client_id);
171+
// Get allowed origins for the client (supports web, iOS, and Android)
172+
let allowed_origins =
173+
ctx.config_loader.get_passkey_config(&params.client_id).allowed_origins;
174+
let allowed_origins_refs: Vec<&str> =
175+
allowed_origins.iter().map(|s| s.as_str()).collect();
173176

174177
// Verify client data JSON and consume challenge
175178
let challenge_storage = PasskeyChallengeStorage::new(ctx.storage_db.clone());
176179
PasskeyVerifier::verify_client_data_json(
177180
&attach_passkey_data.client_data_json,
178181
omni_account.as_ref(),
179-
expected_origin,
182+
&allowed_origins_refs,
180183
"webauthn.create", // For passkey registration/attachment
181184
|challenge, omni_account| {
182185
challenge_storage
@@ -400,6 +403,8 @@ pub fn register_get_hyperliquid_signature_data<
400403
},
401404
};
402405

406+
debug!("main_address: {:?}", main_address);
407+
403408
Ok(GetHyperliquidSignatureDataResponse {
404409
main_address,
405410
hyperliquid_signature_data: HyperliquidSignatureData { action, nonce, signature },

tee-worker/omni-executor/rpc-server/src/methods/omni/list_passkey.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use jsonrpsee::{types::ErrorObject, RpcModule};
77
use oe_core::intent::executor::IntentExecutor;
88
use oe_primitives::UserId;
99
use oe_storage::PasskeyStorage;
10+
use tracing::debug;
1011

1112
#[derive(Debug, Deserialize, Serialize, Clone)]
1213
pub struct ListPasskeyParams {
@@ -34,6 +35,8 @@ pub fn register_list_passkey<CrossChainIntentExecutor: IntentExecutor + Send + S
3435
.register_async_method("omni_listPasskey", |params, ctx, _| async move {
3536
let params = parse_rpc_params::<ListPasskeyParams>(params)?;
3637

38+
debug!("Received omni_listPasskey, params: {:?}", params);
39+
3740
let omni_account = params
3841
.user_id
3942
.to_omni_account(&params.client_id)

tee-worker/omni-executor/rpc-server/src/methods/omni/mod.rs

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -182,18 +182,3 @@ pub fn check_backend_response<T: Codec>(response: &ApiResponse<T>, op: &str) ->
182182
}
183183
Ok(())
184184
}
185-
186-
// Passkey helper functions
187-
pub fn get_rp_id_for_client(client_id: &str) -> &str {
188-
match client_id {
189-
"wildmeta" => "app.wildmeta.ai",
190-
_ => "localhost", // Development/testing
191-
}
192-
}
193-
194-
pub fn get_origin_for_client(client_id: &str) -> &str {
195-
match client_id {
196-
"wildmeta" => "https://app.wildmeta.ai",
197-
_ => "http://localhost:3000", // Development/testing
198-
}
199-
}

tee-worker/omni-executor/rpc-server/src/server.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use tracing::info;
2424
pub struct RpcContext<CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static> {
2525
pub shielding_key: ShieldingKey,
2626
pub storage_db: Arc<StorageDB>,
27+
pub config_loader: Arc<ConfigLoader>,
2728
pub mailer_factory: Arc<MailerFactory>,
2829
pub oauth2_factory: Arc<OAuth2ConfigFactory>,
2930
pub jwt_rsa_private_key: Vec<u8>,
@@ -51,6 +52,7 @@ impl<CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static>
5152
pub fn new(
5253
shielding_key: ShieldingKey,
5354
storage_db: Arc<StorageDB>,
55+
config_loader: Arc<ConfigLoader>,
5456
mailer_factory: Arc<MailerFactory>,
5557
oauth2_factory: Arc<OAuth2ConfigFactory>,
5658
jwt_rsa_private_key: Vec<u8>,
@@ -69,6 +71,7 @@ impl<CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static>
6971
) -> Self {
7072
Self {
7173
shielding_key,
74+
config_loader,
7275
storage_db,
7376
mailer_factory,
7477
oauth2_factory,
@@ -111,11 +114,12 @@ pub async fn start_server<CrossChainIntentExecutor: IntentExecutor + Send + Sync
111114
) -> Result<(), Box<dyn std::error::Error>> {
112115
let config_loader_arc = Arc::new(config_loader.clone());
113116
let mailer_factory = Arc::new(MailerFactory::new(config_loader_arc.clone()));
114-
let oauth2_factory = Arc::new(OAuth2ConfigFactory::new(config_loader_arc));
117+
let oauth2_factory = Arc::new(OAuth2ConfigFactory::new(config_loader_arc.clone()));
115118

116119
let ctx = RpcContext::new(
117120
shielding_key,
118121
storage_db,
122+
config_loader_arc,
119123
mailer_factory,
120124
oauth2_factory,
121125
jwt_rsa_private_key.clone(),

tee-worker/omni-executor/rpc-server/src/verify_auth.rs

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,6 @@ pub fn verify_passkey_authentication<
311311
ctx: Arc<RpcContext<CrossChainIntentExecutor>>,
312312
passkey_data: &PasskeyData,
313313
) -> Result<(), AuthenticationError> {
314-
use crate::methods::omni::{get_origin_for_client, get_rp_id_for_client};
315314
use oe_crypto::passkey::{ClientData, PasskeyVerifier};
316315
use oe_storage::PasskeyStorage;
317316

@@ -324,13 +323,12 @@ pub fn verify_passkey_authentication<
324323
AuthenticationError::PasskeyError(format!("Failed to parse client data: {}", e))
325324
})?;
326325

327-
let expected_origin = get_origin_for_client(&passkey_data.client_id);
328-
if client_data.origin != expected_origin {
329-
return Err(AuthenticationError::PasskeyError(format!(
330-
"Client data origin mismatch: expected '{}', got '{}'",
331-
expected_origin,
332-
client_data.origin.as_str()
333-
)));
326+
let allowed_origins =
327+
ctx.config_loader.get_passkey_config(&passkey_data.client_id).allowed_origins;
328+
if !allowed_origins.iter().any(|origin| origin == &client_data.origin) {
329+
return Err(AuthenticationError::PasskeyError(
330+
oe_crypto::passkey::PasskeyError::OriginVerificationFailed.to_string(),
331+
));
334332
}
335333

336334
const EXPECTED_PASSKEY_TYPE: &str = "webauthn.get";
@@ -391,9 +389,8 @@ pub fn verify_passkey_authentication<
391389
// CRITICAL SECURITY CHECK: Verify RP ID hash
392390
// The first 32 bytes of auth data must be SHA-256(RP ID) to prevent phishing attacks
393391
// This ensures the authenticator signed for the correct domain
394-
let expected_rp_id = get_rp_id_for_client(&passkey_data.client_id);
395-
396-
PasskeyVerifier::verify_rp_id_hash(&auth_data_bytes, expected_rp_id).map_err(|e| {
392+
let expected_rp_id = ctx.config_loader.get_passkey_config(&passkey_data.client_id).rp_id;
393+
PasskeyVerifier::verify_rp_id_hash(&auth_data_bytes, &expected_rp_id).map_err(|e| {
397394
AuthenticationError::PasskeyError(format!(
398395
"RP ID validation failed: {}. Expected RP ID: '{}' for client_id: '{}'",
399396
e, expected_rp_id, passkey_data.client_id

0 commit comments

Comments
 (0)