Skip to content

Commit b50e1ed

Browse files
authored
SiweExternal Auth - Login through static React EOA (#123)
1 parent 0c94da4 commit b50e1ed

File tree

6 files changed

+152
-33
lines changed

6 files changed

+152
-33
lines changed

Diff for: Thirdweb.Console/Program.cs

+25
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,31 @@
489489

490490
#endregion
491491

492+
#region InAppWallet - SiweExternal
493+
494+
// var inAppWalletSiweExternal = await InAppWallet.Create(client: client, authProvider: AuthProvider.SiweExternal);
495+
// if (!await inAppWalletSiweExternal.IsConnected())
496+
// {
497+
// _ = await inAppWalletSiweExternal.LoginWithSiweExternal(
498+
// isMobile: false,
499+
// browserOpenAction: (url) =>
500+
// {
501+
// var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
502+
// _ = Process.Start(psi);
503+
// },
504+
// forceWalletIds: new List<string> { "io.metamask", "com.coinbase.wallet", "xyz.abs" }
505+
// );
506+
// }
507+
// var inAppWalletOAuthAddress = await inAppWalletSiweExternal.GetAddress();
508+
// Console.WriteLine($"InAppWallet SiweExternal address: {inAppWalletOAuthAddress}");
509+
510+
// var inAppWalletAuthDetails = inAppWalletSiweExternal.GetUserAuthDetails();
511+
// Console.WriteLine($"InAppWallet OAuth auth details: {JsonConvert.SerializeObject(inAppWalletAuthDetails, Formatting.Indented)}");
512+
513+
// await inAppWalletSiweExternal.Disconnect();
514+
515+
#endregion
516+
492517
#region Smart Wallet - Gasless Transaction
493518

494519
// var smartWallet = await SmartWallet.Create(privateKeyWallet, 78600);

Diff for: Thirdweb/Thirdweb.Wallets/IThirdwebWallet.cs

+27-22
Original file line numberDiff line numberDiff line change
@@ -12,69 +12,69 @@ public interface IThirdwebWallet
1212
/// <summary>
1313
/// Gets the Thirdweb client associated with the wallet.
1414
/// </summary>
15-
ThirdwebClient Client { get; }
15+
public ThirdwebClient Client { get; }
1616

1717
/// <summary>
1818
/// Gets the account type of the wallet.
1919
/// </summary>
20-
ThirdwebAccountType AccountType { get; }
20+
public ThirdwebAccountType AccountType { get; }
2121

2222
/// <summary>
2323
/// Gets the address of the wallet.
2424
/// </summary>
2525
/// <returns>The wallet address.</returns>
26-
Task<string> GetAddress();
26+
public Task<string> GetAddress();
2727

2828
/// <summary>
2929
/// Signs a raw message using Ethereum's signing method.
3030
/// </summary>
3131
/// <param name="rawMessage">The raw message to sign.</param>
3232
/// <returns>The signed message.</returns>
33-
Task<string> EthSign(byte[] rawMessage);
33+
public Task<string> EthSign(byte[] rawMessage);
3434

3535
/// <summary>
3636
/// Signs a message using Ethereum's signing method.
3737
/// </summary>
3838
/// <param name="message">The message to sign.</param>
3939
/// <returns>The signed message.</returns>
40-
Task<string> EthSign(string message);
40+
public Task<string> EthSign(string message);
4141

4242
/// <summary>
4343
/// Recovers the address from a signed message using Ethereum's signing method.
4444
/// </summary>
4545
/// <param name="message">The UTF-8 encoded message.</param>
4646
/// <param name="signature">The signature.</param>
4747
/// <returns>The recovered address.</returns>
48-
Task<string> RecoverAddressFromEthSign(string message, string signature);
48+
public Task<string> RecoverAddressFromEthSign(string message, string signature);
4949

5050
/// <summary>
5151
/// Signs a raw message using personal signing.
5252
/// </summary>
5353
/// <param name="rawMessage">The raw message to sign.</param>
5454
/// <returns>The signed message.</returns>
55-
Task<string> PersonalSign(byte[] rawMessage);
55+
public Task<string> PersonalSign(byte[] rawMessage);
5656

5757
/// <summary>
5858
/// Signs a message using personal signing.
5959
/// </summary>
6060
/// <param name="message">The message to sign.</param>
6161
/// <returns>The signed message.</returns>
62-
Task<string> PersonalSign(string message);
62+
public Task<string> PersonalSign(string message);
6363

6464
/// <summary>
6565
/// Recovers the address from a signed message using personal signing.
6666
/// </summary>
6767
/// <param name="message">The UTF-8 encoded and prefixed message.</param>
6868
/// <param name="signature">The signature.</param>
6969
/// <returns>The recovered address.</returns>
70-
Task<string> RecoverAddressFromPersonalSign(string message, string signature);
70+
public Task<string> RecoverAddressFromPersonalSign(string message, string signature);
7171

7272
/// <summary>
7373
/// Signs typed data (version 4).
7474
/// </summary>
7575
/// <param name="json">The JSON representation of the typed data.</param>
7676
/// <returns>The signed data.</returns>
77-
Task<string> SignTypedDataV4(string json);
77+
public Task<string> SignTypedDataV4(string json);
7878

7979
/// <summary>
8080
/// Signs typed data (version 4).
@@ -84,7 +84,7 @@ public interface IThirdwebWallet
8484
/// <param name="data">The data to sign.</param>
8585
/// <param name="typedData">The typed data.</param>
8686
/// <returns>The signed data.</returns>
87-
Task<string> SignTypedDataV4<T, TDomain>(T data, TypedData<TDomain> typedData)
87+
public Task<string> SignTypedDataV4<T, TDomain>(T data, TypedData<TDomain> typedData)
8888
where TDomain : IDomain;
8989

9090
/// <summary>
@@ -96,40 +96,40 @@ Task<string> SignTypedDataV4<T, TDomain>(T data, TypedData<TDomain> typedData)
9696
/// <param name="typedData">The typed data.</param>
9797
/// <param name="signature">The signature.</param>
9898
/// <returns>The recovered address.</returns>
99-
Task<string> RecoverAddressFromTypedDataV4<T, TDomain>(T data, TypedData<TDomain> typedData, string signature)
99+
public Task<string> RecoverAddressFromTypedDataV4<T, TDomain>(T data, TypedData<TDomain> typedData, string signature)
100100
where TDomain : IDomain;
101101

102102
/// <summary>
103103
/// Checks if the wallet is connected.
104104
/// </summary>
105105
/// <returns>True if connected, otherwise false.</returns>
106-
Task<bool> IsConnected();
106+
public Task<bool> IsConnected();
107107

108108
/// <summary>
109109
/// Signs a transaction.
110110
/// </summary>
111111
/// <param name="transaction">The transaction to sign.</param>
112112
/// <returns>The signed transaction.</returns>
113-
Task<string> SignTransaction(ThirdwebTransactionInput transaction);
113+
public Task<string> SignTransaction(ThirdwebTransactionInput transaction);
114114

115115
/// <summary>
116116
/// Sends a transaction.
117117
/// </summary>
118118
/// <param name="transaction">The transaction to send.</param>
119119
/// <returns>The transaction hash.</returns>
120-
Task<string> SendTransaction(ThirdwebTransactionInput transaction);
120+
public Task<string> SendTransaction(ThirdwebTransactionInput transaction);
121121

122122
/// <summary>
123123
/// Sends a transaction and waits for its receipt.
124124
/// </summary>
125125
/// <param name="transaction">The transaction to execute.</param>
126126
/// <returns>The transaction receipt.</returns>
127-
Task<ThirdwebTransactionReceipt> ExecuteTransaction(ThirdwebTransactionInput transaction);
127+
public Task<ThirdwebTransactionReceipt> ExecuteTransaction(ThirdwebTransactionInput transaction);
128128

129129
/// <summary>
130130
/// Disconnects the wallet (if using InAppWallet, clears session)
131131
/// </summary>
132-
Task Disconnect();
132+
public Task Disconnect();
133133

134134
/// <summary>
135135
/// 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<string> RecoverAddressFromTypedDataV4<T, TDomain>(T data, TypedData<TDomain
144144
/// <param name="jwt">The JWT token if linking custom JWT auth.</param>
145145
/// <param name="payload">The login payload if linking custom AuthEndpoint auth.</param>
146146
/// <returns>A list of <see cref="LinkedAccount"/> objects.</returns>
147-
Task<List<LinkedAccount>> LinkAccount(
147+
public Task<List<LinkedAccount>> LinkAccount(
148148
IThirdwebWallet walletToLink,
149149
string otp = null,
150150
bool? isMobile = null,
@@ -160,13 +160,13 @@ Task<List<LinkedAccount>> LinkAccount(
160160
/// Unlinks an account (auth method) from the current wallet.
161161
/// </summary>
162162
/// <param name="accountToUnlink">The linked account to unlink. Same type returned by <see cref="GetLinkedAccounts"/>.</param>
163-
Task<List<LinkedAccount>> UnlinkAccount(LinkedAccount accountToUnlink);
163+
public Task<List<LinkedAccount>> UnlinkAccount(LinkedAccount accountToUnlink);
164164

165165
/// <summary>
166166
/// Returns a list of linked accounts to the current wallet.
167167
/// </summary>
168168
/// <returns>A list of <see cref="LinkedAccount"/> objects.</returns>
169-
Task<List<LinkedAccount>> GetLinkedAccounts();
169+
public Task<List<LinkedAccount>> GetLinkedAccounts();
170170

171171
/// <summary>
172172
/// Signs an EIP-7702 authorization to invoke contract functions to an externally owned account.
@@ -175,13 +175,13 @@ Task<List<LinkedAccount>> LinkAccount(
175175
/// <param name="contractAddress">The address of the contract.</param>
176176
/// <param name="willSelfExecute">Set to true if the wallet will also be the executor of the transaction, otherwise false.</param>
177177
/// <returns>The signed authorization as an <see cref="EIP7702Authorization"/> that can be used with <see cref="ThirdwebTransactionInput.AuthorizationList"/>.</returns>
178-
Task<EIP7702Authorization> SignAuthorization(BigInteger chainId, string contractAddress, bool willSelfExecute);
178+
public Task<EIP7702Authorization> SignAuthorization(BigInteger chainId, string contractAddress, bool willSelfExecute);
179179

180180
/// <summary>
181181
/// Attempts to set the active network to the specified chain ID.
182182
/// </summary>
183183
/// <param name="chainId">The chain ID to switch to.</param>
184-
Task SwitchNetwork(BigInteger chainId);
184+
public Task SwitchNetwork(BigInteger chainId);
185185
}
186186

187187
/// <summary>
@@ -280,4 +280,9 @@ public class LoginPayloadData
280280
/// Initializes a new instance of the <see cref="LoginPayloadData"/> class.
281281
/// </summary>
282282
public LoginPayloadData() { }
283+
284+
public override string ToString()
285+
{
286+
return JsonConvert.SerializeObject(this);
287+
}
283288
}

Diff for: Thirdweb/Thirdweb.Wallets/InAppWallet/EcosystemWallet/EcosystemWallet.cs

+80
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ public static async Task<EcosystemWallet> Create(
123123
Thirdweb.AuthProvider.Twitch => "Twitch",
124124
Thirdweb.AuthProvider.Steam => "Steam",
125125
Thirdweb.AuthProvider.Backend => "Backend",
126+
Thirdweb.AuthProvider.SiweExternal => "SiweExternal",
126127
Thirdweb.AuthProvider.Default => string.IsNullOrEmpty(email) ? "Phone" : "Email",
127128
_ => throw new ArgumentException("Invalid AuthProvider"),
128129
};
@@ -497,6 +498,10 @@ public async Task<List<LinkedAccount>> LinkAccount(
497498
case "Guest":
498499
serverRes = await ecosystemWallet.PreAuth_Guest().ConfigureAwait(false);
499500
break;
501+
case "SiweExternal":
502+
// TODO: Allow enforcing wallet ids in linking flow?
503+
serverRes = await ecosystemWallet.PreAuth_SiweExternal(isMobile ?? false, browserOpenAction, null, mobileRedirectScheme, browser).ConfigureAwait(false);
504+
break;
500505
case "Google":
501506
case "Apple":
502507
case "Facebook":
@@ -700,6 +705,81 @@ public async Task<string> LoginWithOauth(
700705

701706
#endregion
702707

708+
#region SiweExternal
709+
710+
private async Task<Server.VerifyResult> PreAuth_SiweExternal(
711+
bool isMobile,
712+
Action<string> browserOpenAction,
713+
List<string> forceWalletIds = null,
714+
string mobileRedirectScheme = "thirdweb://",
715+
IThirdwebBrowser browser = null,
716+
CancellationToken cancellationToken = default
717+
)
718+
{
719+
var redirectUrl = isMobile ? mobileRedirectScheme : "http://localhost:8789/";
720+
var loginUrl = $"https://static.thirdweb.com/auth/siwe?redirectUrl={redirectUrl}";
721+
if (forceWalletIds != null && forceWalletIds.Count > 0)
722+
{
723+
loginUrl += $"&wallets={string.Join(",", forceWalletIds)}";
724+
}
725+
726+
browser ??= new InAppWalletBrowser();
727+
var browserResult = await browser.Login(this.Client, loginUrl, redirectUrl, browserOpenAction, cancellationToken).ConfigureAwait(false);
728+
switch (browserResult.Status)
729+
{
730+
case BrowserStatus.Success:
731+
break;
732+
case BrowserStatus.UserCanceled:
733+
throw new TaskCanceledException(browserResult.Error ?? "LoginWithSiwe was cancelled.");
734+
case BrowserStatus.Timeout:
735+
throw new TimeoutException(browserResult.Error ?? "LoginWithSiwe timed out.");
736+
case BrowserStatus.UnknownError:
737+
default:
738+
throw new Exception($"Failed to login with {this.AuthProvider}: {browserResult.Status} | {browserResult.Error}");
739+
}
740+
var callbackUrl =
741+
browserResult.Status != BrowserStatus.Success
742+
? throw new Exception($"Failed to login with {this.AuthProvider}: {browserResult.Status} | {browserResult.Error}")
743+
: browserResult.CallbackUrl;
744+
745+
while (string.IsNullOrEmpty(callbackUrl))
746+
{
747+
if (cancellationToken.IsCancellationRequested)
748+
{
749+
throw new TaskCanceledException("LoginWithSiwe was cancelled.");
750+
}
751+
await ThirdwebTask.Delay(100, cancellationToken).ConfigureAwait(false);
752+
}
753+
754+
string signature;
755+
string payload;
756+
var decodedUrl = HttpUtility.UrlDecode(callbackUrl);
757+
Uri uri = new(decodedUrl);
758+
var queryString = uri.Query;
759+
var queryDict = HttpUtility.ParseQueryString(queryString);
760+
signature = queryDict["signature"];
761+
payload = HttpUtility.UrlDecode(queryDict["payload"]);
762+
var payloadData = JsonConvert.DeserializeObject<LoginPayloadData>(payload);
763+
764+
var serverRes = await this.EmbeddedWallet.SignInWithSiweRawAsync(payloadData, signature).ConfigureAwait(false);
765+
return serverRes;
766+
}
767+
768+
public async Task<string> LoginWithSiweExternal(
769+
bool isMobile,
770+
Action<string> browserOpenAction,
771+
List<string> forceWalletIds = null,
772+
string mobileRedirectScheme = "thirdweb://",
773+
IThirdwebBrowser browser = null,
774+
CancellationToken cancellationToken = default
775+
)
776+
{
777+
var serverRes = await this.PreAuth_SiweExternal(isMobile, browserOpenAction, forceWalletIds, mobileRedirectScheme, browser, cancellationToken).ConfigureAwait(false);
778+
return await this.PostAuth(serverRes).ConfigureAwait(false);
779+
}
780+
781+
#endregion
782+
703783
#region Siwe
704784

705785
private async Task<Server.VerifyResult> PreAuth_Siwe(IThirdwebWallet siweSigner, BigInteger chainId)

Diff for: Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet.Authentication/Server.cs

+13-10
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,7 @@ internal Server(ThirdwebClient client, IThirdwebHttpClient httpClient)
5757
internal override async Task<List<LinkedAccount>> UnlinkAccountAsync(string currentAccountToken, LinkedAccount linkedAccount)
5858
{
5959
var uri = MakeUri2024("/account/disconnect");
60-
var request = new HttpRequestMessage(HttpMethod.Post, uri)
61-
{
62-
Content = MakeHttpContent(linkedAccount)
63-
};
60+
var request = new HttpRequestMessage(HttpMethod.Post, uri) { Content = MakeHttpContent(linkedAccount) };
6461
var response = await this.SendHttpWithAuthAsync(request, currentAccountToken).ConfigureAwait(false);
6562
await CheckStatusCodeAsync(response).ConfigureAwait(false);
6663

@@ -72,10 +69,7 @@ internal override async Task<List<LinkedAccount>> UnlinkAccountAsync(string curr
7269
internal override async Task<List<LinkedAccount>> LinkAccountAsync(string currentAccountToken, string authTokenToConnect)
7370
{
7471
var uri = MakeUri2024("/account/connect");
75-
var request = new HttpRequestMessage(HttpMethod.Post, uri)
76-
{
77-
Content = MakeHttpContent(new { accountAuthTokenToConnect = authTokenToConnect })
78-
};
72+
var request = new HttpRequestMessage(HttpMethod.Post, uri) { Content = MakeHttpContent(new { accountAuthTokenToConnect = authTokenToConnect }) };
7973
var response = await this.SendHttpWithAuthAsync(request, currentAccountToken).ConfigureAwait(false);
8074
await CheckStatusCodeAsync(response).ConfigureAwait(false);
8175

@@ -91,7 +85,7 @@ internal override async Task<List<LinkedAccount>> GetLinkedAccountsAsync(string
9185
await CheckStatusCodeAsync(response).ConfigureAwait(false);
9286

9387
var res = await DeserializeAsync<AccountConnectResponse>(response).ConfigureAwait(false);
94-
return res == null || res.LinkedAccounts == null || res.LinkedAccounts.Count == 0 ? [] : res.LinkedAccounts;
88+
return res == null || res.LinkedAccounts == null || res.LinkedAccounts.Count == 0 ? new List<LinkedAccount>() : res.LinkedAccounts;
9589
}
9690

9791
// embedded-wallet/embedded-wallet-shares GET
@@ -150,7 +144,16 @@ internal override async Task<VerifyResult> VerifySiweAsync(LoginPayloadData payl
150144
{
151145
var uri = MakeUri2024("/login/siwe/callback");
152146
var content = MakeHttpContent(new { signature, payload });
153-
var response = await this._httpClient.PostAsync(uri.ToString(), content).ConfigureAwait(false);
147+
this._httpClient.AddHeader("origin", payload.Domain);
148+
ThirdwebHttpResponseMessage response = null;
149+
try
150+
{
151+
response = await this._httpClient.PostAsync(uri.ToString(), content).ConfigureAwait(false);
152+
}
153+
finally
154+
{
155+
this._httpClient.RemoveHeader("origin");
156+
}
154157
await CheckStatusCodeAsync(response).ConfigureAwait(false);
155158

156159
var authResult = await DeserializeAsync<AuthResultType>(response).ConfigureAwait(false);

Diff for: Thirdweb/Thirdweb.Wallets/InAppWallet/EmbeddedWallet/EmbeddedWallet.SIWE.cs

+5
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,9 @@ internal partial class EmbeddedWallet
1313

1414
return await this._server.VerifySiweAsync(payload, signature).ConfigureAwait(false);
1515
}
16+
17+
public async Task<Server.VerifyResult> SignInWithSiweRawAsync(LoginPayloadData payload, string signature)
18+
{
19+
return await this._server.VerifySiweAsync(payload, signature).ConfigureAwait(false);
20+
}
1621
}

Diff for: Thirdweb/Thirdweb.Wallets/InAppWallet/InAppWallet.Types.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ public enum AuthProvider
2424
Github,
2525
Twitch,
2626
Steam,
27-
Backend
27+
Backend,
28+
SiweExternal,
2829
}
2930

3031
/// <summary>

0 commit comments

Comments
 (0)