Skip to content

Commit b822828

Browse files
authored
Add Google OAuth2 login support with client-specific credentials (#3754)
1 parent a4fa125 commit b822828

File tree

10 files changed

+403
-69
lines changed

10 files changed

+403
-69
lines changed

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

Lines changed: 68 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@ const DEFAULT_MAILER_TYPE: &str = "sendgrid";
4040
const DEFAULT_MAILER_API_KEY: &str = "";
4141
const DEFAULT_MAILER_FROM_EMAIL: &str = "[email protected]";
4242
const DEFAULT_MAILER_FROM_NAME: &str = "Heima Verify";
43-
const DEFAULT_GOOGLE_CLIENT_ID: &str = "";
44-
const DEFAULT_GOOGLE_CLIENT_SECRET: &str = "";
4543
const DEFAULT_ETHEREUM_URL: &str = "https://eth-mainnet.g.alchemy.com/v2/";
4644
const DEFAULT_SOLANA_URL: &str = "https://solana-mainnet.g.alchemy.com/v2/";
4745
const DEFAULT_BSC_URL: &str = "https://bnb-mainnet.g.alchemy.com/v2/";
@@ -86,11 +84,16 @@ impl Default for MailerConfig {
8684
}
8785
}
8886

87+
#[derive(Debug, Clone)]
88+
pub struct GoogleOAuth2Config {
89+
pub client_id: String,
90+
pub client_secret: String,
91+
}
92+
8993
#[derive(Debug, Clone)]
9094
pub struct ConfigLoader {
9195
pub mailer_configs: HashMap<String, MailerConfig>,
92-
pub google_client_id: String,
93-
pub google_client_secret: String,
96+
pub google_oauth2_configs: HashMap<String, GoogleOAuth2Config>,
9497
pub ethereum_url: String,
9598
pub solana_url: String,
9699
pub bsc_url: String,
@@ -137,24 +140,6 @@ impl ConfigLoader {
137140
info!("Executing: {}", std::env::args().collect::<Vec<_>>().join(" "));
138141

139142
let vars: HashMap<&str, EnvVar> = HashMap::from([
140-
(
141-
"google_client_id",
142-
EnvVar {
143-
env_key: "OE_GOOGLE_CLIENT_ID",
144-
default: DEFAULT_GOOGLE_CLIENT_ID,
145-
sensitive: false,
146-
optional: false,
147-
},
148-
),
149-
(
150-
"google_client_secret",
151-
EnvVar {
152-
env_key: "OE_GOOGLE_CLIENT_SECRET",
153-
default: DEFAULT_GOOGLE_CLIENT_SECRET,
154-
sensitive: true,
155-
optional: false,
156-
},
157-
),
158143
(
159144
"ethereum_url",
160145
EnvVar {
@@ -350,11 +335,11 @@ impl ConfigLoader {
350335
let get_opt = |key: &str| get_env_value(&vars[key]);
351336

352337
let mailer_configs = Self::load_mailer_configs();
338+
let google_oauth2_configs = Self::load_google_oauth2_configs();
353339

354340
ConfigLoader {
355341
mailer_configs,
356-
google_client_id: get("google_client_id"),
357-
google_client_secret: get("google_client_secret"),
342+
google_oauth2_configs,
358343
ethereum_url: append_key(&get("ethereum_url")),
359344
solana_url: append_key(&get("solana_url")),
360345
bsc_url: append_key(&get("bsc_url")),
@@ -466,4 +451,63 @@ impl ConfigLoader {
466451
clients.sort();
467452
clients
468453
}
454+
455+
/// Load Google OAuth2 configurations for multiple clients from environment variables
456+
/// Format: OE_GOOGLE_CLIENT_ID_{CLIENT}, OE_GOOGLE_CLIENT_SECRET_{CLIENT}
457+
/// CLIENT can be WILDMETA, HEIMA, etc.
458+
fn load_google_oauth2_configs() -> HashMap<String, GoogleOAuth2Config> {
459+
let mut configs = HashMap::new();
460+
461+
let env_vars: HashMap<String, String> = std::env::vars().collect();
462+
463+
let mut clients = std::collections::HashSet::new();
464+
for key in env_vars.keys() {
465+
if key.starts_with("OE_GOOGLE_CLIENT_ID_") {
466+
if let Some(client) = key.strip_prefix("OE_GOOGLE_CLIENT_ID_") {
467+
info!("Found Google OAuth2 configuration for client: {}", client);
468+
clients.insert(client.to_lowercase());
469+
}
470+
}
471+
}
472+
473+
info!("Total discovered Google OAuth2 clients: {:?}", clients);
474+
475+
if clients.is_empty() {
476+
warn!("No Google OAuth2 configurations found in environment variables.");
477+
return configs;
478+
}
479+
480+
for client in clients {
481+
let client_upper = client.to_uppercase();
482+
483+
let client_id =
484+
std::env::var(format!("OE_GOOGLE_CLIENT_ID_{}", client_upper)).unwrap_or_default();
485+
let client_secret = std::env::var(format!("OE_GOOGLE_CLIENT_SECRET_{}", client_upper))
486+
.unwrap_or_default();
487+
488+
if client_id.is_empty() || client_secret.is_empty() {
489+
warn!(
490+
"Incomplete Google OAuth2 config for client '{}': client_id_empty={}, client_secret_empty={}",
491+
client,
492+
client_id.is_empty(),
493+
client_secret.is_empty()
494+
);
495+
continue;
496+
}
497+
498+
let config = GoogleOAuth2Config { client_id, client_secret };
499+
500+
info!("Loaded Google OAuth2 config for client '{}'", client);
501+
502+
configs.insert(client.clone(), config);
503+
}
504+
505+
configs
506+
}
507+
508+
/// Get Google OAuth2 configuration for a specific client
509+
pub fn get_google_oauth2_config(&self, client_id: &str) -> Option<GoogleOAuth2Config> {
510+
let client_key = client_id.to_lowercase();
511+
self.google_oauth2_configs.get(&client_key).cloned()
512+
}
469513
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
mod config;
22

3-
pub use config::{ConfigLoader, MailerConfig, MailerType};
3+
pub use config::{ConfigLoader, GoogleOAuth2Config, MailerConfig, MailerType};

tee-worker/omni-executor/executor-primitives/src/auth.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ pub enum OmniAuth {
101101
Web3(String, Identity, HeimaMultiSignature), // (client_id, Signer, Signature)
102102
Email(String, Email, VerificationCode), // (client_id, Email, VerificationCode)
103103
AuthToken(JwtToken),
104-
OAuth2(Identity, OAuth2Data), // (Sender, OAuth2Data)
104+
OAuth2(String, Identity, OAuth2Data), // (client_id, Sender, OAuth2Data)
105105
Passkey(PasskeyData),
106106
}
107107

@@ -176,6 +176,7 @@ pub struct OAuth2Data {
176176
pub code: String,
177177
pub state: String,
178178
pub redirect_uri: String,
179+
pub uid: String, // A unique identifier for the user/session requesting the OAuth2
179180
}
180181

181182
#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@@ -257,7 +258,7 @@ pub fn to_omni_auth(
257258
UserAuth::OAuth2(data) => {
258259
let identity =
259260
Identity::try_from(user_id.clone()).map_err(|_| "Invalid user ID format")?;
260-
OmniAuth::OAuth2(identity, data.clone())
261+
OmniAuth::OAuth2(client_id.to_string(), identity, data.clone())
261262
},
262263
UserAuth::Passkey(data) => OmniAuth::Passkey(data.clone()),
263264
};
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
use config_loader::{ConfigLoader, GoogleOAuth2Config};
2+
use std::collections::HashMap;
3+
use std::sync::Arc;
4+
5+
pub struct GoogleOAuth2Factory {
6+
config_loader: Arc<ConfigLoader>,
7+
config_cache: std::sync::RwLock<HashMap<String, GoogleOAuth2Config>>,
8+
}
9+
10+
impl GoogleOAuth2Factory {
11+
pub fn new(config_loader: Arc<ConfigLoader>) -> Self {
12+
Self { config_loader, config_cache: std::sync::RwLock::new(HashMap::new()) }
13+
}
14+
15+
pub fn get_google_config_for_client(
16+
&self,
17+
client_id: &str,
18+
) -> Result<GoogleOAuth2Config, Box<dyn std::error::Error>> {
19+
let client_key = client_id.to_lowercase();
20+
21+
let cache = self.config_cache.read().map_err(|e| format!("Failed to read cache: {}", e))?;
22+
if let Some(config) = cache.get(&client_key) {
23+
return Ok(config.clone());
24+
}
25+
26+
drop(cache);
27+
28+
let config = self.config_loader.get_google_oauth2_config(client_id).ok_or_else(|| {
29+
let available_clients = self.config_loader.list_available_clients();
30+
format!(
31+
"No Google OAuth2 configuration found for client '{}'. Available clients: {:?}",
32+
client_id, available_clients
33+
)
34+
})?;
35+
36+
tracing::info!("Loaded Google OAuth2 config for client '{}'", client_id);
37+
38+
let mut cache =
39+
self.config_cache.write().map_err(|e| format!("Failed to write cache: {}", e))?;
40+
cache.insert(client_key.clone(), config.clone());
41+
42+
Ok(config)
43+
}
44+
}
45+
46+
#[cfg(test)]
47+
mod tests {
48+
use super::*;
49+
use config_loader::ConfigLoader;
50+
51+
#[test]
52+
fn test_google_oauth2_factory_caching() {
53+
std::env::set_var("OE_GOOGLE_CLIENT_ID_TESTCLIENT", "test_client_id");
54+
std::env::set_var("OE_GOOGLE_CLIENT_SECRET_TESTCLIENT", "test_client_secret");
55+
56+
let config = ConfigLoader::from_env();
57+
let factory = GoogleOAuth2Factory::new(Arc::new(config));
58+
59+
let config1 = factory
60+
.get_google_config_for_client("testclient")
61+
.expect("Should create config");
62+
let config2 = factory
63+
.get_google_config_for_client("testclient")
64+
.expect("Should get cached config");
65+
66+
assert_eq!(config1.client_id, config2.client_id);
67+
assert_eq!(config1.client_secret, config2.client_secret);
68+
}
69+
70+
#[test]
71+
fn test_google_oauth2_factory_missing_client() {
72+
let config = ConfigLoader::from_env();
73+
let factory = GoogleOAuth2Factory::new(Arc::new(config));
74+
75+
let result = factory.get_google_config_for_client("nonexistent");
76+
assert!(result.is_err());
77+
assert!(result.unwrap_err().to_string().contains("No Google OAuth2 configuration found"));
78+
}
79+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ mod auth_utils;
33
mod config;
44
mod detailed_error;
55
mod error_code;
6+
mod google_oauth2_factory;
67
mod mailer_factory;
78
mod methods;
89
mod middlewares;

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

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
1-
use crate::server::RpcContext;
1+
use crate::{
2+
detailed_error::DetailedError, error_code::EXTERNAL_API_ERROR_CODE, server::RpcContext,
3+
};
24
use executor_core::intent_executor::IntentExecutor;
3-
use executor_primitives::{utils::hex::ToHexPrefixed, Identity, Web2IdentityType};
5+
use executor_crypto::hashing::blake2_256;
6+
use executor_primitives::Hash;
47
use executor_storage::{OAuth2StateVerifierStorage, Storage};
58
use heima_identity_verification::web2::google;
69
use jsonrpsee::{
710
types::{ErrorCode, ErrorObject},
811
RpcModule,
912
};
13+
use parity_scale_codec::Encode;
14+
use serde::Deserialize;
15+
use tracing::error;
16+
17+
#[derive(Debug, Deserialize)]
18+
struct GetOAuth2GoogleAuthorizationUrlParams {
19+
pub uid: String, // A unique identifier for the user/session requesting the OAuth2 URL
20+
pub redirect_uri: String,
21+
pub client_id: String,
22+
}
1023

1124
pub fn register_get_oauth2_google_authorization_url<
1225
EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static,
@@ -21,20 +34,39 @@ pub fn register_get_oauth2_google_authorization_url<
2134
.register_async_method(
2235
"omni_getOAuth2GoogleAuthorizationUrl",
2336
|params, ctx, _| async move {
24-
match params.parse::<(String, String)>() {
25-
Ok((google_account, redirect_uri)) => {
26-
let google_identity =
27-
Identity::from_web2_account(&google_account, Web2IdentityType::Google);
28-
let authorization_data =
29-
google::get_authorize_data(&ctx.google_client_id, &redirect_uri);
30-
let storage = OAuth2StateVerifierStorage::new(ctx.storage_db.clone());
31-
storage
32-
.insert(&google_identity.hash(), authorization_data.state.clone())
33-
.map_err(|_| ErrorCode::InternalError)?;
34-
Ok::<String, ErrorObject>(authorization_data.authorize_url.to_hex())
35-
},
36-
Err(_) => Err(ErrorCode::ParseError.into()),
37-
}
37+
let params = params
38+
.parse::<GetOAuth2GoogleAuthorizationUrlParams>()
39+
.map_err(|_| ErrorCode::ParseError)?;
40+
41+
let google_config = ctx
42+
.google_oauth2_factory
43+
.get_google_config_for_client(&params.client_id)
44+
.map_err(|e| {
45+
error!(
46+
"Failed to get Google OAuth2 config for client '{}': {}",
47+
params.client_id, e
48+
);
49+
DetailedError::new(
50+
EXTERNAL_API_ERROR_CODE,
51+
"Failed to get Google OAuth2 configuration",
52+
)
53+
.with_field("client_id")
54+
.with_received(&params.client_id)
55+
.with_reason(format!("Error: {}", e))
56+
.to_error_object()
57+
})?;
58+
59+
let authorization_data =
60+
google::get_authorize_data(&google_config.client_id, &params.redirect_uri);
61+
let storage = OAuth2StateVerifierStorage::new(ctx.storage_db.clone());
62+
let key: Hash =
63+
blake2_256((params.client_id.clone(), params.uid.clone()).encode().as_slice())
64+
.into();
65+
66+
storage
67+
.insert(&key, authorization_data.state.clone())
68+
.map_err(|_| ErrorCode::InternalError)?;
69+
Ok::<String, ErrorObject>(authorization_data.authorize_url)
3870
},
3971
)
4072
.expect("Failed to register omni_getOAuth2GoogleAuthorizationUrl method");

0 commit comments

Comments
 (0)