implemented 2FA via email and Authenticator App
This commit is contained in:
parent
377eeb4186
commit
e52cfa83f5
11 changed files with 248 additions and 3 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -285,3 +285,5 @@ fabric.properties
|
||||||
/markdownblog/mdfiles/*
|
/markdownblog/mdfiles/*
|
||||||
/envvars.env
|
/envvars.env
|
||||||
/markdownblog/static
|
/markdownblog/static
|
||||||
|
|
||||||
|
/*.pem
|
||||||
|
|
@ -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()}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
|
|
@ -25,3 +25,7 @@ header, main, footer {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mt-3 {
|
||||||
|
margin-top: 3em;
|
||||||
|
}
|
||||||
57
markdownblog/markdownblog/templates/2fa/add-device.html
Normal file
57
markdownblog/markdownblog/templates/2fa/add-device.html
Normal 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 %}
|
||||||
|
|
||||||
41
markdownblog/markdownblog/templates/2fa/complete-fido.html
Normal file
41
markdownblog/markdownblog/templates/2fa/complete-fido.html
Normal 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 %}
|
||||||
39
markdownblog/markdownblog/templates/2fa/devices.html
Normal file
39
markdownblog/markdownblog/templates/2fa/devices.html
Normal 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 %}
|
||||||
76
markdownblog/markdownblog/templates/2fa/verify.html
Normal file
76
markdownblog/markdownblog/templates/2fa/verify.html
Normal 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 %}
|
||||||
|
|
@ -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 %}
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue