Skip to content

Commit

Permalink
Migrate OTP Flow to v2 (#52)
Browse files Browse the repository at this point in the history
  • Loading branch information
0xFirekeeper authored Aug 8, 2024
1 parent 3f5b090 commit 59087bd
Show file tree
Hide file tree
Showing 19 changed files with 222 additions and 679 deletions.
79 changes: 42 additions & 37 deletions Thirdweb.Console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,28 +22,28 @@
Console.WriteLine($"Contract read result: {readResult}");

// Create wallets (this is an advanced use case, typically one wallet is plenty)
var privateKeyWallet = await PrivateKeyWallet.Create(client: client, privateKeyHex: privateKey);
var walletAddress = await privateKeyWallet.GetAddress();
// var privateKeyWallet = await PrivateKeyWallet.Create(client: client, privateKeyHex: privateKey);
// var walletAddress = await privateKeyWallet.GetAddress();

var chainData = await Utils.FetchThirdwebChainDataAsync(client, 421614);
Console.WriteLine($"Chain data: {JsonConvert.SerializeObject(chainData, Formatting.Indented)}");
// var chainData = await Utils.FetchThirdwebChainDataAsync(client, 421614);
// Console.WriteLine($"Chain data: {JsonConvert.SerializeObject(chainData, Formatting.Indented)}");

var inAppWalletOAuth = await InAppWallet.Create(client: client, authProvider: AuthProvider.Telegram);
if (!await inAppWalletOAuth.IsConnected())
{
_ = await inAppWalletOAuth.LoginWithOauth(
isMobile: false,
(url) =>
{
var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
_ = Process.Start(psi);
},
"thirdweb://",
new InAppWalletBrowser()
);
}
var inAppWalletOAuthAddress = await inAppWalletOAuth.GetAddress();
Console.WriteLine($"InAppWallet OAuth address: {inAppWalletOAuthAddress}");
// var inAppWalletOAuth = await InAppWallet.Create(client: client, authProvider: AuthProvider.Telegram);
// if (!await inAppWalletOAuth.IsConnected())
// {
// _ = await inAppWalletOAuth.LoginWithOauth(
// isMobile: false,
// (url) =>
// {
// var psi = new ProcessStartInfo { FileName = url, UseShellExecute = true };
// _ = Process.Start(psi);
// },
// "thirdweb://",
// new InAppWalletBrowser()
// );
// }
// var inAppWalletOAuthAddress = await inAppWalletOAuth.GetAddress();
// Console.WriteLine($"InAppWallet OAuth address: {inAppWalletOAuthAddress}");

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

Expand Down Expand Up @@ -124,7 +124,7 @@
// }


// var inAppWallet = await InAppWallet.Create(client: client, email: "firekeeper+awsless@thirdweb.com"); // or email: null, phoneNumber: "+1234567890"
var inAppWallet = await InAppWallet.Create(client: client, email: "firekeeper+otpv2@thirdweb.com"); // or email: null, phoneNumber: "+1234567890"

// var inAppWallet = await InAppWallet.Create(client: client, authprovider: AuthProvider.Google); // or email: null, phoneNumber: "+1234567890"

Expand All @@ -150,22 +150,27 @@
// Console.WriteLine($"InAppWallet address: {address}");
// }

// await inAppWallet.SendOTP();
// Console.WriteLine("Please submit the OTP.");
// retry:
// var otp = Console.ReadLine();
// (var inAppWalletAddress, var canRetry) = await inAppWallet.SubmitOTP(otp);
// if (inAppWalletAddress == null && canRetry)
// {
// Console.WriteLine("Please submit the OTP again.");
// goto retry;
// }
// if (inAppWalletAddress == null)
// {
// Console.WriteLine("OTP login failed. Please try again.");
// return;
// }
// Console.WriteLine($"InAppWallet address: {inAppWalletAddress}");
if (await inAppWallet.IsConnected())
{
Console.WriteLine($"InAppWallet address: {await inAppWallet.GetAddress()}");
return;
}
await inAppWallet.SendOTP();
Console.WriteLine("Please submit the OTP.");
retry:
var otp = Console.ReadLine();
(var inAppWalletAddress, var canRetry) = await inAppWallet.SubmitOTP(otp);
if (inAppWalletAddress == null && canRetry)
{
Console.WriteLine("Please submit the OTP again.");
goto retry;
}
if (inAppWalletAddress == null)
{
Console.WriteLine("OTP login failed. Please try again.");
return;
}
Console.WriteLine($"InAppWallet address: {inAppWalletAddress}");
// }

// Prepare a transaction directly, or with Contract.Prepare
Expand Down
7 changes: 0 additions & 7 deletions Thirdweb.Tests/Thirdweb.Utils/Thirdweb.Utils.Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -456,15 +456,8 @@ public async Task FetchThirdwebChainDataAsync_ReturnsChainData_WhenResponseIsSuc
Assert.NotNull(chainData.NativeCurrency.Name);
Assert.NotNull(chainData.NativeCurrency.Symbol);
Assert.Equal(18, chainData.NativeCurrency.Decimals);
Assert.NotNull(chainData.Features);
Assert.NotNull(chainData.Faucets);
Assert.NotNull(chainData.Explorers);
Assert.NotNull(chainData.RedFlags);
Assert.Null(chainData.Parent);

chainId = 42161;
chainData = await Utils.FetchThirdwebChainDataAsync(_client, chainId);
Assert.NotNull(chainData.Parent);
}

