From a4f73b57b594ab070ebe8585b20d1f91630d334e Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Mon, 13 May 2024 10:18:59 +0200 Subject: [PATCH] Adding support for discord oauth verification (#2710) --- .github/workflows/ci.yml | 1 + local-setup/.env.dev | 2 + .../interfaces/identity/definitions.ts | 14 +- tee-worker/docker/docker-compose.yml | 2 + .../docker/lit-discord-identity-test.yml | 24 ++ .../docker/multiworker-docker-compose.yml | 6 + .../data-providers/src/discord_official.rs | 103 ++++++- .../litentry/core/data-providers/src/lib.rs | 18 ++ .../identity-verification/src/web2/discord.rs | 10 + .../identity-verification/src/web2/mod.rs | 86 ++++-- .../core/mock-server/src/discord_official.rs | 65 +++- .../litentry/core/mock-server/src/lib.rs | 2 + .../primitives/src/validation_data.rs | 14 +- .../common/utils/identity-helper.ts | 77 +++-- .../di_substrate_identity.test.ts | 8 +- .../ts-tests/integration-tests/di_vc.test.ts | 7 +- .../discord_identity.test.ts | 290 ++++++++++++++++++ .../ts-tests/integration-tests/dr_vc.test.ts | 5 +- .../twitter_identity.test.ts | 8 +- 19 files changed, 664 insertions(+), 78 deletions(-) create mode 100644 tee-worker/docker/lit-discord-identity-test.yml create mode 100644 tee-worker/litentry/core/identity-verification/src/web2/discord.rs create mode 100644 tee-worker/ts-tests/integration-tests/discord_identity.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 625a9da41e..10a866a4a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -727,6 +727,7 @@ jobs: - test_name: lit-test-failed-parentchain-extrinsic - test_name: lit-scheduled-enclave-test - test_name: lit-twitter-identity-test + - test_name: lit-discord-identity-test steps: - uses: actions/checkout@v4 diff --git a/local-setup/.env.dev b/local-setup/.env.dev index 41df7aede8..967f79e4f1 100644 --- a/local-setup/.env.dev +++ b/local-setup/.env.dev @@ -29,6 +29,8 @@ TWITTER_AUTH_TOKEN_V2= TWITTER_CLIENT_ID= TWITTER_CLIENT_SECRET= DISCORD_AUTH_TOKEN= +DISCORD_CLIENT_ID= +DISCORD_CLIENT_SECRET= ACHAINABLE_AUTH_KEY= ONEBLOCK_NOTION_KEY= NODEREAL_API_KEY= diff --git a/tee-worker/client-api/parachain-api/prepare-build/interfaces/identity/definitions.ts b/tee-worker/client-api/parachain-api/prepare-build/interfaces/identity/definitions.ts index 6fb27c0be2..f7a6f38284 100644 --- a/tee-worker/client-api/parachain-api/prepare-build/interfaces/identity/definitions.ts +++ b/tee-worker/client-api/parachain-api/prepare-build/interfaces/identity/definitions.ts @@ -70,22 +70,32 @@ export default { TwitterValidationData: { _enum: { PublicTweet: "PublicTweet", - OAuth2: "OAuth2", + OAuth2: "TwitterOAuth2", }, }, PublicTweet: { tweet_id: "Vec", }, - OAuth2: { + TwitterOAuth2: { code: "Vec", state: "Vec", redirect_uri: "Vec", }, DiscordValidationData: { + _enum: { + PublicMessage: "PublicMessage", + OAuth2: "DiscordOAuth2", + }, + }, + PublicMessage: { channel_id: "Vec", message_id: "Vec", guild_id: "Vec", }, + DiscordOAuth2: { + code: "Vec", + redirect_uri: "Vec", + }, Web3ValidationData: { _enum: { Substrate: "Web3CommonValidationData", diff --git a/tee-worker/docker/docker-compose.yml b/tee-worker/docker/docker-compose.yml index 2ffe01b268..e3f4dca04c 100644 --- a/tee-worker/docker/docker-compose.yml +++ b/tee-worker/docker/docker-compose.yml @@ -122,6 +122,8 @@ services: - TWITTER_CLIENT_ID= - TWITTER_CLIENT_SECRET= - DISCORD_OFFICIAL_URL=http://localhost:19527 + - DISCORD_CLIENT_ID= + - DISCORD_CLIENT_SECRET= - LITENTRY_DISCORD_MICROSERVICE_URL=http://localhost:19527 - DISCORD_AUTH_TOKEN= - ACHAINABLE_URL=http://localhost:19527 diff --git a/tee-worker/docker/lit-discord-identity-test.yml b/tee-worker/docker/lit-discord-identity-test.yml new file mode 100644 index 0000000000..b94bffec33 --- /dev/null +++ b/tee-worker/docker/lit-discord-identity-test.yml @@ -0,0 +1,24 @@ +services: + lit-discord-identity-test: + image: litentry/litentry-cli:latest + container_name: litentry-discord-identity-test + volumes: + - ../ts-tests:/ts-tests + - ../client-api:/client-api + - ../cli:/usr/local/worker-cli + build: + context: .. + dockerfile: build.Dockerfile + target: deployed-client + depends_on: + litentry-node: + condition: service_healthy + litentry-worker-1: + condition: service_healthy + networks: + - litentry-test-network + entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh discord_identity.test.ts 2>&1' " + restart: "no" +networks: + litentry-test-network: + driver: bridge diff --git a/tee-worker/docker/multiworker-docker-compose.yml b/tee-worker/docker/multiworker-docker-compose.yml index d715a5a63b..42590c9128 100644 --- a/tee-worker/docker/multiworker-docker-compose.yml +++ b/tee-worker/docker/multiworker-docker-compose.yml @@ -123,6 +123,8 @@ services: - TWITTER_CLIENT_ID= - TWITTER_CLIENT_SECRET= - DISCORD_OFFICIAL_URL=http://localhost:19527 + - DISCORD_CLIENT_ID= + - DISCORD_CLIENT_SECRET= - LITENTRY_DISCORD_MICROSERVICE_URL=http://localhost:19527 - DISCORD_AUTH_TOKEN= - ACHAINABLE_URL=http://localhost:19527 @@ -184,6 +186,8 @@ services: - TWITTER_CLIENT_ID= - TWITTER_CLIENT_SECRET= - DISCORD_OFFICIAL_URL=http://localhost:19527 + - DISCORD_CLIENT_ID= + - DISCORD_CLIENT_SECRET= - LITENTRY_DISCORD_MICROSERVICE_URL=http://localhost:19527 - DISCORD_AUTH_TOKEN= - ACHAINABLE_URL=http://localhost:19527 @@ -245,6 +249,8 @@ services: - TWITTER_CLIENT_ID= - TWITTER_CLIENT_SECRET= - DISCORD_OFFICIAL_URL=http://localhost:19527 + - DISCORD_CLIENT_ID= + - DISCORD_CLIENT_SECRET= - LITENTRY_DISCORD_MICROSERVICE_URL=http://localhost:19527 - DISCORD_AUTH_TOKEN= - ACHAINABLE_URL=http://localhost:19527 diff --git a/tee-worker/litentry/core/data-providers/src/discord_official.rs b/tee-worker/litentry/core/data-providers/src/discord_official.rs index 337f35756b..2ef54a333d 100644 --- a/tee-worker/litentry/core/data-providers/src/discord_official.rs +++ b/tee-worker/litentry/core/data-providers/src/discord_official.rs @@ -29,7 +29,13 @@ use itc_rest_client::{ }; use log::*; use serde::{Deserialize, Serialize}; -use std::{format, string::String, vec, vec::Vec}; +use std::{ + collections::HashMap, + format, + string::{String, ToString}, + vec, + vec::Vec, +}; #[derive(Serialize, Deserialize, Debug)] pub struct DiscordMessage { @@ -52,6 +58,23 @@ pub struct DiscordUser { pub discriminator: String, } +#[derive(Serialize, Deserialize, Debug)] +pub struct DiscordUserAccessTokenData { + pub client_id: String, + pub client_secret: String, + pub code: String, + pub redirect_uri: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct DiscordUserAccessToken { + pub access_token: String, + pub refresh_token: String, + pub token_type: String, + pub expires_in: u32, + pub scope: String, +} + impl RestPath for DiscordUser { fn get_path(path: String) -> Result { Ok(path) @@ -64,6 +87,12 @@ impl RestPath for DiscordMessage { } } +impl RestPath for DiscordUserAccessToken { + fn get_path(path: String) -> Result { + Ok(path) + } +} + impl UserInfo for DiscordMessage { fn get_user_id(&self) -> Option { Some(self.author.id.clone()) @@ -89,6 +118,14 @@ impl DiscordOfficialClient { DiscordOfficialClient { client } } + pub fn with_access_token(url: &str, token_type: &str, access_token: &str) -> Self { + let mut headers = Headers::new(); + headers.insert(CONNECTION.as_str(), "close"); + headers.insert(AUTHORIZATION.as_str(), format!("{} {}", token_type, access_token).as_str()); + let client = build_client_with_cert(url, headers); + DiscordOfficialClient { client } + } + pub fn query_message( &mut self, channel_id: Vec, @@ -114,6 +151,29 @@ impl DiscordOfficialClient { .get_with::(path, query.as_slice()) .map_err(|e| Error::RequestError(format!("{:?}", e))) } + + pub fn request_user_access_token( + &mut self, + data: DiscordUserAccessTokenData, + ) -> Result { + debug!("Discord create access token"); + + let path = "/api/oauth2/token".to_string(); + + let mut body = HashMap::new(); + body.insert("client_id".to_string(), data.client_id); + body.insert("client_secret".to_string(), data.client_secret); + body.insert("grant_type".to_string(), "authorization_code".to_string()); + body.insert("code".to_string(), data.code); + body.insert("redirect_uri".to_string(), data.redirect_uri); + + let user_token = self + .client + .post_form_urlencoded_capture::(path, body) + .map_err(|e| Error::RequestError(format!("{:?}", e)))?; + + Ok(user_token) + } } #[cfg(test)] @@ -144,9 +204,44 @@ mod tests { let message = result.unwrap(); assert_eq!(message.id, message_id); - assert_eq!(message.author.id, "001"); - assert_eq!(message.author.username, "elon"); - assert_eq!(message.content, "Hello, litentry."); + assert_eq!(message.author.id, "002"); + assert_eq!(message.author.username, "alice"); + assert_eq!( + message.content, + "63e9a4e993c5dad5f8a19a22057d6c6a86172cf5380711467675061c7ed11bf8" + ); assert_eq!(message.channel_id, channel_id) } + + #[test] + fn get_user_info_work() { + let data_provider_config = init(); + + let user_id = "@me"; + + let mut client = DiscordOfficialClient::new(&data_provider_config); + let result = client.get_user_info(user_id.to_string()); + assert!(result.is_ok(), "query discord error: {:?}", result); + + let user = result.unwrap(); + assert_eq!(user.id, "001".to_string()); + assert_eq!(user.username, "bob"); + assert_eq!(user.discriminator, "0"); + } + + #[test] + fn request_user_access_token_work() { + let data_provider_config = init(); + + let data = DiscordUserAccessTokenData { + client_id: "test-client-id".to_string(), + client_secret: "test-client-secret".to_string(), + code: "test-code".to_string(), + redirect_uri: "http://localhost:3000/redirect".to_string(), + }; + let mut client = DiscordOfficialClient::new(&data_provider_config); + + let result = client.request_user_access_token(data); + assert!(result.is_ok(), "error: {:?}", result); + } } diff --git a/tee-worker/litentry/core/data-providers/src/lib.rs b/tee-worker/litentry/core/data-providers/src/lib.rs index df25728a2f..ac09c1ea46 100644 --- a/tee-worker/litentry/core/data-providers/src/lib.rs +++ b/tee-worker/litentry/core/data-providers/src/lib.rs @@ -171,6 +171,8 @@ pub struct DataProviderConfig { pub twitter_client_id: String, pub twitter_client_secret: String, pub discord_official_url: String, + pub discord_client_id: String, + pub discord_client_secret: String, pub litentry_discord_microservice_url: String, pub discord_auth_token: String, pub achainable_url: String, @@ -219,6 +221,8 @@ impl DataProviderConfig { twitter_client_id: "".to_string(), twitter_client_secret: "".to_string(), discord_official_url: "https://discordapp.com".to_string(), + discord_client_id: "".to_string(), + discord_client_secret: "".to_string(), litentry_discord_microservice_url: "https://tee-microservice.litentry.io:9528" .to_string(), discord_auth_token: "".to_string(), @@ -367,6 +371,12 @@ impl DataProviderConfig { if let Ok(v) = env::var("DISCORD_AUTH_TOKEN") { config.set_discord_auth_token(v); } + if let Ok(v) = env::var("DISCORD_CLIENT_ID") { + config.set_discord_client_id(v); + } + if let Ok(v) = env::var("DISCORD_CLIENT_SECRET") { + config.set_discord_client_secret(v); + } if let Ok(v) = env::var("ACHAINABLE_AUTH_KEY") { config.set_achainable_auth_key(v); } @@ -408,6 +418,14 @@ impl DataProviderConfig { self.discord_official_url = v; Ok(()) } + pub fn set_discord_client_id(&mut self, v: String) { + debug!("set_discord_client_id: {:?}", v); + self.discord_client_id = v; + } + pub fn set_discord_client_secret(&mut self, v: String) { + debug!("set_discord_client_secret: {:?}", v); + self.discord_client_secret = v; + } pub fn set_litentry_discord_microservice_url(&mut self, v: String) -> Result<(), Error> { check_url(&v)?; debug!("set_litentry_discord_microservice_url: {:?}", v); diff --git a/tee-worker/litentry/core/identity-verification/src/web2/discord.rs b/tee-worker/litentry/core/identity-verification/src/web2/discord.rs new file mode 100644 index 0000000000..f6daf6138e --- /dev/null +++ b/tee-worker/litentry/core/identity-verification/src/web2/discord.rs @@ -0,0 +1,10 @@ +use crate::{Error, Result}; +use lc_data_providers::discord_official::DiscordMessage; +use litentry_primitives::ErrorDetail; +use std::vec::Vec; + +pub fn payload_from_discord(discord: &DiscordMessage) -> Result> { + let data = &discord.content; + hex::decode(data.strip_prefix("0x").unwrap_or(data.as_str())) + .map_err(|_| Error::LinkIdentityFailed(ErrorDetail::ParseError)) +} diff --git a/tee-worker/litentry/core/identity-verification/src/web2/mod.rs b/tee-worker/litentry/core/identity-verification/src/web2/mod.rs index 25903f28ac..3b74f16506 100644 --- a/tee-worker/litentry/core/identity-verification/src/web2/mod.rs +++ b/tee-worker/litentry/core/identity-verification/src/web2/mod.rs @@ -20,13 +20,14 @@ extern crate sgx_tstd as std; #[cfg(all(feature = "std", feature = "sgx"))] compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the same time"); +mod discord; pub mod twitter; use crate::{ensure, Error, Result}; use itp_sgx_crypto::ShieldingCryptoDecrypt; use itp_utils::stringify::account_id_to_string; use lc_data_providers::{ - discord_official::{DiscordMessage, DiscordOfficialClient}, + discord_official::{DiscordMessage, DiscordOfficialClient, DiscordUserAccessTokenData}, twitter_official::{Tweet, TwitterOfficialClient, TwitterUserAccessTokenData}, vec_to_string, DataProviderConfig, UserInfo, }; @@ -41,12 +42,6 @@ pub trait DecryptionVerificationPayload { fn decrypt_ciphertext(&self, key: K) -> Result>; } -fn payload_from_discord(discord: &DiscordMessage) -> Result> { - let data = &discord.content; - hex::decode(data.strip_prefix("0x").unwrap_or(data.as_str())) - .map_err(|_| Error::LinkIdentityFailed(ErrorDetail::ParseError)) -} - pub fn verify( who: &Identity, identity: &Identity, @@ -152,33 +147,58 @@ pub fn verify( Ok(user.username) }, }, - Web2ValidationData::Discord(DiscordValidationData { - ref channel_id, - ref message_id, - .. - }) => { - let mut client = DiscordOfficialClient::new(config); - let message: DiscordMessage = client - .query_message(channel_id.to_vec(), message_id.to_vec()) - .map_err(|e| Error::LinkIdentityFailed(e.into_error_detail()))?; - - let user = client - .get_user_info(message.author.id.clone()) - .map_err(|e| Error::LinkIdentityFailed(e.into_error_detail()))?; - - let mut username = message.author.username.clone(); - // if discord user's username is upgraded complete, the discriminator value from api will be "0". - if user.discriminator != "0" { - username.push_str(&'#'.to_string()); - username.push_str(&user.discriminator); - } - let payload = payload_from_discord(&message)?; - ensure!( - payload.as_slice() == raw_msg, - Error::LinkIdentityFailed(ErrorDetail::UnexpectedMessage) - ); + Web2ValidationData::Discord(data) => match data { + DiscordValidationData::PublicMessage { ref channel_id, ref message_id, .. } => { + let mut client = DiscordOfficialClient::new(config); + let message: DiscordMessage = client + .query_message(channel_id.to_vec(), message_id.to_vec()) + .map_err(|e| Error::LinkIdentityFailed(e.into_error_detail()))?; + + let user = client + .get_user_info(message.author.id.clone()) + .map_err(|e| Error::LinkIdentityFailed(e.into_error_detail()))?; + + let mut username = message.author.username.clone(); + // if discord user's username is upgraded complete, the discriminator value from api will be "0". + if user.discriminator != "0" { + username.push_str(&'#'.to_string()); + username.push_str(&user.discriminator); + } + let payload = discord::payload_from_discord(&message)?; + ensure!( + payload.as_slice() == raw_msg, + Error::LinkIdentityFailed(ErrorDetail::UnexpectedMessage) + ); + + Ok(username) + }, + DiscordValidationData::OAuth2 { code, redirect_uri } => { + let mut client = DiscordOfficialClient::new(config); - Ok(username) + let redirect_uri = vec_to_string(redirect_uri.to_vec()) + .map_err(|e| Error::LinkIdentityFailed(e.into_error_detail()))?; + let code = vec_to_string(code.to_vec()) + .map_err(|e| Error::LinkIdentityFailed(e.into_error_detail()))?; + let data = DiscordUserAccessTokenData { + client_id: config.discord_client_id.clone(), + client_secret: config.discord_client_secret.clone(), + code, + redirect_uri, + }; + let user_token = client + .request_user_access_token(data) + .map_err(|e| Error::LinkIdentityFailed(e.into_error_detail()))?; + let mut user_client = DiscordOfficialClient::with_access_token( + &config.discord_official_url, + &user_token.token_type, + &user_token.access_token, + ); + let user = user_client + .get_user_info("@me".to_string()) + .map_err(|e| Error::LinkIdentityFailed(e.into_error_detail()))?; + + Ok(user.username) + }, }, }?; diff --git a/tee-worker/litentry/core/mock-server/src/discord_official.rs b/tee-worker/litentry/core/mock-server/src/discord_official.rs index ae85aec1ce..1bce9db0f3 100644 --- a/tee-worker/litentry/core/mock-server/src/discord_official.rs +++ b/tee-worker/litentry/core/mock-server/src/discord_official.rs @@ -15,7 +15,13 @@ // along with Litentry. If not, see . #![allow(opaque_hidden_inferred_bound)] -use lc_data_providers::discord_official::{DiscordMessage, DiscordMessageAuthor}; +use ita_stf::helpers::get_expected_raw_message; +use lc_data_providers::discord_official::{ + DiscordMessage, DiscordMessageAuthor, DiscordUser, DiscordUserAccessToken, +}; +use litentry_primitives::{Identity, IdentityString}; +use sp_core::{sr25519::Pair as Sr25519Pair, Pair}; +use std::collections::HashMap; use warp::{http::Response, Filter}; pub(crate) fn query_message( @@ -27,13 +33,20 @@ pub(crate) fn query_message( let expected_message_id = "1".to_string(); if expected_channel_id == channel_id && expected_message_id == message_id { + let alice = Sr25519Pair::from_string("//Alice", None).unwrap(); + let discord_identity = Identity::Discord(IdentityString::new(b"alice".to_vec())); + let payload = hex::encode(get_expected_raw_message( + &alice.public().into(), + &discord_identity, + 0, + )); let body = DiscordMessage { id: message_id, channel_id, - content: "Hello, litentry.".into(), + content: payload, author: DiscordMessageAuthor { - id: "001".to_string(), - username: "elon".to_string(), + id: "002".to_string(), + username: "alice".to_string(), }, }; Response::builder().body(serde_json::to_string(&body).unwrap()) @@ -42,3 +55,47 @@ pub(crate) fn query_message( } }) } + +pub(crate) fn get_user_info( +) -> impl Filter + Clone { + warp::get().and(warp::path!("api" / "users" / String)).map(|user_id| { + let current_user = "@me".to_string(); + if current_user == user_id || user_id == "001" { + let body = DiscordUser { + id: "001".to_string(), + username: "bob".to_string(), + discriminator: "0".to_string(), + }; + Response::builder().body(serde_json::to_string(&body).unwrap()) + } else if user_id == "002" { + let body = DiscordUser { + id: user_id, + username: "alice".to_string(), + discriminator: "0".to_string(), + }; + Response::builder().body(serde_json::to_string(&body).unwrap()) + } else { + Response::builder().status(400).body(String::from("Error query")) + } + }) +} + +pub(crate) fn request_user_access_token( +) -> impl Filter + Clone { + warp::post() + .and(warp::path!("api" / "oauth2" / "token")) + .and(warp::body::form()) + .map(|_: HashMap| { + let body = DiscordUserAccessToken { + token_type: "bearer".to_string(), + expires_in: 7200, + access_token: "dGFxeU1MbWRlSVhxSUgxX3VUdUJrM1FTRUtaMmFPdFM0XzMzcVlFSi0xM1dyOjE3MTMzNDEwODQ5NTg6MToxOmF0OjE".to_string(), + refresh_token: "dGFxeU1MbWRlSVhxSUgxX3VUdUJrM1FTRUtaMmFPdFM0XzMzcVlFSi0xM1dyOjE3MTMzNDEwODQ5NTg6MToxOmF0OjE".to_string(), + scope: "identify".to_string(), + }; + + Response::builder() + .header("Content-Type", "application/json") + .body(serde_json::to_string(&body).unwrap()) + }) +} diff --git a/tee-worker/litentry/core/mock-server/src/lib.rs b/tee-worker/litentry/core/mock-server/src/lib.rs index de8e75970c..1c21d9e0a6 100644 --- a/tee-worker/litentry/core/mock-server/src/lib.rs +++ b/tee-worker/litentry/core/mock-server/src/lib.rs @@ -65,6 +65,8 @@ pub fn run(port: u16) -> Result { .or(twitter_official::query_user_by_id()) .or(twitter_official::request_user_access_token()) .or(discord_official::query_message()) + .or(discord_official::get_user_info()) + .or(discord_official::request_user_access_token()) .or(discord_litentry::check_id_hubber()) .or(discord_litentry::check_join()) .or(discord_litentry::has_role()) diff --git a/tee-worker/litentry/primitives/src/validation_data.rs b/tee-worker/litentry/primitives/src/validation_data.rs index 4a690dfce2..cb96641888 100644 --- a/tee-worker/litentry/primitives/src/validation_data.rs +++ b/tee-worker/litentry/primitives/src/validation_data.rs @@ -34,10 +34,16 @@ pub enum TwitterValidationData { #[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, TypeInfo, MaxEncodedLen)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub struct DiscordValidationData { - pub channel_id: ValidationString, - pub message_id: ValidationString, - pub guild_id: ValidationString, +pub enum DiscordValidationData { + PublicMessage { + channel_id: ValidationString, + message_id: ValidationString, + guild_id: ValidationString, + }, + OAuth2 { + code: ValidationString, + redirect_uri: ValidationString, + }, } #[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, TypeInfo, MaxEncodedLen)] diff --git a/tee-worker/ts-tests/integration-tests/common/utils/identity-helper.ts b/tee-worker/ts-tests/integration-tests/common/utils/identity-helper.ts index 24a572227a..952793f587 100644 --- a/tee-worker/ts-tests/integration-tests/common/utils/identity-helper.ts +++ b/tee-worker/ts-tests/integration-tests/common/utils/identity-helper.ts @@ -49,8 +49,17 @@ export function parseIdGraph( return idGraph; } -type TwitterValidationConfig = +type Web2ValidationConfig = | { + identityType: 'Discord'; + context: IntegrationTestContext; + signerIdentitity: CorePrimitivesIdentity; + linkIdentity: CorePrimitivesIdentity; + verificationType: 'PublicMessage' | 'OAuth2'; + validationNonce: number; + } + | { + identityType: 'Twitter'; context: IntegrationTestContext; signerIdentitity: CorePrimitivesIdentity; linkIdentity: CorePrimitivesIdentity; @@ -58,6 +67,7 @@ type TwitterValidationConfig = validationNonce: number; } | { + identityType: 'Twitter'; context: IntegrationTestContext; signerIdentitity: CorePrimitivesIdentity; linkIdentity: CorePrimitivesIdentity; @@ -66,34 +76,61 @@ type TwitterValidationConfig = oauthState: string; }; -export async function buildTwitterValidation(config: TwitterValidationConfig): Promise { +export async function buildWeb2Validation(config: Web2ValidationConfig): Promise { const { context, signerIdentitity, linkIdentity, validationNonce } = config; const msg = generateVerificationMessage(context, signerIdentitity, linkIdentity, validationNonce); - console.log('post verification msg to twitter: ', msg); - - const twitterValidationData = { - Web2Validation: { - Twitter: {}, - }, - }; + console.log(`post verification msg to ${config.identityType}:`, msg); - if (config.verificationType === 'PublicTweet') { - twitterValidationData.Web2Validation.Twitter = { - PublicTweet: { - tweet_id: `0x${Buffer.from(validationNonce.toString(), 'utf8').toString('hex')}`, + if (config.identityType === 'Discord') { + const discordValidationData = { + Web2Validation: { + Discord: {}, }, }; + + if (config.verificationType === 'PublicMessage') { + discordValidationData.Web2Validation.Discord = { + PublicMessage: { + channel_id: `0x${Buffer.from('919848392035794945', 'utf8').toString('hex')}`, + message_id: `0x${Buffer.from('1', 'utf8').toString('hex')}`, + guild_id: `0x${Buffer.from(validationNonce.toString(), 'utf8').toString('hex')}`, + }, + }; + } else { + discordValidationData.Web2Validation.Discord = { + OAuth2: { + code: `0x${Buffer.from('test-oauth-code', 'utf8').toString('hex')}`, + redirect_uri: `0x${Buffer.from('http://test-redirect-uri', 'utf8').toString('hex')}`, + }, + }; + } + + return context.api.createType('LitentryValidationData', discordValidationData); } else { - twitterValidationData.Web2Validation.Twitter = { - OAuth2: { - code: `0x${Buffer.from('test-oauth-code', 'utf8').toString('hex')}`, - state: config.oauthState, - redirect_uri: `0x${Buffer.from('http://test-redirect-uri', 'utf8').toString('hex')}`, + const twitterValidationData = { + Web2Validation: { + Twitter: {}, }, }; - } - return context.api.createType('LitentryValidationData', twitterValidationData); + if (config.verificationType === 'PublicTweet') { + twitterValidationData.Web2Validation.Twitter = { + PublicTweet: { + tweet_id: `0x${Buffer.from(validationNonce.toString(), 'utf8').toString('hex')}`, + }, + }; + } else { + twitterValidationData.Web2Validation.Twitter = { + OAuth2: { + code: `0x${Buffer.from('test-oauth-code', 'utf8').toString('hex')}`, + state: config.oauthState, + redirect_uri: `0x${Buffer.from('http://test-redirect-uri', 'utf8').toString('hex')}`, + }, + }; + } + + return context.api.createType('LitentryValidationData', twitterValidationData); + } } export async function buildValidations( diff --git a/tee-worker/ts-tests/integration-tests/di_substrate_identity.test.ts b/tee-worker/ts-tests/integration-tests/di_substrate_identity.test.ts index 277512ed06..60fdf68d17 100644 --- a/tee-worker/ts-tests/integration-tests/di_substrate_identity.test.ts +++ b/tee-worker/ts-tests/integration-tests/di_substrate_identity.test.ts @@ -9,7 +9,7 @@ import { buildIdentityHelper, buildValidations, initIntegrationTestContext, - buildTwitterValidation, + buildWeb2Validation, } from './common/utils'; import { assertIsInSidechainBlock } from './common/utils/assertion'; import { @@ -87,7 +87,8 @@ describe('Test Identity (direct invocation)', function () { const twitterIdentity = await buildIdentityHelper('mock_user', 'Twitter', context); - const twitterValidation = await buildTwitterValidation({ + const twitterValidation = await buildWeb2Validation({ + identityType: 'Twitter', context, signerIdentitity: aliceSubstrateIdentity, linkIdentity: twitterIdentity, @@ -310,7 +311,8 @@ describe('Test Identity (direct invocation)', function () { const twitterNonce = aliceCurrentNonce++; const twitterIdentity = await buildIdentityHelper('mock_user', 'Twitter', context); - const twitterValidation = await buildTwitterValidation({ + const twitterValidation = await buildWeb2Validation({ + identityType: 'Twitter', context, signerIdentitity: aliceSubstrateIdentity, linkIdentity: twitterIdentity, diff --git a/tee-worker/ts-tests/integration-tests/di_vc.test.ts b/tee-worker/ts-tests/integration-tests/di_vc.test.ts index 2e883f13fd..daed597dea 100644 --- a/tee-worker/ts-tests/integration-tests/di_vc.test.ts +++ b/tee-worker/ts-tests/integration-tests/di_vc.test.ts @@ -1,7 +1,7 @@ import { randomBytes, KeyObject } from 'crypto'; import { step } from 'mocha-steps'; -import { buildTwitterValidation, initIntegrationTestContext } from './common/utils'; -import { assertIsInSidechainBlock, assertVc, assertWorkerError } from './common/utils/assertion'; +import { buildWeb2Validation, initIntegrationTestContext } from './common/utils'; +import { assertIsInSidechainBlock, assertVc } from './common/utils/assertion'; import { getSidechainNonce, createSignedTrustedCallLinkIdentity, @@ -53,7 +53,8 @@ describe('Test Vc (direct invocation)', function () { const twitterNonce = getNextNonce(); const twitterIdentity = await buildIdentityHelper('mock_user', 'Twitter', context); - const twitterValidation = await buildTwitterValidation({ + const twitterValidation = await buildWeb2Validation({ + identityType: 'Twitter', context, signerIdentitity: aliceSubstrateIdentity, linkIdentity: twitterIdentity, diff --git a/tee-worker/ts-tests/integration-tests/discord_identity.test.ts b/tee-worker/ts-tests/integration-tests/discord_identity.test.ts new file mode 100644 index 0000000000..701062f313 --- /dev/null +++ b/tee-worker/ts-tests/integration-tests/discord_identity.test.ts @@ -0,0 +1,290 @@ +import { randomBytes, KeyObject } from 'crypto'; +import { step } from 'mocha-steps'; +import { assert } from 'chai'; +import { + assertIdGraphMutationResult, + assertIdGraphHash, + buildIdentityHelper, + initIntegrationTestContext, + buildWeb2Validation, +} from './common/utils'; +import { assertIsInSidechainBlock } from './common/utils/assertion'; +import { + createSignedTrustedCallLinkIdentity, + createSignedTrustedGetterIdGraph, + decodeIdGraph, + getSidechainNonce, + getTeeShieldingKey, + sendRequestFromGetter, + sendRequestFromTrustedCall, +} from './common/di-utils'; // @fixme move to a better place +import { sleep } from './common/utils'; +import { aesKey } from './common/call'; +import type { IntegrationTestContext } from './common/common-types'; +import type { LitentryValidationData, Web3Network, CorePrimitivesIdentity } from 'parachain-api'; +import type { Vec, Bytes } from '@polkadot/types'; +import type { HexString } from '@polkadot/util/types'; + +describe('Test Discord Identity (direct invocation)', function () { + let context: IntegrationTestContext; + let teeShieldingKey: KeyObject; + let aliceSubstrateIdentity: CorePrimitivesIdentity; + let bobSubstrateIdentity: CorePrimitivesIdentity; + let aliceCurrentNonce = 0; + let bobCurrentNonce = 0; + + const aliceLinkIdentityRequestParams: { + nonce: number; + identity: CorePrimitivesIdentity; + validation: LitentryValidationData; + networks: Bytes | Vec; + }[] = []; + + const bobLinkIdentityRequestParams: { + nonce: number; + identity: CorePrimitivesIdentity; + validation: LitentryValidationData; + networks: Bytes | Vec; + }[] = []; + + this.timeout(6000000); + + before(async () => { + context = await initIntegrationTestContext( + process.env.WORKER_ENDPOINT!, // @fixme evil assertion; centralize env access + process.env.NODE_ENDPOINT! // @fixme evil assertion; centralize env access + ); + teeShieldingKey = await getTeeShieldingKey(context); + + aliceSubstrateIdentity = await context.web3Wallets.substrate.Alice.getIdentity(context); + bobSubstrateIdentity = await context.web3Wallets.substrate.Bob.getIdentity(context); + + aliceCurrentNonce = (await getSidechainNonce(context, aliceSubstrateIdentity)).toNumber(); + bobCurrentNonce = (await getSidechainNonce(context, bobSubstrateIdentity)).toNumber(); + }); + + step('check alice idgraph from sidechain storage before linking', async function () { + const idGraphGetter = await createSignedTrustedGetterIdGraph( + context.api, + context.web3Wallets.substrate.Alice, + aliceSubstrateIdentity + ); + const res = await sendRequestFromGetter(context, teeShieldingKey, idGraphGetter); + const idGraph = decodeIdGraph(context.sidechainRegistry, res.value); + + assert.lengthOf(idGraph, 0); + }); + + step('check bob idgraph from sidechain storage before linking', async function () { + const idGraphGetter = await createSignedTrustedGetterIdGraph( + context.api, + context.web3Wallets.substrate.Bob, + bobSubstrateIdentity + ); + const res = await sendRequestFromGetter(context, teeShieldingKey, idGraphGetter); + const idGraph = decodeIdGraph(context.sidechainRegistry, res.value); + + assert.lengthOf(idGraph, 0); + }); + + step('linking discord identity with public message verification (alice)', async function () { + const nonce = aliceCurrentNonce++; + const discordIdentity = await buildIdentityHelper('alice', 'Discord', context); + const discordValidation = await buildWeb2Validation({ + identityType: 'Discord', + context, + signerIdentitity: aliceSubstrateIdentity, + linkIdentity: discordIdentity, + verificationType: 'PublicMessage', + validationNonce: nonce, + }); + const networks = context.api.createType('Vec', []); + + aliceLinkIdentityRequestParams.push({ + nonce, + identity: discordIdentity, + validation: discordValidation, + networks, + }); + + const idGraphHashResults: HexString[] = []; + let expectedIdGraphs: [CorePrimitivesIdentity, boolean][][] = [ + [ + [aliceSubstrateIdentity, true], + [discordIdentity, true], + ], + ]; + + for (const { nonce, identity, validation, networks } of aliceLinkIdentityRequestParams) { + const requestIdentifier = `0x${randomBytes(32).toString('hex')}`; + const linkIdentityCall = await createSignedTrustedCallLinkIdentity( + context.api, + context.mrEnclave, + context.api.createType('Index', nonce), + context.web3Wallets.substrate.Alice, + aliceSubstrateIdentity, + identity.toHex(), + validation.toHex(), + networks.toHex(), + context.api.createType('Option', aesKey).toHex(), + requestIdentifier, + { + withWrappedBytes: false, + withPrefix: false, + } + ); + const res = await sendRequestFromTrustedCall(context, teeShieldingKey, linkIdentityCall); + + idGraphHashResults.push( + await assertIdGraphMutationResult( + context, + teeShieldingKey, + aliceSubstrateIdentity, + res, + 'LinkIdentityResult', + expectedIdGraphs[0] + ) + ); + expectedIdGraphs = expectedIdGraphs.slice(1, expectedIdGraphs.length); + + await assertIsInSidechainBlock('linkIdentityCall', res); + } + assert.lengthOf(idGraphHashResults, 1); + }); + + step('linking discord identity with oauth2 verification (bob)', async function () { + const nonce = bobCurrentNonce++; + const discordIdentity = await buildIdentityHelper('bob', 'Discord', context); + const discordValidation = await buildWeb2Validation({ + identityType: 'Discord', + context, + signerIdentitity: bobSubstrateIdentity, + linkIdentity: discordIdentity, + validationNonce: nonce, + verificationType: 'OAuth2', + }); + const networks = context.api.createType('Vec', []); + + bobLinkIdentityRequestParams.push({ + nonce, + identity: discordIdentity, + validation: discordValidation, + networks, + }); + + const idGraphHashResults: HexString[] = []; + let expectedIdGraphs: [CorePrimitivesIdentity, boolean][][] = [ + [ + [bobSubstrateIdentity, true], + [discordIdentity, true], + ], + ]; + + for (const { nonce, identity, validation, networks } of bobLinkIdentityRequestParams) { + const requestIdentifier = `0x${randomBytes(32).toString('hex')}`; + const linkIdentityCall = await createSignedTrustedCallLinkIdentity( + context.api, + context.mrEnclave, + context.api.createType('Index', nonce), + context.web3Wallets.substrate.Bob, + bobSubstrateIdentity, + identity.toHex(), + validation.toHex(), + networks.toHex(), + context.api.createType('Option', aesKey).toHex(), + requestIdentifier, + { + withWrappedBytes: false, + withPrefix: true, + } + ); + + const res = await sendRequestFromTrustedCall(context, teeShieldingKey, linkIdentityCall); + + idGraphHashResults.push( + await assertIdGraphMutationResult( + context, + teeShieldingKey, + bobSubstrateIdentity, + res, + 'LinkIdentityResult', + expectedIdGraphs[0] + ) + ); + expectedIdGraphs = expectedIdGraphs.slice(1, expectedIdGraphs.length); + + await assertIsInSidechainBlock('linkIdentityCall', res); + } + assert.lengthOf(idGraphHashResults, 1); + }); + + step('check users sidechain storage after linking (alice)', async function () { + const idGraphGetter = await createSignedTrustedGetterIdGraph( + context.api, + context.web3Wallets.substrate.Alice, + aliceSubstrateIdentity + ); + const res = await sendRequestFromGetter(context, teeShieldingKey, idGraphGetter); + const idGraph = decodeIdGraph(context.sidechainRegistry, res.value); + + for (const { identity } of aliceLinkIdentityRequestParams) { + const identityDump = JSON.stringify(identity.toHuman(), null, 4); + console.debug(`checking identity: ${identityDump}`); + const idGraphNode = idGraph.find(([idGraphNodeIdentity]) => idGraphNodeIdentity.eq(identity)); + assert.isDefined(idGraphNode, `identity not found in idGraph: ${identityDump}`); + const [, idGraphNodeContext] = idGraphNode!; + + const web3networks = idGraphNode![1].web3networks.toHuman(); + assert.deepEqual(web3networks, []); + + assert.equal( + idGraphNodeContext.status.toString(), + 'Active', + `status should be active for identity: ${identityDump}` + ); + console.debug('active ✅'); + } + + await assertIdGraphHash(context, teeShieldingKey, aliceSubstrateIdentity, idGraph); + }); + + step('check users sidechain storage after linking (bob)', async function () { + const idGraphGetter = await createSignedTrustedGetterIdGraph( + context.api, + context.web3Wallets.substrate.Bob, + bobSubstrateIdentity + ); + const res = await sendRequestFromGetter(context, teeShieldingKey, idGraphGetter); + const idGraph = decodeIdGraph(context.sidechainRegistry, res.value); + + for (const { identity } of bobLinkIdentityRequestParams) { + const identityDump = JSON.stringify(identity.toHuman(), null, 4); + console.debug(`checking identity: ${identityDump}`); + const idGraphNode = idGraph.find(([idGraphNodeIdentity]) => idGraphNodeIdentity.eq(identity)); + assert.isDefined(idGraphNode, `identity not found in idGraph: ${identityDump}`); + const [, idGraphNodeContext] = idGraphNode!; + + const web3networks = idGraphNode![1].web3networks.toHuman(); + assert.deepEqual(web3networks, []); + + assert.equal( + idGraphNodeContext.status.toString(), + 'Active', + `status should be active for identity: ${identityDump}` + ); + console.debug('active ✅'); + } + + await assertIdGraphHash(context, teeShieldingKey, bobSubstrateIdentity, idGraph); + }); + + step('check sidechain nonce', async function () { + await sleep(20); + + const aliceNonce = await getSidechainNonce(context, aliceSubstrateIdentity); + assert.equal(aliceNonce.toNumber(), aliceCurrentNonce); + + const bobNonce = await getSidechainNonce(context, bobSubstrateIdentity); + assert.equal(bobNonce.toNumber(), bobCurrentNonce); + }); +}); diff --git a/tee-worker/ts-tests/integration-tests/dr_vc.test.ts b/tee-worker/ts-tests/integration-tests/dr_vc.test.ts index 06439c2215..b4c5d34190 100644 --- a/tee-worker/ts-tests/integration-tests/dr_vc.test.ts +++ b/tee-worker/ts-tests/integration-tests/dr_vc.test.ts @@ -1,6 +1,6 @@ import { randomBytes, KeyObject } from 'crypto'; import { step } from 'mocha-steps'; -import { buildTwitterValidation, initIntegrationTestContext } from './common/utils'; +import { buildWeb2Validation, initIntegrationTestContext } from './common/utils'; import { assertIsInSidechainBlock, assertVc } from './common/utils/assertion'; import { getSidechainNonce, @@ -52,7 +52,8 @@ describe('Test Vc (direct request)', function () { const twitterNonce = getNextNonce(); const twitterIdentity = await buildIdentityHelper('mock_user', 'Twitter', context); - const twitterValidation = await buildTwitterValidation({ + const twitterValidation = await buildWeb2Validation({ + identityType: 'Twitter', context, signerIdentitity: aliceSubstrateIdentity, linkIdentity: twitterIdentity, diff --git a/tee-worker/ts-tests/integration-tests/twitter_identity.test.ts b/tee-worker/ts-tests/integration-tests/twitter_identity.test.ts index fed3ae4268..56da5c2904 100644 --- a/tee-worker/ts-tests/integration-tests/twitter_identity.test.ts +++ b/tee-worker/ts-tests/integration-tests/twitter_identity.test.ts @@ -6,7 +6,7 @@ import { assertIdGraphHash, buildIdentityHelper, initIntegrationTestContext, - buildTwitterValidation, + buildWeb2Validation, } from './common/utils'; import { assertIsInSidechainBlock } from './common/utils/assertion'; import { @@ -91,7 +91,8 @@ describe('Test Twitter Identity (direct invocation)', function () { step('linking twitter identity with public tweet verification (alice)', async function () { const nonce = aliceCurrentNonce++; const twitterIdentity = await buildIdentityHelper('mock_user', 'Twitter', context); - const twitterValidation = await buildTwitterValidation({ + const twitterValidation = await buildWeb2Validation({ + identityType: 'Twitter', context, signerIdentitity: aliceSubstrateIdentity, linkIdentity: twitterIdentity, @@ -166,7 +167,8 @@ describe('Test Twitter Identity (direct invocation)', function () { const nonce = bobCurrentNonce++; const twitterIdentity = await buildIdentityHelper('mock_user_me', 'Twitter', context); - const twitterValidation = await buildTwitterValidation({ + const twitterValidation = await buildWeb2Validation({ + identityType: 'Twitter', context, signerIdentitity: bobSubstrateIdentity, linkIdentity: twitterIdentity,