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

4
.gitignore vendored
View file

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

View file

@ -32,6 +32,7 @@ INSTALLED_APPS = [
'blog',
'markdownblog',
'fontawesomefree',
'django_2fa',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
@ -48,6 +49,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django_2fa.middleware.MFAProctectMiddleware',
]
ROOT_URLCONF = 'markdownblog.urls'
@ -128,7 +130,19 @@ STATIC_URL = 'static/'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
LOGIN_URL = '/accounts/login'
LOGIN_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = "/"
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

@ -24,4 +24,8 @@ header, main, footer {
header, main, footer {
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 class="no-liststyle"><a class="waves-effect" href="{% url 'admin:index' %}"><i class="material-icons">
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
class="material-icons">logout</i>Logout</a></li>
{% if debug %}

View file

@ -1,8 +1,11 @@
from django.contrib import admin
from django.urls import path, include
import django_2fa.urls
urlpatterns = [
path('', include('blog.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

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