From b50e1edf2ba57a9e6968b0db43ea08d52a6b86d6 Mon Sep 17 00:00:00 2001 From: Firekeeper <0xFirekeeper@gmail.com> Date: Sat, 18 Jan 2025 01:55:17 +0700 Subject: [PATCH] SiweExternal Auth - Login through static React EOA (#123) --- Thirdweb.Console/Program.cs | 25 ++++++ Thirdweb/Thirdweb.Wallets/IThirdwebWallet.cs | 49 +++++++----- .../EcosystemWallet/EcosystemWallet.cs | 80 +++++++++++++++++++ .../EmbeddedWallet.Authentication/Server.cs | 23 +++--- .../EmbeddedWallet/EmbeddedWallet.SIWE.cs | 5 ++ .../InAppWallet/InAppWallet.Types.cs | 3 +- 6 files changed, 152 insertions(+), 33 deletions(-) diff --git a/Thirdweb.Console/Program.cs b/Thirdweb.Console/Program.cs index 73a83e66..ad33cfca 100644 --- a/Thirdweb.Console/Program.cs +++ b/Thirdweb.Console/Program.cs @@ -489,6 +489,31 @@ #endregion +#region InAppWallet - SiweExternal + +// var inAppWalletSiweExternal = await InAppWallet.Create(client: client, authProvider: AuthProvider.SiweExternal); +// if (!await inAppWalletSiweExternal.IsConnected()) +// { +// _ = await inAppWalletSiweExternal.LoginWithSiweExternal( +// isMobile: false, +// browserOpenAction: (url) => +// { +// var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true }; +// _ = Process.Start(psi); +// }, +// forceWalletIds: new List { "io.metamask", "com.coinbase.wallet", "xyz.abs" } +// ); +// } +// var inAppWalletOAuthAddress = await inAppWalletSiweExternal.GetAddress(); +// Console.WriteLine($"InAppWallet SiweExternal address: {inAppWalletOAuthAddress}"); + +// var inAppWalletAuthDetails = inAppWalletSiweExternal.GetUserAuthDetails(); +// Console.WriteLine($"InAppWallet OAuth auth details: {JsonConvert.SerializeObject(inAppWalletAuthDetails, Formatting.Indented)}"); + +// await inAppWalletSiweExternal.Disconnect(); + +#endregion + #region Smart Wallet - Gasless Transaction // var smartWallet = await SmartWallet.Create(privateKeyWallet, 78600); diff --git a/Thirdweb/Thirdweb.Wallets/IThirdwebWallet.cs b/Thirdweb/Thirdweb.Wallets/IThirdwebWallet.cs index 83e2129e..3c305004 100644 --- a/Thirdweb/Thirdweb.Wallets/IThirdwebWallet.cs +++ b/Thirdweb/Thirdweb.Wallets/IThirdwebWallet.cs @@ -12,32 +12,32 @@ public interface IThirdwebWallet /// /// Gets the Thirdweb client associated with the wallet. /// - ThirdwebClient Client { get; } + public ThirdwebClient Client { get; } /// /// Gets the account type of the wallet. /// - ThirdwebAccountType AccountType { get; } + public ThirdwebAccountType AccountType { get; } /// /// Gets the address of the wallet. /// /// The wallet address. - Task GetAddress(); + public Task GetAddress(); /// /// Signs a raw message using Ethereum's signing method. /// /// The raw message to sign. /// The signed message. - Task EthSign(byte[] rawMessage); + public Task EthSign(byte[] rawMessage); /// /// Signs a message using Ethereum's signing method. /// /// The message to sign. /// The signed message. - Task EthSign(string message); + public Task EthSign(string message); /// /// Recovers the address from a signed message using Ethereum's signing method. @@ -45,21 +45,21 @@ public interface IThirdwebWallet /// The UTF-8 encoded message. /// The signature. /// The recovered address. - Task RecoverAddressFromEthSign(string message, string signature); + public Task RecoverAddressFromEthSign(string message, string signature); /// /// Signs a raw message using personal signing. /// /// The raw message to sign. /// The signed message. - Task PersonalSign(byte[] rawMessage); + public Task PersonalSign(byte[] rawMessage); /// /// Signs a message using personal signing. /// /// The message to sign. /// The signed message. - Task PersonalSign(string message); + public Task PersonalSign(string message); /// /// Recovers the address from a signed message using personal signing. @@ -67,14 +67,14 @@ public interface IThirdwebWallet /// The UTF-8 encoded and prefixed message. /// The signature. /// The recovered address. - Task RecoverAddressFromPersonalSign(string message, string signature); + public Task RecoverAddressFromPersonalSign(string message, string signature); /// /// Signs typed data (version 4). /// /// The JSON representation of the typed data. /// The signed data. - Task SignTypedDataV4(string json); + public Task SignTypedDataV4(string json); /// /// Signs typed data (version 4). @@ -84,7 +84,7 @@ public interface IThirdwebWallet /// The data to sign. /// The typed data. /// The signed data. - Task SignTypedDataV4(T data, TypedData typedData) + public Task SignTypedDataV4(T data, TypedData typedData) where TDomain : IDomain; /// @@ -96,40 +96,40 @@ Task SignTypedDataV4(T data, TypedData typedData) /// The typed data. /// The signature. /// The recovered address. - Task RecoverAddressFromTypedDataV4(T data, TypedData typedData, string signature) + public Task RecoverAddressFromTypedDataV4(T data, TypedData typedData, string signature) where TDomain : IDomain; /// /// Checks if the wallet is connected. /// /// True if connected, otherwise false. - Task IsConnected(); + public Task IsConnected(); /// /// Signs a transaction. /// /// The transaction to sign. /// The signed transaction. - Task SignTransaction(ThirdwebTransactionInput transaction); + public Task SignTransaction(ThirdwebTransactionInput transaction); /// /// Sends a transaction. /// /// The transaction to send. /// The transaction hash. - Task SendTransaction(ThirdwebTransactionInput transaction); + public Task SendTransaction(ThirdwebTransactionInput transaction); /// /// Sends a transaction and waits for its receipt. /// /// The transaction to execute. /// The transaction receipt. - Task ExecuteTransaction(ThirdwebTransactionInput transaction); + public Task ExecuteTransaction(ThirdwebTransactionInput transaction); /// /// Disconnects the wallet (if using InAppWallet, clears session) /// - Task Disconnect(); + public Task Disconnect(); /// /// Links a new account (auth method) to the current wallet. The current wallet must be connected and the wallet being linked must not be fully connected ie created. @@ -144,7 +144,7 @@ Task RecoverAddressFromTypedDataV4(T data, TypedDataThe JWT token if linking custom JWT auth. /// The login payload if linking custom AuthEndpoint auth. /// A list of objects. - Task> LinkAccount( + public Task> LinkAccount( IThirdwebWallet walletToLink, string otp = null, bool? isMobile = null, @@ -160,13 +160,13 @@ Task> LinkAccount( /// Unlinks an account (auth method) from the current wallet. /// /// The linked account to unlink. Same type returned by . - Task> UnlinkAccount(LinkedAccount accountToUnlink); + public Task> UnlinkAccount(LinkedAccount accountToUnlink); /// /// Returns a list of linked accounts to the current wallet. /// /// A list of objects. - Task> GetLinkedAccounts(); + public Task> GetLinkedAccounts(); /// /// Signs an EIP-7702 authorization to invoke contract functions to an externally owned account. @@ -175,13 +175,13 @@ Task> LinkAccount( /// The address of the contract. /// Set to true if the wallet will also be the executor of the transaction, otherwise false. /// The signed authorization as an that can be used with . - Task SignAuthorization(BigInteger chainId, string contractAddress, bool willSelfExecute); + public Task SignAuthorization(BigInteger chainId, string contractAddress, bool willSelfExecute); /// /// Attempts to set the active network to the specified chain ID. /// /// The chain ID to switch to. - Task SwitchNetwork(BigInteger chainId); + public Task SwitchNetwork(BigInteger chainId); } /// @@ -280,4 +280,9 @@ public class LoginPayloadData /// Initializes a new instance of the class. /// public LoginPayloadData() { } + + public override string ToString() + { + return JsonConvert.SerializeObject(this); + } } diff --git a/Thirdweb/Thirdweb.Wallets/InAppWallet/EcosystemWallet/EcosystemWallet.cs b/Thirdweb/Thirdweb.Wallets/InAppWallet/EcosystemWallet/EcosystemWallet.cs index 51d631a8..4290f9c2 100644 --- a/Thirdweb/Thirdweb.Wallets/InAppWallet/EcosystemWallet/EcosystemWallet.cs +++ b/Thirdweb/Thirdweb.Wallets/InAppWallet/EcosystemWallet/EcosystemWallet.cs @@ -123,6 +123,7 @@ public static async Task Create( Thirdweb.AuthProvider.Twitch => "Twitch", Thirdweb.AuthProvider.Steam => "Steam", Thirdweb.AuthProvider.Backend => "Backend", + Thirdweb.AuthProvider.SiweExternal => "SiweExternal", Thirdweb.AuthProvider.Default => string.IsNullOrEmpty(email) ? "Phone" : "Email", _ => throw new ArgumentException("Invalid AuthProvider"), }; @@ -497,6 +498,10 @@ public async Task> LinkAccount( case "Guest": serverRes = await ecosystemWallet.PreAuth_Guest().ConfigureAwait(false); break; + case "SiweExternal": + // TODO: Allow enforcing wallet ids in linking flow? + serverRes = await ecosystemWallet.PreAuth_SiweExternal(isMobile ?? false, browserOpenAction, null, mobileRedirectScheme, browser).ConfigureAwait(false); + break; case "Google": case "Apple": case "Facebook": @@ -700,6 +705,81 @@ public async Task LoginWithOauth( #endregion + #region SiweExternal + + private async Task PreAuth_SiweExternal( + bool isMobile, + Action browserOpenAction, + List forceWalletIds = null, + string mobileRedirectScheme = "thirdweb://", + IThirdwebBrowser browser = null, + CancellationToken cancellationToken = default + ) + { + var redirectUrl = isMobile ? mobileRedirectScheme : "http://localhost:8789/"; + var loginUrl = $"https://static.thirdweb.com/auth/siwe?redirectUrl={redirectUrl}"; + if (forceWalletIds != null && forceWalletIds.Count > 0) + { + loginUrl += $"&wallets={string.Join(",", forceWalletIds)}"; + } + + browser ??= new InAppWalletBrowser(); + var browserResult = await browser.Login(this.Client, loginUrl, redirectUrl, browserOpenAction, cancellationToken).ConfigureAwait(false); + switch (browserResult.Status) + { + case BrowserStatus.Success: + break; + case BrowserStatus.UserCanceled: + throw new TaskCanceledException(browserResult.Error ?? "LoginWithSiwe was cancelled."); + case BrowserStatus.Timeout: + throw new TimeoutException(browserResult.Error ?? "LoginWithSiwe timed out."); + case BrowserStatus.UnknownError: + default: + throw new Exception($"Failed to login with {this.AuthProvider}: {browserResult.Status} | {browserResult.Error}"); + } + var callbackUrl = + browserResult.Status != BrowserStatus.Success + ? throw new Exception($"Failed to login with {this.AuthProvider}: {browserResult.Status} | {browserResult.Error}") + : browserResult.CallbackUrl; + + while (string.IsNullOrEmpty(callbackUrl)) + { + if (cancellationToken.IsCancellationRequested) + { + throw new TaskCanceledException("LoginWithSiwe was cancelled."); + } + await ThirdwebTask.Delay(100, cancellationToken).ConfigureAwait(false); + } + + string signature; + string payload; + var decodedUrl = HttpUtility.UrlDecode(callbackUrl); + Uri uri = new(decodedUrl); + var queryString = uri.Query; + var queryDict = HttpUtility.ParseQueryString(queryString); + signature = queryDict["signature"]; + payload = HttpUtility.UrlDecode(queryDict["payload"]); + var payloadData = JsonConvert.DeserializeObject(payload); + + var serverRes = await this.EmbeddedWallet.SignInWithSiweRawAsync(payloadData, signature).ConfigureAwait(false); + return serverRes; + } + + public async Task LoginWithSiweExternal( + bool isMobile, + Action browserOpenAction, + List forceWalletIds = null, + string mobileRedirectScheme = "thirdweb://", + IThirdwebBrowser browser = null, + CancellationToken cancellationToken = default + ) + { + var serverRes = await this.PreAuth_SiweExternal(isMobile, browserOpenAction, forceWalletIds, mobileRedirectScheme, browser, cancellationToken).ConfigureAwait(false); + return await this.PostAuth(serverRes).ConfigureAwait(false); + } + + #endregion + #region Siwe private async Task PreAuth_Siwe(IThirdwebWallet siweSigner, BigInteger chainId) diff --git a/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet.Authentication/Server.cs b/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet.Authentication/Server.cs index f4743fe8..e6ded624 100644 --- a/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet.Authentication/Server.cs +++ b/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet.Authentication/Server.cs @@ -57,10 +57,7 @@ internal Server(ThirdwebClient client, IThirdwebHttpClient httpClient) internal override async Task> UnlinkAccountAsync(string currentAccountToken, LinkedAccount linkedAccount) { var uri = MakeUri2024("/account/disconnect"); - var request = new HttpRequestMessage(HttpMethod.Post, uri) - { - Content = MakeHttpContent(linkedAccount) - }; + var request = new HttpRequestMessage(HttpMethod.Post, uri) { Content = MakeHttpContent(linkedAccount) }; var response = await this.SendHttpWithAuthAsync(request, currentAccountToken).ConfigureAwait(false); await CheckStatusCodeAsync(response).ConfigureAwait(false); @@ -72,10 +69,7 @@ internal override async Task> UnlinkAccountAsync(string curr internal override async Task> LinkAccountAsync(string currentAccountToken, string authTokenToConnect) { var uri = MakeUri2024("/account/connect"); - var request = new HttpRequestMessage(HttpMethod.Post, uri) - { - Content = MakeHttpContent(new { accountAuthTokenToConnect = authTokenToConnect }) - }; + var request = new HttpRequestMessage(HttpMethod.Post, uri) { Content = MakeHttpContent(new { accountAuthTokenToConnect = authTokenToConnect }) }; var response = await this.SendHttpWithAuthAsync(request, currentAccountToken).ConfigureAwait(false); await CheckStatusCodeAsync(response).ConfigureAwait(false); @@ -91,7 +85,7 @@ internal override async Task> GetLinkedAccountsAsync(string await CheckStatusCodeAsync(response).ConfigureAwait(false); var res = await DeserializeAsync(response).ConfigureAwait(false); - return res == null || res.LinkedAccounts == null || res.LinkedAccounts.Count == 0 ? [] : res.LinkedAccounts; + return res == null || res.LinkedAccounts == null || res.LinkedAccounts.Count == 0 ? new List() : res.LinkedAccounts; } // embedded-wallet/embedded-wallet-shares GET @@ -150,7 +144,16 @@ internal override async Task VerifySiweAsync(LoginPayloadData payl { var uri = MakeUri2024("/login/siwe/callback"); var content = MakeHttpContent(new { signature, payload }); - var response = await this._httpClient.PostAsync(uri.ToString(), content).ConfigureAwait(false); + this._httpClient.AddHeader("origin", payload.Domain); + ThirdwebHttpResponseMessage response = null; + try + { + response = await this._httpClient.PostAsync(uri.ToString(), content).ConfigureAwait(false); + } + finally + { + this._httpClient.RemoveHeader("origin"); + } await CheckStatusCodeAsync(response).ConfigureAwait(false); var authResult = await DeserializeAsync(response).ConfigureAwait(false); diff --git a/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet/EmbeddedWallet.SIWE.cs b/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet/EmbeddedWallet.SIWE.cs index a36d873b..e7fa3666 100644 --- a/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet/EmbeddedWallet.SIWE.cs +++ b/Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet/EmbeddedWallet.SIWE.cs @@ -13,4 +13,9 @@ internal partial class EmbeddedWallet return await this._server.VerifySiweAsync(payload, signature).ConfigureAwait(false); } + + public async Task SignInWithSiweRawAsync(LoginPayloadData payload, string signature) + { + return await this._server.VerifySiweAsync(payload, signature).ConfigureAwait(false); + } } diff --git a/Thirdweb/Thirdweb.Wallets/InAppWallet/InAppWallet.Types.cs b/Thirdweb/Thirdweb.Wallets/InAppWallet/InAppWallet.Types.cs index 76d45d92..f4a78123 100644 --- a/Thirdweb/Thirdweb.Wallets/InAppWallet/InAppWallet.Types.cs +++ b/Thirdweb/Thirdweb.Wallets/InAppWallet/InAppWallet.Types.cs @@ -24,7 +24,8 @@ public enum AuthProvider Github, Twitch, Steam, - Backend + Backend, + SiweExternal, } ///