-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add request message to sign endpoint
- Loading branch information
1 parent
4a81c23
commit 68bd502
Showing
12 changed files
with
289 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,31 @@ | ||
from pydantic import BaseModel, constr | ||
from typing import Optional | ||
|
||
from pydantic import AnyUrl, BaseModel, Field, field_validator | ||
|
||
from gnosis.eth.utils import fast_is_checksum_address | ||
|
||
|
||
class About(BaseModel): | ||
version: str | ||
|
||
|
||
class Nonce(BaseModel): | ||
nonce: constr(min_length=8, pattern=r"^[A-Za-z0-9]{8,}$") # noqa: F722 | ||
nonce: str = Field(min_length=8, pattern=r"^[A-Za-z0-9]{8,}$") | ||
|
||
|
||
class SiweMessageRequest(BaseModel): | ||
domain: str = Field(pattern="^[^/?#]+$", examples=["domain.com"]) | ||
address: str | ||
chain_id: int | ||
uri: AnyUrl | ||
statement: Optional[str] = Field(default=None) | ||
|
||
@field_validator("address") | ||
def validate_address(cls, value): | ||
if not fast_is_checksum_address(value): | ||
raise ValueError("Invalid Ethereum address") | ||
return value | ||
|
||
|
||
class SiweMessage(BaseModel): | ||
message: str |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
from datetime import UTC, datetime, timedelta | ||
|
||
from siwe.siwe import ISO8601Datetime, SiweMessage, VersionEnum | ||
|
||
from gnosis.eth.utils import fast_to_checksum_address | ||
|
||
from ..config import settings | ||
from .nonce_repository import get_nonce_repository | ||
|
||
|
||
def create_siwe_message( | ||
domain: str, address: str, chain_id: int, uri: str, statement=None | ||
) -> str: | ||
nonce = get_nonce_repository().generate_nonce() | ||
|
||
message = SiweMessage( | ||
domain=domain, | ||
address=fast_to_checksum_address(address), | ||
statement=statement or "Welcome to Safe!", | ||
uri=uri, | ||
version=VersionEnum.one, | ||
chain_id=chain_id, | ||
nonce=nonce, | ||
issued_at=ISO8601Datetime.from_datetime(datetime.now(UTC)), | ||
valid_until=ISO8601Datetime.from_datetime( | ||
datetime.now(UTC) + timedelta(seconds=settings.NONCE_TTL_SECONDS) | ||
), | ||
) | ||
|
||
return message.prepare_message() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import unittest | ||
|
||
from pydantic import ValidationError | ||
|
||
from pydantic_core._pydantic_core import Url | ||
|
||
from ..models import Nonce, SiweMessageRequest | ||
|
||
|
||
class TestNonce(unittest.TestCase): | ||
def test_valid_nonce(self): | ||
valid_nonce_data = {"nonce": "abcd1234"} | ||
nonce_instance = Nonce(nonce=valid_nonce_data["nonce"]) | ||
self.assertEqual(nonce_instance.nonce, valid_nonce_data["nonce"]) | ||
|
||
def test_invalid_nonce_length(self): | ||
with self.assertRaises(ValidationError): | ||
Nonce(nonce="abc") | ||
|
||
def test_invalid_nonce_pattern(self): | ||
with self.assertRaises(ValidationError): | ||
Nonce(nonce="abcd-1234") | ||
|
||
|
||
class TestSiweMessageRequest(unittest.TestCase): | ||
def test_valid_siwe_message_request(self): | ||
siwe_message_request = SiweMessageRequest( | ||
domain="example.com", | ||
address="0x32Be343B94f860124dC4fEe278FDCBD38C102D88", | ||
chain_id=1, | ||
uri=Url("https://example.com/"), | ||
statement="Test statement", | ||
) | ||
self.assertEqual(siwe_message_request.domain, "example.com") | ||
self.assertEqual( | ||
siwe_message_request.address, "0x32Be343B94f860124dC4fEe278FDCBD38C102D88" | ||
) | ||
self.assertEqual(siwe_message_request.chain_id, 1) | ||
self.assertEqual(str(siwe_message_request.uri), "https://example.com/") | ||
self.assertEqual(siwe_message_request.statement, "Test statement") | ||
|
||
def test_invalid_domain(self): | ||
with self.assertRaises(ValidationError): | ||
SiweMessageRequest( | ||
domain="example.com/invalid", | ||
address="0x32Be343B94f860124dC4fEe278FDCBD38C102D88", | ||
chain_id=1, | ||
uri=Url("https://example.com"), | ||
) | ||
|
||
def test_invalid_address(self): | ||
with self.assertRaises(ValidationError): | ||
SiweMessageRequest( | ||
domain="example.com", | ||
address="0xInvalidEthereumAddress", | ||
chain_id=1, | ||
uri=Url("https://example.com"), | ||
statement="Test statement", | ||
) | ||
|
||
def test_invalid_uri(self): | ||
with self.assertRaises(ValidationError): | ||
SiweMessageRequest( | ||
domain="example.com", | ||
address="0x32Be343B94f860124dC4fEe278FDCBD38C102D88", | ||
chain_id=1, | ||
uri=Url("invalid-url"), | ||
statement="Test statement", | ||
) | ||
|
||
def test_optional_statement(self): | ||
siwe_message_request = SiweMessageRequest( | ||
domain="example.com", | ||
address="0x32Be343B94f860124dC4fEe278FDCBD38C102D88", | ||
chain_id=1, | ||
uri=Url("https://example.com"), | ||
) | ||
self.assertIsNone(siwe_message_request.statement) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,56 @@ | ||
import re | ||
import unittest | ||
from unittest import mock | ||
|
||
from fastapi.testclient import TestClient | ||
|
||
from app.main import app | ||
import siwe | ||
|
||
from ..main import app | ||
|
||
|
||
class TestRouterAuth(unittest.TestCase): | ||
client = None | ||
client: TestClient | ||
|
||
@classmethod | ||
def setUpClass(cls): | ||
cls.client = TestClient(app) | ||
|
||
def test_view_about(self): | ||
def test_view_get_nonce(self): | ||
response = self.client.get("/api/v1/auth/nonce") | ||
self.assertEqual(response.status_code, 200) | ||
self.assertTrue( | ||
re.fullmatch(r"^[A-Za-z0-9]{8,}$", response.json().get("nonce")) | ||
) | ||
|
||
@mock.patch("app.siwe.nonce_repository.NonceRepository.generate_nonce") | ||
def test_view_request_siwe_message(self, mock_generate_nonce): | ||
test_domain = "example.com" | ||
test_address = "0x32Be343B94f860124dC4fEe278FDCBD38C102D88" | ||
test_chain_id = 1 | ||
test_uri = "https://example.com/" | ||
test_statement = "Test statement" | ||
test_nonce = "testnonce1234" | ||
|
||
mock_generate_nonce.return_value = test_nonce | ||
|
||
payload = { | ||
"domain": test_domain, | ||
"address": test_address, | ||
"chain_id": test_chain_id, | ||
"uri": test_uri, | ||
"statement": test_statement, | ||
} | ||
|
||
response = self.client.post("/api/v1/auth/messages", json=payload) | ||
|
||
self.assertEqual(response.status_code, 200) | ||
obtained_siwe_message = siwe.SiweMessage.from_message( | ||
response.json().get("message") | ||
) | ||
self.assertEqual(obtained_siwe_message.domain, test_domain) | ||
self.assertEqual(obtained_siwe_message.address, test_address) | ||
self.assertEqual(obtained_siwe_message.chain_id, test_chain_id) | ||
self.assertEqual(obtained_siwe_message.uri, test_uri) | ||
self.assertEqual(obtained_siwe_message.statement, test_statement) | ||
self.assertEqual(obtained_siwe_message.nonce, test_nonce) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import unittest | ||
from unittest import mock | ||
|
||
from siwe.siwe import SiweMessage, VersionEnum | ||
|
||
from ..siwe.message_service import create_siwe_message | ||
|
||
|
||
class TestSiweMessageService(unittest.TestCase): | ||
@mock.patch("app.siwe.nonce_repository.NonceRepository.generate_nonce") | ||
def test_create_siwe_message(self, mock_generate_nonce): | ||
test_domain = "example.com" | ||
test_address = "0x32Be343B94f860124dC4fEe278FDCBD38C102D88" | ||
test_chain_id = 1 | ||
test_uri = "https://example.com" | ||
test_statement = "Test statement" | ||
test_nonce = "testnonce1234" | ||
|
||
mock_generate_nonce.return_value = test_nonce | ||
with mock.patch("app.config.settings.NONCE_TTL_SECONDS", 100): | ||
message_str = create_siwe_message( | ||
domain=test_domain, | ||
address=test_address, | ||
chain_id=test_chain_id, | ||
uri=test_uri, | ||
statement=test_statement, | ||
) | ||
|
||
mock_generate_nonce.assert_called_once() | ||
|
||
siwe_message = SiweMessage.from_message(message_str) | ||
issued_at = siwe_message.issued_at | ||
valid_until = siwe_message.expiration_time | ||
|
||
expected_message = SiweMessage( | ||
domain=test_domain, | ||
address=test_address, | ||
statement=test_statement, | ||
uri=test_uri, | ||
version=VersionEnum.one, | ||
chain_id=test_chain_id, | ||
nonce=test_nonce, | ||
issued_at=issued_at, | ||
valid_until=valid_until, | ||
) | ||
|
||
self.assertEqual(message_str, expected_message.prepare_message()) | ||
|
||
@mock.patch("app.siwe.nonce_repository.NonceRepository.generate_nonce") | ||
def test_create_siwe_message_without_statement(self, mock_generate_nonce): | ||
test_domain = "example.com" | ||
test_address = "0x32Be343B94f860124dC4fEe278FDCBD38C102D88" | ||
test_chain_id = 1 | ||
test_uri = "https://example.com" | ||
test_nonce = "testnonce1234" | ||
|
||
mock_generate_nonce.return_value = test_nonce | ||
with mock.patch("app.config.settings.NONCE_TTL_SECONDS", 100): | ||
message_str = create_siwe_message( | ||
domain=test_domain, | ||
address=test_address, | ||
chain_id=test_chain_id, | ||
uri=test_uri, | ||
) | ||
|
||
mock_generate_nonce.assert_called_once() | ||
|
||
siwe_message = SiweMessage.from_message(message_str) | ||
issued_at = siwe_message.issued_at | ||
valid_until = siwe_message.expiration_time | ||
|
||
expected_message = SiweMessage( | ||
domain=test_domain, | ||
address=test_address, | ||
statement="Welcome to Safe!", | ||
uri=test_uri, | ||
version=VersionEnum.one, | ||
chain_id=test_chain_id, | ||
nonce=test_nonce, | ||
issued_at=issued_at, | ||
valid_until=valid_until, | ||
) | ||
|
||
self.assertEqual(message_str, expected_message.prepare_message()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters