added simple monero auth views and forms

This commit is contained in:
Gonçalo Valério 2019-11-25 17:08:43 +00:00
parent 414d787680
commit c5d68aa1f8
8 changed files with 150 additions and 11 deletions

View File

@ -21,12 +21,13 @@ class MoneroAddressBackend(ModelBackend):
in the future it should be done locally to be more reliable and more
performant.
"""
if not all(address, challenge, signature):
if not all([address, challenge, signature]):
return None
try:
stored_address = Address.objects.get(address=address).select_related("user")
except:
stored_address = (
Address.objects.select_related("user").filter(address=address).first()
)
if not stored_address:
return None
try:

View File

@ -1,8 +1,95 @@
from django import forms
from django.contrib.auth import authenticate
from django.utils.translation import gettext, gettext_lazy as _
from .models import Address
from .validators import validate_monero_address
from .utils import generate_challenge
class SimpleSignUpForm(forms.Form):
username = forms.CharField()
class ChallengeMixin(forms.Form):
"""
Used on authentication forms to make sure an unique challenge is included.
This mixin ensures that the challenge is always controlled by the server.
"""
challenge = forms.CharField()
address = forms.CharField()
def include_challange(self):
new_challenge = generate_challenge()
if not self.data:
self.request.session["current_challenge"] = new_challenge
self.initial["challenge"] = new_challenge
def clean_challenge(self):
challenge = self.cleaned_data.get("challenge")
if not challenge or challenge != self.request.session.get("current_challenge"):
raise forms.ValidationError(_("Invalid or outdated challenge"))
return challenge
class SimpleLoginForm(ChallengeMixin, forms.Form):
"""Basic login form, that can be used as reference for implementation."""
address = forms.CharField(validators=[validate_monero_address])
signature = forms.CharField()
error_messages = {
"invalid_login": _("Please enter a correct Monero address or signature."),
"inactive": _("This account is inactive."),
}
def __init__(self, request=None, *args, **kwargs):
"""When rendering the form (no data provided) a new challenge
must be created."""
super().__init__(*args, **kwargs)
self.request = request
self.user_cache = None
self.include_challange()
def clean(self):
address = self.cleaned_data.get("address")
challenge = self.cleaned_data.get("challenge")
signature = self.cleaned_data.get("signature")
if address and challenge and signature:
self.user_cache = authenticate(
self.request, address=address, challenge=challenge, signature=signature
)
if self.user_cache is None:
raise self.get_invalid_login_error()
else:
self.confirm_login_allowed(self.user_cache)
return self.cleaned_data
def confirm_login_allowed(self, user):
if not user.is_active:
raise forms.ValidationError(
self.error_messages["inactive"], code="inactive"
)
def get_user(self):
return self.user_cache
def get_invalid_login_error(self):
return forms.ValidationError(
self.error_messages["invalid_login"], code="invalid_login"
)
class SimpleSignUpForm(ChallengeMixin, forms.Form):
"""Basic login form, that can be used as reference for implementation."""
username = forms.CharField()
address = forms.CharField(validators=[validate_monero_address])
signature = forms.CharField()
def __init__(self, request=None, *args, **kwargs):
"""When rendering the form (no data provided) a new challenge
must be created."""
super().__init__(*args, **kwargs)
self.request = request
self.include_challange()

View File

@ -1,3 +1,5 @@
from secrets import token_hex
from django.conf import settings
from monerorpc.authproxy import AuthServiceProxy
@ -16,3 +18,8 @@ def verify_signature(address: str, challenge: str, signature: str) -> bool:
)
return result.get("good", False)
def generate_challenge():
"""Generates a new random challenge for the authentication."""
return token_hex(8)

View File

@ -1,14 +1,47 @@
# -*- coding: utf-8 -*-
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.views import LoginView
from django.contrib.auth import get_user_model
from django.views.generic import FormView
from django.forms.utils import ErrorList
from django.conf import settings
from django.urls import reverse
from .forms import SimpleSignUpForm
from monerorpc.authproxy import JSONRPCException
from .forms import SimpleSignUpForm, SimpleLoginForm
from .utils import verify_signature
class MoneroLoginView(LoginView):
pass
form_class = SimpleLoginForm
class MoneroSignUpView(FormView):
template_name = "registration/signup.html"
form_class = SimpleSignUpForm
def get_form(self, form_class=None):
return self.form_class(request=self.request, **self.get_form_kwargs())
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"]
try:
valid_sig = verify_signature(address, challenge, signature)
except JSONRPCException:
form._errors["__all__"] = ErrorList([_("Error connecting to daemon")])
return self.form_invalid(form)
if valid_sig:
user = get_user_model().objects.create(username=username)
user.address_set.create(address=address)
return super().form_valid(form)
else:
form._errors["signature"] = ErrorList([_("Invalid signature")])
return self.form_invalid(form)
def get_success_url(self):
return settings.LOGIN_REDIRECT_URL

View File

@ -111,8 +111,16 @@ USE_TZ = True
STATIC_URL = "/static/"
# Auth Settings
LOGIN_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = "/"
# Monero Cryptolock Settings
# Wallet RPC
AUTHENTICATION_BACKENDS = ["django_cryptolock.backends.MoneroAddressBackend"]
DJCL_MONERO_NETWORK = "mainnet"
MONERO_WALLET_RPC_PROTOCOL = os.environ.get("MONERO_WALLET_RPC_PROTOCOL", "http")
MONERO_WALLET_RPC_HOST = os.environ.get("MONERO_WALLET_RPC_HOST", "localhost:6000")
MONERO_WALLET_RPC_USER = os.environ.get("MONERO_WALLET_RPC_USER")

View File

@ -10,6 +10,7 @@
<li>...</li>
</ul>
<form method="post" action="{% url 'test_logout' %}">
{% csrf_token %}
<input type="submit" value="Logout" />
</form>
{% else %}

View File

@ -1,7 +1,8 @@
{% extends 'django_cryptolock/base.html' %}
{% block content %}
<form>
<form method="post" action="{% url 'test_login' %}">
{% csrf_token %}
{{form}}
<input type="submit" value="Login" />
</form>

View File

@ -1,7 +1,8 @@
{% extends 'django_cryptolock/base.html' %}
{% block content %}
<form>
<form method="post" action="{% url 'test_signup' %}" >
{% csrf_token %}
{{form}}
<input type="submit" value="Sign Up" />
</form>