diff --git a/MANIFEST.in b/MANIFEST.in index 97f4adf..532360c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,4 +3,5 @@ include CONTRIBUTING.rst include HISTORY.rst include LICENSE include README.rst +include requirements.txt recursive-include django_cryptolock *.html *.png *.gif *js *.css *jpg *jpeg *svg *py diff --git a/django_cryptolock/backends.py b/django_cryptolock/backends.py index 87b16fb..99637ba 100644 --- a/django_cryptolock/backends.py +++ b/django_cryptolock/backends.py @@ -1,6 +1,40 @@ from django.contrib.auth.backends import ModelBackend +from django.contrib.auth import get_user_model +from django.core.exceptions import PermissionDenied +from django.utils.translation import gettext_lazy as _ + +from monerorpc.authproxy import AuthServiceProxy, JSONRPCException + +from .models import Address +from .utils import verify_signature + +User = get_user_model() class MoneroAddressBackend(ModelBackend): + """Custom Monero-Cryptolock authentication backend.""" + def authenticate(self, request, address=None, challenge=None, signature=None): - pass + """Validates the provided signature for the given address and challenge. + + This method currently relies on Wallet RPC access to verify the signature, + in the future it should be done locally to be more reliable and more + performant. + """ + if not all(address, challenge, signature): + return None + + try: + stored_address = Address.objects.get(address=address).select_related("user") + except: + return None + + try: + is_valid = verify_signature(address, challenge, signature) + except JSONRPCException: + raise PermissionDenied(_("Error while validating signature")) + + if is_valid: + return stored_address.user + + return None diff --git a/django_cryptolock/migrations/0002_auto_20191015_1141.py b/django_cryptolock/migrations/0002_auto_20191015_1141.py new file mode 100644 index 0000000..33eb58a --- /dev/null +++ b/django_cryptolock/migrations/0002_auto_20191015_1141.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.5 on 2019-10-15 16:41 + +from django.conf import settings +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('django_cryptolock', '0001_initial'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='address', + unique_together={('user', 'address')}, + ), + ] diff --git a/django_cryptolock/models.py b/django_cryptolock/models.py index 6537af4..982c157 100644 --- a/django_cryptolock/models.py +++ b/django_cryptolock/models.py @@ -20,6 +20,7 @@ class Address(TimeStampedModel): verbose_name = _("Address") verbose_name_plural = _("Addresses") + unique_together = ["user", "address"] def __str__(self): """Unicode representation of Address.""" diff --git a/django_cryptolock/utils.py b/django_cryptolock/utils.py new file mode 100644 index 0000000..77aa44d --- /dev/null +++ b/django_cryptolock/utils.py @@ -0,0 +1,18 @@ +from django.conf import settings + +from monerorpc.authproxy import AuthServiceProxy + + +def verify_signature(address: str, challenge: str, signature: str) -> bool: + """Makes a request to wallet RPC to verify address and signature.""" + protocol = settings.MONERO_WALLET_RPC_PROTOCOL + host = settings.MONERO_WALLET_RPC_HOST + user = settings.MONERO_WALLET_RPC_USER + pwd = settings.MONERO_WALLET_RPC_PASS + wallet_rpc = AuthServiceProxy(f"{protocol}://{user}:{pwd}@{host}/json_rpc") + + result = wallet_rpc.verify( + {"data": challenge, "address": address, "signature": signature} + ) + + return result.get("good", False) diff --git a/requirements.txt b/requirements.txt index e5b8042..d2ca5db 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ django>=2.2 django-model-utils>=2.0 monero>=0.6 +python-monerorpc>=0.5.5 diff --git a/tests/settings.py b/tests/settings.py index 3997583..e8e4bed 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -26,3 +26,7 @@ if django.VERSION >= (1, 10): MIDDLEWARE = () else: MIDDLEWARE_CLASSES = () + +MONERO_WALLET_RPC_HOST = "localhost:3030" +MONERO_WALLET_RPC_USER = "test" +MONERO_WALLET_RPC_PASS = "test" diff --git a/tox.ini b/tox.ini index 980b53f..ae0f7f3 100644 --- a/tox.ini +++ b/tox.ini @@ -7,8 +7,8 @@ setenv = PYTHONPATH = {toxinidir}:{toxinidir}/django_cryptolock commands = coverage run --source django_cryptolock runtests.py deps = - django-20: Django>=2.2,<3.0 - -r{toxinidir}/requirements_test.txt + django-22: Django>=2.2 + -r {toxinidir}/requirements_test.txt basepython = py37: python3.7 py36: python3.6