Skip to content
This repository has been archived by the owner on May 24, 2024. It is now read-only.

Add GracePeriod #383

Merged
merged 3 commits into from
Dec 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ build:
parallel: true
verbosity: minimal
test_script:
- cmd: dotnet format --verify-no-changes
- cmd: dotnet test -p:CollectCoverage=true -p:CoverletOutputFormat=opencover -p:ExcludeByFile=\"../stellar-dotnet-sdk/chaos.nacl/**/*.cs\" -p:ExcludeByFile=\"../stellar-dotnet-sdk-xdr/generated/*.cs\" ./stellar-dotnet-sdk-test --test-adapter-path:. --logger:Appveyor
after_test:
- cmd: packages\coveralls.io\1.4.2\tools\coveralls.net.exe --opencover "./stellar-dotnet-sdk-test/coverage.opencover.xml" -r %COVERALLS_REPO_TOKEN%
Expand Down
78 changes: 52 additions & 26 deletions stellar-dotnet-sdk/WebAuthentication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ namespace stellar_dotnet_sdk
/// </summary>
public static class WebAuthentication
{
/// <summary>
/// Give a small grace period for the transaction time to account for clock drift.
/// </summary>
public const int GracePeriod = 60 * 5;

/// <summary>
/// Build a challenge transaction you can use for Stellar Web Authentication.
/// </summary>
Expand All @@ -26,15 +31,17 @@ public static class WebAuthentication
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ArgumentException"></exception>
public static Transaction BuildChallengeTransaction(KeyPair serverKeypair, string clientAccountId,
string homeDomain, string webAuthDomain, byte[] nonce = null, DateTimeOffset? now = null, TimeSpan? timeout = null,
string homeDomain, string webAuthDomain, byte[] nonce = null, DateTimeOffset? now = null,
TimeSpan? timeout = null,
Network network = null, string clientDomain = null, KeyPair clientKeypair = null)
{
if (string.IsNullOrEmpty(clientAccountId)) throw new ArgumentNullException(nameof(clientAccountId));

if (StrKey.DecodeVersionByte(clientAccountId) != StrKey.VersionByte.ACCOUNT_ID)
throw new InvalidWebAuthenticationException($"{nameof(clientAccountId)} is not a valid account id");
var clientAccountKeypair = KeyPair.FromAccountId(clientAccountId);
return BuildChallengeTransaction(serverKeypair, clientAccountKeypair, homeDomain, webAuthDomain, nonce, now, timeout,
return BuildChallengeTransaction(serverKeypair, clientAccountKeypair, homeDomain, webAuthDomain, nonce, now,
timeout,
network, clientDomain, clientKeypair);
}

Expand All @@ -55,14 +62,16 @@ public static Transaction BuildChallengeTransaction(KeyPair serverKeypair, strin
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ArgumentException"></exception>
public static Transaction BuildChallengeTransaction(KeyPair serverKeypair, KeyPair clientAccountId,
string homeDomain, string webAuthDomain, byte[] nonce = null, DateTimeOffset? now = null, TimeSpan? timeout = null,
string homeDomain, string webAuthDomain, byte[] nonce = null, DateTimeOffset? now = null,
TimeSpan? timeout = null,
Network network = null, string clientDomain = null, KeyPair clientSigningKey = null)
{
if (serverKeypair is null) throw new ArgumentNullException(nameof(serverKeypair));
if (clientAccountId is null) throw new ArgumentNullException(nameof(clientAccountId));
if (string.IsNullOrEmpty(homeDomain)) throw new ArgumentNullException(nameof(homeDomain));
if (string.IsNullOrEmpty(webAuthDomain)) throw new ArgumentNullException(nameof(webAuthDomain));
if (!string.IsNullOrEmpty(clientDomain) && clientSigningKey is null) throw new ArgumentNullException(nameof(clientSigningKey));
if (!string.IsNullOrEmpty(clientDomain) && clientSigningKey is null)
throw new ArgumentNullException(nameof(clientSigningKey));

if (nonce is null)
{
Expand Down Expand Up @@ -108,9 +117,10 @@ public static Transaction BuildChallengeTransaction(KeyPair serverKeypair, KeyPa

if (!string.IsNullOrEmpty(clientDomain))
{
var clientDomainOperation = new ManageDataOperation.Builder("client_domain", Encoding.UTF8.GetBytes(clientDomain))
.SetSourceAccount(clientSigningKey)
.Build();
var clientDomainOperation =
new ManageDataOperation.Builder("client_domain", Encoding.UTF8.GetBytes(clientDomain))
.SetSourceAccount(clientSigningKey)
.Build();

txBuilder.AddOperation(clientDomainOperation);
}
Expand Down Expand Up @@ -142,10 +152,12 @@ public static Transaction BuildChallengeTransaction(KeyPair serverKeypair, KeyPa
/// <param name="now">Current time, defaults to DateTimeOffset.Now</param>
/// <returns>The client account id</returns>
/// <exception cref="InvalidWebAuthenticationException"></exception>
public static string ReadChallengeTransaction(Transaction transaction, string serverAccountId, string homeDomain, string webAuthDomain,
public static string ReadChallengeTransaction(Transaction transaction, string serverAccountId,
string homeDomain, string webAuthDomain,
Network network = null, DateTimeOffset? now = null)
{
return ReadChallengeTransaction(transaction, serverAccountId, new string[1] { homeDomain }, webAuthDomain, network, now);
return ReadChallengeTransaction(transaction, serverAccountId, new string[1] { homeDomain }, webAuthDomain,
network, now);
}

/// <summary>
Expand All @@ -165,10 +177,11 @@ public static string ReadChallengeTransaction(Transaction transaction, string se
/// <param name="homeDomain">The server home domain</param>
/// <param name="webAuthDomain">The server auth domain</param>
/// <param name="network">The network the transaction was submitted to, defaults to Network.Current</param>
/// <param name="now">Current time, defaults to DateTimeOffset.Now</param>
/// <param name="now">Current time, defaults to DateTimeOffset.Now + GracePeriod</param>
/// <returns>The client account id</returns>
/// <exception cref="InvalidWebAuthenticationException"></exception>
public static string ReadChallengeTransaction(Transaction transaction, string serverAccountId, string[] homeDomains, string webAuthDomain,
public static string ReadChallengeTransaction(Transaction transaction, string serverAccountId,
string[] homeDomains, string webAuthDomain,
Network network = null, DateTimeOffset? now = null)
{
network = network ?? Network.Current;
Expand Down Expand Up @@ -198,7 +211,8 @@ public static string ReadChallengeTransaction(Transaction transaction, string se
throw new InvalidWebAuthenticationException("Challenge transaction operation must have source account");

if (homeDomains == null || homeDomains.Length == 0)
throw new InvalidWebAuthenticationException("Invalid homeDomains: a home domain must be provided for verification");
throw new InvalidWebAuthenticationException(
"Invalid homeDomains: a home domain must be provided for verification");

string matchedHomeDomain = "";

Expand All @@ -212,14 +226,16 @@ public static string ReadChallengeTransaction(Transaction transaction, string se
}

if (string.IsNullOrEmpty(matchedHomeDomain))
throw new InvalidWebAuthenticationException("Invalid homeDomains: the transaction's operation key name does not match the expected home domain");
throw new InvalidWebAuthenticationException(
"Invalid homeDomains: the transaction's operation key name does not match the expected home domain");

var subsequentOperations = transaction.Operations;
foreach (var op in subsequentOperations.Skip(1))
{
if (!(op is ManageDataOperation))
{
throw new InvalidWebAuthenticationException("The transaction has operations that are not of type 'manageData'");
throw new InvalidWebAuthenticationException(
"The transaction has operations that are not of type 'manageData'");
}

var opManageData = (ManageDataOperation)op;
Expand All @@ -230,16 +246,19 @@ public static string ReadChallengeTransaction(Transaction transaction, string se

var opDataValue = opManageData.Value != null ? Encoding.UTF8.GetString(opManageData.Value) : null;

if (opManageData.Name == "web_auth_domain" && (opManageData.Value == null || opDataValue != webAuthDomain))
if (opManageData.Name == "web_auth_domain" &&
(opManageData.Value == null || opDataValue != webAuthDomain))
{
throw new InvalidWebAuthenticationException($"Invalid 'web_auth_domain' value. Expected: {webAuthDomain} Actual: {opDataValue}");
throw new InvalidWebAuthenticationException(
$"Invalid 'web_auth_domain' value. Expected: {webAuthDomain} Actual: {opDataValue}");
}
}

var clientAccountKeypair = operation.SourceAccount;

if (clientAccountKeypair.IsMuxedAccount)
throw new InvalidWebAuthenticationException("Challenge transaction operation source account cannot be a muxed account");
throw new InvalidWebAuthenticationException(
"Challenge transaction operation source account cannot be a muxed account");

var clientAccountId = clientAccountKeypair.Address;

Expand All @@ -262,18 +281,20 @@ public static string ReadChallengeTransaction(Transaction transaction, string se
if (!ValidateSignedBy(transaction, serverAccountId, network))
throw new InvalidWebAuthenticationException("Challenge transaction not signed by server");

if (!ValidateTimeBounds(transaction.TimeBounds, now ?? DateTimeOffset.Now))
if (!ValidateTimeBounds(transaction.TimeBounds, now ?? DateTimeOffset.Now.AddSeconds(GracePeriod)))
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the actual change here

throw new InvalidWebAuthenticationException("Challenge transaction expired");

return clientAccountId;
}

public static ICollection<string> VerifyChallengeTransactionThreshold(Transaction transaction,
string serverAccountId,
int threshold, Dictionary<string, int> signerSummary, string homeDomain, string webAuthDomain, Network network = null, DateTimeOffset? now = null)
int threshold, Dictionary<string, int> signerSummary, string homeDomain, string webAuthDomain,
Network network = null, DateTimeOffset? now = null)
{
var signersFound =
VerifyChallengeTransactionSigners(transaction, serverAccountId, signerSummary.Keys.ToArray(), homeDomain, webAuthDomain, network,
VerifyChallengeTransactionSigners(transaction, serverAccountId, signerSummary.Keys.ToArray(),
homeDomain, webAuthDomain, network,
now);
var weight = signersFound.Sum(signer => signerSummary[signer]);
if (weight < threshold)
Expand All @@ -300,7 +321,8 @@ public static ICollection<string> VerifyChallengeTransactionThreshold(Transactio
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public static string[] VerifyChallengeTransactionSigners(Transaction transaction, string serverAccountId,
ICollection<string> signers, string homeDomain, string webAuthDomain, Network network = null, DateTimeOffset? now = null)
ICollection<string> signers, string homeDomain, string webAuthDomain, Network network = null,
DateTimeOffset? now = null)
{
if (!signers.Any())
throw new ArgumentException($"{nameof(signers)} must be non-empty");
Expand Down Expand Up @@ -341,14 +363,16 @@ public static string[] VerifyChallengeTransactionSigners(Transaction transaction

if (clientSigningKey != null)
{
clientSigningKeyFound = !string.IsNullOrEmpty(allSignersFound.FirstOrDefault(signer => signer == clientSigningKey.Address));
clientSigningKeyFound =
!string.IsNullOrEmpty(allSignersFound.FirstOrDefault(signer => signer == clientSigningKey.Address));
}

if (serverSigner is null)
throw new InvalidWebAuthenticationException("Challenge transaction not signed by server");

if (clientSigningKey != null && !clientSigningKeyFound)
throw new InvalidWebAuthenticationException("Challenge Transaction not signed by the source account of the 'client_domain' ");
throw new InvalidWebAuthenticationException(
"Challenge Transaction not signed by the source account of the 'client_domain' ");

if (allSignersFound.Count == 1)
throw new InvalidWebAuthenticationException("Challenge transaction not signed by client");
Expand Down Expand Up @@ -380,12 +404,14 @@ public static string[] VerifyChallengeTransactionSigners(Transaction transaction
/// <returns>True if the transaction is valid</returns>
/// <exception cref="InvalidWebAuthenticationException"></exception>
[Obsolete("Use VerifyChallengeTransactionThreshold and VerifyChallengeTransactionSigners")]
public static bool VerifyChallengeTransaction(Transaction transaction, string serverAccountId, string homeDomain, string webAuthDomain,
public static bool VerifyChallengeTransaction(Transaction transaction, string serverAccountId,
string homeDomain, string webAuthDomain,
Network network = null, DateTimeOffset? now = null)
{
network = network ?? Network.Current;

var clientAccountId = ReadChallengeTransaction(transaction, serverAccountId, homeDomain, webAuthDomain, network, now);
var clientAccountId =
ReadChallengeTransaction(transaction, serverAccountId, homeDomain, webAuthDomain, network, now);

if (!ValidateSignedBy(transaction, clientAccountId, network))
throw new InvalidWebAuthenticationException("Challenge transaction not signed by client");
Expand Down Expand Up @@ -437,4 +463,4 @@ private static ICollection<string> VerifyTransactionSignatures(Transaction trans
return signersFound.ToArray();
}
}
}
}