Skip to content

Commit

Permalink
SiweExternal Auth - Login through static React EOA (#123)
Browse files Browse the repository at this point in the history
  • Loading branch information
0xFirekeeper authored Jan 17, 2025
1 parent 0c94da4 commit b50e1ed
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 33 deletions.
25 changes: 25 additions & 0 deletions Thirdweb.Console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> { "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);
Expand Down
49 changes: 27 additions & 22 deletions Thirdweb/Thirdweb.Wallets/IThirdwebWallet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,69 +12,69 @@ public interface IThirdwebWallet
/// <summary>
/// Gets the Thirdweb client associated with the wallet.
/// </summary>
ThirdwebClient Client { get; }
public ThirdwebClient Client { get; }

/// <summary>
/// Gets the account type of the wallet.
/// </summary>
ThirdwebAccountType AccountType { get; }
public ThirdwebAccountType AccountType { get; }

/// <summary>
/// Gets the address of the wallet.
/// </summary>
/// <returns>The wallet address.</returns>
Task<string> GetAddress();
public Task<string> GetAddress();

/// <summary>
/// Signs a raw message using Ethereum's signing method.
/// </summary>
/// <param name="rawMessage">The raw message to sign.</param>
/// <returns>The signed message.</returns>
Task<string> EthSign(byte[] rawMessage);
public Task<string> EthSign(byte[] rawMessage);

/// <summary>
/// Signs a message using Ethereum's signing method.
/// </summary>
/// <param name="message">The message to sign.</param>
/// <returns>The signed message.</returns>
Task<string> EthSign(string message);
public Task<string> EthSign(string message);

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

/// <summary>
/// Signs a raw message using personal signing.
/// </summary>
/// <param name="rawMessage">The raw message to sign.</param>
/// <returns>The signed message.</returns>
Task<string> PersonalSign(byte[] rawMessage);
public Task<string> PersonalSign(byte[] rawMessage);

/// <summary>
/// Signs a message using personal signing.
/// </summary>
/// <param name="message">The message to sign.</param>
/// <returns>The signed message.</returns>
Task<string> PersonalSign(string message);
public Task<string> PersonalSign(string message);

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

/// <summary>
/// Signs typed data (version 4).
/// </summary>
/// <param name="json">The JSON representation of the typed data.</param>
/// <returns>The signed data.</returns>
Task<string> SignTypedDataV4(string json);
public Task<string> SignTypedDataV4(string json);

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

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

/// <summary>
/// Checks if the wallet is connected.
/// </summary>
/// <returns>True if connected, otherwise false.</returns>
Task<bool> IsConnected();
public Task<bool> IsConnected();

/// <summary>
/// Signs a transaction.
/// </summary>
/// <param name="transaction">The transaction to sign.</param>
/// <returns>The signed transaction.</returns>
Task<string> SignTransaction(ThirdwebTransactionInput transaction);
public Task<string> SignTransaction(ThirdwebTransactionInput transaction);

/// <summary>
/// Sends a transaction.
/// </summary>
/// <param name="transaction">The transaction to send.</param>
/// <returns>The transaction hash.</returns>
Task<string> SendTransaction(ThirdwebTransactionInput transaction);
public Task<string> SendTransaction(ThirdwebTransactionInput transaction);

/// <summary>
/// Sends a transaction and waits for its receipt.
/// </summary>
/// <param name="transaction">The transaction to execute.</param>
/// <returns>The transaction receipt.</returns>
Task<ThirdwebTransactionReceipt> ExecuteTransaction(ThirdwebTransactionInput transaction);
public Task<ThirdwebTransactionReceipt> ExecuteTransaction(ThirdwebTransactionInput transaction);

/// <summary>
/// Disconnects the wallet (if using InAppWallet, clears session)
/// </summary>
Task Disconnect();
public Task Disconnect();

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

/// <summary>
/// Returns a list of linked accounts to the current wallet.
/// </summary>
/// <returns>A list of <see cref="LinkedAccount"/> objects.</returns>
Task<List<LinkedAccount>> GetLinkedAccounts();
public Task<List<LinkedAccount>> GetLinkedAccounts();

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

/// <summary>
/// Attempts to set the active network to the specified chain ID.
/// </summary>
/// <param name="chainId">The chain ID to switch to.</param>
Task SwitchNetwork(BigInteger chainId);
public Task SwitchNetwork(BigInteger chainId);
}

/// <summary>
Expand Down Expand Up @@ -280,4 +280,9 @@ public class LoginPayloadData
/// Initializes a new instance of the <see cref="LoginPayloadData"/> class.
/// </summary>
public LoginPayloadData() { }

public override string ToString()
{
return JsonConvert.SerializeObject(this);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ public static async Task<EcosystemWallet> 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"),
};
Expand Down Expand Up @@ -497,6 +498,10 @@ public async Task<List<LinkedAccount>> 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":
Expand Down Expand Up @@ -700,6 +705,81 @@ public async Task<string> LoginWithOauth(

#endregion

#region SiweExternal

private async Task<Server.VerifyResult> PreAuth_SiweExternal(
bool isMobile,
Action<string> browserOpenAction,
List<string> 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<LoginPayloadData>(payload);

var serverRes = await this.EmbeddedWallet.SignInWithSiweRawAsync(payloadData, signature).ConfigureAwait(false);
return serverRes;
}

public async Task<string> LoginWithSiweExternal(
bool isMobile,
Action<string> browserOpenAction,
List<string> 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<Server.VerifyResult> PreAuth_Siwe(IThirdwebWallet siweSigner, BigInteger chainId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,7 @@ internal Server(ThirdwebClient client, IThirdwebHttpClient httpClient)
internal override async Task<List<LinkedAccount>> 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);

Expand All @@ -72,10 +69,7 @@ internal override async Task<List<LinkedAccount>> UnlinkAccountAsync(string curr
internal override async Task<List<LinkedAccount>> 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);

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

var res = await DeserializeAsync<AccountConnectResponse>(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<LinkedAccount>() : res.LinkedAccounts;
}

// embedded-wallet/embedded-wallet-shares GET
Expand Down Expand Up @@ -150,7 +144,16 @@ internal override async Task<VerifyResult> 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<AuthResultType>(response).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,9 @@ internal partial class EmbeddedWallet

return await this._server.VerifySiweAsync(payload, signature).ConfigureAwait(false);
}

public async Task<Server.VerifyResult> SignInWithSiweRawAsync(LoginPayloadData payload, string signature)
{
return await this._server.VerifySiweAsync(payload, signature).ConfigureAwait(false);
}
}
3 changes: 2 additions & 1 deletion Thirdweb/Thirdweb.Wallets/InAppWallet/InAppWallet.Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ public enum AuthProvider
Github,
Twitch,
Steam,
Backend
Backend,
SiweExternal,
}

/// <summary>
Expand Down

0 comments on commit b50e1ed

Please sign in to comment.