implemented 2FA via email and Authenticator App

This commit is contained in:
CDaut 2022-06-27 23:16:44 +02:00 committed by CDaut
parent 377eeb4186
commit e52cfa83f5
11 changed files with 248 additions and 3 deletions

2
.gitignore vendored
View file

@ -285,3 +285,5 @@ fabric.properties
/markdownblog/mdfiles/* /markdownblog/mdfiles/*
/envvars.env /envvars.env
/markdownblog/static /markdownblog/static
/*.pem

View file

@ -10,6 +10,7 @@ from django.shortcuts import render, redirect
from blog.models import Topic, Tag, Blogpost from blog.models import Topic, Tag, Blogpost
from django.template import Template, Context from django.template import Template, Context
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django_2fa.decorators import mfa_login_required
def render_md_file(path) -> Template: def render_md_file(path) -> Template:
@ -53,6 +54,7 @@ def index(request) -> HttpResponse:
@login_required @login_required
@mfa_login_required
def edit(request, id) -> HttpResponse: def edit(request, id) -> HttpResponse:
blogpost = Blogpost.objects.get(pk=id) blogpost = Blogpost.objects.get(pk=id)
mdfile_content = open(blogpost.mdfile, "r").read() mdfile_content = open(blogpost.mdfile, "r").read()
@ -94,6 +96,7 @@ def edit(request, id) -> HttpResponse:
@login_required @login_required
@csrf_exempt @csrf_exempt
@mfa_login_required
def order(request) -> HttpResponse: def order(request) -> HttpResponse:
if request.method == "POST": if request.method == "POST":
root_id = int(request.POST['rootID']) if request.POST['rootID'] != 'root_list' else None root_id = int(request.POST['rootID']) if request.POST['rootID'] != 'root_list' else None
@ -114,6 +117,7 @@ def order(request) -> HttpResponse:
@login_required @login_required
@mfa_login_required
def addpost(request) -> HttpResponse: def addpost(request) -> HttpResponse:
context = {'alltopics': Topic.objects.all().order_by('name').values(), 'markdown': '', context = {'alltopics': Topic.objects.all().order_by('name').values(), 'markdown': '',
'roottopics': Topic.objects.all().filter(rootTopic=None), 'roottopics': Topic.objects.all().filter(rootTopic=None),
@ -154,6 +158,7 @@ def addpost(request) -> HttpResponse:
# @login_required # @login_required
# @mfa_login_required
# def createmocks(request, objtype, n) -> HttpResponse: # def createmocks(request, objtype, n) -> HttpResponse:
# topics = TopicFactory.create_batch(n) # topics = TopicFactory.create_batch(n)
# #
@ -171,6 +176,7 @@ def addpost(request) -> HttpResponse:
@login_required @login_required
@mfa_login_required
def addtopic(request): def addtopic(request):
context = {'roottopics': Topic.objects.all().filter(rootTopic=None), 'allposts': Blogpost.objects.all()} context = {'roottopics': Topic.objects.all().filter(rootTopic=None), 'allposts': Blogpost.objects.all()}

View file

@ -32,6 +32,7 @@ INSTALLED_APPS = [
'blog', 'blog',
'markdownblog', 'markdownblog',
'fontawesomefree', 'fontawesomefree',
'django_2fa',
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
@ -48,6 +49,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django_2fa.middleware.MFAProctectMiddleware',
] ]
ROOT_URLCONF = 'markdownblog.urls' ROOT_URLCONF = 'markdownblog.urls'
@ -128,7 +130,19 @@ STATIC_URL = 'static/'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
LOGIN_URL = '/accounts/login'
LOGIN_REDIRECT_URL = "/" LOGIN_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = "/" LOGOUT_REDIRECT_URL = "/"
CSRF_HEADER_NAME = "X-CSRFToken" CSRF_HEADER_NAME = "X-CSRFToken"
# multifactor auth
MFA_URL = '/accounts/2fa/login'
SALT_KEY = os.environ['SALT_KEY']
MFA_ISSUER_NAME = 'MDBlog'
# Configure mail here
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

View file

@ -25,3 +25,7 @@ header, main, footer {
padding-left: 0; padding-left: 0;
} }
} }
.mt-3 {
margin-top: 3em;
}

View file

@ -0,0 +1,57 @@
{% extends 'base/base.html' %}
{% load i18n static %}
{% block title %}
Add new 2FA device
{% endblock %}
{% block includehere %}
{% endblock %}
{% block content %}
<div class="col s12">
<h3>{% translate 'Add new 2FA device' %}</h3>
<hr>
{% if form.errors and not form.non_field_errors %}
<p class="errornote">
{% if form.errors.items|length == 1 %}{% translate "Please correct the error below." %}{% else %}
{% translate "Please correct the errors below." %}{% endif %}
</p>
{% endif %}
<form action="{{ request.path }}" method="post" id="add-device-form" class="col s12">
{% csrf_token %}
<input type="hidden" name="{{ next_field }}" value="{{ next }}">
{% if form.non_field_errors %}
<div class="form-row">
{{ form.non_field_errors }}
</div>
{% endif %}
<div class="row">
<div class="input-field col s12 l6">
{{ form.name.errors }}
{{ form.name }}
{{ form.name.label_tag }}
</div>
</div>
<div class="row">
<div class="input-field col s12 l6">
{{ form.device_type.errors }}
{{ form.device_type }}
{{ form.device_type.label_tag }}
</div>
</div>
{% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %}
<div class="row">
<div class="input-field col s12 l6">
<button class="btn waves-effect waves-light" type="submit">
{% translate 'Continue' %}
</button>
</div>
</div>
</form>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
var elems = document.querySelectorAll('select');
var instances = M.FormSelect.init(elems);
});
</script>
{% endblock %}

View file

@ -0,0 +1,41 @@
{% extends "base/base.html" %}
{% load i18n static %}
{% block title %}{% translate "Complete setup for 2FA Device" %}{% endblock %}
{% block content %}
<div id="content-main" class="col s12">
<h3>{% translate 'Hardware Key Setup' %}</h3>
<hr>
<h4 class="tc">{% translate 'Insert your hardware key to complete setup for:' %} {{ device.name }}</h4>
</div>
<script
src="{% static '2fa/axios.min.js' %}"></script>
<script src="{% static '2fa/cbor.js' %}"></script>
<script>
function start_reg() {
axios.post("{% url 'django_2fa:fido-reg-begin' device.id %}", {}, {responseType: 'arraybuffer'})
.then((response) => {
return CBOR.decode(response.data);
})
.then((options) => {
return navigator.credentials.create(options);
})
.then((attestation) => {
console.log(attestation);
var data = CBOR.encode({
"attestationObject": new Uint8Array(attestation.response.attestationObject),
"clientDataJSON": new Uint8Array(attestation.response.clientDataJSON)
});
return axios.post("{% url 'django_2fa:fido-reg-complete' device.id %}", data, {});
})
.then((response) => {
location.href = "{{ next }}";
})
.catch((e) => {
console.error(e);
alert('{% translate "Error in hardware key registration" %}');
});
}
start_reg();
</script>
{% endblock %}

View file

@ -0,0 +1,39 @@
{% extends 'base/base.html' %}
{% load i18n static %}
{% block title %}
Manage 2FA devices
{% endblock %}
{% block includehere %}
{% endblock %}
{% block content %}
<div id="content-main" class="col s12">
<h3>{% translate 'Manage 2FA devices' %}</h3>
<hr>
<a href="{% url 'django_2fa:device-add' %}" class="waves-effect waves-light btn mt-3">
Add Device
</a>
<ul class="collection">
{% for d in devices %}
<li class="collection-item avatar">
<i class="material-icons circle">
{% if d.device_type == 'email' %}mail
{% elif d.device_type == 'app' %}phone_android
{% else %}vpn_key{% endif %}
</i>
<span class="title"><strong>{{ d.name }} - {{ d.get_device_type_display }}</strong></span>
<p>
{% if not d.setup_complete %}
<a href="{% url 'django_2fa:device-complete' d.id %}">Complete Setup</a><br>
{% endif %}
Added: {{ d.created|date:"D, dS M Y" }}
</p>
<a href="{% url 'django_2fa:device-remove' d.id %}" title="remove" class="secondary-content"
onclick="return confirm('Are sure you wish to remove this device?')">
<i class="material-icons">delete</i>
</a>
</li>
{% endfor %}
</ul>
</div>
{% endblock %}

View file

@ -0,0 +1,76 @@
{% extends "base/base.html" %}
{% load i18n static %}
{% block title %}{% translate "Verify authenticator app" %}{% endblock %}
{% block content %}
<div class="col s12">
{% if complete_setup %}
<h3>{% translate 'Verify your authorization device.' %}</h3>
{% else %}
<h3>{% translate 'Please authenticate with your 2FA token' %}</h3>
{% endif %}
<hr>
{% if form.errors and not form.non_field_errors %}
<p class="errornote">
{% if form.errors.items|length == 1 %}{% translate "Please correct the error below." %}{% else %}
{% translate "Please correct the errors below." %}{% endif %}
</p>
{% endif %}
<div class="row">
<form action="{{ request.path }}" method="post" id="mfa-form" class="col s12">
{% csrf_token %}
<input type="hidden" name="{{ next_field }}" value="{{ next }}">
{% if form.non_field_errors %}
<div class="form-row">
{{ form.non_field_errors }}
</div>
{% endif %}
<div class="row">
<b><p>{% translate '2FA Factor:' %} {{ device.name }}</p></b>
<p>
{% if device.device_type == 'email' %}
{% translate 'Check your e-mail for your authorization code.' %}
{% elif device.device_type == 'app' %}
{% if not complete_setup %}
{% translate 'Enter code from your authenicator application.' %}{% endif %}
{% else %}
{% translate 'Use your hardware token to complete sign in.' %}
{% endif %}
</p>
</div>
<div class="row">
{% if complete_setup and device.device_type == 'app' %}
<p>
{% translate 'Scan QR below in your authenicator app, then enter a code to verify your device.' %}
<br>
<canvas id="qr"></canvas>
<br><br><strong>{% translate 'Provision URL:' %}</strong><br>
<pre>{{ device.provision_url }}</pre>
</p>
{% endif %}
<div class="input-field col s12 l6">
{{ form.mfa_code.errors }}
{{ form.mfa_code }}
{{ form.mfa_code.label_tag }}
</div>
</div>
<div class="row">
<div class="input-field col s12 l6">
<button class="btn waves-effect waves-light" type="submit">{% translate 'Continue' %}</button>
</div>
</div>
</form>
</div>
</div>
{% if complete_setup and device.device_type == 'app' %}
<script src="{% static '2fa/qrious.min.js' %}"></script>
<script>
(function () {
var qr = new QRious({
element: document.getElementById('qr'),
value: '{{ device.provision_url }}',
size: 200
});
})();
</script>
{% endif %}
{% endblock %}

View file

@ -61,6 +61,8 @@
</li> </li>
<li class="no-liststyle"><a class="waves-effect" href="{% url 'admin:index' %}"><i class="material-icons"> <li class="no-liststyle"><a class="waves-effect" href="{% url 'admin:index' %}"><i class="material-icons">
admin_panel_settings</i>Admin panel</a></li> admin_panel_settings</i>Admin panel</a></li>
<li class="no-liststyle"><a class="waves-effect" href="{% url 'django_2fa:devices' %}"><i class="material-icons">
vpn_key</i>Manage 2FA</a></li>
<li class="no-liststyle"><a class="waves-effect" href="{% url 'logout' %}"><i <li class="no-liststyle"><a class="waves-effect" href="{% url 'logout' %}"><i
class="material-icons">logout</i>Logout</a></li> class="material-icons">logout</i>Logout</a></li>
{% if debug %} {% if debug %}

View file

@ -1,8 +1,11 @@
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
import django_2fa.urls
urlpatterns = [ urlpatterns = [
path('', include('blog.urls')), path('', include('blog.urls')),
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('accounts/', include('django.contrib.auth.urls')) path('accounts/', include('django.contrib.auth.urls')),
path('accounts/2fa/', include(django_2fa.urls)),
] ]

View file

@ -4,3 +4,4 @@ factory-boy==3.2.1
markdown2==2.4.3 markdown2==2.4.3
fontawesomefree==6.1.1 fontawesomefree==6.1.1
pygments==2.12.0 pygments==2.12.0
django-2fa==0.9.0