[Fact(Timeout = 120000)]
Expand Down
28 changes: 0 additions & 28 deletions Thirdweb/Thirdweb.Utils/ThirdwebChainData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,6 @@ public class ThirdwebChainData
[JsonProperty("icon")]
public ThirdwebChainIcon Icon { get; set; }

[JsonProperty("features")]
public List<ThirdwebChainFeature> Features { get; set; }

[JsonProperty("faucets")]
public List<object> Faucets { get; set; }

Expand All @@ -62,12 +59,6 @@ public class ThirdwebChainData

[JsonProperty("testnet")]
public bool Testnet { get; set; }

[JsonProperty("redFlags")]
public List<object> RedFlags { get; set; }

[JsonProperty("parent")]
public ThirdwebChainParent Parent { get; set; }
}

[JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
Expand Down Expand Up @@ -99,13 +90,6 @@ public class ThirdwebChainIcon
public string Format { get; set; }
}

[JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
public class ThirdwebChainFeature
{
[JsonProperty("name")]
public string Name { get; set; }
}

public class ThirdwebChainEns
{
[JsonProperty("registry")]
Expand All @@ -127,18 +111,6 @@ public class ThirdwebChainExplorer
public ThirdwebChainIcon Icon { get; set; }
}

public class ThirdwebChainParent
{
[JsonProperty("type")]
public string Type { get; set; }

[JsonProperty("chain")]
public string Chain { get; set; }

[JsonProperty("bridges")]
public List<ThirdwebChainBridge> Bridges { get; set; }
}

public class ThirdwebChainBridge
{
[JsonProperty("url")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,123 +6,20 @@ namespace Thirdweb.EWS
{
internal class AWS
{
private const string awsRegion = "us-west-2";
private const string cognitoAppClientId = "2e02ha2ce6du13ldk8pai4h3d0";
private static readonly string cognitoIdentityPoolId = $"{awsRegion}:2ad7ab1e-f48b-48a6-adfa-ac1090689c26";
private static readonly string cognitoUserPoolId = $"{awsRegion}_UFwLcZIpq";
private static readonly string recoverySharePasswordLambdaFunctionName = $"arn:aws:lambda:{awsRegion}:324457261097:function:recovery-share-password-GenerateRecoverySharePassw-bbE5ZbVAToil";
private static readonly string recoverySharePasswordLambdaFunctionNameV2 = "arn:aws:lambda:us-west-2:324457261097:function:lambda-thirdweb-auth-enc-key-prod-ThirdwebAuthEncKeyFunction";

internal static async Task SignUpCognitoUserAsync(string emailAddress, string userName, Type thirdwebHttpClientType)
{
emailAddress ??= "[email protected]";

var client = thirdwebHttpClientType.GetConstructor(Type.EmptyTypes).Invoke(null) as IThirdwebHttpClient;
var endpoint = $"https://cognito-idp.{awsRegion}.amazonaws.com/";
var payload = new
{
ClientId = cognitoAppClientId,
Username = userName,
Password = Secrets.Random(12),
UserAttributes = new[] { new { Name = "email", Value = emailAddress } }
};

var content = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, "application/x-amz-json-1.1");

client.AddHeader("X-Amz-Target", "AWSCognitoIdentityProviderService.SignUp");

var response = await client.PostAsync(endpoint, content).ConfigureAwait(false);

if (!response.IsSuccessStatusCode)
{
var responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new Exception($"Sign-up failed: {responseBody}");
}
}

internal static async Task<string> StartCognitoUserAuth(string userName, Type thirdwebHttpClientType)
{
var client = thirdwebHttpClientType.GetConstructor(Type.EmptyTypes).Invoke(null) as IThirdwebHttpClient;
var endpoint = $"https://cognito-idp.{awsRegion}.amazonaws.com/";
var payload = new
{
AuthFlow = "CUSTOM_AUTH",
ClientId = cognitoAppClientId,
AuthParameters = new Dictionary<string, string> { { "USERNAME", userName } },
ClientMetadata = new Dictionary<string, string>()
};

var content = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, "application/x-amz-json-1.1");

client.AddHeader("X-Amz-Target", "AWSCognitoIdentityProviderService.InitiateAuth");

var response = await client.PostAsync(endpoint, content).ConfigureAwait(false);

var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);

if (!response.IsSuccessStatusCode)
{
var errorResponse = JsonConvert.DeserializeObject<ErrorResponse>(responseContent);
if (errorResponse.Type == "UserNotFoundException")
{
return null;
}
throw new Exception($"Authentication initiation failed: {responseContent}");
}

var jsonResponse = JsonConvert.DeserializeObject<StartAuthResponse>(responseContent);
return jsonResponse.Session;
}

internal static async Task<TokenCollection> FinishCognitoUserAuth(string userName, string otp, string sessionId, Type thirdwebHttpClientType)
{
var client = thirdwebHttpClientType.GetConstructor(Type.EmptyTypes).Invoke(null) as IThirdwebHttpClient;
var endpoint = $"https://cognito-idp.{awsRegion}.amazonaws.com/";
var payload = new
{
ChallengeName = "CUSTOM_CHALLENGE",
ClientId = cognitoAppClientId,
ChallengeResponses = new Dictionary<string, string> { { "USERNAME", userName }, { "ANSWER", otp } },
Session = sessionId
};

var content = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, "application/x-amz-json-1.1");

client.AddHeader("X-Amz-Target", "AWSCognitoIdentityProviderService.RespondToAuthChallenge");

var response = await client.PostAsync(endpoint, content).ConfigureAwait(false);

var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);

if (!response.IsSuccessStatusCode)
{
var errorResponse = JsonConvert.DeserializeObject<ErrorResponse>(responseContent);
if (errorResponse.Type == "NotAuthorizedException")
{
throw new VerificationException("The session expired", false);
}
if (errorResponse.Type == "UserNotFoundException")
{
throw new InvalidOperationException("The user was not found");
}
throw new Exception($"Challenge response failed: {responseContent}");
}
private const string AWS_REGION = "us-west-2";

var jsonResponse = JsonConvert.DeserializeObject<FinishAuthResponse>(responseContent);
var result = jsonResponse.AuthenticationResult ?? throw new VerificationException("The OTP is incorrect", true);
return new TokenCollection(result.AccessToken.ToString(), result.IdToken.ToString(), result.RefreshToken.ToString());
}
private static readonly string recoverySharePasswordLambdaFunctionNameV2 = $"arn:aws:lambda:{AWS_REGION}:324457261097:function:lambda-thirdweb-auth-enc-key-prod-ThirdwebAuthEncKeyFunction";

internal static async Task<MemoryStream> InvokeRecoverySharePasswordLambdaV2Async(string identityId, string token, string invokePayload, Type thirdwebHttpClientType)
internal static async Task<MemoryStream> InvokeRecoverySharePasswordLambdaAsync(string identityId, string token, string invokePayload, Type thirdwebHttpClientType)
{
var credentials = await GetTemporaryCredentialsV2Async(identityId, token, thirdwebHttpClientType).ConfigureAwait(false);
var credentials = await GetTemporaryCredentialsAsync(identityId, token, thirdwebHttpClientType).ConfigureAwait(false);
return await InvokeLambdaWithTemporaryCredentialsAsync(credentials, invokePayload, thirdwebHttpClientType, recoverySharePasswordLambdaFunctionNameV2).ConfigureAwait(false);
}

private static async Task<AwsCredentials> GetTemporaryCredentialsV2Async(string identityId, string token, Type thirdwebHttpClientType)
private static async Task<AwsCredentials> GetTemporaryCredentialsAsync(string identityId, string token, Type thirdwebHttpClientType)
{
var client = thirdwebHttpClientType.GetConstructor(Type.EmptyTypes).Invoke(null) as IThirdwebHttpClient;
var endpoint = $"https://cognito-identity.{awsRegion}.amazonaws.com/";
var endpoint = $"https://cognito-identity.{AWS_REGION}.amazonaws.com/";

var payloadForGetCredentials = new { IdentityId = identityId, Logins = new Dictionary<string, string> { { "cognito-identity.amazonaws.com", token } } };

Expand All @@ -148,67 +45,9 @@ private static async Task<AwsCredentials> GetTemporaryCredentialsV2Async(string
};
}

internal static async Task<MemoryStream> InvokeRecoverySharePasswordLambdaAsync(string idToken, string invokePayload, Type thirdwebHttpClientType)
{
var credentials = await GetTemporaryCredentialsAsync(idToken, thirdwebHttpClientType).ConfigureAwait(false);
return await InvokeLambdaWithTemporaryCredentialsAsync(credentials, invokePayload, thirdwebHttpClientType, recoverySharePasswordLambdaFunctionName).ConfigureAwait(false);
}

