Skip to content

Commit

Permalink
All-in-one Login with Oauth Flow (#20)
Browse files Browse the repository at this point in the history
* All-in-one Login with Oauth Flow

* Update Thirdweb.Transactions.Tests.cs

---------

Signed-off-by: Firekeeper <[email protected]>
  • Loading branch information
0xFirekeeper authored May 15, 2024
1 parent d0d24d7 commit bd059d4
Show file tree
Hide file tree
Showing 5 changed files with 261 additions and 31 deletions.
56 changes: 35 additions & 21 deletions Thirdweb.Console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Newtonsoft.Json;
using Nethereum.RPC.Eth.DTOs;
using Nethereum.Hex.HexTypes;
using System.Diagnostics;

DotEnv.Load();

Expand All @@ -22,32 +23,45 @@

// Create wallets (this is an advanced use case, typically one wallet is plenty)
var privateKeyWallet = await PrivateKeyWallet.Create(client: client, privateKeyHex: privateKey);
var inAppWallet = await InAppWallet.Create(client: client, email: "[email protected]"); // or email: null, phoneNumber: "+1234567890"

// // Reset InAppWallet (optional step for testing login flow)
// if (await inAppWallet.IsConnected())
// {
// await inAppWallet.Disconnect();
// }
// var inAppWallet = await InAppWallet.Create(client: client, email: "[email protected]"); // or email: null, phoneNumber: "+1234567890"
var inAppWallet = await InAppWallet.Create(client: client, authprovider: AuthProvider.Google); // or email: null, phoneNumber: "+1234567890"

// Reset InAppWallet (optional step for testing login flow)
if (await inAppWallet.IsConnected())
{
await inAppWallet.Disconnect();
}

// Relog if InAppWallet not logged in
if (!await inAppWallet.IsConnected())
{
await inAppWallet.SendOTP();
Console.WriteLine("Please submit the OTP.");
var otp = Console.ReadLine();
(var inAppWalletAddress, var canRetry) = await inAppWallet.SubmitOTP(otp);
if (inAppWalletAddress == null && canRetry)
{
Console.WriteLine("Please submit the OTP again.");
otp = Console.ReadLine();
(inAppWalletAddress, _) = await inAppWallet.SubmitOTP(otp);
}
if (inAppWalletAddress == null)
{
Console.WriteLine("OTP login failed. Please try again.");
return;
}
var address = await inAppWallet.LoginWithOauth(
isMobile: false,
(url) =>
{
var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
_ = Process.Start(psi);
},
"thirdweb://",
new InAppWalletBrowser()
);
Console.WriteLine($"InAppWallet address: {address}");
// await inAppWallet.SendOTP();
// Console.WriteLine("Please submit the OTP.");
// var otp = Console.ReadLine();
// (var inAppWalletAddress, var canRetry) = await inAppWallet.SubmitOTP(otp);
// if (inAppWalletAddress == null && canRetry)
// {
// Console.WriteLine("Please submit the OTP again.");
// otp = Console.ReadLine();
// (inAppWalletAddress, _) = await inAppWallet.SubmitOTP(otp);
// }
// if (inAppWalletAddress == null)
// {
// Console.WriteLine("OTP login failed. Please try again.");
// return;
// }
}

// Create smart wallet with InAppWallet signer
Expand Down
9 changes: 6 additions & 3 deletions Thirdweb.Tests/Thirdweb.Transactions.Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -221,10 +221,13 @@ public async Task EstimateTotalCosts_HigherThanGasCostsByValue()
_ = transaction.SetValue(new BigInteger(1000000000000000000)); // 100 gwei accounting for fluctuations
_ = transaction.SetGasLimit(21000);

var totalCosts = await ThirdwebTransaction.EstimateTotalCosts(transaction);
var gasCosts = await ThirdwebTransaction.EstimateGasCosts(transaction);
var totalCostsTask = ThirdwebTransaction.EstimateTotalCosts(transaction);
var gasCostsTask = ThirdwebTransaction.EstimateGasCosts(transaction);

Assert.True(totalCosts.wei > gasCosts.wei);
var costs = await Task.WhenAll(totalCostsTask, gasCostsTask);

Assert.True(costs[0].wei > costs[1].wei);
Assert.True(costs[0].wei - costs[1].wei == transaction.Input.Value.Value);
}

