diff --git a/Cargo.lock b/Cargo.lock index a8c5b01..4b47eb4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3401,7 +3401,7 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.10", + "rustls-webpki 0.103.13", "subtle", "zeroize", ] @@ -3440,9 +3440,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring", diff --git a/deny.toml b/deny.toml index 61d9b14..27f4aec 100644 --- a/deny.toml +++ b/deny.toml @@ -8,7 +8,10 @@ unknown-registry = "deny" [advisories] ignore = [ { id = "RUSTSEC-2023-0071", reason = "a potential timing attack to recover a private key from rsa crate. however, the rsa library is used either for tests or signature verification, so private key material is not exposed." }, - { id = "RUSTSEC-2026-0049", reason = "`rustls-webpki` (requires upstream dep updates); low severity, requires compromised CA (2026-03-23)"} + { id = "RUSTSEC-2026-0049", reason = "`rustls-webpki` (requires upstream dep updates); low severity, requires compromised CA (2026-03-23)" }, + { id = "RUSTSEC-2026-0098", reason = "`rustls-webpki` 0.101.7 via AWS SDK rustls 0.21.x; no fix in the 0.101.x range, requires upstream AWS SDK to bump rustls" }, + { id = "RUSTSEC-2026-0099", reason = "`rustls-webpki` 0.101.7 via AWS SDK rustls 0.21.x; no fix in the 0.101.x range, requires upstream AWS SDK to bump rustls" }, + { id = "RUSTSEC-2026-0104", reason = "`rustls-webpki` 0.101.7 via AWS SDK rustls 0.21.x; no fix in the 0.101.x range, requires upstream AWS SDK to bump rustls" }, ] [licenses] diff --git a/src/auth.rs b/src/auth.rs index ac68c74..ddebb85 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -140,6 +140,7 @@ impl AuthHandler { expected_factor_scope: FactorScope, expected_challenge_context: ChallengeContext, challenge_token: String, + client_name: Option<&str>, ) -> Result<(String, BackupMetadata), AuthError> { // Step 1: Verify that the authorization type is supported // `ECKeyPair` is the only supported factor type for `Sync` scope, other factors are rejected. @@ -184,6 +185,7 @@ impl AuthHandler { signature, &challenge_token_payload, expected_factor_scope, + client_name, ) .await? } @@ -222,6 +224,7 @@ impl AuthHandler { expected_challenge_context: ChallengeContext, turnkey_provider_id: Option, is_sync_factor: bool, + client_name: Option<&str>, ) -> Result { // Step 1: Verify that the authorization type is valid for the factor scope // Sync factors must be EC keypairs - passkeys and OIDC accounts are not allowed as sync factors @@ -267,6 +270,7 @@ impl AuthHandler { signature, &challenge_token_payload, turnkey_provider_id.ok_or_else(|| AuthError::MissingTurnkeyProviderId)?, + client_name, ) .await? } @@ -439,10 +443,11 @@ impl AuthHandler { signature: &str, challenge_token_payload: &[u8], turnkey_provider_id: String, + client_name: Option<&str>, ) -> Result<(Factor, FactorToLookup), AuthError> { let claims = self .oidc_token_verifier - .verify_token(oidc_token, public_key.to_string()) + .verify_token(oidc_token, public_key.to_string(), client_name) .await?; verify_signature(public_key, signature, challenge_token_payload)?; @@ -486,10 +491,11 @@ impl AuthHandler { signature: &str, challenge_token_payload: &[u8], expected_factor_scope: FactorScope, + client_name: Option<&str>, ) -> Result<(String, BackupMetadata), AuthError> { let claims = self .oidc_token_verifier - .verify_token(oidc_token, public_key.to_string()) + .verify_token(oidc_token, public_key.to_string(), client_name) .await?; verify_signature(public_key, signature, challenge_token_payload)?; diff --git a/src/headers.rs b/src/headers.rs new file mode 100644 index 0000000..415a554 --- /dev/null +++ b/src/headers.rs @@ -0,0 +1,4 @@ +use http::HeaderName; + +pub static CLIENT_NAME: HeaderName = HeaderName::from_static("client-name"); +pub static CLIENT_VERSION: HeaderName = HeaderName::from_static("client-version"); diff --git a/src/lib.rs b/src/lib.rs index da525fc..04bd9b0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ pub mod auth; pub mod backup_storage; pub mod challenge_manager; pub mod factor_lookup; +pub mod headers; pub mod kms_jwe; pub mod middleware; pub mod oidc_nonce_verifier; diff --git a/src/oidc_token_verifier.rs b/src/oidc_token_verifier.rs index 88ab325..5f8631a 100644 --- a/src/oidc_token_verifier.rs +++ b/src/oidc_token_verifier.rs @@ -103,6 +103,7 @@ impl OidcTokenVerifier { &self, token: &OidcToken, expected_public_key_sec1_base64: String, + client_name: Option<&str>, ) -> Result, OidcTokenVerifierError> { // Step 1: Extract the token and other parameters based on the OIDC provider let (oidc_token, jwk_set_url, client_id, issuer_url) = match token { @@ -115,7 +116,7 @@ impl OidcTokenVerifier { OidcToken::Apple { token } => ( token, self.environment.apple_jwk_set_url(), - self.environment.apple_client_id(), + self.environment.apple_client_id(client_name), self.environment.apple_issuer_url(), ), }; @@ -123,12 +124,12 @@ impl OidcTokenVerifier { // Load the public keys from the OIDC provider let signature_keys = self.get_jwk_set(&jwk_set_url).await?.as_ref().clone(); - // Step 3: Create the token verifier + // Step 3: Create the token verifier. let token_verifier = CoreIdTokenVerifier::new_public_client(client_id, issuer_url.clone(), signature_keys) .set_issue_time_verifier_fn(issue_time_verifier); - // Step 4: Verify the token and extract claims + // Step 4: Parse the OIDC token let oidc_token = CoreIdToken::from_str(oidc_token).map_err(|err| { tracing::warn!(message = "Failed to parse OIDC token", err = ?err); OidcTokenVerifierError::TokenParseError @@ -141,11 +142,11 @@ impl OidcTokenVerifier { OidcNonceVerifier::new(expected_public_key_sec1_base64), ) .map_err(|err| { - tracing::error!(message = "Token verification error", err = ?err, issuer = ?issuer_url); + tracing::error!(message = "Token verification error", err = ?err, issuer = ?issuer_url, client_name = ?client_name); match err { ClaimsVerificationError::InvalidNonce(e) => OidcTokenVerifierError::InvalidNonce(e.clone()), - _ => OidcTokenVerifierError::TokenVerificationError + _ => OidcTokenVerifierError::TokenVerificationError, } })?; @@ -226,16 +227,17 @@ mod tests { provider: OidcProvider, token: String, public_key: String, + client_name: Option<&str>, ) -> Result, OidcTokenVerifierError> { match provider { OidcProvider::Google => { verifier - .verify_token(&OidcToken::Google { token }, public_key) + .verify_token(&OidcToken::Google { token }, public_key, client_name) .await } OidcProvider::Apple => { verifier - .verify_token(&OidcToken::Apple { token }, public_key) + .verify_token(&OidcToken::Apple { token }, public_key, client_name) .await } } @@ -257,8 +259,14 @@ mod tests { let token = oidc_server.generate_token(provider.into(), None, &public_key); // Verify the token - let result = - verify_token_for_provider(&verifier, provider, token, public_key.clone()).await; + let result = verify_token_for_provider( + &verifier, + provider, + token, + public_key.clone(), + Some("ios-id"), + ) + .await; // The test should pass with a valid token assert!(result.is_ok()); @@ -281,8 +289,14 @@ mod tests { let token = oidc_server.generate_expired_token(provider.into()); // Verify the token - let result = - verify_token_for_provider(&verifier, provider, token, public_key.clone()).await; + let result = verify_token_for_provider( + &verifier, + provider, + token, + public_key.clone(), + Some("ios-id"), + ) + .await; // The test should fail with an expired token assert!(result.is_err()); @@ -309,8 +323,14 @@ mod tests { let token = oidc_server.generate_incorrectly_signed_token(provider.into()); // Verify the token - let result = - verify_token_for_provider(&verifier, provider, token, public_key.clone()).await; + let result = verify_token_for_provider( + &verifier, + provider, + token, + public_key.clone(), + Some("ios-id"), + ) + .await; // The test should fail with an incorrectly signed token assert!(result.is_err()); @@ -338,8 +358,14 @@ mod tests { oidc_server.generate_token_with_incorrect_issuer(provider.into(), &public_key); // Verify the token - let result = - verify_token_for_provider(&verifier, provider, token, public_key.clone()).await; + let result = verify_token_for_provider( + &verifier, + provider, + token, + public_key.clone(), + Some("ios-id"), + ) + .await; // The test should fail with an incorrect issuer assert!(result.is_err()); @@ -367,8 +393,14 @@ mod tests { oidc_server.generate_token_with_incorrect_audience(provider.into(), &public_key); // Verify the token - let result = - verify_token_for_provider(&verifier, provider, token, public_key.clone()).await; + let result = verify_token_for_provider( + &verifier, + provider, + token, + public_key.clone(), + Some("ios-id"), + ) + .await; // The test should fail with an incorrect audience assert!(result.is_err()); @@ -396,8 +428,14 @@ mod tests { oidc_server.generate_token_with_incorrect_issued_at(provider.into(), &public_key); // Verify the token - let result = - verify_token_for_provider(&verifier, provider, token, public_key.clone()).await; + let result = verify_token_for_provider( + &verifier, + provider, + token, + public_key.clone(), + Some("ios-id"), + ) + .await; // The test should fail with an incorrect issued_at assert!(result.is_err()); @@ -431,9 +469,14 @@ mod tests { let token = oidc_server.generate_token(provider.into(), None, &correct_public_key); // Verify the token but pass a different public key - let result = - verify_token_for_provider(&verifier, provider, token, incorrect_public_key.clone()) - .await; + let result = verify_token_for_provider( + &verifier, + provider, + token, + incorrect_public_key.clone(), + Some("ios-id"), + ) + .await; // The test should fail with an incorrect public key assert!(result.is_err()); @@ -460,6 +503,7 @@ mod tests { OidcProvider::Google, token.clone(), public_key.clone(), + None, ) .await .unwrap(); // The first time is successful @@ -470,6 +514,7 @@ mod tests { OidcProvider::Google, token.clone(), public_key.clone(), + None, ) .await; assert!(result.is_err()); @@ -484,9 +529,14 @@ mod tests { let new_token = oidc_server.generate_token(OidcProvider::Google.into(), None, &public_key); assert_ne!(token, new_token); - let result = - verify_token_for_provider(&verifier, OidcProvider::Google, token, public_key.clone()) - .await; + let result = verify_token_for_provider( + &verifier, + OidcProvider::Google, + token, + public_key.clone(), + None, + ) + .await; assert!(result.is_err()); assert!(matches!( result, diff --git a/src/routes/add_factor.rs b/src/routes/add_factor.rs index 6865870..9e4edce 100644 --- a/src/routes/add_factor.rs +++ b/src/routes/add_factor.rs @@ -4,6 +4,7 @@ use crate::auth::{AuthError, AuthHandler}; use crate::backup_storage::BackupStorage; use crate::challenge_manager::{ChallengeContext, ChallengeManager, ChallengeType, NewFactorType}; use crate::factor_lookup::{FactorLookup, FactorScope, FactorToLookup}; +use crate::headers::CLIENT_NAME; use crate::turnkey_activity::{ verify_turnkey_activity_parameters, verify_turnkey_activity_webauthn_stamp, }; @@ -15,6 +16,7 @@ use axum::{Extension, Json}; use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}; use base64::Engine; use chrono::Duration; +use http::HeaderMap; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use webauthn_rs::prelude::PublicKeyCredential; @@ -64,8 +66,10 @@ pub async fn handler( Extension(challenge_manager): Extension>, Extension(factor_lookup): Extension>, Extension(auth_handler): Extension, + headers: HeaderMap, request: Json, ) -> Result, ErrorResponse> { + let client_name = headers.get(&CLIENT_NAME).and_then(|v| v.to_str().ok()); // Step 1: Check authorization for the existing factor and get the backup ID let (backup_id, expected_new_factor) = match &request.existing_factor_authorization { Authorization::Passkey { credential, .. } => { @@ -204,7 +208,9 @@ pub async fn handler( (backup_id, new_factor_type) } Authorization::OidcAccount { .. } | Authorization::EcKeypair { .. } => { - // TODO/FIXME: Implement the logic for verifying the existing factor for OIDC and EC keypair + // TODO/FIXME: Implement the logic for verifying the existing factor for OIDC and EC keypair. + // When implementing OIDC here, pass `client_name` to `validate_oidc_authentication` so the + // correct provider client_id (per `Environment::{google,apple}_client_id`) is used. return Err(ErrorResponse::bad_request("not_supported", "Not supported")); } }; @@ -252,6 +258,7 @@ pub async fn handler( ChallengeContext::AddFactorByNewFactor {}, request.turnkey_provider_id.clone(), false, // not a sync factor + client_name, ) .await?; diff --git a/src/routes/add_sync_factor.rs b/src/routes/add_sync_factor.rs index 142739d..326f17d 100644 --- a/src/routes/add_sync_factor.rs +++ b/src/routes/add_sync_factor.rs @@ -4,9 +4,11 @@ use crate::auth::AuthHandler; use crate::backup_storage::BackupStorage; use crate::challenge_manager::ChallengeContext; use crate::factor_lookup::{FactorLookup, FactorScope}; +use crate::headers::CLIENT_NAME; use crate::redis_cache::RedisCacheManager; use crate::types::{Authorization, ErrorResponse}; use axum::{Extension, Json}; +use http::HeaderMap; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -35,8 +37,11 @@ pub async fn handler( Extension(factor_lookup): Extension>, Extension(redis_cache_manager): Extension>, Extension(auth_handler): Extension, + headers: HeaderMap, request: Json, ) -> Result, ErrorResponse> { + let client_name = headers.get(&CLIENT_NAME).and_then(|v| v.to_str().ok()); + // Step 1: Validate the new sync factor using AuthHandler let validation_result = auth_handler .validate_factor_registration( @@ -45,6 +50,7 @@ pub async fn handler( ChallengeContext::AddSyncFactor {}, None, true, // is_sync_factor + client_name, ) .await?; diff --git a/src/routes/create_backup.rs b/src/routes/create_backup.rs index 6271f8a..0787da8 100644 --- a/src/routes/create_backup.rs +++ b/src/routes/create_backup.rs @@ -4,6 +4,7 @@ use crate::auth::AuthHandler; use crate::backup_storage::BackupStorage; use crate::challenge_manager::ChallengeContext; use crate::factor_lookup::{FactorLookup, FactorScope}; +use crate::headers::CLIENT_NAME; use crate::redis_cache::RedisCacheManager; use crate::types::backup_metadata::{BackupMetadata, ExportedBackupMetadata}; use crate::types::encryption_key::BackupEncryptionKey; @@ -12,6 +13,7 @@ use crate::utils::extract_fields_from_multipart; use crate::{normalize_hex_32, validate_backup_account_id}; use axum::extract::Multipart; use axum::{extract::Extension, Json}; +use http::HeaderMap; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -57,8 +59,10 @@ pub async fn handler( Extension(factor_lookup): Extension>, Extension(auth_handler): Extension, Extension(redis_cache_manager): Extension>, + headers: HeaderMap, mut multipart: Multipart, ) -> Result, ErrorResponse> { + let client_name = headers.get(&CLIENT_NAME).and_then(|v| v.to_str().ok()); // Step 1: Parse multipart form data. It should include the main JSON payload with parameters // and the attached backup file. let multipart_fields = extract_fields_from_multipart(&mut multipart).await?; @@ -106,6 +110,7 @@ pub async fn handler( ChallengeContext::Create {}, request.turnkey_provider_id.clone(), false, // not a sync factor + client_name, ) .await?; @@ -121,6 +126,7 @@ pub async fn handler( ChallengeContext::Create {}, None, true, // is a sync factor + client_name, ) .await?; diff --git a/src/routes/delete_backup.rs b/src/routes/delete_backup.rs index af5af3c..bcca656 100644 --- a/src/routes/delete_backup.rs +++ b/src/routes/delete_backup.rs @@ -4,9 +4,11 @@ use crate::auth::AuthHandler; use crate::backup_storage::BackupStorage; use crate::challenge_manager::ChallengeContext; use crate::factor_lookup::{FactorLookup, FactorScope}; +use crate::headers::CLIENT_NAME; use crate::types::{Authorization, ErrorResponse}; use axum::http::StatusCode; use axum::{Extension, Json}; +use http::HeaderMap; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use tracing::Instrument; @@ -23,8 +25,11 @@ pub async fn handler( Extension(backup_storage): Extension>, Extension(factor_lookup): Extension>, Extension(auth_handler): Extension, + headers: HeaderMap, request: Json, ) -> Result { + let client_name = headers.get(&CLIENT_NAME).and_then(|v| v.to_str().ok()); + // Step 1: Auth. Verify the solved challenge let (backup_id, _backup_metadata) = auth_handler .verify( @@ -32,6 +37,7 @@ pub async fn handler( FactorScope::Sync, ChallengeContext::DeleteBackup {}, request.challenge_token.clone(), + client_name, ) .await?; diff --git a/src/routes/delete_factor.rs b/src/routes/delete_factor.rs index 218936d..1dadfb5 100644 --- a/src/routes/delete_factor.rs +++ b/src/routes/delete_factor.rs @@ -4,11 +4,13 @@ use crate::auth::AuthHandler; use crate::backup_storage::{BackupStorage, DeletionResult}; use crate::challenge_manager::ChallengeContext; use crate::factor_lookup::{FactorLookup, FactorScope}; +use crate::headers::CLIENT_NAME; use crate::types::backup_metadata::ExportedBackupMetadata; use crate::types::encryption_key::BackupEncryptionKey; use crate::types::{Authorization, Environment, ErrorResponse}; use aide::transform::TransformOperation; use axum::{Extension, Json}; +use http::HeaderMap; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use tracing::Instrument; @@ -53,8 +55,10 @@ pub async fn handler( Extension(backup_storage): Extension>, Extension(factor_lookup): Extension>, Extension(auth_handler): Extension, + headers: HeaderMap, request: Json, ) -> Result, ErrorResponse> { + let client_name = headers.get(&CLIENT_NAME).and_then(|v| v.to_str().ok()); // Step 1: Extract the factor IDs from the request let factor_id = request.factor_id.clone(); let encryption_key = request.encryption_key.clone(); @@ -79,6 +83,7 @@ pub async fn handler( factor_id: request.factor_id.clone(), }, request.challenge_token.clone(), + client_name, ) .await?; diff --git a/src/routes/retrieve_from_challenge.rs b/src/routes/retrieve_from_challenge.rs index 213eea6..5b94995 100644 --- a/src/routes/retrieve_from_challenge.rs +++ b/src/routes/retrieve_from_challenge.rs @@ -4,6 +4,7 @@ use crate::auth::AuthHandler; use crate::backup_storage::BackupStorage; use crate::challenge_manager::ChallengeContext; use crate::factor_lookup::FactorScope; +use crate::headers::{CLIENT_NAME, CLIENT_VERSION}; use crate::redis_cache::RedisCacheManager; use crate::types::backup_metadata::ExportedBackupMetadata; use crate::types::{Authorization, ErrorResponse}; @@ -49,6 +50,8 @@ pub async fn handler( headers: HeaderMap, request: Json, ) -> Result, ErrorResponse> { + let client_name = headers.get(&CLIENT_NAME).and_then(|v| v.to_str().ok()); + // Step 1: Auth. Verify the solved challenge let (backup_id, backup_metadata) = auth_handler .verify( @@ -56,20 +59,16 @@ pub async fn handler( FactorScope::Main, ChallengeContext::Retrieve {}, request.challenge_token.clone(), + client_name, ) .await?; let client_version = headers - .get("client-version") - .and_then(|v| v.to_str().ok()) - .unwrap_or_default(); - - let client_name = headers - .get("client-name") + .get(&CLIENT_VERSION) .and_then(|v| v.to_str().ok()) .unwrap_or_default(); - let span = tracing::info_span!("retrieve_backup_from_challenge", backup_id = %backup_id, client_version = %client_version, client_name = %client_name); + let span = tracing::info_span!("retrieve_backup_from_challenge", backup_id = %backup_id, client_version = %client_version, client_name = %client_name.unwrap_or_default()); async move { // Step 2: Fetch the backup from S3 diff --git a/src/routes/retrieve_metadata.rs b/src/routes/retrieve_metadata.rs index 566306b..4b364b7 100644 --- a/src/routes/retrieve_metadata.rs +++ b/src/routes/retrieve_metadata.rs @@ -4,9 +4,11 @@ use crate::auth::{AuthError, AuthHandler}; use crate::backup_storage::BackupStorage; use crate::challenge_manager::ChallengeContext; use crate::factor_lookup::FactorScope; +use crate::headers::CLIENT_NAME; use crate::types::backup_metadata::ExportedBackupMetadata; use crate::types::{Authorization, ErrorResponse}; use axum::{extract::Extension, Json}; +use http::HeaderMap; use schemars::JsonSchema; use serde::Deserialize; @@ -28,8 +30,11 @@ pub struct RetrieveMetadataRequest { pub async fn handler( Extension(auth_handler): Extension, Extension(backup_storage): Extension>, + headers: HeaderMap, Json(request): Json, ) -> Result, ErrorResponse> { + let client_name = headers.get(&CLIENT_NAME).and_then(|v| v.to_str().ok()); + // Step 1: Auth. Verify the solved challenge let backup_metadata = auth_handler .verify( @@ -37,6 +42,7 @@ pub async fn handler( FactorScope::Sync, ChallengeContext::RetrieveMetadata {}, request.challenge_token, + client_name, ) .await; diff --git a/src/routes/sync_backup.rs b/src/routes/sync_backup.rs index 1f4d416..417aac3 100644 --- a/src/routes/sync_backup.rs +++ b/src/routes/sync_backup.rs @@ -4,12 +4,14 @@ use crate::auth::AuthHandler; use crate::backup_storage::BackupStorage; use crate::challenge_manager::ChallengeContext; use crate::factor_lookup::FactorScope; +use crate::headers::CLIENT_NAME; use crate::normalize_hex_32; use crate::redis_cache::RedisCacheManager; use crate::types::{Authorization, Environment, ErrorResponse}; use crate::utils::extract_fields_from_multipart; use axum::extract::Multipart; use axum::{extract::Extension, Json}; +use http::HeaderMap; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use tracing::Instrument; @@ -53,8 +55,10 @@ pub async fn handler( Extension(backup_storage): Extension>, Extension(auth_handler): Extension, Extension(redis_cache_manager): Extension>, + headers: HeaderMap, mut multipart: Multipart, ) -> Result, ErrorResponse> { + let client_name = headers.get(&CLIENT_NAME).and_then(|v| v.to_str().ok()); // Step 1: Parse multipart form data. It should include the main JSON payload with parameters // and the attached backup file. let multipart_fields = extract_fields_from_multipart(&mut multipart).await?; @@ -99,6 +103,7 @@ pub async fn handler( FactorScope::Sync, ChallengeContext::Sync {}, request.challenge_token, + client_name, ) .await?; diff --git a/src/routes/verify_factor.rs b/src/routes/verify_factor.rs index 31e2541..e6274d8 100644 --- a/src/routes/verify_factor.rs +++ b/src/routes/verify_factor.rs @@ -1,6 +1,7 @@ use crate::auth::AuthHandler; use crate::challenge_manager::ChallengeContext; use crate::factor_lookup::FactorScope; +use crate::headers::{CLIENT_NAME, CLIENT_VERSION}; use crate::types::{Authorization, ErrorResponse}; use aide::transform::TransformOperation; use axum::{Extension, Json}; @@ -35,26 +36,24 @@ pub async fn handler( headers: HeaderMap, request: Json, ) -> Result, ErrorResponse> { + let client_name = headers.get(&CLIENT_NAME).and_then(|v| v.to_str().ok()); + let (backup_id, _backup_metadata) = auth_handler .verify( &request.authorization, FactorScope::Main, ChallengeContext::VerifyFactor {}, request.challenge_token.clone(), + client_name, ) .await?; let client_version = headers - .get("client-version") - .and_then(|v| v.to_str().ok()) - .unwrap_or_default(); - - let client_name = headers - .get("client-name") + .get(&CLIENT_VERSION) .and_then(|v| v.to_str().ok()) .unwrap_or_default(); - let span = tracing::info_span!("verify_factor", backup_id = %backup_id, client_version = %client_version, client_name = %client_name); + let span = tracing::info_span!("verify_factor", backup_id = %backup_id, client_version = %client_version, client_name = %client_name.unwrap_or_default()); async move { Ok(Json(VerifyFactorResponse { backup_id })) } .instrument(span) diff --git a/src/types/environment.rs b/src/types/environment.rs index e71fa65..7725a08 100644 --- a/src/types/environment.rs +++ b/src/types/environment.rs @@ -216,7 +216,6 @@ impl Environment { } } - /// The client ID for the Google OIDC provider #[must_use] pub fn google_client_id(&self) -> ClientId { match self { @@ -262,17 +261,22 @@ impl Environment { } } - /// The client ID for the Apple OIDC provider + /// Returns the Apple OIDC client ID to use for token verification. /// - /// # Panics - /// Will not panic. Values are hardcoded per environment. + /// Uses the `client-name` header to select the correct bundle ID: + /// - `"ios-id"` → World ID app (`org.world.id` / `org.world.staging.id`) + /// - anything else → World Money app (`org.worldcoin.insight` / `org.worldcoin.insight.staging`) #[must_use] - pub fn apple_client_id(&self) -> ClientId { - match self { - Self::Production => ClientId::new("org.worldcoin.insight".to_string()), - Self::Staging => ClientId::new("org.worldcoin.insight.staging".to_string()), - Self::Development { .. } => ClientId::new("placeholder".to_string()), - } + pub fn apple_client_id(&self, client_name: Option<&str>) -> ClientId { + let bundle_id = match (self, client_name) { + (Self::Production, Some("ios-id" | "android-id")) => "org.world.id", + (Self::Production, _) => "org.worldcoin.insight", + (Self::Staging, Some("ios-id")) => "org.world.staging.id", + (Self::Staging, Some("android-id")) => "org.world.id.staging", + (Self::Staging, _) => "org.worldcoin.insight.staging", + (Self::Development { .. }, _) => "placeholder", + }; + ClientId::new(bundle_id.to_string()) } /// Issuer URL for the Apple OIDC provider @@ -313,3 +317,94 @@ impl Environment { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_apple_client_id_production() { + let env = Environment::Production; + assert_eq!(env.apple_client_id(None).as_str(), "org.worldcoin.insight"); + assert_eq!(env.apple_client_id(Some("ios-id")).as_str(), "org.world.id"); + assert_eq!( + env.apple_client_id(Some("android-id")).as_str(), + "org.world.id" + ); + assert_eq!( + env.apple_client_id(Some("ios-money")).as_str(), + "org.worldcoin.insight" + ); + assert_eq!( + env.apple_client_id(Some("unknown")).as_str(), + "org.worldcoin.insight" + ); + } + + #[test] + fn test_apple_client_id_staging() { + let env = Environment::Staging; + assert_eq!( + env.apple_client_id(None).as_str(), + "org.worldcoin.insight.staging" + ); + assert_eq!( + env.apple_client_id(Some("ios-id")).as_str(), + "org.world.staging.id" + ); + assert_eq!( + env.apple_client_id(Some("android-id")).as_str(), + "org.world.id.staging" + ); + assert_eq!( + env.apple_client_id(Some("ios-money")).as_str(), + "org.worldcoin.insight.staging" + ); + assert_eq!( + env.apple_client_id(Some("unknown")).as_str(), + "org.worldcoin.insight.staging" + ); + } + + #[test] + fn test_apple_client_id_development() { + let env = Environment::development(None); + assert_eq!(env.apple_client_id(None).as_str(), "placeholder"); + assert_eq!(env.apple_client_id(Some("ios-id")).as_str(), "placeholder"); + assert_eq!( + env.apple_client_id(Some("android-id")).as_str(), + "placeholder" + ); + assert_eq!( + env.apple_client_id(Some("ios-money")).as_str(), + "placeholder" + ); + } + + #[test] + fn test_google_client_id_production() { + let env = Environment::Production; + assert_eq!( + env.google_client_id().as_str(), + "730924878354-jvi49m445q2mv6s1dn4oklm8i4vlpct9.apps.googleusercontent.com" + ); + } + + #[test] + fn test_google_client_id_staging() { + let env = Environment::Staging; + assert_eq!( + env.google_client_id().as_str(), + "730924878354-jvi49m445q2mv6s1dn4oklm8i4vlpct9.apps.googleusercontent.com" + ); + } + + #[test] + fn test_google_client_id_development() { + let env = Environment::development(None); + assert_eq!( + env.google_client_id().as_str(), + "949370763172-0pu3c8c3rmp8ad665jsb1qkf8lai592i.apps.googleusercontent.com" + ); + } +}