private static async Task<AwsCredentials> GetTemporaryCredentialsAsync(string idToken, Type thirdwebHttpClientType)
{
var client = thirdwebHttpClientType.GetConstructor(Type.EmptyTypes).Invoke(null) as IThirdwebHttpClient;
var endpoint = $"https://cognito-identity.{awsRegion}.amazonaws.com/";

var payloadForGetId = new { IdentityPoolId = cognitoIdentityPoolId, Logins = new Dictionary<string, string> { { $"cognito-idp.{awsRegion}.amazonaws.com/{cognitoUserPoolId}", idToken } } };

var content = new StringContent(JsonConvert.SerializeObject(payloadForGetId), Encoding.UTF8, "application/x-amz-json-1.1");

client.AddHeader("X-Amz-Target", "AWSCognitoIdentityService.GetId");

var response = await client.PostAsync(endpoint, content).ConfigureAwait(false);

var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);

if (!response.IsSuccessStatusCode)
{
throw new Exception($"Failed to get identity ID: {responseContent}");
}

var identityIdResponse = JsonConvert.DeserializeObject<GetIdResponse>(responseContent);

var payloadForGetCredentials = new
{
IdentityId = identityIdResponse.IdentityId,
Logins = new Dictionary<string, string> { { $"cognito-idp.{awsRegion}.amazonaws.com/{cognitoUserPoolId}", idToken } }
};

content = new StringContent(JsonConvert.SerializeObject(payloadForGetCredentials), Encoding.UTF8, "application/x-amz-json-1.1");

client.RemoveHeader("X-Amz-Target");
client.AddHeader("X-Amz-Target", "AWSCognitoIdentityService.GetCredentialsForIdentity");

response = await client.PostAsync(endpoint, content).ConfigureAwait(false);

responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);

if (!response.IsSuccessStatusCode)
{
throw new Exception($"Failed to get credentials: {responseContent}");
}

var credentialsResponse = JsonConvert.DeserializeObject<GetCredentialsForIdentityResponse>(responseContent);

return new AwsCredentials
{
AccessKeyId = credentialsResponse.Credentials.AccessKeyId,
SecretAccessKey = credentialsResponse.Credentials.SecretKey,
SessionToken = credentialsResponse.Credentials.SessionToken
};
}

private static async Task<MemoryStream> InvokeLambdaWithTemporaryCredentialsAsync(AwsCredentials credentials, string invokePayload, Type thirdwebHttpClientType, string lambdaFunction)
{
var endpoint = $"https://lambda.{awsRegion}.amazonaws.com/2015-03-31/functions/{lambdaFunction}/invocations";
var endpoint = $"https://lambda.{AWS_REGION}.amazonaws.com/2015-03-31/functions/{lambdaFunction}/invocations";
var requestBody = new StringContent(invokePayload, Encoding.UTF8, "application/json");

var client = thirdwebHttpClientType.GetConstructor(Type.EmptyTypes).Invoke(null) as IThirdwebHttpClient;
Expand All @@ -219,18 +58,18 @@ private static async Task<MemoryStream> InvokeLambdaWithTemporaryCredentialsAsyn

var canonicalUri = "/2015-03-31/functions/" + Uri.EscapeDataString(lambdaFunction) + "/invocations";
var canonicalQueryString = "";
var canonicalHeaders = $"host:lambda.{awsRegion}.amazonaws.com\nx-amz-date:{amzDate}\n";
var canonicalHeaders = $"host:lambda.{AWS_REGION}.amazonaws.com\nx-amz-date:{amzDate}\n";
var signedHeaders = "host;x-amz-date";

using var sha256 = SHA256.Create();
var payloadHash = ToHexString(sha256.ComputeHash(Encoding.UTF8.GetBytes(invokePayload)));
var canonicalRequest = $"POST\n{canonicalUri}\n{canonicalQueryString}\n{canonicalHeaders}\n{signedHeaders}\n{payloadHash}";

var algorithm = "AWS4-HMAC-SHA256";
var credentialScope = $"{dateStamp}/{awsRegion}/lambda/aws4_request";
var credentialScope = $"{dateStamp}/{AWS_REGION}/lambda/aws4_request";
var stringToSign = $"{algorithm}\n{amzDate}\n{credentialScope}\n{ToHexString(sha256.ComputeHash(Encoding.UTF8.GetBytes(canonicalRequest)))}";

var signingKey = GetSignatureKey(credentials.SecretAccessKey, dateStamp, awsRegion, "lambda");
var signingKey = GetSignatureKey(credentials.SecretAccessKey, dateStamp, AWS_REGION, "lambda");
var signature = ToHexString(HMACSHA256(signingKey, stringToSign));

var authorizationHeader = $"{algorithm} Credential={credentials.AccessKeyId}/{credentialScope}, SignedHeaders={signedHeaders}, Signature={signature}";
Expand Down
Loading

0 comments on commit 59087bd

Please sign in to comment.