1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2024-12-25 15:45:23 +00:00

OTP improvements

This commit is contained in:
Real-Gecko 2020-10-19 14:26:08 +06:00
parent 1f642a4381
commit 5a19f0c949
13 changed files with 182 additions and 149 deletions

View file

@ -1,6 +1,6 @@
from appsettings.settings import app_settings from appsettings.settings import app_settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.forms import ModelForm, ValidationError from django.forms import EmailField, Form, ModelForm, ValidationError
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from .models import UserInstance, UserSSHKey from .models import UserInstance, UserSSHKey
@ -69,3 +69,7 @@ class UserSSHKeyForm(ModelForm):
class Meta: class Meta:
model = UserSSHKey model = UserSSHKey
fields = ('keyname', 'keypublic') fields = ('keyname', 'keypublic')
class EmailOTPForm(Form):
email = EmailField(label=_('Email'))

View file

@ -8,7 +8,15 @@
{% block page_heading %}{% trans "User Profile" %}: {{ user }}{% endblock page_heading %} {% block page_heading %}{% trans "User Profile" %}: {{ user }}{% endblock page_heading %}
{% block page_heading_extra %} {% block page_heading_extra %}
<a href="{% url 'accounts:user_instance_create' user.id %}" class="btn btn-success"> {% if otp_enabled %}
<a href="{% url 'accounts:admin_email_otp' user.id %}" class="btn btn-secondary" title="{% trans "Email OTP QR code" %}">
{% icon 'qrcode' %}
</a>
{% endif %}
<a href="{% url 'admin:user_update' user.id %}?next={% url 'accounts:account' user.id %}" class="btn btn-primary" title="{% trans "Edit user" %}">
{% icon 'pencil' %}
</a>
<a href="{% url 'accounts:user_instance_create' user.id %}" class="btn btn-success" title="{% trans "Create user instance" %}">
{% icon 'plus' %} {% icon 'plus' %}
</a> </a>
{% endblock page_heading_extra %} {% endblock page_heading_extra %}
@ -21,11 +29,6 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#public-keys">{% trans "Public Keys" %}</a> <a class="nav-link" data-toggle="tab" href="#public-keys">{% trans "Public Keys" %}</a>
</li> </li>
{% if totp_url %}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#otp">{% trans "OTP QR Code" %}</a>
</li>
{% endif %}
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
@ -82,12 +85,5 @@
</tbody> </tbody>
</table> </table>
</div> </div>
{% if totp_url %}
<div class="tab-pane fade" id="otp">
<div class="text-center">
{% qr_from_text totp_url image_format="png" %}
</div>
</div>
{% endif %}
</div> </div>
{% endblock content %} {% endblock content %}

View file

@ -0,0 +1,7 @@
{% load i18n %}
{% load qr_code %}
{% blocktrans %}
Scan this QR code to get OTP for account '{{ user }}'
{% endblocktrans %}
<br>
{% qr_from_text totp_url %}

View file

@ -0,0 +1,32 @@
{% extends "base.html" %}
{% load bootstrap4 %}
{% load icons %}
{% load i18n %}
{% block title %}{{ title }}{% endblock %}
{% block page_heading %}{{ title }}{% endblock page_heading %}
{% block content %}
<div class="alert alert-info">
{% blocktrans %}
Enter email address OTP QR code will be sent to.
{% endblocktrans %}
</div>
<div class="card">
<div class="card-body">
<form id="create-update" action="" method="post">
{% csrf_token %}
{% bootstrap_form form layout='horizontal' %}
</form>
</div>
<div class="card-footer">
<div class="form-group mb-0 float-right">
<a class="btn btn-primary" href="javascript:history.back()">{% icon 'arrow-left' %} {% trans "Cancel" %}</a>
<button type="submit" form="create-update" class="btn btn-success">
{% icon 'envelope-o' %} {% trans "Send" %}
</button>
</div>
</div>
</div>
{% endblock content %}

View file

@ -1,38 +1,14 @@
{% extends 'base.html' %}
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
{% load bootstrap4 %} {% load bootstrap4 %}
<!DOCTYPE html>
<html lang="en">
<head> {% block title %}WebVirtCloud{% endblock title %}
<meta charset="utf-8"> {% block page_heading %}WebVirtCloud{% endblock page_heading %}
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="WebVirtMgr panel for manage virtual machine">
<meta name="author" content="anatoliy.guskov@gmail.com">
<title>{% trans "WebVirtCloud" %} - {% trans "Sign In" %}</title> {% block content %}
<div class="row">
<!-- Bootstrap Core CSS -->
<link href="{% static "css/wvc-main.min.css" %}" rel="stylesheet" id="wvc_css">
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<div class="container">
<div>
<div class="page-header text-center">
<a class="" href="/"><h1>WebVirtCloud</h1></a>
</div>
<div class="row">
<div class="col-6 offset-3" role="main"> <div class="col-6 offset-3" role="main">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
@ -44,19 +20,11 @@
{% bootstrap_field form.username layout='horizontal' %} {% bootstrap_field form.username layout='horizontal' %}
{% bootstrap_field form.password layout='horizontal' %} {% bootstrap_field form.password layout='horizontal' %}
{% bootstrap_field form.otp_token layout='horizontal' %} {% bootstrap_field form.otp_token layout='horizontal' %}
<a href="{% url 'accounts:email_otp' %}" class="float-right mb-2">{% trans "I do not have/lost my OTP!" %}</a>
<button class="btn btn-lg btn-success btn-block" type="submit">{% trans "Sign In" %}</button> <button class="btn btn-lg btn-success btn-block" type="submit">{% trans "Sign In" %}</button>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> {% endblock content %}
</div>
<!-- jQuery -->
<script src="{% static "js/jquery.js" %}"></script>
<!-- Bootstrap Core JavaScript -->
<script src="{% static "js/bootstrap.bundle.min.js" %}"></script>
</body>
</html>

