recovery codes
This commit is contained in:
@@ -7,6 +7,7 @@ from .models import *
|
||||
#from django.template.context import RequestContext
|
||||
from .views import login
|
||||
from .Common import send
|
||||
from . import recovery
|
||||
|
||||
def sendEmail(request,username,secret):
|
||||
"""Send Email to the user after rendering `mfa_email_token_template`"""
|
||||
@@ -34,6 +35,7 @@ def start(request):
|
||||
from django.core.urlresolvers import reverse
|
||||
except:
|
||||
from django.urls import reverse
|
||||
recovery.genTokens(request, True) #recovery tokens
|
||||
return HttpResponseRedirect(reverse(getattr(settings,'MFA_REDIRECT_AFTER_REGISTRATION','mfa_home')))
|
||||
context["invalid"] = True
|
||||
else:
|
||||
|
||||
@@ -16,6 +16,7 @@ from .views import login, reset_cookie
|
||||
import datetime
|
||||
from .Common import get_redirect_url
|
||||
from django.utils import timezone
|
||||
from . import recovery
|
||||
|
||||
|
||||
def recheck(request):
|
||||
@@ -66,6 +67,7 @@ def complete_reg(request):
|
||||
uk.owned_by_enterprise = getattr(settings, "MFA_OWNED_BY_ENTERPRISE", False)
|
||||
uk.key_type = "FIDO2"
|
||||
uk.save()
|
||||
recovery.genTokens(request, True) #recovery tokens
|
||||
return HttpResponse(simplejson.dumps({'status': 'OK'}))
|
||||
except Exception as exp:
|
||||
import traceback
|
||||
|
||||
@@ -7,6 +7,7 @@ from django.template.context_processors import csrf
|
||||
from .models import *
|
||||
import user_agents
|
||||
from django.utils import timezone
|
||||
from . import recovery
|
||||
|
||||
def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
|
||||
x=''.join(random.choice(chars) for _ in range(size))
|
||||
@@ -75,6 +76,7 @@ def add(request):
|
||||
tk.properties["user_agent"]=ua
|
||||
tk.save()
|
||||
context["success"]=True
|
||||
recovery.genTokens(request, True) #recovery tokens
|
||||
# tk.properties["user_agent"]=ua
|
||||
# tk.save()
|
||||
# context["success"]=True
|
||||
|
||||
@@ -15,6 +15,7 @@ from .views import login
|
||||
from .Common import get_redirect_url
|
||||
import datetime
|
||||
from django.utils import timezone
|
||||
from . import recovery
|
||||
|
||||
def recheck(request):
|
||||
context = csrf(request)
|
||||
@@ -98,6 +99,7 @@ def bind(request):
|
||||
uk.properties = {"device":simplejson.loads(device.json),"cert":cert_hash}
|
||||
uk.key_type = "U2F"
|
||||
uk.save()
|
||||
recovery.genTokens(request, True) #recovery tokens
|
||||
return HttpResponse("OK")
|
||||
|
||||
def sign(username):
|
||||
|
||||
70
mfa/recovery.py
Normal file
70
mfa/recovery.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from django.shortcuts import render
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.http import HttpResponse
|
||||
from .Common import get_redirect_url
|
||||
from .models import *
|
||||
import simplejson
|
||||
from django.conf import settings
|
||||
import random
|
||||
import string
|
||||
import logging
|
||||
|
||||
def invalidate_token(key):
|
||||
key.enabled = 0
|
||||
key.save()
|
||||
|
||||
def token_left(request, username=None):
|
||||
if not username:
|
||||
username = request.user.username
|
||||
return len(User_Keys.objects.filter(username=username, key_type="RECOVERY", enabled=True))
|
||||
|
||||
def delTokens(request):
|
||||
#Only when all MFA have been deactivated, or to generate new !
|
||||
for key in User_Keys.objects.filter(username=request.user.username, key_type = "RECOVERY"):
|
||||
if key.username == request.user.username:
|
||||
key.delete()
|
||||
|
||||
def newTokens(username):
|
||||
for newkey in range(5):
|
||||
token = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(6))
|
||||
uk=User_Keys()
|
||||
uk.username=username
|
||||
uk.properties={"secret_key":token}
|
||||
uk.key_type="RECOVERY"
|
||||
uk.enabled=True
|
||||
uk.save()
|
||||
|
||||
def genTokens(request, softGen=False):
|
||||
if not softGen or (softGen and token_left(request) == 0):
|
||||
#Delete old ones
|
||||
delTokens(request)
|
||||
number = 5
|
||||
#Then generate new one
|
||||
newTokens(request.user.username)
|
||||
return HttpResponse("Success")
|
||||
|
||||
|
||||
def verify_login(username,token):
|
||||
for key in User_Keys.objects.filter(username=username, key_type = "RECOVERY"):
|
||||
logging.critical(key.properties["secret_key"])
|
||||
if key.properties["secret_key"] == token and key.enabled:
|
||||
invalidate_token(key)
|
||||
newRecoveryGen = False
|
||||
if token_left(None, username) == 0:
|
||||
newRecoveryGen = True
|
||||
newTokens(username)
|
||||
return [True, key.id, newRecoveryGen]
|
||||
return [False]
|
||||
|
||||
def getTokens(request):
|
||||
tokens = []
|
||||
enable = []
|
||||
for key in User_Keys.objects.filter(username=request.user.username, key_type = "RECOVERY"):
|
||||
tokens.append(key.properties["secret_key"])
|
||||
enable.append(1 if key.enabled else 0)
|
||||
return HttpResponse(simplejson.dumps({"keys":tokens, "enable":enable}))
|
||||
|
||||
@never_cache
|
||||
def start(request):
|
||||
"""Start Managing recovery tokens"""
|
||||
return render(request,"RECOVERY/Add.html",get_redirect_url())
|
||||
@@ -82,28 +82,40 @@
|
||||
<th>Status</th>
|
||||
<th>Delete</th>
|
||||
</tr>
|
||||
{% for key in keys %}
|
||||
<tr>
|
||||
{% if keys %}
|
||||
{% for key in keys %}
|
||||
<tr>
|
||||
|
||||
<td>{{ key.key_type }}</td>
|
||||
<td>{{ key.added_on }}</td>
|
||||
<td>{{ key.expires }}</td>
|
||||
<td>{% if key.device %}{{ key.device }}{% endif %}</td>
|
||||
<td>{{ key.last_used }}</td>
|
||||
{% if key.key_type in HIDE_DISABLE %}
|
||||
<td>{% if key.enabled %}On{% else %} Off{% endif %}</td>
|
||||
{% else %}
|
||||
<td><input type="checkbox" id="toggle_{{ key.id }}" {% if key.enabled %}checked{% endif %} data-onstyle="success" data-offstyle="danger" onchange="toggleKey({{ key.id }})" data-toggle="toggle" class="status_chk"></td>
|
||||
{% endif %}
|
||||
<td>{% if key.key_type in HIDE_DISABLE %}
|
||||
----
|
||||
{% else %}
|
||||
<a href="javascript:void(0)" onclick="deleteKey({{ key.id }},'{{ key.key_type }}')"> <span class="fa fa-trash fa-solid fa-trash-can bi bi-trash-fill"></span></a></td>
|
||||
<td>{{ key.key_type }}</td>
|
||||
<td>{{ key.added_on }}</td>
|
||||
<td>{{ key.expires }}</td>
|
||||
<td>{% if key.device %}{{ key.device }}{% endif %}</td>
|
||||
<td>{{ key.last_used }}</td>
|
||||
{% if key.key_type in HIDE_DISABLE %}
|
||||
<td>{% if key.enabled %}On{% else %} Off{% endif %}</td>
|
||||
{% else %}
|
||||
<td><input type="checkbox" id="toggle_{{ key.id }}" {% if key.enabled %}checked{% endif %} data-onstyle="success" data-offstyle="danger" onchange="toggleKey({{ key.id }})" data-toggle="toggle" class="status_chk"></td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% empty %}
|
||||
<td>{% if key.key_type in HIDE_DISABLE %}
|
||||
----
|
||||
{% else %}
|
||||
<a href="javascript:void(0)" onclick="deleteKey({{ key.id }},'{{ key.key_type }}')"> <span class="fa fa-trash fa-solid fa-trash-can bi bi-trash-fill"></span></a></td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
|
||||
<td>RECOVERY</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>On</td>
|
||||
<td><a href="{% url 'manage_recovery_codes' %}"> <span class="fa fa-wrench fa-solid fa-wrench bi bi-wrench-fill"></span></a></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="7" align="center">You didn't have any keys yet.</td> </tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
88
mfa/templates/RECOVERY/Add.html
Normal file
88
mfa/templates/RECOVERY/Add.html
Normal file
@@ -0,0 +1,88 @@
|
||||
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% block head %}
|
||||
<style>
|
||||
#two-factor-steps {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
padding: 15px;
|
||||
}
|
||||
.row{
|
||||
margin: 0px;
|
||||
}
|
||||
.crossed{
|
||||
text-decoration: line-through;
|
||||
}
|
||||
</style>
|
||||
<script src="{% static 'mfa/js/qrious.min.js' %}" type="text/javascript"></script>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function addToken() {
|
||||
listToken()
|
||||
});
|
||||
function listToken() {
|
||||
$.ajax({
|
||||
"url":"{% url 'get_recovery_tokens' %}",dataType:"JSON",
|
||||
success:function (data) {
|
||||
let htmlkey="";
|
||||
for (let i = 0; i < data.keys.length; i++) {
|
||||
if (data.enable[i] == 0)
|
||||
{
|
||||
htmlkey +="<pre class='crossed'>" +data.keys[i] + "</pre>" + "<br>"
|
||||
}
|
||||
else
|
||||
{
|
||||
htmlkey +="<pre>" +data.keys[i] + "</pre>" + "<br>"
|
||||
}
|
||||
};
|
||||
document.getElementById('tokens').innerHTML = htmlkey
|
||||
}
|
||||
})
|
||||
};
|
||||
function confirmRegenerateTokens() {
|
||||
$("#modal-title").html("Are you sure you want to regenerate your recovery tokens?")
|
||||
$("#modal-body").html("<button onclick='regenerateTokens()' class='btn btn-success'>Regenerate</button")
|
||||
$("#popUpModal").modal('show')
|
||||
}
|
||||
function regenerateTokens() {
|
||||
$.ajax({
|
||||
"url":"{% url 'regen_recovery_tokens' %}",
|
||||
success:function (data) {
|
||||
console.warn("ksfvkjs")
|
||||
listToken()
|
||||
$("#popUpModal").modal('hide')
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<br/>
|
||||
<br/>
|
||||
<div class="container d-flex justify-content-center">
|
||||
<div class="col-md-6 col-md-offset-3" id="two-factor-steps">
|
||||
<div class="row" align="center">
|
||||
<h4>Token List</h4>
|
||||
</div>
|
||||
|
||||
<div id="tokens">
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row" align="center">
|
||||
<button onclick="confirmRegenerateTokens()" class="btn btn-default btn-secondary" role="button">Regenarate tokens</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div align="center" class="alert alert-success" style="display: none" id="return">
|
||||
|
||||
<a href="{{redirect_html}}"> {{reg_success_msg}}</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% include "modal.html" %}
|
||||
{% endblock %}
|
||||
@@ -40,7 +40,7 @@
|
||||
<fieldset>
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12">
|
||||
<p>Enter the 6-digits on your authenticator.</p>
|
||||
<p>Enter the 6-digits on your authenticator. Or input a recovery code</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
13
mfa/totp.py
13
mfa/totp.py
@@ -11,7 +11,10 @@ import pyotp
|
||||
from .views import login
|
||||
import datetime
|
||||
from django.utils import timezone
|
||||
from . import recovery
|
||||
import random
|
||||
|
||||
|
||||
def verify_login(request,username,token):
|
||||
for key in User_Keys.objects.filter(username=username,key_type = "TOTP"):
|
||||
totp = pyotp.TOTP(key.properties["secret_key"])
|
||||
@@ -28,6 +31,7 @@ def recheck(request):
|
||||
if verify_login(request,request.user.username, token=request.POST["otp"]):
|
||||
import time
|
||||
request.session["mfa"]["rechecked_at"] = time.time()
|
||||
recovery.genTokens(request, True) #recovery tokens
|
||||
return HttpResponse(simplejson.dumps({"recheck": True}), content_type="application/json")
|
||||
else:
|
||||
return HttpResponse(simplejson.dumps({"recheck": False}), content_type="application/json")
|
||||
@@ -38,6 +42,7 @@ def auth(request):
|
||||
context=csrf(request)
|
||||
if request.method=="POST":
|
||||
res=verify_login(request,request.session["base_username"],token = request.POST["otp"])
|
||||
resBackup=recovery.verify_login(request.session["base_username"], token=request.POST["otp"])
|
||||
if res[0]:
|
||||
mfa = {"verified": True, "method": "TOTP","id":res[1]}
|
||||
if getattr(settings, "MFA_RECHECK", False):
|
||||
@@ -45,6 +50,14 @@ def auth(request):
|
||||
+ datetime.timedelta(
|
||||
seconds=random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))))
|
||||
request.session["mfa"] = mfa
|
||||
return login(request)
|
||||
elif resBackup[0]:
|
||||
mfa = {"verified": True, "method": "TOTP","id":resBackup[1], "newRecoveryGen":resBackup[2]}
|
||||
if getattr(settings, "MFA_RECHECK", False):
|
||||
mfa["next_check"] = datetime.datetime.timestamp((datetime.datetime.now()
|
||||
+ datetime.timedelta(
|
||||
seconds=random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))))
|
||||
request.session["mfa"] = mfa
|
||||
return login(request)
|
||||
context["invalid"]=True
|
||||
return render(request,"TOTP/Auth.html", context)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from . import views,totp,U2F,TrustedDevice,helpers,FIDO2,Email
|
||||
from . import views,totp,U2F,TrustedDevice,helpers,FIDO2,Email,recovery
|
||||
#app_name='mfa'
|
||||
|
||||
try:
|
||||
@@ -12,6 +12,10 @@ urlpatterns = [
|
||||
url(r'totp/auth', totp.auth, name="totp_auth"),
|
||||
url(r'totp/recheck', totp.recheck, name="totp_recheck"),
|
||||
|
||||
url(r'recovery/start', recovery.start, name="manage_recovery_codes"),
|
||||
url(r'recovery/getTokens', recovery.getTokens, name="get_recovery_tokens"),
|
||||
url(r'recovery/genTokens', recovery.genTokens, name="regen_recovery_tokens"),
|
||||
|
||||
url(r'email/start/', Email.start , name="start_email"),
|
||||
url(r'email/auth/', Email.auth , name="email_auth"),
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ def index(request):
|
||||
setattr(k,"device",parse(k.properties.get("user_agent","-----")))
|
||||
elif k.key_type == "FIDO2":
|
||||
setattr(k,"device",k.properties.get("type","----"))
|
||||
elif k.key_type == "RECOVERY":
|
||||
continue
|
||||
keys.append(k)
|
||||
context["keys"]=keys
|
||||
return render(request,"MFA.html",context)
|
||||
@@ -30,7 +32,7 @@ def verify(request,username):
|
||||
request.session["base_username"] = username
|
||||
#request.session["base_password"] = password
|
||||
keys=User_Keys.objects.filter(username=username,enabled=1)
|
||||
methods=list(set([k.key_type for k in keys]))
|
||||
methods=list(set([k.key_type for k in keys if k.key_type != "RECOVERY"]))
|
||||
|
||||
if "Trusted Device" in methods and not request.session.get("checked_trusted_device",False):
|
||||
if TrustedDevice.verify(request):
|
||||
|
||||
Reference in New Issue
Block a user