Fixes and applied comments

This commit is contained in:
Spitap
2022-08-31 21:13:15 +02:00
parent cf4f6ed224
commit d0113dd2cc
9 changed files with 98 additions and 211 deletions

View File

@@ -2,7 +2,7 @@ from django.shortcuts import render
from django.views.decorators.cache import never_cache from django.views.decorators.cache import never_cache
from django.template.context_processors import csrf from django.template.context_processors import csrf
from django.contrib.auth.hashers import make_password, PBKDF2PasswordHasher from django.contrib.auth.hashers import make_password, PBKDF2PasswordHasher
from django.http import HttpResponse,FileResponse,HttpResponseNotFound from django.http import HttpResponse,HttpResponseNotFound
from .Common import get_redirect_url from .Common import get_redirect_url
from .models import * from .models import *
import simplejson import simplejson
@@ -42,28 +42,25 @@ def genTokens(request):
uk.username = request.user.username uk.username = request.user.username
uk.properties={"secret_keys":hashedKeys, "salt":salt} uk.properties={"secret_keys":hashedKeys, "salt":salt}
uk.key_type="RECOVERY" uk.key_type="RECOVERY"
uk.enabled = False uk.enabled = True
uk.save() uk.save()
request.session["recovery_keys"]=clearKeys
return HttpResponse(simplejson.dumps({"keys":clearKeys})) return HttpResponse(simplejson.dumps({"keys":clearKeys}))
def download_codes(request):
if not "recovery_keys" in request.session:
return HttpResponseNotFound("This page isn't valid anymore.")
response = HttpResponse('\n'.join(request.session["recovery_keys"]),content_type='text/text')
response['Content-Disposition'] = 'attachment; filename = Recovery Codes.txt'
return response
def verify_login(request, username,token): def verify_login(request, username, token):
key = User_Keys.objects.filter(username=username, key_type = "RECOVERY") for key in User_Keys.objects.filter(username=username, key_type = "RECOVERY"):
secret_keys = key.properties["secret_keys"] secret_keys = key.properties["secret_keys"]
salt = key.properties["salt"] salt = key.properties["salt"]
hashedToken = make_password(token, salt, "pbkdf2_sha256_custom") hashedToken = make_password(token, salt, "pbkdf2_sha256_custom")
if hashedToken == secret_keys[0]: for i in range(len(secret_keys)):
secret_keys.pop(0) if hashedToken == secret_keys[i]:
secret_keys.pop(i)
key.properties["secret_keys"] = secret_keys key.properties["secret_keys"] = secret_keys
key.save() key.save()
return [True, key.id, len(secret_keys) == 0] return [True, key.id, len(secret_keys) == 0]
if len(secret_keys) == 0:
#Show a message ?
return [False]
return [False] return [False]
def getTokenLeft(request): def getTokenLeft(request):
@@ -73,15 +70,27 @@ def getTokenLeft(request):
keyLeft += len(key.properties["secret_keys"]) keyLeft += len(key.properties["secret_keys"])
return HttpResponse(simplejson.dumps({"left":keyLeft})) return HttpResponse(simplejson.dumps({"left":keyLeft}))
def recheck(request):
context = csrf(request)
context["mode"]="recheck"
if request.method == "POST":
if verify_login(request,request.user.username, token=request.POST["recovery"])[0]:
import time
request.session["mfa"]["rechecked_at"] = time.time()
return HttpResponse(simplejson.dumps({"recheck": True}), content_type="application/json")
else:
return HttpResponse(simplejson.dumps({"recheck": False}), content_type="application/json")
return render(request,"RECOVERY/recheck.html", context)
@never_cache @never_cache
def auth(request): def auth(request):
from .views import login from .views import login
context=csrf(request) context=csrf(request)
if request.method=="POST": if request.method=="POST":
tokenLength = len(request.POST["otp"]) tokenLength = len(request.POST["recovery"])
if tokenLength == 11 and "RECOVERY" not in settings.MFA_UNALLOWED_METHODS: if tokenLength == 11 and "RECOVERY" not in settings.MFA_UNALLOWED_METHODS:
#Backup code check #Backup code check
resBackup=verify_login(request, request.session["base_username"], token=request.POST["otp"]) resBackup=verify_login(request, request.session["base_username"], token=request.POST["recovery"])
if resBackup[0]: if resBackup[0]:
mfa = {"verified": True, "method": "RECOVERY","id":resBackup[1], "lastBackup":resBackup[2]} mfa = {"verified": True, "method": "RECOVERY","id":resBackup[1], "lastBackup":resBackup[2]}
if getattr(settings, "MFA_RECHECK", False): if getattr(settings, "MFA_RECHECK", False):
@@ -92,15 +101,16 @@ def auth(request):
if resBackup[2]: if resBackup[2]:
#If the last bakup code has just been used, we return a response insead of redirecting to login #If the last bakup code has just been used, we return a response insead of redirecting to login
context["lastBackup"] = True context["lastBackup"] = True
return render(request,"TOTP/Auth.html", context) return render(request,"RECOVERY/Auth.html", context)
return login(request) return login(request)
context["invalid"]=True
elif request.method=="GET": elif request.method=="GET":
mfa = request.session["mfa"] mfa = request.session.get("mfa")
if mfa and mfa["verified"] and mfa["lastBackup"]: if mfa and mfa["verified"] and mfa["lastBackup"]:
return login(request) return login(request)
context["invalid"]=True return render(request,"RECOVERY/Auth.html", context)
return render(request,"TOTP/Auth.html", context)
@never_cache @never_cache
def start(request): def start(request):

