diff --git a/parachain/primitives/src/identity.rs b/parachain/primitives/src/identity.rs index 7cd00ac379..0607d4e2ff 100644 --- a/parachain/primitives/src/identity.rs +++ b/parachain/primitives/src/identity.rs @@ -335,6 +335,9 @@ pub enum Identity { #[codec(index = 10)] Passkey(IdentityString), + + #[codec(index = 11)] + Apple(IdentityString), } impl Identity { @@ -348,6 +351,7 @@ impl Identity { | Self::Google(..) | Self::Pumpx(..) | Self::Passkey(..) + | Self::Apple(..) ) } @@ -383,7 +387,8 @@ impl Identity { | Identity::Email(_) | Identity::Google(_) | Identity::Pumpx(_) - | Identity::Passkey(_) => Vec::new(), + | Identity::Passkey(_) + | Identity::Apple(_) => Vec::new(), } } @@ -402,7 +407,8 @@ impl Identity { | Identity::Email(_) | Identity::Google(_) | Identity::Pumpx(_) - | Identity::Passkey(_) => networks.is_empty(), + | Identity::Passkey(_) + | Identity::Apple(_) => networks.is_empty(), } } @@ -425,6 +431,7 @@ impl Identity { | Identity::Github(_) | Identity::Email(_) | Identity::Google(_) + | Identity::Apple(_) | Identity::Pumpx(_) | Identity::Passkey(_) => None, } @@ -484,6 +491,10 @@ impl Identity { hasher.update(b"google"); hasher.update(String::from_utf8(handle.inner.to_vec()).unwrap_or_default()); }, + Identity::Apple(handle) => { + hasher.update(b"apple"); + hasher.update(String::from_utf8(handle.inner.to_vec()).unwrap_or_default()); + }, Identity::Pumpx(handle) => { // TODO: this type will be removed. hasher.update(b"pumpx"); @@ -592,6 +603,11 @@ impl Identity { str::from_utf8(handle.inner_ref()) .map_err(|_| "google handle conversion error")? ), + Identity::Apple(handle) => format!( + "apple:{}", + str::from_utf8(handle.inner_ref()) + .map_err(|_| "apple handle conversion error")? + ), Identity::Pumpx(handle) => format!( "pumpx:{}", str::from_utf8(handle.inner_ref()) @@ -627,6 +643,9 @@ impl Identity { Web2IdentityType::Google => { Identity::Google(IdentityString::new(handle.as_bytes().to_vec())) }, + Web2IdentityType::Apple => { + Identity::Apple(IdentityString::new(handle.as_bytes().to_vec())) + }, Web2IdentityType::Pumpx => { Identity::Pumpx(IdentityString::new(handle.as_bytes().to_vec())) }, @@ -641,6 +660,7 @@ pub enum Web2IdentityType { Github, Email, Google, + Apple, Pumpx, } @@ -720,6 +740,7 @@ mod tests { Identity::Bitcoin(..) => false, Identity::Solana(..) => false, Identity::Google(..) => true, + Identity::Apple(..) => true, Identity::Pumpx(..) => true, Identity::Passkey(..) => true, } @@ -742,6 +763,7 @@ mod tests { Identity::Bitcoin(..) => true, Identity::Solana(..) => true, Identity::Google(..) => false, + Identity::Apple(..) => false, Identity::Pumpx(..) => false, Identity::Passkey(..) => false, } @@ -764,6 +786,7 @@ mod tests { Identity::Bitcoin(..) => false, Identity::Solana(..) => false, Identity::Google(..) => false, + Identity::Apple(..) => false, Identity::Pumpx(..) => false, Identity::Passkey(..) => false, } @@ -786,6 +809,7 @@ mod tests { Identity::Bitcoin(..) => false, Identity::Solana(..) => false, Identity::Google(..) => false, + Identity::Apple(..) => false, Identity::Pumpx(..) => false, Identity::Passkey(..) => false, } @@ -808,6 +832,7 @@ mod tests { Identity::Bitcoin(..) => true, Identity::Solana(..) => false, Identity::Google(..) => false, + Identity::Apple(..) => false, Identity::Pumpx(..) => false, Identity::Passkey(..) => false, } @@ -830,6 +855,7 @@ mod tests { Identity::Bitcoin(..) => false, Identity::Solana(..) => true, Identity::Google(..) => false, + Identity::Apple(..) => false, Identity::Pumpx(..) => false, Identity::Passkey(..) => false, } diff --git a/tee-worker/identity/litentry/core/evm-dynamic-assertions/src/lib.rs b/tee-worker/identity/litentry/core/evm-dynamic-assertions/src/lib.rs index f6bc776249..fb25d36d14 100644 --- a/tee-worker/identity/litentry/core/evm-dynamic-assertions/src/lib.rs +++ b/tee-worker/identity/litentry/core/evm-dynamic-assertions/src/lib.rs @@ -207,6 +207,7 @@ pub fn identity_with_networks_to_token(identity: &IdentityNetworkTuple) -> Token Identity::Google(str) => (8, str.inner_ref().to_vec()), Identity::Pumpx(str) => (9, str.inner_ref().to_vec()), Identity::Passkey(str) => (10, str.inner_ref().to_vec()), + Identity::Apple(str) => (11, str.inner_ref().to_vec()), }; let networks: Vec = identity.1.iter().map(network_to_token).collect(); Token::Tuple(vec![Token::Uint(type_index.into()), Token::Bytes(value), Token::Array(networks)]) diff --git a/tee-worker/identity/litentry/core/native-task/receiver/src/authentication_utils.rs b/tee-worker/identity/litentry/core/native-task/receiver/src/authentication_utils.rs index 7464caa207..ffb4be5eda 100644 --- a/tee-worker/identity/litentry/core/native-task/receiver/src/authentication_utils.rs +++ b/tee-worker/identity/litentry/core/native-task/receiver/src/authentication_utils.rs @@ -14,6 +14,7 @@ use sp_core::{blake2_256, crypto::AccountId32 as AccountId, H256}; #[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)] pub enum OAuth2Provider { Google, + Apple, } #[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)] @@ -100,6 +101,9 @@ pub fn verify_tca_oauth2_authentication( OAuth2Provider::Google => { verify_google_oauth2(data_providers_config, sender_identity_hash, omni_account, payload) }, + OAuth2Provider::Apple => Err(AuthenticationError::OAuth2Error(String::from( + "Apple OAuth2 not yet supported in identity worker", + ))), } } diff --git a/tee-worker/identity/service/src/prometheus_metrics.rs b/tee-worker/identity/service/src/prometheus_metrics.rs index be96c15cb7..5854e2cef0 100644 --- a/tee-worker/identity/service/src/prometheus_metrics.rs +++ b/tee-worker/identity/service/src/prometheus_metrics.rs @@ -300,6 +300,7 @@ fn handle_stf_call_request(req: RequestType, time: f64) { Identity::Bitcoin(_) => "Bitcoin".into(), Identity::Solana(_) => "Solana".into(), Identity::Google(_) => "Google".into(), + Identity::Apple(_) => "Apple".into(), Identity::Pumpx(_) => "Pumpx".into(), Identity::Passkey(_) => "Passkey".into(), }, diff --git a/tee-worker/omni-executor/config-loader/src/config.rs b/tee-worker/omni-executor/config-loader/src/config.rs index 263b4ada78..cea1aebb71 100644 --- a/tee-worker/omni-executor/config-loader/src/config.rs +++ b/tee-worker/omni-executor/config-loader/src/config.rs @@ -85,7 +85,7 @@ impl Default for MailerConfig { } #[derive(Debug, Clone)] -pub struct GoogleOAuth2Config { +pub struct OAuth2Config { pub client_id: String, pub client_secret: String, } @@ -93,7 +93,7 @@ pub struct GoogleOAuth2Config { #[derive(Debug, Clone)] pub struct ConfigLoader { pub mailer_configs: HashMap, - pub google_oauth2_configs: HashMap, + pub oauth2_configs: HashMap>, // client -> provider -> config pub ethereum_url: String, pub solana_url: String, pub bsc_url: String, @@ -335,11 +335,11 @@ impl ConfigLoader { let get_opt = |key: &str| get_env_value(&vars[key]); let mailer_configs = Self::load_mailer_configs(); - let google_oauth2_configs = Self::load_google_oauth2_configs(); + let oauth2_configs = Self::load_oauth2_configs(); ConfigLoader { mailer_configs, - google_oauth2_configs, + oauth2_configs, ethereum_url: append_key(&get("ethereum_url")), solana_url: append_key(&get("solana_url")), bsc_url: append_key(&get("bsc_url")), @@ -452,62 +452,82 @@ impl ConfigLoader { clients } - /// Load Google OAuth2 configurations for multiple clients from environment variables - /// Format: OE_GOOGLE_CLIENT_ID_{CLIENT}, OE_GOOGLE_CLIENT_SECRET_{CLIENT} + /// Load OAuth2 configurations for all supported providers from environment variables + /// Format: OE_{PROVIDER}_CLIENT_ID_{CLIENT}, OE_{PROVIDER}_CLIENT_SECRET_{CLIENT} + /// PROVIDER can be GOOGLE, APPLE, etc. /// CLIENT can be WILDMETA, HEIMA, etc. - fn load_google_oauth2_configs() -> HashMap { - let mut configs = HashMap::new(); - - let env_vars: HashMap = std::env::vars().collect(); - - let mut clients = std::collections::HashSet::new(); - for key in env_vars.keys() { - if key.starts_with("OE_GOOGLE_CLIENT_ID_") { - if let Some(client) = key.strip_prefix("OE_GOOGLE_CLIENT_ID_") { - info!("Found Google OAuth2 configuration for client: {}", client); - clients.insert(client.to_lowercase()); + fn load_oauth2_configs() -> HashMap> { + let providers = vec!["GOOGLE", "APPLE"]; + let mut all_configs: HashMap> = HashMap::new(); + + for provider in providers { + let provider_lower = provider.to_lowercase(); + let prefix = format!("OE_{}_CLIENT_ID_", provider); + + let env_vars: HashMap = std::env::vars().collect(); + let mut clients = std::collections::HashSet::new(); + + for key in env_vars.keys() { + if key.starts_with(&prefix) { + if let Some(client) = key.strip_prefix(&prefix) { + info!( + "Found {} OAuth2 configuration for client: {}", + provider_lower, client + ); + clients.insert(client.to_lowercase()); + } } } - } - info!("Total discovered Google OAuth2 clients: {:?}", clients); + info!("Total discovered {} OAuth2 clients: {:?}", provider_lower, clients); - if clients.is_empty() { - warn!("No Google OAuth2 configurations found in environment variables."); - return configs; - } - - for client in clients { - let client_upper = client.to_uppercase(); - - let client_id = - std::env::var(format!("OE_GOOGLE_CLIENT_ID_{}", client_upper)).unwrap_or_default(); - let client_secret = std::env::var(format!("OE_GOOGLE_CLIENT_SECRET_{}", client_upper)) - .unwrap_or_default(); - - if client_id.is_empty() || client_secret.is_empty() { + if clients.is_empty() { warn!( - "Incomplete Google OAuth2 config for client '{}': client_id_empty={}, client_secret_empty={}", - client, - client_id.is_empty(), - client_secret.is_empty() + "No {} OAuth2 configurations found in environment variables.", + provider_lower ); continue; } - let config = GoogleOAuth2Config { client_id, client_secret }; + for client in clients { + let client_upper = client.to_uppercase(); + + let client_id = + std::env::var(format!("OE_{}_CLIENT_ID_{}", provider, client_upper)) + .unwrap_or_default(); + let client_secret = + std::env::var(format!("OE_{}_CLIENT_SECRET_{}", provider, client_upper)) + .unwrap_or_default(); + + if client_id.is_empty() || client_secret.is_empty() { + warn!( + "Incomplete {} OAuth2 config for client '{}': client_id_empty={}, client_secret_empty={}", + provider_lower, + client, + client_id.is_empty(), + client_secret.is_empty() + ); + continue; + } - info!("Loaded Google OAuth2 config for client '{}'", client); + let config = OAuth2Config { client_id, client_secret }; - configs.insert(client.clone(), config); + info!("Loaded {} OAuth2 config for client '{}'", provider_lower, client); + + all_configs + .entry(client.clone()) + .or_default() + .insert(provider_lower.clone(), config); + } } - configs + all_configs } - /// Get Google OAuth2 configuration for a specific client - pub fn get_google_oauth2_config(&self, client_id: &str) -> Option { + /// Get OAuth2 configuration for a specific client and provider + pub fn get_oauth2_config(&self, client_id: &str, provider: &str) -> Option { let client_key = client_id.to_lowercase(); - self.google_oauth2_configs.get(&client_key).cloned() + let provider_key = provider.to_lowercase(); + self.oauth2_configs.get(&client_key)?.get(&provider_key).cloned() } } diff --git a/tee-worker/omni-executor/config-loader/src/lib.rs b/tee-worker/omni-executor/config-loader/src/lib.rs index e65de89bcc..fe3bb3ec6d 100644 --- a/tee-worker/omni-executor/config-loader/src/lib.rs +++ b/tee-worker/omni-executor/config-loader/src/lib.rs @@ -1,3 +1,3 @@ mod config; -pub use config::{ConfigLoader, GoogleOAuth2Config, MailerConfig, MailerType}; +pub use config::{ConfigLoader, MailerConfig, MailerType, OAuth2Config}; diff --git a/tee-worker/omni-executor/executor-primitives/src/auth.rs b/tee-worker/omni-executor/executor-primitives/src/auth.rs index 4523c5d375..b34c9a6eb4 100644 --- a/tee-worker/omni-executor/executor-primitives/src/auth.rs +++ b/tee-worker/omni-executor/executor-primitives/src/auth.rs @@ -36,6 +36,7 @@ pub enum UserId { Bitcoin(String), // hex-encoded Solana(String), // base58-encoded Google(String), + Apple(String), Passkey(String), // unique user_id, even for multiple credential_id } @@ -89,6 +90,9 @@ impl TryFrom for Identity { UserId::Google(handle) => { Ok(Identity::Google(IdentityString::new(handle.as_bytes().to_vec()))) }, + UserId::Apple(handle) => { + Ok(Identity::Apple(IdentityString::new(handle.as_bytes().to_vec()))) + }, UserId::Passkey(handle) => { Ok(Identity::Passkey(IdentityString::new(handle.as_bytes().to_vec()))) }, @@ -101,7 +105,7 @@ pub enum OmniAuth { Web3(String, Identity, HeimaMultiSignature), // (client_id, Signer, Signature) Email(String, Email, VerificationCode), // (client_id, Email, VerificationCode) AuthToken(JwtToken), - OAuth2(String, Identity, OAuth2Data), // (client_id, Sender, OAuth2Data) + OAuth2(String, OAuth2Data), // (client_id, OAuth2Data) Passkey(PasskeyData), } @@ -129,6 +133,9 @@ impl TryFrom for UserId { Identity::Google(handle) => { Ok(UserId::Google(String::from_utf8(handle.inner.to_vec()).map_err(|_| ())?)) }, + Identity::Apple(handle) => { + Ok(UserId::Apple(String::from_utf8(handle.inner.to_vec()).map_err(|_| ())?)) + }, Identity::Pumpx(handle) => { Ok(UserId::Pumpx(String::from_utf8(handle.inner.to_vec()).map_err(|_| ())?)) }, @@ -165,9 +172,16 @@ impl From for OmniAccountAuthType { } } -#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Encode, Decode, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum OAuth2Provider { Google, + Apple, +} + +#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct OAuth2VerificationData { + pub state: String, + pub nonce: String, } #[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -175,8 +189,9 @@ pub struct OAuth2Data { pub provider: OAuth2Provider, pub code: String, pub state: String, - pub redirect_uri: String, + pub redirect_uri: Option, pub uid: String, // A unique identifier for the user/session requesting the OAuth2 + pub id_token: String, } #[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -255,11 +270,7 @@ pub fn to_omni_auth( OmniAuth::Web3(client_id.to_string(), identity, signature.clone().into()) }, UserAuth::AuthToken(token) => OmniAuth::AuthToken(token.clone()), - UserAuth::OAuth2(data) => { - let identity = - Identity::try_from(user_id.clone()).map_err(|_| "Invalid user ID format")?; - OmniAuth::OAuth2(client_id.to_string(), identity, data.clone()) - }, + UserAuth::OAuth2(data) => OmniAuth::OAuth2(client_id.to_string(), data.clone()), UserAuth::Passkey(data) => OmniAuth::Passkey(data.clone()), }; diff --git a/tee-worker/omni-executor/executor-storage/src/oauth2_state_verifier.rs b/tee-worker/omni-executor/executor-storage/src/oauth2_state_verifier.rs index 33dae1a9f7..dd7b26a945 100644 --- a/tee-worker/omni-executor/executor-storage/src/oauth2_state_verifier.rs +++ b/tee-worker/omni-executor/executor-storage/src/oauth2_state_verifier.rs @@ -1,5 +1,5 @@ use crate::Storage; -use executor_primitives::Hash; +use executor_primitives::{Hash, OAuth2VerificationData}; use rocksdb::DB; use std::sync::Arc; @@ -15,7 +15,7 @@ impl OAuth2StateVerifierStorage { } } -impl Storage for OAuth2StateVerifierStorage { +impl Storage for OAuth2StateVerifierStorage { fn db(&self) -> Arc { self.db.clone() } diff --git a/tee-worker/omni-executor/heima/identity-verification/src/web2/apple/mod.rs b/tee-worker/omni-executor/heima/identity-verification/src/web2/apple/mod.rs new file mode 100644 index 0000000000..d21a684099 --- /dev/null +++ b/tee-worker/omni-executor/heima/identity-verification/src/web2/apple/mod.rs @@ -0,0 +1,69 @@ +use serde::Deserialize; + +pub use super::oauth2_common::AuthorizeData; + +pub const BASE_URL: &str = "https://appleid.apple.com/auth/authorize"; +pub const SCOPES: &str = "email"; + +#[derive(Deserialize)] +pub struct IdToken { + pub iss: String, + pub aud: String, + pub sub: String, + pub email: String, + pub email_verified: bool, + #[serde(default)] + pub nonce: Option, + pub iat: u64, + pub exp: u64, + #[serde(default)] + pub is_private_email: Option, + #[serde(default)] + pub c_hash: Option, + #[serde(default)] + pub auth_time: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + use std::format; + use url::Url; + + #[test] + fn test_get_authorize_data() { + let client_id = "com.example.app"; + let redirect_uri = "https://example.com/callback"; + let authorize_data = super::super::oauth2_common::get_authorize_data( + BASE_URL, + client_id, + Some(redirect_uri), + SCOPES, + true, + ); + + let authorize_url = Url::parse(&authorize_data.authorize_url).unwrap(); + std::println!("{:?}", authorize_url.as_str()); + let expected_url = format!("https://appleid.apple.com/auth/authorize?response_type=code&client_id=com.example.app&scope=email&state={}&nonce={}&redirect_uri=https%3A%2F%2Fexample.com%2Fcallback&response_mode=form_post", authorize_data.state, authorize_data.nonce); + + assert_eq!(authorize_url.as_str(), expected_url); + } + + #[test] + fn decode_id_token_works() { + let token = "eyJraWQiOiJZUXJxZE1ENGJxIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiaW8ud2lsZG1ldGEuYXBwIiwiZXhwIjoxNzYwNjcxMjA3LCJpYXQiOjE3NjA1ODQ4MDcsInN1YiI6IjAwMDE3OS5kMmJjM2EyYzQ3Njg0YmQ5OTY0NGE0ZDU3MWVjM2IzNy4wMzE4IiwiY19oYXNoIjoiUjA0M2lDWGF5c213am5welQ3MmtzZyIsImVtYWlsIjoiaG1iaHJ0NW05dEBwcml2YXRlcmVsYXkuYXBwbGVpZC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaXNfcHJpdmF0ZV9lbWFpbCI6dHJ1ZSwiYXV0aF90aW1lIjoxNzYwNTg0ODA3LCJub25jZV9zdXBwb3J0ZWQiOnRydWV9.cLUNFwx9TdCw-mH2QIMqJdnq1zzT-ODlV5saNQyjWlOYomxyRAEJKBF7F-3D-a3EJN9p1rLZ_LOeg6zB3OlAys4VgaSpg-YCqDs49a3hNrdh5NS5Lo6mwDW9qqGY6MdTUUGZiOe9ciYw76HatZsDRwiUDl22vZJHLFEhkXXZ7FDzHTAJs08THIWZZB7lXB6uEdbHiRUFrKlajV36C_PMJnHRtrgF3Tm_TElHZpFiir_47qNqfaX536D7cm3F-Z0-9nonQOHGb_swbMfS-3eGQueAPhIG5_xAR5MPMEySlUVbRu0WD0UqPYk66NzX_6jIowbHKzh-horyIODhFYqPjA"; + let id_token: IdToken = super::super::oauth2_common::decode_id_token(token).unwrap(); + + assert_eq!(id_token.iss, "https://appleid.apple.com"); + assert_eq!(id_token.aud, "io.wildmeta.app"); + assert_eq!(id_token.exp, 1760671207); + assert_eq!(id_token.iat, 1760584807); + assert_eq!(id_token.sub, "000179.d2bc3a2c47684bd99644a4d571ec3b37.0318"); + assert_eq!(id_token.email, "hmbhrt5m9t@privaterelay.appleid.com"); + assert_eq!(id_token.email_verified, true); + assert_eq!(id_token.is_private_email, Some(true)); + assert_eq!(id_token.c_hash, Some("R043iCXaysmwjnpzT72ksg".to_string())); + assert_eq!(id_token.auth_time, Some(1760584807)); + assert_eq!(id_token.nonce, None); + } +} diff --git a/tee-worker/omni-executor/heima/identity-verification/src/web2/google/mod.rs b/tee-worker/omni-executor/heima/identity-verification/src/web2/google/mod.rs index 3247ea2421..e1860fa9d6 100644 --- a/tee-worker/omni-executor/heima/identity-verification/src/web2/google/mod.rs +++ b/tee-worker/omni-executor/heima/identity-verification/src/web2/google/mod.rs @@ -1,58 +1,26 @@ -use crate::helpers; -use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD}; use serde::Deserialize; -use url::Url; -const BASE_URL: &str = "https://accounts.google.com/o/oauth2/v2/auth"; -const SCOPES: &str = "openid email"; +pub use super::oauth2_common::AuthorizeData; + +pub const BASE_URL: &str = "https://accounts.google.com/o/oauth2/v2/auth"; +pub const SCOPES: &str = "openid email"; #[derive(Deserialize)] pub struct IdToken { pub iss: String, pub azp: String, - pub email_verified: bool, - pub at_hash: String, pub aud: String, - pub exp: u64, - pub iat: u64, pub sub: String, - pub hd: String, pub email: String, -} - -pub struct AuthorizeData { - pub authorize_url: String, - pub state: String, -} - -pub fn get_authorize_data(client_id: &str, redirect_uri: &str) -> AuthorizeData { - let state = helpers::generate_alphanumeric_otp(32); - let mut authorize_url = Url::parse(BASE_URL).expect("Failed to parse URL"); - authorize_url.query_pairs_mut().extend_pairs(&[ - ("response_type", "code"), - ("client_id", client_id), - ("redirect_uri", redirect_uri), - ("scope", SCOPES), - ("state", &state), - ]); - - AuthorizeData { authorize_url: authorize_url.into(), state } -} - -pub fn decode_id_token(token: &str) -> Result { - let parts: Vec<&str> = token.split('.').collect(); - if parts.len() != 3 { - return Err("Invalid token format"); - } - let payload = base64_decode(parts[1])?; - let claims: IdToken = serde_json::from_str(&payload).map_err(|_| "Failed to parse claims")?; - Ok(claims) -} - -fn base64_decode(input: &str) -> Result { - let decoded = &BASE64_URL_SAFE_NO_PAD.decode(input).map_err(|_| "Failed to decode base64")?; - - Ok(String::from_utf8_lossy(decoded).to_string()) + pub email_verified: bool, + #[serde(default)] + pub nonce: Option, + pub iat: u64, + pub exp: u64, + #[serde(default)] + pub hd: Option, + #[serde(default)] + pub at_hash: Option, } #[cfg(test)] @@ -65,11 +33,17 @@ mod tests { fn test_get_authorize_data() { let client_id = "client_id"; let redirect_uri = "http://localhost:8080"; - let authorize_data = get_authorize_data(client_id, redirect_uri); + let authorize_data = super::super::oauth2_common::get_authorize_data( + BASE_URL, + client_id, + Some(redirect_uri), + SCOPES, + false, + ); let authorize_url = Url::parse(&authorize_data.authorize_url).unwrap(); std::println!("{:?}", authorize_url.as_str()); - let expected_url = format!("https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=client_id&redirect_uri=http%3A%2F%2Flocalhost%3A8080&scope=openid+email&state={}", authorize_data.state); + let expected_url = format!("https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=client_id&scope=openid+email&state={}&nonce={}&redirect_uri=http%3A%2F%2Flocalhost%3A8080", authorize_data.state, authorize_data.nonce); assert_eq!(authorize_url.as_str(), expected_url); } @@ -77,15 +51,15 @@ mod tests { #[test] fn decode_id_token_works() { let token = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjM2MjgyNTg2MDExMTNlNjU3NmE0NTMzNzM2NWZlOGI4OTczZDE2NzEiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI2ODYyOTM4MTAwNjktbTBhNzVwYm9mMWVwbzJzZzkyYTU3cHRtazg1c2FnbGYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI2ODYyOTM4MTAwNjktbTBhNzVwYm9mMWVwbzJzZzkyYTU3cHRtazg1c2FnbGYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMDE2NTk5MjE1MTM4NzY4MzIwNDgiLCJoZCI6Imthd2FnYXJiby10ZWNoLmlvIiwiZW1haWwiOiJmcmFuY2lzY29Aa2F3YWdhcmJvLXRlY2guaW8iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IlBuYndCRVIzTnVBa055dFplR2wzcGciLCJpYXQiOjE3MzMyMzU4NDcsImV4cCI6MTczMzIzOTQ0N30.n4gYeYhp2U1ud4bZNW02xMJadki_93CzlcsJnr8F6eIBXwu4-CbsqToNNn40Kq780Wwz44MqnrEIU8dkBLqBc6MBWkMqzQV-RteEXMiZSOAhkNl8dIzds4vDZUnXunom4y-RYcW7yFMu_Vzpdi9A1NmgMvKVf9wqgfTJrqmPwaUh1GfgV8e7SrqHJiI3XVTE_zIxQVdjybR-7dXGh2B9LaXtA1m8v47tNkvtifa7KUw-miSIVt0of0Dq3keETLyptf8HJ1HouwpACMnxSH-Foq3r5EVp3lfGmkmf5dWMxweagsi7-hMhSKsGY2q2g3gy8xxsCaS1Q3uiB1Htw1Dn7Q"; - let id_token = decode_id_token(token).unwrap(); + let id_token: IdToken = super::super::oauth2_common::decode_id_token(token).unwrap(); assert_eq!(id_token.iss, "https://accounts.google.com"); assert_eq!( id_token.azp, "686293810069-m0a75pbof1epo2sg92a57ptmk85saglf.apps.googleusercontent.com" ); - assert!(id_token.email_verified); - assert_eq!(id_token.at_hash, "PnbwBER3NuAkNytZeGl3pg"); + assert_eq!(id_token.email_verified, true); + assert_eq!(id_token.at_hash, Some("PnbwBER3NuAkNytZeGl3pg".to_string())); assert_eq!( id_token.aud, "686293810069-m0a75pbof1epo2sg92a57ptmk85saglf.apps.googleusercontent.com" @@ -93,7 +67,8 @@ mod tests { assert_eq!(id_token.exp, 1733239447); assert_eq!(id_token.iat, 1733235847); assert_eq!(id_token.sub, "101659921513876832048"); - assert_eq!(id_token.hd, "kawagarbo-tech.io"); + assert_eq!(id_token.hd, Some("kawagarbo-tech.io".to_string())); assert_eq!(id_token.email, "francisco@kawagarbo-tech.io"); + assert_eq!(id_token.nonce, None); } } diff --git a/tee-worker/omni-executor/heima/identity-verification/src/web2/mod.rs b/tee-worker/omni-executor/heima/identity-verification/src/web2/mod.rs index abe14867b4..e6f8d85bae 100644 --- a/tee-worker/omni-executor/heima/identity-verification/src/web2/mod.rs +++ b/tee-worker/omni-executor/heima/identity-verification/src/web2/mod.rs @@ -1,5 +1,7 @@ +pub mod apple; pub mod email; pub mod google; +pub mod oauth2_common; use executor_primitives::{Identity, Web2IdentityType, Web2ValidationData}; use executor_storage::{Storage, StorageDB, VerificationCodeStorage}; diff --git a/tee-worker/omni-executor/heima/identity-verification/src/web2/oauth2_common.rs b/tee-worker/omni-executor/heima/identity-verification/src/web2/oauth2_common.rs new file mode 100644 index 0000000000..6eccd154cf --- /dev/null +++ b/tee-worker/omni-executor/heima/identity-verification/src/web2/oauth2_common.rs @@ -0,0 +1,81 @@ +use crate::helpers; +use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD}; +use executor_primitives::OAuth2Provider; +use serde::de::DeserializeOwned; +use url::Url; + +use super::{apple, google}; + +pub struct AuthorizeData { + pub authorize_url: String, + pub state: String, + pub nonce: String, +} + +pub struct OAuth2ProviderConfig { + pub base_url: &'static str, + pub scopes: &'static str, + pub use_response_mode: bool, +} + +impl OAuth2ProviderConfig { + pub fn from_provider(provider: OAuth2Provider) -> Self { + match provider { + OAuth2Provider::Google => Self { + base_url: google::BASE_URL, + scopes: google::SCOPES, + use_response_mode: false, + }, + OAuth2Provider::Apple => { + Self { base_url: apple::BASE_URL, scopes: apple::SCOPES, use_response_mode: true } + }, + } + } +} + +pub fn get_authorize_data( + base_url: &str, + client_id: &str, + redirect_uri: Option<&str>, + scope: &str, + include_response_mode: bool, +) -> AuthorizeData { + let state = helpers::generate_alphanumeric_otp(32); + let nonce = helpers::generate_alphanumeric_otp(32); + let mut authorize_url = Url::parse(base_url).expect("Failed to parse URL"); + + let mut params = vec![ + ("response_type", "code"), + ("client_id", client_id), + ("scope", scope), + ("state", &state), + ("nonce", &nonce), + ]; + + if let Some(uri) = redirect_uri { + params.push(("redirect_uri", uri)); + } + + authorize_url.query_pairs_mut().extend_pairs(¶ms); + + if include_response_mode { + authorize_url.query_pairs_mut().append_pair("response_mode", "form_post"); + } + + AuthorizeData { authorize_url: authorize_url.into(), state, nonce } +} + +pub fn decode_id_token(token: &str) -> Result { + let parts: Vec<&str> = token.split('.').collect(); + if parts.len() != 3 { + return Err("Invalid token format"); + } + let payload = base64_decode(parts[1])?; + serde_json::from_str(&payload).map_err(|_| "Failed to parse claims") +} + +fn base64_decode(input: &str) -> Result { + let decoded = &BASE64_URL_SAFE_NO_PAD.decode(input).map_err(|_| "Failed to decode base64")?; + + Ok(String::from_utf8_lossy(decoded).to_string()) +} diff --git a/tee-worker/omni-executor/oauth-providers/src/client.rs b/tee-worker/omni-executor/oauth-providers/src/client.rs new file mode 100644 index 0000000000..f906be42c3 --- /dev/null +++ b/tee-worker/omni-executor/oauth-providers/src/client.rs @@ -0,0 +1,62 @@ +use reqwest::{Client, ClientBuilder}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct OAuth2TokenResponse { + pub access_token: String, + pub expires_in: u64, + pub id_token: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_token: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub scope: Option, + pub token_type: String, +} + +pub struct OAuth2Client { + client: Client, + client_id: String, + client_secret: String, + token_endpoint: String, +} + +impl OAuth2Client { + pub fn new(client_id: String, client_secret: String, token_endpoint: String) -> Self { + let client = ClientBuilder::new() + .redirect(reqwest::redirect::Policy::none()) + .build() + .unwrap(); + + Self { client, client_id, client_secret, token_endpoint } + } + + pub async fn exchange_code_for_token( + &self, + code: String, + redirect_uri: Option, + ) -> Result { + let mut params = vec![ + ("code", code), + ("client_id", self.client_id.clone()), + ("client_secret", self.client_secret.clone()), + ("grant_type", "authorization_code".to_string()), + ]; + + if let Some(uri) = redirect_uri { + params.push(("redirect_uri", uri)); + } + + let response = self + .client + .post(&self.token_endpoint) + .form(¶ms) + .send() + .await + .map_err(|e| e.to_string())? + .json::() + .await + .map_err(|e| e.to_string())?; + + Ok(response.id_token) + } +} diff --git a/tee-worker/omni-executor/oauth-providers/src/google.rs b/tee-worker/omni-executor/oauth-providers/src/google.rs deleted file mode 100644 index c3781eb134..0000000000 --- a/tee-worker/omni-executor/oauth-providers/src/google.rs +++ /dev/null @@ -1,55 +0,0 @@ -use reqwest::{Client, ClientBuilder}; -use serde::{Deserialize, Serialize}; - -const OAUTH2_TOKEN_ENDPOINT: &str = "https://oauth2.googleapis.com/token"; - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct GoogleOAuth2TokenResponse { - access_token: String, - expires_in: u64, - id_token: String, - scope: String, - token_type: String, -} - -pub struct GoogleOAuth2Client { - client: Client, - client_id: String, - client_secret: String, -} - -impl GoogleOAuth2Client { - pub fn new(client_id: String, client_secret: String) -> Self { - let client = ClientBuilder::new() - .redirect(reqwest::redirect::Policy::none()) - .build() - .unwrap(); - - Self { client, client_id, client_secret } - } - - pub async fn exchange_code_for_token( - &self, - code: String, - redirect_uri: String, - ) -> Result { - let response = self - .client - .post(OAUTH2_TOKEN_ENDPOINT) - .form(&[ - ("code", code), - ("client_id", self.client_id.clone()), - ("client_secret", self.client_secret.clone()), - ("redirect_uri", redirect_uri), - ("grant_type", "authorization_code".to_string()), - ]) - .send() - .await - .map_err(|e| e.to_string())? - .json::() - .await - .map_err(|e| e.to_string())?; - - Ok(response.id_token) - } -} diff --git a/tee-worker/omni-executor/oauth-providers/src/lib.rs b/tee-worker/omni-executor/oauth-providers/src/lib.rs index e333d5747f..ad50462c04 100644 --- a/tee-worker/omni-executor/oauth-providers/src/lib.rs +++ b/tee-worker/omni-executor/oauth-providers/src/lib.rs @@ -1 +1,5 @@ -pub mod google; +pub mod client; +pub mod provider_config; + +pub use client::{OAuth2Client, OAuth2TokenResponse}; +pub use provider_config::{AppleProviderConfig, GoogleProviderConfig, OAuth2ProviderConfig}; diff --git a/tee-worker/omni-executor/oauth-providers/src/provider_config.rs b/tee-worker/omni-executor/oauth-providers/src/provider_config.rs new file mode 100644 index 0000000000..ece9955636 --- /dev/null +++ b/tee-worker/omni-executor/oauth-providers/src/provider_config.rs @@ -0,0 +1,37 @@ +pub trait OAuth2ProviderConfig { + fn token_endpoint(&self) -> &'static str; + fn authorize_endpoint(&self) -> &'static str; + fn scope(&self) -> &'static str; +} + +pub struct GoogleProviderConfig; + +impl OAuth2ProviderConfig for GoogleProviderConfig { + fn token_endpoint(&self) -> &'static str { + "https://oauth2.googleapis.com/token" + } + + fn authorize_endpoint(&self) -> &'static str { + "https://accounts.google.com/o/oauth2/v2/auth" + } + + fn scope(&self) -> &'static str { + "openid email" + } +} + +pub struct AppleProviderConfig; + +impl OAuth2ProviderConfig for AppleProviderConfig { + fn token_endpoint(&self) -> &'static str { + "https://appleid.apple.com/auth/token" + } + + fn authorize_endpoint(&self) -> &'static str { + "https://appleid.apple.com/auth/authorize" + } + + fn scope(&self) -> &'static str { + "email" + } +} diff --git a/tee-worker/omni-executor/rpc-server/src/google_oauth2_factory.rs b/tee-worker/omni-executor/rpc-server/src/google_oauth2_factory.rs deleted file mode 100644 index 56f155f40a..0000000000 --- a/tee-worker/omni-executor/rpc-server/src/google_oauth2_factory.rs +++ /dev/null @@ -1,79 +0,0 @@ -use config_loader::{ConfigLoader, GoogleOAuth2Config}; -use std::collections::HashMap; -use std::sync::Arc; - -pub struct GoogleOAuth2Factory { - config_loader: Arc, - config_cache: std::sync::RwLock>, -} - -impl GoogleOAuth2Factory { - pub fn new(config_loader: Arc) -> Self { - Self { config_loader, config_cache: std::sync::RwLock::new(HashMap::new()) } - } - - pub fn get_google_config_for_client( - &self, - client_id: &str, - ) -> Result> { - let client_key = client_id.to_lowercase(); - - let cache = self.config_cache.read().map_err(|e| format!("Failed to read cache: {}", e))?; - if let Some(config) = cache.get(&client_key) { - return Ok(config.clone()); - } - - drop(cache); - - let config = self.config_loader.get_google_oauth2_config(client_id).ok_or_else(|| { - let available_clients = self.config_loader.list_available_clients(); - format!( - "No Google OAuth2 configuration found for client '{}'. Available clients: {:?}", - client_id, available_clients - ) - })?; - - tracing::info!("Loaded Google OAuth2 config for client '{}'", client_id); - - let mut cache = - self.config_cache.write().map_err(|e| format!("Failed to write cache: {}", e))?; - cache.insert(client_key.clone(), config.clone()); - - Ok(config) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use config_loader::ConfigLoader; - - #[test] - fn test_google_oauth2_factory_caching() { - std::env::set_var("OE_GOOGLE_CLIENT_ID_TESTCLIENT", "test_client_id"); - std::env::set_var("OE_GOOGLE_CLIENT_SECRET_TESTCLIENT", "test_client_secret"); - - let config = ConfigLoader::from_env(); - let factory = GoogleOAuth2Factory::new(Arc::new(config)); - - let config1 = factory - .get_google_config_for_client("testclient") - .expect("Should create config"); - let config2 = factory - .get_google_config_for_client("testclient") - .expect("Should get cached config"); - - assert_eq!(config1.client_id, config2.client_id); - assert_eq!(config1.client_secret, config2.client_secret); - } - - #[test] - fn test_google_oauth2_factory_missing_client() { - let config = ConfigLoader::from_env(); - let factory = GoogleOAuth2Factory::new(Arc::new(config)); - - let result = factory.get_google_config_for_client("nonexistent"); - assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("No Google OAuth2 configuration found")); - } -} diff --git a/tee-worker/omni-executor/rpc-server/src/lib.rs b/tee-worker/omni-executor/rpc-server/src/lib.rs index 959c434e33..bd6c52006e 100644 --- a/tee-worker/omni-executor/rpc-server/src/lib.rs +++ b/tee-worker/omni-executor/rpc-server/src/lib.rs @@ -3,10 +3,10 @@ mod auth_utils; mod config; mod detailed_error; mod error_code; -mod google_oauth2_factory; mod mailer_factory; mod methods; mod middlewares; +mod oauth2_factory; mod server; mod task; mod validation_helpers; diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/get_oauth2_authorization_data.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/get_oauth2_authorization_data.rs new file mode 100644 index 0000000000..f53edb70bf --- /dev/null +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/get_oauth2_authorization_data.rs @@ -0,0 +1,122 @@ +use crate::{ + detailed_error::DetailedError, error_code::EXTERNAL_API_ERROR_CODE, server::RpcContext, +}; +use executor_core::intent_executor::IntentExecutor; +use executor_crypto::hashing::blake2_256; +use executor_primitives::{Hash, OAuth2Provider, OAuth2VerificationData}; +use executor_storage::{OAuth2StateVerifierStorage, Storage}; +use heima_identity_verification::web2::oauth2_common; +use jsonrpsee::{ + types::{ErrorCode, ErrorObject}, + RpcModule, +}; +use parity_scale_codec::Encode; +use serde::{Deserialize, Serialize}; +use tracing::error; + +#[derive(Debug, Deserialize)] +struct GetOAuth2AuthorizationDataParams { + pub provider: String, // "google" or "apple" + pub uid: String, // A unique identifier for the user/session requesting the OAuth2 URL + pub redirect_uri: Option, + pub client_id: String, +} + +#[derive(Debug, Serialize, Clone)] +struct OAuth2AuthorizationData { + pub authorize_url: String, + pub client_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub redirect_uri: Option, + pub state: String, + pub nonce: String, + pub scope: String, + pub response_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub response_mode: Option, +} + +pub fn register_get_oauth2_authorization_data< + EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, + SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, + CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, +>( + module: &mut RpcModule< + RpcContext, + >, +) { + module + .register_async_method("omni_getOAuth2AuthorizationData", |params, ctx, _| async move { + let params = params + .parse::() + .map_err(|_| ErrorCode::ParseError)?; + + let provider = match params.provider.to_lowercase().as_str() { + "google" => OAuth2Provider::Google, + "apple" => OAuth2Provider::Apple, + _ => { + error!("Unsupported OAuth2 provider: {}", params.provider); + return Err(DetailedError::new( + ErrorCode::InvalidParams.code(), + "Invalid OAuth2 provider", + ) + .with_field("provider") + .with_received(¶ms.provider) + .with_expected("google, apple") + .to_error_object()); + }, + }; + + let oauth2_config = + ctx.oauth2_factory.get_config(¶ms.client_id, provider).map_err(|e| { + error!( + "Failed to get {} OAuth2 config for client '{}': {}", + params.provider, params.client_id, e + ); + DetailedError::new( + EXTERNAL_API_ERROR_CODE, + "Failed to get OAuth2 configuration", + ) + .with_field("client_id") + .with_received(¶ms.client_id) + .with_reason(format!("Error: {}", e)) + .to_error_object() + })?; + + let provider_config = oauth2_common::OAuth2ProviderConfig::from_provider(provider); + + let authorize_data = oauth2_common::get_authorize_data( + provider_config.base_url, + &oauth2_config.client_id, + params.redirect_uri.as_deref(), + provider_config.scopes, + provider_config.use_response_mode, + ); + + let storage = OAuth2StateVerifierStorage::new(ctx.storage_db.clone()); + let key: Hash = + blake2_256((params.client_id.clone(), params.uid.clone()).encode().as_slice()) + .into(); + + let verification_data = OAuth2VerificationData { + state: authorize_data.state.clone(), + nonce: authorize_data.nonce.clone(), + }; + + storage.insert(&key, verification_data).map_err(|_| ErrorCode::InternalError)?; + + let response_mode = provider_config.use_response_mode.then(|| "form_post".to_string()); + + Ok::(OAuth2AuthorizationData { + authorize_url: authorize_data.authorize_url, + client_id: params.client_id, + redirect_uri: params.redirect_uri, + state: authorize_data.state, + nonce: authorize_data.nonce, + scope: provider_config.scopes.to_string(), + response_type: "code".to_string(), + response_mode, + }) + }) + .expect("Failed to register omni_getOAuth2AuthorizationData method"); +} diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/get_oauth2_google_authorization_url.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/get_oauth2_google_authorization_url.rs deleted file mode 100644 index 496a0437cb..0000000000 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/get_oauth2_google_authorization_url.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::{ - detailed_error::DetailedError, error_code::EXTERNAL_API_ERROR_CODE, server::RpcContext, -}; -use executor_core::intent_executor::IntentExecutor; -use executor_crypto::hashing::blake2_256; -use executor_primitives::Hash; -use executor_storage::{OAuth2StateVerifierStorage, Storage}; -use heima_identity_verification::web2::google; -use jsonrpsee::{ - types::{ErrorCode, ErrorObject}, - RpcModule, -}; -use parity_scale_codec::Encode; -use serde::Deserialize; -use tracing::error; - -#[derive(Debug, Deserialize)] -struct GetOAuth2GoogleAuthorizationUrlParams { - pub uid: String, // A unique identifier for the user/session requesting the OAuth2 URL - pub redirect_uri: String, - pub client_id: String, -} - -pub fn register_get_oauth2_google_authorization_url< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, - CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, ->( - module: &mut RpcModule< - RpcContext, - >, -) { - module - .register_async_method( - "omni_getOAuth2GoogleAuthorizationUrl", - |params, ctx, _| async move { - let params = params - .parse::() - .map_err(|_| ErrorCode::ParseError)?; - - let google_config = ctx - .google_oauth2_factory - .get_google_config_for_client(¶ms.client_id) - .map_err(|e| { - error!( - "Failed to get Google OAuth2 config for client '{}': {}", - params.client_id, e - ); - DetailedError::new( - EXTERNAL_API_ERROR_CODE, - "Failed to get Google OAuth2 configuration", - ) - .with_field("client_id") - .with_received(¶ms.client_id) - .with_reason(format!("Error: {}", e)) - .to_error_object() - })?; - - let authorization_data = - google::get_authorize_data(&google_config.client_id, ¶ms.redirect_uri); - let storage = OAuth2StateVerifierStorage::new(ctx.storage_db.clone()); - let key: Hash = - blake2_256((params.client_id.clone(), params.uid.clone()).encode().as_slice()) - .into(); - - storage - .insert(&key, authorization_data.state.clone()) - .map_err(|_| ErrorCode::InternalError)?; - Ok::(authorization_data.authorize_url) - }, - ) - .expect("Failed to register omni_getOAuth2GoogleAuthorizationUrl method"); -} diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/login_with_oauth2.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/login_with_oauth2.rs index 252e3bf170..1a3ed10b80 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/login_with_oauth2.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/login_with_oauth2.rs @@ -11,7 +11,7 @@ use executor_crypto::jwt; use executor_primitives::{utils::hex::ToHexPrefixed, OAuth2Data, OAuth2Provider}; use heima_authentication::{ auth_token::{AuthOptions, AuthTokenClaims}, - constants::{AUTH_TOKEN_ACCESS_TYPE, AUTH_TOKEN_EXPIRATION_DAYS}, + constants::{AUTH_TOKEN_EXPIRATION_DAYS, AUTH_TOKEN_ID_TYPE}, }; use heima_primitives::Identity; use jsonrpsee::{types::ErrorObject, RpcModule}; @@ -23,8 +23,9 @@ pub struct LoginWithOAuth2Params { pub provider: String, pub code: String, pub state: String, - pub redirect_uri: String, + pub redirect_uri: Option, pub uid: String, + pub id_token: String, } #[derive(Serialize, Clone)] @@ -36,6 +37,7 @@ pub struct LoginWithOAuth2Response { fn parse_oauth2_provider(provider: &str) -> Result { match provider.to_lowercase().as_str() { "google" => Ok(OAuth2Provider::Google), + "apple" => Ok(OAuth2Provider::Apple), _ => { error!("Unsupported OAuth2 provider: {}", provider); Err(ErrorCode::InvalidParams) @@ -89,7 +91,7 @@ pub fn register_login_with_oauth2< DetailedError::new(PARSE_ERROR_CODE, "Invalid provider") .with_field("provider") .with_received(¶ms.provider) - .with_expected("google") + .with_expected("google, apple") .to_error_object() })?; @@ -99,6 +101,7 @@ pub fn register_login_with_oauth2< state: params.state.clone(), redirect_uri: params.redirect_uri.clone(), uid: params.uid.clone(), + id_token: params.id_token.clone(), }; let verified_identity = @@ -117,7 +120,7 @@ pub fn register_login_with_oauth2< let access_token = create_jwt_for_user( verified_identity.clone(), - AUTH_TOKEN_ACCESS_TYPE, + AUTH_TOKEN_ID_TYPE, ¶ms.client_id, &ctx.jwt_rsa_private_key, ) @@ -129,7 +132,7 @@ pub fn register_login_with_oauth2< })?; let user_id = match &verified_identity { - Identity::Google(identity_string) => { + Identity::Google(identity_string) | Identity::Apple(identity_string) => { std::str::from_utf8(identity_string.inner_ref()) .map_err(|_| { error!("Failed to convert identity to string"); diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/mod.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/mod.rs index 3081225fc0..a14c1fcf83 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/mod.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/mod.rs @@ -10,8 +10,8 @@ use get_health::*; mod get_next_intent_id; use get_next_intent_id::*; -mod get_oauth2_google_authorization_url; -use get_oauth2_google_authorization_url::*; +mod get_oauth2_authorization_data; +use get_oauth2_authorization_data::*; mod get_shielding_key; use get_shielding_key::*; @@ -96,7 +96,7 @@ pub fn register_omni< register_get_shielding_key(module); register_submit_native_task(module); register_request_email_verification_code(module); - register_get_oauth2_google_authorization_url(module); + register_get_oauth2_authorization_data(module); register_get_web3_sign_in_message(module); register_user_login(module); register_login_with_oauth2(module); diff --git a/tee-worker/omni-executor/rpc-server/src/oauth2_factory.rs b/tee-worker/omni-executor/rpc-server/src/oauth2_factory.rs new file mode 100644 index 0000000000..e02ee697d4 --- /dev/null +++ b/tee-worker/omni-executor/rpc-server/src/oauth2_factory.rs @@ -0,0 +1,107 @@ +use config_loader::{ConfigLoader, OAuth2Config}; +use executor_primitives::OAuth2Provider; +use std::collections::HashMap; +use std::sync::Arc; + +pub struct OAuth2ConfigFactory { + config_loader: Arc, + config_cache: std::sync::RwLock>, // (client_id, provider) -> config +} + +impl OAuth2ConfigFactory { + pub fn new(config_loader: Arc) -> Self { + Self { config_loader, config_cache: std::sync::RwLock::new(HashMap::new()) } + } + + pub fn get_config( + &self, + client_id: &str, + provider: OAuth2Provider, + ) -> Result> { + let client_key = client_id.to_lowercase(); + let provider_str = match provider { + OAuth2Provider::Google => "google", + OAuth2Provider::Apple => "apple", + }; + let cache_key = (client_key.clone(), provider_str.to_string()); + + let cache = self.config_cache.read().map_err(|e| format!("Failed to read cache: {}", e))?; + if let Some(config) = cache.get(&cache_key) { + return Ok(config.clone()); + } + + drop(cache); + + let config = + self.config_loader.get_oauth2_config(client_id, provider_str).ok_or_else(|| { + let available_clients = self.config_loader.list_available_clients(); + format!( + "No {} OAuth2 configuration found for client '{}'. Available clients: {:?}", + provider_str, client_id, available_clients + ) + })?; + + tracing::info!("Loaded {} OAuth2 config for client '{}'", provider_str, client_id); + + let mut cache = + self.config_cache.write().map_err(|e| format!("Failed to write cache: {}", e))?; + cache.insert(cache_key, config.clone()); + + Ok(config) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use config_loader::ConfigLoader; + use executor_primitives::OAuth2Provider; + + #[test] + fn test_oauth2_factory_caching_google() { + std::env::set_var("OE_GOOGLE_CLIENT_ID_TESTCLIENT", "test_google_client_id"); + std::env::set_var("OE_GOOGLE_CLIENT_SECRET_TESTCLIENT", "test_google_secret"); + + let config = ConfigLoader::from_env(); + let factory = OAuth2ConfigFactory::new(Arc::new(config)); + + let config1 = factory + .get_config("testclient", OAuth2Provider::Google) + .expect("Should create config"); + let config2 = factory + .get_config("testclient", OAuth2Provider::Google) + .expect("Should get cached config"); + + assert_eq!(config1.client_id, config2.client_id); + assert_eq!(config1.client_secret, config2.client_secret); + } + + #[test] + fn test_oauth2_factory_caching_apple() { + std::env::set_var("OE_APPLE_CLIENT_ID_TESTCLIENT", "test_apple_client_id"); + std::env::set_var("OE_APPLE_CLIENT_SECRET_TESTCLIENT", "test_apple_secret"); + + let config = ConfigLoader::from_env(); + let factory = OAuth2ConfigFactory::new(Arc::new(config)); + + let config1 = factory + .get_config("testclient", OAuth2Provider::Apple) + .expect("Should create config"); + let config2 = factory + .get_config("testclient", OAuth2Provider::Apple) + .expect("Should get cached config"); + + assert_eq!(config1.client_id, config2.client_id); + assert_eq!(config1.client_secret, config2.client_secret); + } + + #[test] + fn test_oauth2_factory_missing_client() { + let config = ConfigLoader::from_env(); + let factory = OAuth2ConfigFactory::new(Arc::new(config)); + + let result = factory.get_config("nonexistent", OAuth2Provider::Google); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("No google OAuth2 configuration found")); + } +} diff --git a/tee-worker/omni-executor/rpc-server/src/server.rs b/tee-worker/omni-executor/rpc-server/src/server.rs index dfea075762..73884ac8de 100644 --- a/tee-worker/omni-executor/rpc-server/src/server.rs +++ b/tee-worker/omni-executor/rpc-server/src/server.rs @@ -1,5 +1,5 @@ -use crate::google_oauth2_factory::GoogleOAuth2Factory; use crate::mailer_factory::MailerFactory; +use crate::oauth2_factory::OAuth2ConfigFactory; use crate::{ methods::register_methods, middlewares::{HttpMiddleware, RpcMiddleware}, @@ -31,7 +31,7 @@ pub(crate) struct RpcContext< pub shielding_key: ShieldingKey, pub storage_db: Arc, pub mailer_factory: Arc, - pub google_oauth2_factory: Arc, + pub oauth2_factory: Arc, pub jwt_rsa_private_key: Vec, pub pumpx_api: Arc>, // we could save copying client (and other objects) around when P-1527 is done @@ -61,7 +61,7 @@ impl< shielding_key: ShieldingKey, storage_db: Arc, mailer_factory: Arc, - google_oauth2_factory: Arc, + oauth2_factory: Arc, jwt_rsa_private_key: Vec, pumpx_api: Arc>, signer_client: Arc>, @@ -81,7 +81,7 @@ impl< shielding_key, storage_db, mailer_factory, - google_oauth2_factory, + oauth2_factory, jwt_rsa_private_key, pumpx_api, signer_client, @@ -146,13 +146,13 @@ pub async fn start_server< ) -> Result<(), Box> { let config_loader_arc = Arc::new(config_loader.clone()); let mailer_factory = Arc::new(MailerFactory::new(config_loader_arc.clone())); - let google_oauth2_factory = Arc::new(GoogleOAuth2Factory::new(config_loader_arc)); + let oauth2_factory = Arc::new(OAuth2ConfigFactory::new(config_loader_arc)); let ctx = RpcContext::new( shielding_key, storage_db, mailer_factory, - google_oauth2_factory, + oauth2_factory, jwt_rsa_private_key.clone(), pumpx_api, signer_client, diff --git a/tee-worker/omni-executor/rpc-server/src/verify_auth.rs b/tee-worker/omni-executor/rpc-server/src/verify_auth.rs index 19775d2e8b..769ec829a1 100644 --- a/tee-worker/omni-executor/rpc-server/src/verify_auth.rs +++ b/tee-worker/omni-executor/rpc-server/src/verify_auth.rs @@ -11,8 +11,10 @@ use heima_authentication::{ constants::AUTH_TOKEN_ID_TYPE, web3::HeimaMessagePayload, }; -use heima_identity_verification::web2::google::decode_id_token; -use oauth_providers::google::GoogleOAuth2Client; +use heima_identity_verification::web2::{apple, google, oauth2_common}; +use oauth_providers::{ + AppleProviderConfig, GoogleProviderConfig, OAuth2Client, OAuth2ProviderConfig, +}; use parity_scale_codec::Encode; use std::{fmt::Display, sync::Arc}; @@ -62,14 +64,8 @@ pub async fn verify_auth< OmniAuth::Email(ref client_id, ref email, ref verification_code) => { verify_email_authentication(ctx, client_id, email, verification_code) }, - OmniAuth::OAuth2(ref client_id, ref sender, ref oauth2_data) => { - let verified_identity = - verify_oauth2_authentication(ctx, client_id, oauth2_data).await?; - if sender.hash() == verified_identity.hash() { - Ok(()) - } else { - Err(AuthenticationError::OAuth2Error("Identity mismatch".to_string())) - } + OmniAuth::OAuth2(ref client_id, ref oauth2_data) => { + verify_oauth2_authentication(ctx, client_id, oauth2_data).await.map(|_| ()) }, OmniAuth::AuthToken(ref auth_token) => verify_auth_token_authentication( &ctx.jwt_rsa_private_key, @@ -159,12 +155,10 @@ pub async fn verify_oauth2_authentication< client_id: &str, payload: &OAuth2Data, ) -> Result { - match payload.provider { - OAuth2Provider::Google => verify_google_oauth2(ctx, client_id, payload).await, - } + verify_oauth2_provider(ctx, client_id, payload).await } -async fn verify_google_oauth2< +async fn verify_oauth2_provider< EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, @@ -175,34 +169,105 @@ async fn verify_google_oauth2< ) -> Result { let state_verifier_storage = OAuth2StateVerifierStorage::new(ctx.storage_db.clone()); let key: Hash = blake2_256((client_id, &payload.uid).encode().as_slice()).into(); - let Ok(Some(stored_state)) = state_verifier_storage.get(&key) else { + let Ok(Some(verification_data)) = state_verifier_storage.get(&key) else { return Err(AuthenticationError::OAuth2Error("State verifier not found".to_string())); }; - if stored_state != payload.state { + if verification_data.state != payload.state { return Err(AuthenticationError::OAuth2Error("State verifier mismatch".to_string())); } - let google_config = - ctx.google_oauth2_factory.get_google_config_for_client(client_id).map_err(|e| { + let provider_str = match payload.provider { + OAuth2Provider::Google => "google", + OAuth2Provider::Apple => "apple", + }; + + let oauth2_config = + ctx.oauth2_factory.get_config(client_id, payload.provider).map_err(|e| { AuthenticationError::OAuth2Error(format!( - "Failed to get Google OAuth2 config for client '{}': {}", - client_id, e + "Failed to get {} OAuth2 config for client '{}': {}", + provider_str, client_id, e )) })?; - let google_client = - GoogleOAuth2Client::new(google_config.client_id, google_config.client_secret); + let email = match payload.provider { + OAuth2Provider::Google => { + let id_token: google::IdToken = oauth2_common::decode_id_token(&payload.id_token) + .map_err(|_| { + AuthenticationError::OAuth2Error("Could not decode Google id token".to_string()) + })?; + + verify_id_token_claims( + &id_token.aud, + id_token.nonce.as_deref(), + &oauth2_config.client_id, + &verification_data.nonce, + )?; + + id_token.email + }, + OAuth2Provider::Apple => { + let id_token: apple::IdToken = oauth2_common::decode_id_token(&payload.id_token) + .map_err(|_| { + AuthenticationError::OAuth2Error("Could not decode Apple id token".to_string()) + })?; + + verify_id_token_claims( + &id_token.aud, + id_token.nonce.as_deref(), + &oauth2_config.client_id, + &verification_data.nonce, + )?; + + id_token.email + }, + }; + + let token_endpoint = match payload.provider { + OAuth2Provider::Google => GoogleProviderConfig.token_endpoint(), + OAuth2Provider::Apple => AppleProviderConfig.token_endpoint(), + }; + + let oauth2_client = OAuth2Client::new( + oauth2_config.client_id, + oauth2_config.client_secret, + token_endpoint.to_string(), + ); + let code = payload.code.clone(); let redirect_uri = payload.redirect_uri.clone(); - let token = google_client.exchange_code_for_token(code, redirect_uri).await.map_err(|_| { - AuthenticationError::OAuth2Error("Could not exchange code for token".to_string()) + let _token = oauth2_client.exchange_code_for_token(code, redirect_uri).await.map_err(|e| { + AuthenticationError::OAuth2Error(format!("Could not exchange code for token: {}", e)) })?; - let id_token = decode_id_token(&token) - .map_err(|_| AuthenticationError::OAuth2Error("Could not decode id token".to_string()))?; - let google_identity = Identity::from_web2_account(&id_token.email, Web2IdentityType::Google); - Ok(google_identity) + let identity_type = match payload.provider { + OAuth2Provider::Google => Web2IdentityType::Google, + OAuth2Provider::Apple => Web2IdentityType::Apple, + }; + + let identity = Identity::from_web2_account(&email, identity_type); + + Ok(identity) +} + +fn verify_id_token_claims( + aud: &str, + nonce: Option<&str>, + client_id: &str, + expected_nonce: &str, +) -> Result<(), AuthenticationError> { + if aud != client_id { + return Err(AuthenticationError::OAuth2Error( + "ID token audience does not match client_id".to_string(), + )); + } + let Some(nonce) = nonce else { + return Err(AuthenticationError::OAuth2Error("ID token missing nonce".to_string())); + }; + if nonce != expected_nonce { + return Err(AuthenticationError::OAuth2Error("ID token nonce mismatch".to_string())); + } + Ok(()) } #[cfg(test)]