[Fact]
Expand Down
34 changes: 34 additions & 0 deletions Thirdweb/Thirdweb.Wallets/InAppWallet/IThirdwebBrowser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
public interface IThirdwebBrowser
{
Task<BrowserResult> Login(string loginUrl, string redirectUrl, Action<string> browserOpenAction, CancellationToken cancellationToken = default);
}

public enum BrowserStatus
{
Success,
UserCanceled,
Timeout,
UnknownError,
}

public class BrowserResult
{
public BrowserStatus status { get; }

public string callbackUrl { get; }

public string error { get; }

public BrowserResult(BrowserStatus status, string callbackUrl)
{
this.status = status;
this.callbackUrl = callbackUrl;
}

public BrowserResult(BrowserStatus status, string callbackUrl, string error)
{
this.status = status;
this.callbackUrl = callbackUrl;
this.error = error;
}
}
90 changes: 83 additions & 7 deletions Thirdweb/Thirdweb.Wallets/InAppWallet/InAppWallet.cs
Original file line number Diff line number Diff line change
@@ -1,42 +1,65 @@
using System.Web;
using Nethereum.Signer;
using Thirdweb.EWS;

namespace Thirdweb
{
public enum AuthProvider
{
Default,
Google,
Apple,
Facebook,
// JWT,
// AuthEndpoint
}

public class InAppWallet : PrivateKeyWallet
{
internal EmbeddedWallet _embeddedWallet;
internal string _email;
internal string _phoneNumber;
internal string _authProvider;

internal InAppWallet(ThirdwebClient client, string email, string phoneNumber, EmbeddedWallet embeddedWallet, EthECKey ecKey)
internal InAppWallet(ThirdwebClient client, string email, string phoneNumber, string authProvider, EmbeddedWallet embeddedWallet, EthECKey ecKey)
: base(client, ecKey)
{
_email = email;
_phoneNumber = phoneNumber;
_embeddedWallet = embeddedWallet;
_authProvider = authProvider;
}

public static async Task<InAppWallet> Create(ThirdwebClient client, string email = null, string phoneNumber = null)
public static async Task<InAppWallet> Create(ThirdwebClient client, string email = null, string phoneNumber = null, AuthProvider authprovider = AuthProvider.Default)
{
if (string.IsNullOrEmpty(email) && string.IsNullOrEmpty(phoneNumber))
if (string.IsNullOrEmpty(email) && string.IsNullOrEmpty(phoneNumber) && authprovider == AuthProvider.Default)
{
throw new ArgumentException("Email or Phone Number must be provided to login.");
throw new ArgumentException("Email, Phone Number, or OAuth Provider must be provided to login.");
}

var authproviderStr = authprovider switch
{
AuthProvider.Google => "Google",
AuthProvider.Apple => "Apple",
AuthProvider.Facebook => "Facebook",
AuthProvider.Default => string.IsNullOrEmpty(email) ? "PhoneOTP" : "EmailOTP",
_ => throw new ArgumentException("Invalid AuthProvider"),
};

var embeddedWallet = new EmbeddedWallet(client);
EthECKey ecKey;
try
{
var user = await embeddedWallet.GetUserAsync(email, email == null ? "PhoneOTP" : "EmailOTP");
if (!string.IsNullOrEmpty(authproviderStr)) { }
var user = await embeddedWallet.GetUserAsync(email, authproviderStr);
ecKey = new EthECKey(user.Account.PrivateKey);
}
catch
{
Console.WriteLine("User not found. Please call InAppWallet.SendOTP() to initialize the login process.");
Console.WriteLine("User not found. Please call InAppWallet.SendOTP() or InAppWallet.LoginWithOauth to initialize the login process.");
ecKey = null;
}
return new InAppWallet(client, email, phoneNumber, embeddedWallet, ecKey);
return new InAppWallet(client, email, phoneNumber, authproviderStr, embeddedWallet, ecKey);
}

public override async Task Disconnect()
Expand All @@ -45,6 +68,59 @@ public override async Task Disconnect()
await _embeddedWallet.SignOutAsync();
}

#region OAuth2 Flow

public virtual async Task<string> LoginWithOauth(
bool isMobile,
Action<string> browserOpenAction,
string mobileRedirectScheme = "thirdweb://",
IThirdwebBrowser browser = null,
CancellationToken cancellationToken = default
)
{
if (isMobile && string.IsNullOrEmpty(mobileRedirectScheme))
{
throw new ArgumentNullException(nameof(mobileRedirectScheme), "Mobile redirect scheme cannot be null or empty on this platform.");
}

var platform = "unity";
var redirectUrl = isMobile ? mobileRedirectScheme : "http://localhost:8789/";
var loginUrl = await _embeddedWallet.FetchHeadlessOauthLoginLinkAsync(_authProvider);
loginUrl = $"{loginUrl}?platform={platform}&redirectUrl={redirectUrl}&developerClientId={_client.ClientId}&authOption={_authProvider}";

browser ??= new InAppWalletBrowser();
var browserResult = await browser.Login(loginUrl, redirectUrl, browserOpenAction, cancellationToken);
var callbackUrl =
browserResult.status != BrowserStatus.Success
? throw new Exception($"Failed to login with {_authProvider}: {browserResult.status} | {browserResult.error}")
: browserResult.callbackUrl;

while (string.IsNullOrEmpty(callbackUrl))
{
if (cancellationToken.IsCancellationRequested)
{
throw new TaskCanceledException("LoginWithOauth was cancelled.");
}
await Task.Delay(100, cancellationToken);
}

var decodedUrl = HttpUtility.UrlDecode(callbackUrl);
Uri uri = new(decodedUrl);
var queryString = uri.Query;
var queryDict = HttpUtility.ParseQueryString(queryString);
var authResultJson = queryDict["authResult"];

var res = await _embeddedWallet.SignInWithOauthAsync(_authProvider, authResultJson, null);
if (res.User == null)
{
throw new Exception("Failed to login with OAuth2");
}
_ecKey = new EthECKey(res.User.Account.PrivateKey);
return await GetAddress();
}

#endregion

#region OTP Flow

public async Task SendOTP()
Expand Down
103 changes: 103 additions & 0 deletions Thirdweb/Thirdweb.Wallets/InAppWallet/InAppWalletBrowser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using System.Net;

namespace Thirdweb
{
public class InAppWalletBrowser : IThirdwebBrowser
{
private TaskCompletionSource<BrowserResult> _taskCompletionSource;

private readonly string closePageResponse =
@"
<html>
<head>
<style>
body {
font-family: Arial, sans-serif;
background-color: #2c2c2c;
color: #ffffff;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
flex-direction: column;
}
.container {
background-color: #3c3c3c;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.3);
text-align: center;
}
.instruction {
margin-top: 20px;
font-size: 18px;
}
</style>
</head>
<body>
<div class='container'>
<b>DONE!</b>
<div class='instruction'>
You can close this tab/window now.
</div>
</div>
</body>
</html>";

public async Task<BrowserResult> Login(string loginUrl, string redirectUrl, Action<string> browserOpenAction, CancellationToken cancellationToken = default)
{
_taskCompletionSource = new TaskCompletionSource<BrowserResult>();

cancellationToken.Register(() =>
{
_taskCompletionSource?.TrySetCanceled();
});

using var httpListener = new HttpListener();

try
{
redirectUrl = AddForwardSlashIfNecessary(redirectUrl);
httpListener.Prefixes.Add(redirectUrl);
httpListener.Start();
_ = httpListener.BeginGetContext(IncomingHttpRequest, httpListener);

Console.WriteLine($"Opening browser with URL: {loginUrl}");
browserOpenAction.Invoke(loginUrl);

var completedTask = await Task.WhenAny(_taskCompletionSource.Task, Task.Delay(TimeSpan.FromSeconds(30), cancellationToken));
return completedTask == _taskCompletionSource.Task ? await _taskCompletionSource.Task : new BrowserResult(BrowserStatus.Timeout, null, "The operation timed out.");
}
finally
{
httpListener.Stop();
}
}

private void IncomingHttpRequest(IAsyncResult result)
{
var httpListener = (HttpListener)result.AsyncState;
var httpContext = httpListener.EndGetContext(result);
var httpRequest = httpContext.Request;
var httpResponse = httpContext.Response;
var buffer = System.Text.Encoding.UTF8.GetBytes(closePageResponse);

httpResponse.ContentLength64 = buffer.Length;
var output = httpResponse.OutputStream;
output.Write(buffer, 0, buffer.Length);
output.Close();

_taskCompletionSource.SetResult(new BrowserResult(BrowserStatus.Success, httpRequest.Url.ToString()));
}

private string AddForwardSlashIfNecessary(string url)
{
string forwardSlash = "/";
if (!url.EndsWith(forwardSlash))
{
url += forwardSlash;
}
return url;
}
}
}

0 comments on commit bd059d4

Please sign in to comment.