View file

@ -1,41 +0,0 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="WebVirtMgr panel for manage virtual machine">
<meta name="author" content="anatoliy.guskov@gmail.com">
<title>{% block title %}{% endblock %}</title>
<!-- Bootstrap Core CSS -->
<link href="{% static "css/wvc-main.min.css" %}" rel="stylesheet" id="wvc_css">
<!-- SB admin CSS -->
<link href="{% static "css/signin.css" %}" rel="stylesheet">
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<div class="container">
{% block content %}{% endblock %}
</div>
<!-- jQuery -->
<script src="{% static "js/jquery.js" %}"></script>
<!-- Bootstrap Core JavaScript -->
<script src="{% static "js/bootstrap.bundle.min.js" %}"></script>
</body>
</html>

View file

@ -1,12 +1,18 @@
{% extends "base_auth.html" %} {% extends "base.html" %}
{% load i18n %} {% load i18n %}
{% load static %}
{% block title %}{% trans "WebVirtCloud" %} - {% trans "Sign In" %}{% endblock %} {% block title %}{% trans "WebVirtCloud" %} - {% trans "Sign In" %}{% endblock %}
{% block style %}
<link href="{% static "css/signin.css" %}" rel="stylesheet">
{% endblock style %}
{% block content %} {% block content %}
<div> <div class="page-header">
<div class="page-header">
<a class="" href="/"><h1>WebVirtCloud</h1></a> <a class="" href="/"><h1>WebVirtCloud</h1></a>
</div> </div>
<div class="col-12" role="main"> <div class="col-12" role="main">
{% if form.errors %} {% if form.errors %}
<div class="alert alert-danger"> <div class="alert alert-danger">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button> <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
@ -20,6 +26,5 @@
<input type="hidden" name="next" value="{{ next }}"> <input type="hidden" name="next" value="{{ next }}">
<button class="btn btn-lg btn-success btn-block" type="submit">{% trans "Sign In" %}</button> <button class="btn btn-lg btn-success btn-block" type="submit">{% trans "Sign In" %}</button>
</form> </form>
</div> </div>
</div>
{% endblock %} {% endblock %}

View file

@ -20,10 +20,14 @@ urlpatterns = [
] ]
if settings.OTP_ENABLED: if settings.OTP_ENABLED:
urlpatterns += path( urlpatterns += [
path(
'login/', 'login/',
LoginView.as_view(template_name='accounts/otp_login.html', authentication_form=OTPAuthenticationForm), LoginView.as_view(template_name='accounts/otp_login.html', authentication_form=OTPAuthenticationForm),
name='login', name='login',
), ),
path('email_otp/', views.email_otp, name='email_otp'),
path('admin_email_otp/<int:user_id>/', views.admin_email_otp, name='admin_email_otp'),
]
else: else:
urlpatterns += path('login/', LoginView.as_view(template_name='login.html'), name='login'), urlpatterns += path('login/', LoginView.as_view(template_name='login.html'), name='login'),

View file

@ -2,6 +2,9 @@ import base64
import binascii import binascii
import struct import struct
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.utils.translation import gettext as _
from django_otp import devices_for_user from django_otp import devices_for_user
from django_otp.plugins.otp_totp.models import TOTPDevice from django_otp.plugins.otp_totp.models import TOTPDevice
@ -12,6 +15,9 @@ def get_user_totp_device(user):
if isinstance(device, TOTPDevice): if isinstance(device, TOTPDevice):
return device return device
device = user.totpdevice_set.create()
return device
def validate_ssh_key(key): def validate_ssh_key(key):
array = key.encode().split() array = key.encode().split()
@ -37,3 +43,20 @@ def validate_ssh_key(key):
return True return True
else: else:
return False return False
def send_email_with_otp(user, device):
send_mail(
_('OTP QR Code'),
_('Please view HTML version of this message.'),
None,
[user.email],
html_message=render_to_string(
'accounts/email/otp.html',
{
'totp_url': device.config_url,
'user': user,
},
),
fail_silently=False,
)

View file

