Skip to content

Commit

Permalink
feat: SEP-10, adds a 5 minute grace period to read_challenge_transact…
Browse files Browse the repository at this point in the history
…ion min_time constraint
  • Loading branch information
overcat committed Sep 18, 2021
1 parent 8d40fbf commit 93d46ed
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 49 deletions.
16 changes: 9 additions & 7 deletions stellar_sdk/sep/stellar_web_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def __init__(
transaction: TransactionEnvelope,
client_account_id: str,
matched_home_domain: str,
memo: Optional[int] = None
memo: Optional[int] = None,
) -> None:
self.transaction = transaction
self.client_account_id = client_account_id
Expand Down Expand Up @@ -81,7 +81,7 @@ def build_challenge_transaction(
timeout: int = 900,
client_domain: Optional[str] = None,
client_signing_key: Optional[str] = None,
memo: Optional[int] = None
memo: Optional[int] = None,
) -> str:
"""Returns a valid `SEP0010 <https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md>`_
challenge transaction which you can use for Stellar Web Authentication.
Expand Down Expand Up @@ -216,7 +216,11 @@ def read_challenge_transaction(
)

current_time = time.time()
if current_time < min_time or current_time > max_time:
# Apply a grace period to the challenge MinTime to account for
# clock drift between the server and client
# https://github.com/StellarCN/py-stellar-base/issues/524
grace_period = 60 * 5
if current_time < min_time - grace_period or current_time > max_time:
raise InvalidSep10ChallengeError(
"Transaction is not within range of the specified timebounds."
)
Expand Down Expand Up @@ -274,9 +278,7 @@ def read_challenge_transaction(
elif isinstance(transaction.memo, IdMemo):
memo = transaction.memo.memo_id
else:
raise InvalidSep10ChallengeError(
"Invalid memo, only ID memos are permitted"
)
raise InvalidSep10ChallengeError("Invalid memo, only ID memos are permitted")

# verify any subsequent operations are manage data ops and source account is the server
for op in transaction.operations[1:]:
Expand Down Expand Up @@ -311,7 +313,7 @@ def read_challenge_transaction(
transaction=transaction_envelope,
client_account_id=client_account.account_muxed or client_account.account_id,
matched_home_domain=matched_home_domain,
memo=memo
memo=memo,
)


Expand Down
167 changes: 125 additions & 42 deletions tests/sep/test_stellar_web_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def test_challenge_transaction_mux_client_account_id_permitted(self):
def test_challenge_transaction_id_memo_as_int_permitted(self):
server_kp = Keypair.random()
client_account_id = Keypair.random().public_key
memo = randrange(0, 2**64)
memo = randrange(0, 2 ** 64)
timeout = 600
network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE
home_domain = "example.com"
Expand All @@ -108,7 +108,7 @@ def test_challenge_transaction_id_memo_as_int_permitted(self):
web_auth_domain=web_auth_domain,
network_passphrase=network_passphrase,
timeout=timeout,
memo=memo
memo=memo,
)

transaction = TransactionEnvelope.from_xdr(
Expand All @@ -133,23 +133,23 @@ def test_challenge_transaction_non_id_memo_not_permitted(self):
web_auth_domain=web_auth_domain,
network_passphrase=network_passphrase,
timeout=timeout,
memo=memo
memo=memo,
)

def test_challenge_transaction_muxed_client_account_with_memo_not_permitted(self):
server_kp = Keypair.random()
client_account_id = (
"MAAAAAAAAAAAJURAAB2X52XFQP6FBXLGT6LWOOWMEXWHEWBDVRZ7V5WH34Y22MPFBHUHY"
)
memo = randrange(0, 2**64)
memo = randrange(0, 2 ** 64)
timeout = 600
network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE
home_domain = "example.com"
web_auth_domain = "auth.example.com"

with pytest.raises(
ValueError,
match="memos are not valid for challenge transactions with a muxed client account"
match="memos are not valid for challenge transactions with a muxed client account",
):
build_challenge_transaction(
server_secret=server_kp.secret,
Expand All @@ -158,7 +158,7 @@ def test_challenge_transaction_muxed_client_account_with_memo_not_permitted(self
web_auth_domain=web_auth_domain,
network_passphrase=network_passphrase,
timeout=timeout,
memo=memo
memo=memo,
)

def test_challenge_transaction_with_client_domain(self):
Expand Down Expand Up @@ -894,6 +894,85 @@ def test_verify_challenge_tx_not_within_range_of_the_specified_timebounds(self):
network_passphrase,
)

def test_verify_challenge_tx_valid_timebounds_with_grace_period(self):
server_kp = Keypair.random()
client_kp = Keypair.random()
network_passphrase = Network.PUBLIC_NETWORK_PASSPHRASE
home_domain = "example.com"
web_auth_domain = "auth.example.com"
now = int(time.time())
nonce = os.urandom(48)
nonce_encoded = base64.b64encode(nonce)
server_account = Account(server_kp.public_key, -1)
challenge_te = (
TransactionBuilder(server_account, network_passphrase, 100)
.append_manage_data_op(
data_name="{} auth".format(home_domain),
data_value=nonce_encoded,
source=client_kp.public_key,
)
.append_manage_data_op(
data_name="web_auth_domain",
data_value=web_auth_domain,
source=server_account.account,
)
.add_time_bounds(now + 5 * 59, now + 60 * 60)
.build()
)

challenge_te.sign(server_kp)
challenge_te.sign(client_kp)
challenge_tx_signed = challenge_te.to_xdr()
verify_challenge_transaction(
challenge_tx_signed,
server_kp.public_key,
home_domain,
web_auth_domain,
network_passphrase,
)

def test_verify_challenge_tx_invalid_timebounds_with_grace_period(self):
server_kp = Keypair.random()
client_kp = Keypair.random()
network_passphrase = Network.PUBLIC_NETWORK_PASSPHRASE
home_domain = "example.com"
web_auth_domain = "auth.example.com"
now = int(time.time())
nonce = os.urandom(48)
nonce_encoded = base64.b64encode(nonce)
server_account = Account(server_kp.public_key, -1)
challenge_te = (
TransactionBuilder(server_account, network_passphrase, 100)
.append_manage_data_op(
data_name="{} auth".format(home_domain),
data_value=nonce_encoded,
source=client_kp.public_key,
)
.append_manage_data_op(
data_name="web_auth_domain",
data_value=web_auth_domain,
source=server_account.account,
)
.add_time_bounds(now + 5 * 61, now + 60 * 60)
.build()
)

challenge_te.sign(server_kp)
challenge_te.sign(client_kp)
challenge_tx_signed = challenge_te.to_xdr()

with pytest.raises(
InvalidSep10ChallengeError,
match="Transaction is not within range of the specified timebounds.",
):
verify_challenge_transaction(
challenge_tx_signed,
server_kp.public_key,
home_domain,
web_auth_domain,
network_passphrase,
)

def test_verify_challenge_transaction_auth_domain_mismatch_raise(self):
server_kp = Keypair.random()
client_kp = Keypair.random()
Expand Down Expand Up @@ -1857,7 +1936,7 @@ def test_read_challenge_transaction_with_memo_permitted(self):
network_passphrase = Network.PUBLIC_NETWORK_PASSPHRASE
home_domain = "example.com"
web_auth_domain = "auth.example.com"
memo = randrange(0, 2**64)
memo = randrange(0, 2 ** 64)

challenge = build_challenge_transaction(
server_secret=server_kp.secret,
Expand All @@ -1866,7 +1945,7 @@ def test_read_challenge_transaction_with_memo_permitted(self):
web_auth_domain=web_auth_domain,
network_passphrase=network_passphrase,
timeout=timeout,
memo=memo
memo=memo,
)
challenge_transaction = read_challenge_transaction(
challenge_transaction=challenge,
Expand All @@ -1889,26 +1968,28 @@ def test_read_challenge_transaction_mux_client_id_with_memo_not_permitted(self):
nonce = os.urandom(48)
nonce_encoded = base64.b64encode(nonce)

challenge = TransactionBuilder(
source_account=server_account,
network_passphrase=network_passphrase
).append_manage_data_op(
data_name=f"{home_domain} auth",
data_value=nonce_encoded,
source=client_account_id
).append_manage_data_op(
data_name="web_auth_domain",
data_value=home_domain,
source=server_account.account,
).add_id_memo(
randrange(0, 2**64)
).set_timeout(
timeout
).build()
challenge = (
TransactionBuilder(
source_account=server_account, network_passphrase=network_passphrase
)
.append_manage_data_op(
data_name=f"{home_domain} auth",
data_value=nonce_encoded,
source=client_account_id,
)
.append_manage_data_op(
data_name="web_auth_domain",
data_value=home_domain,
source=server_account.account,
)
.add_id_memo(randrange(0, 2 ** 64))
.set_timeout(timeout)
.build()
)

with pytest.raises(
InvalidSep10ChallengeError,
match="Invalid challenge, memos are not permitted if the client account is muxed"
match="Invalid challenge, memos are not permitted if the client account is muxed",
):
read_challenge_transaction(
challenge_transaction=challenge.to_xdr(),
Expand All @@ -1928,26 +2009,28 @@ def test_read_challenge_transaction_with_non_id_memo_not_permitted(self):
nonce = os.urandom(48)
nonce_encoded = base64.b64encode(nonce)

challenge = TransactionBuilder(
source_account=server_account,
network_passphrase=network_passphrase
).append_manage_data_op(
data_name=f"{home_domain} auth",
data_value=nonce_encoded,
source=client_account_id
).append_manage_data_op(
data_name="web_auth_domain",
data_value=home_domain,
source=server_account.account,
).add_text_memo(
"test"
).set_timeout(
timeout
).build()
challenge = (
TransactionBuilder(
source_account=server_account, network_passphrase=network_passphrase
)
.append_manage_data_op(
data_name=f"{home_domain} auth",
data_value=nonce_encoded,
source=client_account_id,
)
.append_manage_data_op(
data_name="web_auth_domain",
data_value=home_domain,
source=server_account.account,
)
.add_text_memo("test")
.set_timeout(timeout)
.build()
)

with pytest.raises(
InvalidSep10ChallengeError,
match="Invalid memo, only ID memos are permitted"
match="Invalid memo, only ID memos are permitted",
):
read_challenge_transaction(
challenge_transaction=challenge.to_xdr(),
Expand Down

0 comments on commit 93d46ed

Please sign in to comment.