View File

@@ -21,46 +21,24 @@
.tooltip { .tooltip {
position: relative; position: relative;
display: inline-block; display: inline-block;
}
.tooltip .tooltiptext {
visibility: hidden;
width: 140px;
background-color: #555;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 5px;
position: absolute;
z-index: 1;
bottom: 150%;
left: 50%;
margin-left: -75px;
opacity: 0;
transition: opacity 0.3s;
}
.tooltip .tooltiptext::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: #555 transparent transparent transparent;
}
.tooltip:hover .tooltiptext {
visibility: visible;
opacity: 1;
}
.return{
margin: 1px;
} }
</style>
.toolbtn {
border-radius: 7px;
}
.toolbtn:hover {
background-color: gray;
transition: 0.2s;
}
.toolbtn:active {
background-color: green;
transition: 0.2s;
}
</style>
<script src="{% static 'mfa/js/qrious.min.js' %}" type="text/javascript"></script> <script src="{% static 'mfa/js/qrious.min.js' %}" type="text/javascript"></script>
<script type="text/javascript"> <script type="text/javascript">
var clearCodes;
$(document).ready(function checkTokenLeft() { $(document).ready(function checkTokenLeft() {
$.ajax({"url":"{% url 'get_recovery_token_left' %}", dataType:"JSON", $.ajax({"url":"{% url 'get_recovery_token_left' %}", dataType:"JSON",
success:function (data) { success:function (data) {
@@ -93,18 +71,35 @@
<div class='row'><div class='offset-4 col-md-4' style='background-color:#f0f0f0;padding: 10px'> <div class='row'><div class='offset-4 col-md-4' style='background-color:#f0f0f0;padding: 10px'>
<div class='row'> <div class='row'>
<div class="col-6 offset-6"> <div class="col-6 offset-6">
<span onclick='window.location.href="{% url 'download_recovery' %}"' class='fa fa-download' title="Download" <span onclick='download_recovery()' class='fa fa-download toolbtn' title="Download"
style='cursor:pointer'></span>&nbsp;&nbsp; style='cursor:pointer'></span>&nbsp;&nbsp;
<span class='fa fa-clipboard' title="Copy" onclick="copy()" style='cursor:pointer'></span> <span class='fa fa-clipboard toolbtn' title="Copy" onclick="copy()" style='cursor:pointer'></span>
</div></div><div id='recovery_codes'><pre>`; </div></div><div id='recovery_codes'><pre>`;
for (let i = 0; i < data.keys.length; i++) { for (let i = 0; i < data.keys.length; i++) {
htmlkey +="- " +data.keys[i] + "\n" htmlkey +="- " +data.keys[i] + "\n"
} }
document.getElementById('tokens').innerHTML = htmlkey+"</pre></div></div></div>" document.getElementById('tokens').innerHTML = htmlkey+"</pre></div></div></div>"
$("#popUpModal").modal('hide') $("#popUpModal").modal('hide')
clearCodes = data.keys
} }
}) })
} }
function download_recovery() {
var element = document.createElement('a');
var text = "";
for(let i = 0; i < clearCodes.length; i++)
{
text = text + clearCodes[i]
if (i < clearCodes.length - 1) { text = text + "\n"}
}
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', 'Recovery Codes.txt');
element.style.display = 'none';
document.body.appendChild(element);
element.click();
console.log(element.innerHTML)
document.body.removeChild(element);
}
</script> </script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}

View File

@@ -1,94 +1,14 @@
<script type="application/javascript"> {% extends "mfa_auth_base.html" %}
$(document).ready(function showWarningLastBackup() { {% block head %}
{% if lastBackup %} <style>
$("#modal-title").html("Last backup code used !") .row{
$("#modal-body").html("Don't forget to regenerate new backup code after login !") margin-left: 15px;
$('#modal-footer').html(`<FORM METHOD="GET" ACTION="{% url 'recovery_auth' %}" Id="confirmLogin" onSubmit="" name="recoveryLastBackupConfirm">
<input type='submit'class='btn btn-lg btn-success btn-block' value='Continue'>`)
$("#popUpModal").modal('show')
{% endif %}
return
});
function send_totp() {
$.ajax({"url":"{% url 'totp_recheck' %}", method:"POST",dataType:"JSON",
data:{"csrfmiddlewaretoken":"{{ csrf_token }}","otp":$("#otp").val()},
success:function (data) {
if (data["recheck"])
mfa_success_function();
else {
mfa_failed_function();
} }
} </style>
}) {% endblock %}
} {% block content %}
function tryToAuth() { <br/>
const otp_length = $("#otp").val().length <br/>
if (otp_length == 6) { {% include "RECOVERY/recheck.html" with mode='auth' %}
document.getElementById("formLogin").submit();
}
else if (otp_length == 11) {
const form = document.getElementById("formLogin");
form.setAttribute("ACTION", "{% url 'recovery_auth' %}")
form.submit();
}
}
</script>
<div class='container'>
<div class="row">
<div class="col-sm-10 col-sm-offset-1 col-xs-12 col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2"> {% endblock %}
<div class="panel panel-default card">
<div class="panel-heading card-header">
<strong>Recovery Code</strong>
</div>
<div class="panel-body card-body">
<FORM METHOD="POST" ACTION="{% url 'recovery_auth' %}" Id="formLogin" onSubmit="" name="FrontPage_Form1">
{% csrf_token %}
{% if invalid %}
<div class="alert alert-danger">
Sorry, The provided token is not valid.
</div>
{% endif %}
<fieldset>
<div class="row">
<div class="col-sm-12 col-md-12">
<p>Enter enter your next recovery code</p>
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12">
<div class="form-group">
<div class="input-group mb-3">
<span class="input-group-addon input-group-text">
<i class="glyphicon glyphicon-lock bi bi-lock"></i>
</span>
<input class="form-control" size="11" value="" placeholder="e.g npXiX-7dZgK" name="otp" type="text" id="otp" autofocus>
</div>
</div>
<div class="form-group d-grid gap-2">
<input type="button" onclick="{% if mode == "recheck" %} send_totp() {% else %} tryToAuth() {% endif %}" class="btn btn-lg btn-success btn-block" value="Sign in">
</div>
</div>
</fieldset>
</FORM>
</div>
<div class="row">
<div class="col-md-12 mb-3" style="padding-left: 25px">
{% if request.session.mfa_methods|length > 1 %}
<a href="{% url 'mfa_methods_list' %}">Select Another Method</a>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% include "modal.html" %}

View File

@@ -9,9 +9,9 @@
{% endif %} {% endif %}
return return
}); });
function send_totp() { function send_recovery() {
$.ajax({"url":"{% url 'totp_recheck' %}", method:"POST",dataType:"JSON", $.ajax({"url":"{% url 'recovery_recheck' %}", method:"POST",dataType:"JSON",
data:{"csrfmiddlewaretoken":"{{ csrf_token }}","otp":$("#otp").val()}, data:{"csrfmiddlewaretoken":"{{ csrf_token }}","recovery":$("#recovery").val()},
success:function (data) { success:function (data) {
if (data["recheck"]) if (data["recheck"])
mfa_success_function(); mfa_success_function();
@@ -21,17 +21,6 @@
} }
}) })
} }
function tryToAuth() {
const otp_length = $("#otp").val().length
if (otp_length == 6) {
document.getElementById("formLogin").submit();
}
else if (otp_length == 11) {
const form = document.getElementById("formLogin");
form.setAttribute("ACTION", "{% url 'recovery_auth' %}")
form.submit();
}
}
</script> </script>
<div class='container'> <div class='container'>
<div class="row"> <div class="row">
@@ -39,17 +28,17 @@
<div class="col-sm-10 col-sm-offset-1 col-xs-12 col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2"> <div class="col-sm-10 col-sm-offset-1 col-xs-12 col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
<div class="panel panel-default card"> <div class="panel panel-default card">
<div class="panel-heading card-header"> <div class="panel-heading card-header">
<strong> One Time Password</strong> <strong> Recovery code</strong>
</div> </div>
<div class="panel-body card-body"> <div class="panel-body card-body">
<FORM METHOD="POST" ACTION="{% url 'totp_auth' %}" Id="formLogin" onSubmit="" name="FrontPage_Form1"> <FORM METHOD="POST" ACTION="{% url 'recovery_auth' %}" Id="formLogin" onSubmit="" name="FrontPage_Form1">
{% csrf_token %} {% csrf_token %}
{% if invalid %} {% if invalid %}
<div class="alert alert-danger"> <div class="alert alert-danger">
Sorry, The provided token is not valid. Sorry, The provided code is not valid.
</div> </div>
{% endif %} {% endif %}
{% if quota %} {% if quota %}
@@ -60,7 +49,7 @@
<fieldset> <fieldset>
<div class="row"> <div class="row">
<div class="col-sm-12 col-md-12"> <div class="col-sm-12 col-md-12">
<p>Enter the 6-digits on your authenticator. Or input a recovery code</p> <p>Enter the 11-digits on your authenticator. Or input a recovery code</p>
</div> </div>
</div> </div>
@@ -71,14 +60,14 @@
<span class="input-group-addon input-group-text"> <span class="input-group-addon input-group-text">
<i class="glyphicon glyphicon-lock bi bi-lock"></i> <i class="glyphicon glyphicon-lock bi bi-lock"></i>
</span> </span>
<input class="form-control" size="6" MaxLength="11" value="" placeholder="e.g 55552" name="otp" type="text" id="otp" autofocus> <input class="form-control" size="11" MaxLength="11" value="" placeholder="e.g abcde-fghij" name="recovery" type="text" id="recovery" autofocus>
</div> </div>
</div> </div>
<div class="form-group d-grid gap-2"> <div class="form-group d-grid gap-2">
<input type="button" onclick="{% if mode == "recheck" %} send_totp() {% else %} tryToAuth() {% endif %}" class="btn btn-lg btn-success btn-block" value="Sign in"> <input type="{% if mode == "auth" %}submit{% elif mode == 'recheck' %}button{% endif %}" {% if mode == "recheck" %}onclick="send_recovery()" {% endif %} class="btn btn-lg btn-success btn-block" value="Sign in">
</div> </div>
</div> </div>
</fieldset> </fieldset>

View File

@@ -1,14 +1,4 @@
<script type="application/javascript"> <script type="application/javascript">
$(document).ready(function showWarningLastBackup() {
{% if lastBackup %}
$("#modal-title").html("Last backup code used !")
$("#modal-body").html("Don't forget to regenerate new backup code after login !")
$('#modal-footer').html(`<FORM METHOD="GET" ACTION="{% url 'recovery_auth' %}" Id="confirmLogin" onSubmit="" name="recoveryLastBackupConfirm">
<input type='submit'class='btn btn-lg btn-success btn-block' value='Continue'>`)
$("#popUpModal").modal('show')
{% endif %}
return
});
function send_totp() { function send_totp() {
$.ajax({"url":"{% url 'totp_recheck' %}", method:"POST",dataType:"JSON", $.ajax({"url":"{% url 'totp_recheck' %}", method:"POST",dataType:"JSON",
data:{"csrfmiddlewaretoken":"{{ csrf_token }}","otp":$("#otp").val()}, data:{"csrfmiddlewaretoken":"{{ csrf_token }}","otp":$("#otp").val()},
@@ -21,17 +11,6 @@
} }
}) })
} }
function tryToAuth() {
const otp_length = $("#otp").val().length
if (otp_length == 6) {
document.getElementById("formLogin").submit();
}
else if (otp_length == 11) {
const form = document.getElementById("formLogin");
form.setAttribute("ACTION", "{% url 'recovery_auth' %}")
form.submit();
}
}
</script> </script>
<div class='container'> <div class='container'>
<div class="row"> <div class="row">
@@ -60,7 +39,7 @@
<fieldset> <fieldset>
<div class="row"> <div class="row">
<div class="col-sm-12 col-md-12"> <div class="col-sm-12 col-md-12">
<p>Enter the 6-digits on your authenticator. Or input a recovery code</p> <p>Enter the 6-digits on your authenticator</p>
</div> </div>
</div> </div>
@@ -71,15 +50,14 @@
<span class="input-group-addon input-group-text"> <span class="input-group-addon input-group-text">
<i class="glyphicon glyphicon-lock bi bi-lock"></i> <i class="glyphicon glyphicon-lock bi bi-lock"></i>
</span> </span>
<input class="form-control" size="6" MaxLength="11" value="" placeholder="e.g 55552" name="otp" type="text" id="otp" autofocus> <input class="form-control" size="6" MaxLength="6" value="" placeholder="e.g 55552" name="otp" type="text" id="otp" autofocus>
</div> </div>
</div> </div>
<div class="form-group d-grid gap-2"> <div class="form-group d-grid gap-2">
<input type="button" onclick="{% if mode == "recheck" %} send_totp() {% else %} tryToAuth() {% endif %}" class="btn btn-lg btn-success btn-block" value="Sign in"> <input type="{% if mode == "auth" %}submit{% elif mode == 'recheck' %}button{% endif %}" {% if mode == "recheck" %}onclick="send_totp()" {% endif %} class="btn btn-lg btn-success btn-block" value="Sign in"> </div>
</div>
</div> </div>
</fieldset> </fieldset>
</FORM> </FORM>