@ -1,7 +1,7 @@
from admin.decorators import superuser_only from admin.decorators import superuser_only
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import update_session_auth_hash from django.contrib.auth import get_user_model, update_session_auth_hash
from django.contrib.auth.decorators import permission_required from django.contrib.auth.decorators import permission_required
from django.contrib.auth.forms import PasswordChangeForm from django.contrib.auth.forms import PasswordChangeForm
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
@ -9,11 +9,11 @@ from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from instances.models import Instance from instances.models import Instance
from accounts.forms import ProfileForm, UserSSHKeyForm from accounts.forms import EmailOTPForm, ProfileForm, UserSSHKeyForm
from accounts.models import * from accounts.models import *
from . import forms from . import forms
from .utils import get_user_totp_device from .utils import get_user_totp_device, send_email_with_otp
def profile(request): def profile(request):
@ -65,13 +65,15 @@ def account(request, user_id):
user_insts = UserInstance.objects.filter(user_id=user_id) user_insts = UserInstance.objects.filter(user_id=user_id)
instances = Instance.objects.all().order_by("name") instances = Instance.objects.all().order_by("name")
publickeys = UserSSHKey.objects.filter(user_id=user_id) publickeys = UserSSHKey.objects.filter(user_id=user_id)
if settings.OTP_ENABLED:
device = get_user_totp_device(user)
if not device:
device = user.totpdevice_set.create()
totp_url = device.config_url
return render(request, "account.html", locals()) return render(
request, "account.html", {
'user': user,
'user_insts': user_insts,
'instances': instances,
'publickeys': publickeys,
'otp_enabled': settings.OTP_ENABLED,
})
@permission_required("accounts.change_password", raise_exception=True) @permission_required("accounts.change_password", raise_exception=True)
@ -141,3 +143,33 @@ def user_instance_delete(request, pk):
"common/confirm_delete.html", "common/confirm_delete.html",
{"object": user_instance}, {"object": user_instance},
) )
def email_otp(request):
form = EmailOTPForm(request.POST or None)
if form.is_valid():
UserModel = get_user_model()
try:
user = UserModel.objects.get(email=form.cleaned_data['email'])
except UserModel.DoesNotExist:
pass
else:
device = get_user_totp_device(user)
send_email_with_otp(user, device)
messages.success(request, _('OTP Sent to %s') % form.cleaned_data['email'])
return redirect('accounts:login')
return render(request, 'accounts/email_otp_form.html', {
'form': form,
'title': _('Email OTP'),
})
@superuser_only
def admin_email_otp(request, user_id):
user = get_object_or_404(get_user_model(), pk=user_id)
device = get_user_totp_device(user)
send_email_with_otp(user, device)
messages.success(request, _('OTP QR code was emailed to user %s') % user)
return redirect('accounts:account', user.id)

View file

@ -120,12 +120,12 @@ def user_update(request, pk):
user = get_object_or_404(User, pk=pk) user = get_object_or_404(User, pk=pk)
attributes = UserAttributes.objects.get(user=user) attributes = UserAttributes.objects.get(user=user)
user_form = forms.UserForm(request.POST or None, instance=user) user_form = forms.UserForm(request.POST or None, instance=user)
attributes_form = forms.UserAttributesForm( attributes_form = forms.UserAttributesForm(request.POST or None, instance=attributes)
request.POST or None, instance=attributes)
if user_form.is_valid() and attributes_form.is_valid(): if user_form.is_valid() and attributes_form.is_valid():
user_form.save() user_form.save()
attributes_form.save() attributes_form.save()
return redirect("admin:user_list") next = request.GET.get('next')
return redirect(next or "admin:user_list")
return render( return render(
request, request,
@ -146,8 +146,7 @@ def user_update_password(request, pk):
if form.is_valid(): if form.is_valid():
user = form.save() user = form.save()
update_session_auth_hash(request, user) # Important! update_session_auth_hash(request, user) # Important!
messages.success(request, _( messages.success(request, _("User password changed: {}".format(user.username)))
"User password changed: {}".format(user.username)))
return redirect("admin:user_list") return redirect("admin:user_list")
else: else:
messages.error(request, _("Wrong Data Provided")) messages.error(request, _("Wrong Data Provided"))
@ -159,8 +158,8 @@ def user_update_password(request, pk):
"accounts/change_password_form.html", "accounts/change_password_form.html",
{ {
"form": form, "form": form,
"user": user.username "user": user.username,
} },
) )

View file

@ -34,7 +34,9 @@
<body> <body>
{% if request.user.is_authenticated %}
{% include 'navbar.html' %} {% include 'navbar.html' %}
{% endif %}
<div role="main" class="container"> <div role="main" class="container">
<div class"row"> <div class"row">

View file

@ -183,3 +183,5 @@ SHOW_PROFILE_EDIT_PASSWORD = True
OTP_ENABLED = False OTP_ENABLED = False
LOGIN_REQUIRED_IGNORE_VIEW_NAMES = ['accounts:email_otp']