First implementation of authentication views for DRF projects
This commit is contained in:
parent
fd6d243bfe
commit
c1fc972668
|
@ -76,4 +76,4 @@ Quickstart
|
||||||
...
|
...
|
||||||
]
|
]
|
||||||
|
|
||||||
More detailed information can be found in the documentation.
|
More detailed information can be found in the [documentation](#documentation).
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from rest_framework.status import HTTP_200_OK
|
||||||
|
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.authtoken.models import Token
|
||||||
|
|
||||||
|
from monerorpc.authproxy import JSONRPCException
|
||||||
|
|
||||||
|
from .models import Address, Challenge
|
||||||
|
from .forms import SimpleSignUpForm, SimpleLoginForm
|
||||||
|
from .utils import verify_signature
|
||||||
|
from .mixins import CreateUserMixin, CreateChallengeMixin
|
||||||
|
|
||||||
|
|
||||||
|
class CryptoLockAPITokenLoginView(CreateChallengeMixin, APIView):
|
||||||
|
"""Endpoint to login the user with cryptocurrency wallet address.
|
||||||
|
|
||||||
|
Using the default token backend.
|
||||||
|
"""
|
||||||
|
|
||||||
|
http_method_names = ["get", "post"]
|
||||||
|
|
||||||
|
def post(self, request, format=None):
|
||||||
|
"""Authenticates the user using the provided signature."""
|
||||||
|
form = SimpleLoginForm(request, request.data)
|
||||||
|
if not form.is_valid():
|
||||||
|
return Response(form.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
challenge = form.cleaned_data["challenge"]
|
||||||
|
Challenge.objects.invalidate(challenge)
|
||||||
|
Challenge.objects.clean_expired()
|
||||||
|
|
||||||
|
token = Token.objects.create(user=form.user_cache)
|
||||||
|
return Response({"token": token.key}, status=HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class CryptoLockAPISignUpView(CreateUserMixin, CreateChallengeMixin, APIView):
|
||||||
|
"""Endpoint to create a new user using cryptocurrency wallet address."""
|
||||||
|
|
||||||
|
http_method_names = ["get", "post"]
|
||||||
|
|
||||||
|
def post(self, request, format=None):
|
||||||
|
"""Verifies the signature and creates a new user account."""
|
||||||
|
form = SimpleSignUpForm(request, request.data)
|
||||||
|
if not form.is_valid():
|
||||||
|
return Response(form.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
username = form.cleaned_data["username"]
|
||||||
|
address = form.cleaned_data["address"]
|
||||||
|
challenge = form.cleaned_data["challenge"]
|
||||||
|
signature = form.cleaned_data["signature"]
|
||||||
|
network = [n[1] for n in Address.NETWORKS if n[0] == form.network][0]
|
||||||
|
|
||||||
|
try:
|
||||||
|
valid_sig = verify_signature(
|
||||||
|
network, address, challenge, signature, self.request
|
||||||
|
)
|
||||||
|
except JSONRPCException:
|
||||||
|
return Response(
|
||||||
|
{"__all__": [_("Error connecting to Monero daemon")]},
|
||||||
|
status=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
)
|
||||||
|
|
||||||
|
if valid_sig:
|
||||||
|
self.create_user(username, challenge, address, form.network)
|
||||||
|
return Response({}, status=status.HTTP_201_CREATED)
|
||||||
|
else:
|
||||||
|
return Response(
|
||||||
|
{"signature": [_("Invalid signature")]},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
|
@ -0,0 +1,30 @@
|
||||||
|
from django.db import transaction
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
from pybitid import bitid
|
||||||
|
|
||||||
|
from .models import Challenge
|
||||||
|
from .serializers import ChallengeSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class CreateChallengeMixin:
|
||||||
|
"""Add create challenge on get functionality to API views."""
|
||||||
|
|
||||||
|
def get(self, request, format=None):
|
||||||
|
"""Returns a new challenge for the login."""
|
||||||
|
serializer = ChallengeSerializer(instance=Challenge.objects.generate())
|
||||||
|
serializer.data["challenge"] = bitid.build_uri(
|
||||||
|
request.build_absolute_uri(), serializer.data["challenge"]
|
||||||
|
)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class CreateUserMixin:
|
||||||
|
@transaction.atomic
|
||||||
|
def create_user(self, username, challenge, address, network):
|
||||||
|
user = get_user_model().objects.create(username=username)
|
||||||
|
user.address_set.create(address=address, network=network)
|
||||||
|
Challenge.objects.invalidate(challenge)
|
|
@ -0,0 +1,10 @@
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from .models import Challenge
|
||||||
|
from .forms import SimpleSignUpForm
|
||||||
|
|
||||||
|
|
||||||
|
class ChallengeSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Challenge
|
||||||
|
fields = ["challenge", "expires"]
|
|
@ -1,4 +1,5 @@
|
||||||
import warnings
|
import warnings
|
||||||
|
from typing import Union
|
||||||
from secrets import token_hex
|
from secrets import token_hex
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -38,6 +39,21 @@ def verify_bitcoin_signature(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_signature(
|
||||||
|
network: str, address: str, challenge: str, signature: str, request: HttpRequest
|
||||||
|
):
|
||||||
|
valid_sig = False
|
||||||
|
|
||||||
|
if network == "Bitcoin":
|
||||||
|
valid_sig = verify_bitcoin_signature(
|
||||||
|
address, challenge, signature, request=request
|
||||||
|
)
|
||||||
|
elif network == "Monero":
|
||||||
|
valid_sig = verify_monero_signature(address, challenge, signature)
|
||||||
|
|
||||||
|
return valid_sig
|
||||||
|
|
||||||
|
|
||||||
def generate_challenge():
|
def generate_challenge():
|
||||||
"""Generates a new random challenge for the authentication."""
|
"""Generates a new random challenge for the authentication."""
|
||||||
num_bytes = getattr(settings, "DJCL_CHALLENGE_BYTES", 16)
|
num_bytes = getattr(settings, "DJCL_CHALLENGE_BYTES", 16)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.contrib.auth.views import LoginView
|
from django.contrib.auth.views import LoginView
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.views.generic import FormView
|
from django.views.generic import FormView
|
||||||
from django.forms.utils import ErrorList
|
from django.forms.utils import ErrorList
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -9,8 +8,9 @@ from django.conf import settings
|
||||||
from monerorpc.authproxy import JSONRPCException
|
from monerorpc.authproxy import JSONRPCException
|
||||||
|
|
||||||
from .forms import SimpleSignUpForm, SimpleLoginForm
|
from .forms import SimpleSignUpForm, SimpleLoginForm
|
||||||
from .utils import verify_monero_signature, verify_bitcoin_signature
|
from .utils import verify_signature
|
||||||
from .models import Address, Challenge
|
from .models import Address, Challenge
|
||||||
|
from .mixins import CreateUserMixin
|
||||||
|
|
||||||
|
|
||||||
class CryptoLockLoginView(LoginView):
|
class CryptoLockLoginView(LoginView):
|
||||||
|
@ -25,7 +25,7 @@ class CryptoLockLoginView(LoginView):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class CryptoLockSignUpView(FormView):
|
class CryptoLockSignUpView(CreateUserMixin, FormView):
|
||||||
template_name = "django_cryptolock/signup.html"
|
template_name = "django_cryptolock/signup.html"
|
||||||
form_class = SimpleSignUpForm
|
form_class = SimpleSignUpForm
|
||||||
|
|
||||||
|
@ -33,22 +33,24 @@ class CryptoLockSignUpView(FormView):
|
||||||
return self.form_class(request=self.request, **self.get_form_kwargs())
|
return self.form_class(request=self.request, **self.get_form_kwargs())
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
|
username = form.cleaned_data["username"]
|
||||||
|
address = form.cleaned_data["address"]
|
||||||
|
challenge = form.cleaned_data["challenge"]
|
||||||
|
signature = form.cleaned_data["signature"]
|
||||||
|
network = [n[1] for n in Address.NETWORKS if n[0] == form.network][0]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
valid_sig = self.verify_signature(form)
|
valid_sig = verify_signature(
|
||||||
|
network, address, challenge, signature, self.request
|
||||||
|
)
|
||||||
except JSONRPCException:
|
except JSONRPCException:
|
||||||
form._errors["__all__"] = ErrorList(
|
form._errors["__all__"] = ErrorList(
|
||||||
[_("Error connecting to Monero daemon")]
|
[_("Error connecting to Monero daemon")]
|
||||||
)
|
)
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
|
|
||||||
username = form.cleaned_data["username"]
|
|
||||||
address = form.cleaned_data["address"]
|
|
||||||
challenge = form.cleaned_data["challenge"]
|
|
||||||
|
|
||||||
if valid_sig:
|
if valid_sig:
|
||||||
user = get_user_model().objects.create(username=username)
|
self.create_user(username, challenge, address, form.network)
|
||||||
user.address_set.create(address=address, network=form.network)
|
|
||||||
Challenge.objects.invalidate(challenge)
|
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
else:
|
else:
|
||||||
form._errors["signature"] = ErrorList([_("Invalid signature")])
|
form._errors["signature"] = ErrorList([_("Invalid signature")])
|
||||||
|
@ -56,20 +58,3 @@ class CryptoLockSignUpView(FormView):
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return settings.LOGIN_REDIRECT_URL
|
return settings.LOGIN_REDIRECT_URL
|
||||||
|
|
||||||
def verify_signature(self, form):
|
|
||||||
address = form.cleaned_data["address"]
|
|
||||||
challenge = form.cleaned_data["challenge"]
|
|
||||||
signature = form.cleaned_data["signature"]
|
|
||||||
bitcoin = form.network == Address.NETWORK_BITCOIN
|
|
||||||
monero = form.network == Address.NETWORK_MONERO
|
|
||||||
valid_sig = False
|
|
||||||
|
|
||||||
if bitcoin:
|
|
||||||
valid_sig = verify_bitcoin_signature(
|
|
||||||
address, challenge, signature, request=self.request
|
|
||||||
)
|
|
||||||
elif monero:
|
|
||||||
valid_sig = verify_monero_signature(address, challenge, signature)
|
|
||||||
|
|
||||||
return valid_sig
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
|
djangorestframework>=3.9.3
|
||||||
bumpversion==0.5.3
|
bumpversion==0.5.3
|
||||||
wheel==0.30.0
|
wheel==0.30.0
|
||||||
twine==3.1.0
|
twine==3.1.0
|
||||||
|
|
1
setup.py
1
setup.py
|
@ -58,6 +58,7 @@ setup(
|
||||||
packages=["django_cryptolock"],
|
packages=["django_cryptolock"],
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
install_requires=requirements,
|
install_requires=requirements,
|
||||||
|
extras_require={"drf": ["djangorestframework>=3.9.3"]},
|
||||||
license="MIT",
|
license="MIT",
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
keywords="django-cryptolock",
|
keywords="django-cryptolock",
|
||||||
|
|
|
@ -1,6 +1,16 @@
|
||||||
"""
|
"""
|
||||||
Set of functions and constants that help testing the existing functionality
|
Set of functions and constants that help testing the existing functionality
|
||||||
"""
|
"""
|
||||||
|
from pybitid import bitid
|
||||||
|
|
||||||
|
from django_cryptolock.models import Challenge
|
||||||
|
|
||||||
|
DUMMY_CREDS = {"username": "test", "password": "insecure"}
|
||||||
|
VALID_MONERO_ADDRESS = "46fYuhPAdsxMbEeMg97LhSbFPamdiCw7C6b19VEcZSmV6xboWFZuZQ9MTbj1wLszhUExHi63CMtsWjDTrRDqegZiPVebgYq"
|
||||||
|
VALID_BITCOIN_ADDRESS = "1N5attoW1FviYGnLmRu9xjaPMKTkWxtUCW"
|
||||||
|
VALID_BITCOIN_SIG = "H5wI5uqhRCxBpyre2mYkjLxNKPi/TCj9IaHhmfnF8Wn1Pac6gsuYsd2GqTNpy/JFDv3HBSOD75pk2OsGDxE7U4o="
|
||||||
|
VALID_BITID_URI = "bitid://www.django-cryptolock.test/?x=44d91949c7b2eb20"
|
||||||
|
EXAMPLE_LOGIN_URL = "https://www.django-cryptolock.test/"
|
||||||
|
|
||||||
|
|
||||||
def set_monero_settings(settings):
|
def set_monero_settings(settings):
|
||||||
|
@ -15,3 +25,7 @@ def set_bitcoin_settings(settings):
|
||||||
"django_cryptolock.backends.BitcoinAddressBackend",
|
"django_cryptolock.backends.BitcoinAddressBackend",
|
||||||
"django.contrib.auth.backends.ModelBackend",
|
"django.contrib.auth.backends.ModelBackend",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def gen_challenge():
|
||||||
|
return bitid.build_uri(EXAMPLE_LOGIN_URL, Challenge.objects.generate())
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
# -*- coding: utf-8
|
# -*- coding: utf-8
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
import django
|
import django
|
||||||
|
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = "!z^^097u*@)yq#w1n14m%uh-l67#h&uft9p+m%$$(0y(s%-q7o"
|
SECRET_KEY = "!z^^097u*@)yq#w1n14m%uh-l67#h&uft9p+m%$$(0y(s%-q7o"
|
||||||
|
@ -17,6 +20,7 @@ INSTALLED_APPS = [
|
||||||
"django.contrib.auth",
|
"django.contrib.auth",
|
||||||
"django.contrib.contenttypes",
|
"django.contrib.contenttypes",
|
||||||
"django.contrib.sites",
|
"django.contrib.sites",
|
||||||
|
"rest_framework.authtoken",
|
||||||
"django_cryptolock",
|
"django_cryptolock",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -27,6 +31,13 @@ if django.VERSION >= (1, 10):
|
||||||
else:
|
else:
|
||||||
MIDDLEWARE_CLASSES = ()
|
MIDDLEWARE_CLASSES = ()
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
|
"DIRS": [os.path.join(BASE_DIR, "tests/templates")],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
AUTHENTICATION_BACKENDS = [
|
AUTHENTICATION_BACKENDS = [
|
||||||
"django_cryptolock.backends.MoneroAddressBackend",
|
"django_cryptolock.backends.MoneroAddressBackend",
|
||||||
"django_cryptolock.backends.BitcoinAddressBackend",
|
"django_cryptolock.backends.BitcoinAddressBackend",
|
||||||
|
|
|
@ -0,0 +1,209 @@
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
from rest_framework.status import (
|
||||||
|
HTTP_200_OK,
|
||||||
|
HTTP_201_CREATED,
|
||||||
|
HTTP_405_METHOD_NOT_ALLOWED,
|
||||||
|
HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
from model_mommy import mommy
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from django_cryptolock.models import Address, Challenge
|
||||||
|
from .helpers import (
|
||||||
|
VALID_BITCOIN_ADDRESS,
|
||||||
|
VALID_MONERO_ADDRESS,
|
||||||
|
gen_challenge,
|
||||||
|
set_bitcoin_settings,
|
||||||
|
set_monero_settings,
|
||||||
|
)
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def api_client():
|
||||||
|
return APIClient()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("method", ["put", "patch", "delete", "head", "options"])
|
||||||
|
def test_methods_not_allowed_for_token_login(api_client, method):
|
||||||
|
func = getattr(api_client, method)
|
||||||
|
response = func(reverse_lazy("api_token_login"))
|
||||||
|
assert response.status_code == HTTP_405_METHOD_NOT_ALLOWED
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_new_token_login_challenge(api_client):
|
||||||
|
response = api_client.get(reverse_lazy("api_token_login"))
|
||||||
|
assert response.status_code == HTTP_200_OK
|
||||||
|
assert "challenge" in response.json().keys()
|
||||||
|
assert "expires" in response.json().keys()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"addr,set_backend,network",
|
||||||
|
[
|
||||||
|
(VALID_MONERO_ADDRESS, set_monero_settings, "monero"),
|
||||||
|
(VALID_BITCOIN_ADDRESS, set_bitcoin_settings, "bitcoin"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_token_login_fails_invalid_data(
|
||||||
|
api_client, settings, addr, set_backend, network
|
||||||
|
):
|
||||||
|
settings.DJCL_MONERO_NETWORK = "mainnet"
|
||||||
|
set_backend(settings)
|
||||||
|
|
||||||
|
net = Address.NETWORK_BITCOIN if network == "bitcoin" else Address.NETWORK_MONERO
|
||||||
|
user = mommy.make(User)
|
||||||
|
mommy.make(Address, user=user, address=addr, network=net)
|
||||||
|
challenge = gen_challenge()
|
||||||
|
|
||||||
|
with patch(f"django_cryptolock.backends.verify_{network}_signature") as sig_mock:
|
||||||
|
sig_mock.return_value = False
|
||||||
|
response = api_client.post(
|
||||||
|
reverse_lazy("api_token_login"),
|
||||||
|
{"challenge": challenge, "address": addr, "signature": "something"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||||
|
errors = response.json()
|
||||||
|
assert "Please enter a correct address or signature." in errors["__all__"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"addr,set_backend,network",
|
||||||
|
[
|
||||||
|
(VALID_MONERO_ADDRESS, set_monero_settings, "monero"),
|
||||||
|
(VALID_BITCOIN_ADDRESS, set_bitcoin_settings, "bitcoin"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_token_login_succeeds(api_client, settings, addr, set_backend, network):
|
||||||
|
settings.DJCL_MONERO_NETWORK = "mainnet"
|
||||||
|
set_backend(settings)
|
||||||
|
|
||||||
|
net = Address.NETWORK_BITCOIN if network == "bitcoin" else Address.NETWORK_MONERO
|
||||||
|
user = mommy.make(User)
|
||||||
|
mommy.make(Address, user=user, address=addr, network=net)
|
||||||
|
challenge = gen_challenge()
|
||||||
|
|
||||||
|
with patch(f"django_cryptolock.backends.verify_{network}_signature") as sig_mock:
|
||||||
|
sig_mock.return_value = True
|
||||||
|
response = api_client.post(
|
||||||
|
reverse_lazy("api_token_login"),
|
||||||
|
{"challenge": challenge, "address": addr, "signature": "something"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == HTTP_200_OK
|
||||||
|
assert "token" in response.json().keys()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("method", ["put", "patch", "delete", "head", "options"])
|
||||||
|
def test_methods_not_allowed_for_sign_up(api_client, method):
|
||||||
|
func = getattr(api_client, method)
|
||||||
|
response = func(reverse_lazy("api_signup"))
|
||||||
|
assert response.status_code == HTTP_405_METHOD_NOT_ALLOWED
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_new_sign_up_challenge(api_client):
|
||||||
|
response = api_client.get(reverse_lazy("api_signup"))
|
||||||
|
assert response.status_code == HTTP_200_OK
|
||||||
|
assert "challenge" in response.json().keys()
|
||||||
|
assert "expires" in response.json().keys()
|
||||||
|
|
||||||
|
|
||||||
|
def test_sign_up_fails_no_input(api_client):
|
||||||
|
response = api_client.post(reverse_lazy("api_signup"))
|
||||||
|
errors = response.json()
|
||||||
|
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||||
|
assert "This field is required." in errors["challenge"]
|
||||||
|
assert "This field is required." in errors["address"]
|
||||||
|
assert "This field is required." in errors["signature"]
|
||||||
|
assert "This field is required." in errors["username"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"addr,set_backend",
|
||||||
|
[
|
||||||
|
(VALID_MONERO_ADDRESS, set_monero_settings),
|
||||||
|
(VALID_BITCOIN_ADDRESS, set_bitcoin_settings),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_sign_up_fails_duplicate_address(api_client, settings, addr, set_backend):
|
||||||
|
settings.DJCL_MONERO_NETWORK = "mainnet"
|
||||||
|
set_backend(settings)
|
||||||
|
challenge = gen_challenge()
|
||||||
|
mommy.make(Address, address=addr)
|
||||||
|
response = api_client.post(
|
||||||
|
reverse_lazy("api_signup"),
|
||||||
|
{
|
||||||
|
"challenge": challenge,
|
||||||
|
"address": addr,
|
||||||
|
"signature": "something",
|
||||||
|
"username": "user",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||||
|
errors = response.json()
|
||||||
|
assert "This address already exists" in errors["address"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"addr,set_backend",
|
||||||
|
[
|
||||||
|
(VALID_MONERO_ADDRESS, set_monero_settings),
|
||||||
|
(VALID_BITCOIN_ADDRESS, set_bitcoin_settings),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_sign_up_fails_invalid_signature(api_client, settings, addr, set_backend):
|
||||||
|
settings.DJCL_MONERO_NETWORK = "mainnet"
|
||||||
|
set_backend(settings)
|
||||||
|
challenge = gen_challenge()
|
||||||
|
|
||||||
|
with patch("django_cryptolock.api_views.verify_signature") as sig_mock:
|
||||||
|
sig_mock.return_value = False
|
||||||
|
response = api_client.post(
|
||||||
|
reverse_lazy("api_signup"),
|
||||||
|
{
|
||||||
|
"challenge": challenge,
|
||||||
|
"address": addr,
|
||||||
|
"signature": "something",
|
||||||
|
"username": "user",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||||
|
errors = response.json()
|
||||||
|
assert "Invalid signature" in errors["signature"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"addr,set_backend",
|
||||||
|
[
|
||||||
|
(VALID_MONERO_ADDRESS, set_monero_settings),
|
||||||
|
(VALID_BITCOIN_ADDRESS, set_bitcoin_settings),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_sign_up_succeeds(api_client, settings, addr, set_backend):
|
||||||
|
settings.DJCL_MONERO_NETWORK = "mainnet"
|
||||||
|
set_backend(settings)
|
||||||
|
challenge = gen_challenge()
|
||||||
|
|
||||||
|
with patch("django_cryptolock.api_views.verify_signature") as sig_mock:
|
||||||
|
sig_mock.return_value = True
|
||||||
|
response = api_client.post(
|
||||||
|
reverse_lazy("api_signup"),
|
||||||
|
{
|
||||||
|
"challenge": challenge,
|
||||||
|
"address": addr,
|
||||||
|
"signature": "something",
|
||||||
|
"username": "user",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == HTTP_201_CREATED
|
|
@ -9,18 +9,20 @@ from model_mommy import mommy
|
||||||
|
|
||||||
from django_cryptolock.models import Address
|
from django_cryptolock.models import Address
|
||||||
|
|
||||||
from .helpers import set_monero_settings, set_bitcoin_settings
|
from .helpers import (
|
||||||
|
set_monero_settings,
|
||||||
|
set_bitcoin_settings,
|
||||||
|
DUMMY_CREDS,
|
||||||
|
VALID_BITCOIN_ADDRESS,
|
||||||
|
VALID_BITCOIN_SIG,
|
||||||
|
VALID_BITID_URI,
|
||||||
|
VALID_MONERO_ADDRESS,
|
||||||
|
EXAMPLE_LOGIN_URL,
|
||||||
|
)
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
DUMMY_CREDS = {"username": "test", "password": "insecure"}
|
|
||||||
VALID_MONERO_ADDRESS = "46fYuhPAdsxMbEeMg97LhSbFPamdiCw7C6b19VEcZSmV6xboWFZuZQ9MTbj1wLszhUExHi63CMtsWjDTrRDqegZiPVebgYq"
|
|
||||||
VALID_BITCOIN_ADDRESS = "1N5attoW1FviYGnLmRu9xjaPMKTkWxtUCW"
|
|
||||||
VALID_BITCOIN_SIG = "H5wI5uqhRCxBpyre2mYkjLxNKPi/TCj9IaHhmfnF8Wn1Pac6gsuYsd2GqTNpy/JFDv3HBSOD75pk2OsGDxE7U4o="
|
|
||||||
VALID_BITID_URI = "bitid://www.django-cryptolock.test/?x=44d91949c7b2eb20"
|
|
||||||
EXAMPLE_LOGIN_URL = "https://www.django-cryptolock.test/"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def existing_user():
|
def existing_user():
|
||||||
|
|
|
@ -11,12 +11,15 @@ from pybitid import bitid
|
||||||
from django_cryptolock.forms import SimpleLoginForm, SimpleSignUpForm
|
from django_cryptolock.forms import SimpleLoginForm, SimpleSignUpForm
|
||||||
from django_cryptolock.models import Address, Challenge
|
from django_cryptolock.models import Address, Challenge
|
||||||
|
|
||||||
from .helpers import set_monero_settings, set_bitcoin_settings
|
from .helpers import (
|
||||||
|
set_monero_settings,
|
||||||
|
set_bitcoin_settings,
|
||||||
|
VALID_MONERO_ADDRESS,
|
||||||
|
VALID_BITCOIN_ADDRESS,
|
||||||
|
)
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
VALID_MONERO_ADDRESS = "46fYuhPAdsxMbEeMg97LhSbFPamdiCw7C6b19VEcZSmV6xboWFZuZQ9MTbj1wLszhUExHi63CMtsWjDTrRDqegZiPVebgYq"
|
|
||||||
VALID_BITCOIN_ADDRESS = "1N5attoW1FviYGnLmRu9xjaPMKTkWxtUCW"
|
|
||||||
FUTURE_TIME = timezone.now() + timedelta(minutes=15)
|
FUTURE_TIME = timezone.now() + timedelta(minutes=15)
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,18 @@
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from django_cryptolock.api_views import (
|
||||||
|
CryptoLockAPITokenLoginView,
|
||||||
|
CryptoLockAPISignUpView,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r"^", include("django_cryptolock.urls", namespace="django_cryptolock"))
|
path(
|
||||||
|
"api/token_login", CryptoLockAPITokenLoginView.as_view(), name="api_token_login"
|
||||||
|
),
|
||||||
|
path("api/signup", CryptoLockAPISignUpView.as_view(), name="api_signup"),
|
||||||
|
url(r"^", include("django_cryptolock.urls", namespace="django_cryptolock")),
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in New Issue