View File

@@ -5,7 +5,6 @@ from .Common import get_redirect_url
from .models import * from .models import *
from django.template.context_processors import csrf from django.template.context_processors import csrf
import simplejson import simplejson
from django.template.context import RequestContext
from django.conf import settings from django.conf import settings
import pyotp import pyotp
from .views import login from .views import login
@@ -27,7 +26,7 @@ def recheck(request):
context = csrf(request) context = csrf(request)
context["mode"]="recheck" context["mode"]="recheck"
if request.method == "POST": if request.method == "POST":
if verify_login(request,request.user.username, token=request.POST["otp"]): if verify_login(request,request.user.username, token=request.POST["otp"])[0]:
import time import time
request.session["mfa"]["rechecked_at"] = time.time() request.session["mfa"]["rechecked_at"] = time.time()
return HttpResponse(simplejson.dumps({"recheck": True}), content_type="application/json") return HttpResponse(simplejson.dumps({"recheck": True}), content_type="application/json")

View File

@@ -16,7 +16,7 @@ urlpatterns = [
url(r'recovery/getTokenLeft', recovery.getTokenLeft, name="get_recovery_token_left"), url(r'recovery/getTokenLeft', recovery.getTokenLeft, name="get_recovery_token_left"),
url(r'recovery/genTokens', recovery.genTokens, name="regen_recovery_tokens"), url(r'recovery/genTokens', recovery.genTokens, name="regen_recovery_tokens"),
url(r'recovery/auth', recovery.auth, name="recovery_auth"), url(r'recovery/auth', recovery.auth, name="recovery_auth"),
url(r'recovery/download_codes', recovery.download_codes, name="download_recovery"), url(r'recovery/recheck', recovery.recheck, name="recovery_recheck"),
url(r'email/start/', Email.start , name="start_email"), url(r'email/start/', Email.start , name="start_email"),
url(r'email/auth/', Email.auth , name="email_auth"), url(r'email/auth/', Email.auth , name="email_auth"),

View File

@@ -32,7 +32,7 @@ def verify(request,username):
request.session["base_username"] = username request.session["base_username"] = username
#request.session["base_password"] = password #request.session["base_password"] = password
keys=User_Keys.objects.filter(username=username,enabled=1) keys=User_Keys.objects.filter(username=username,enabled=1)
methods=list(set([k.key_type for k in keys if k.key_type != "RECOVERY"])) methods=list(set([k.key_type for k in keys]))
if "Trusted Device" in methods and not request.session.get("checked_trusted_device",False): if "Trusted Device" in methods and not request.session.get("checked_trusted_device",False):
if TrustedDevice.verify(request): if TrustedDevice.verify(request):
@@ -40,10 +40,6 @@ def verify(request,username):
methods.remove("Trusted Device") methods.remove("Trusted Device")
request.session["mfa_methods"] = methods request.session["mfa_methods"] = methods
if "TOTP" not in methods and "RECOVERY" not in settings.MFA_UNALLOWED_METHODS:
#Add the "totp" option if user doesn't have totp auth (case with fido auth and backup code for instace)
methods.append("TOTP")
if len(methods)==1: if len(methods)==1:
return HttpResponseRedirect(reverse(methods[0].lower()+"_auth")) return HttpResponseRedirect(reverse(methods[0].lower()+"_auth"))
return show_methods(request) return show_methods(request)