added simple monero auth views and forms
This commit is contained in:
parent
414d787680
commit
c5d68aa1f8
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
<li>...</li>
|
||||
</ul>
|
||||
<form method="post" action="{% url 'test_logout' %}">
|
||||
{% csrf_token %}
|
||||
<input type="submit" value="Logout" />
|
||||
</form>
|
||||
{% else %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue