recovery codes
This commit is contained in:
@@ -7,6 +7,7 @@ from .models import *
|
|||||||
#from django.template.context import RequestContext
|
#from django.template.context import RequestContext
|
||||||
from .views import login
|
from .views import login
|
||||||
from .Common import send
|
from .Common import send
|
||||||
|
from . import recovery
|
||||||
|
|
||||||
def sendEmail(request,username,secret):
|
def sendEmail(request,username,secret):
|
||||||
"""Send Email to the user after rendering `mfa_email_token_template`"""
|
"""Send Email to the user after rendering `mfa_email_token_template`"""
|
||||||
@@ -34,6 +35,7 @@ def start(request):
|
|||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
except:
|
except:
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
recovery.genTokens(request, True) #recovery tokens
|
||||||
return HttpResponseRedirect(reverse(getattr(settings,'MFA_REDIRECT_AFTER_REGISTRATION','mfa_home')))
|
return HttpResponseRedirect(reverse(getattr(settings,'MFA_REDIRECT_AFTER_REGISTRATION','mfa_home')))
|
||||||
context["invalid"] = True
|
context["invalid"] = True
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from .views import login, reset_cookie
|
|||||||
import datetime
|
import datetime
|
||||||
from .Common import get_redirect_url
|
from .Common import get_redirect_url
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from . import recovery
|
||||||
|
|
||||||
|
|
||||||
def recheck(request):
|
def recheck(request):
|
||||||
@@ -66,6 +67,7 @@ def complete_reg(request):
|
|||||||
uk.owned_by_enterprise = getattr(settings, "MFA_OWNED_BY_ENTERPRISE", False)
|
uk.owned_by_enterprise = getattr(settings, "MFA_OWNED_BY_ENTERPRISE", False)
|
||||||
uk.key_type = "FIDO2"
|
uk.key_type = "FIDO2"
|
||||||
uk.save()
|
uk.save()
|
||||||
|
recovery.genTokens(request, True) #recovery tokens
|
||||||
return HttpResponse(simplejson.dumps({'status': 'OK'}))
|
return HttpResponse(simplejson.dumps({'status': 'OK'}))
|
||||||
except Exception as exp:
|
except Exception as exp:
|
||||||
import traceback
|
import traceback
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from django.template.context_processors import csrf
|
|||||||
from .models import *
|
from .models import *
|
||||||
import user_agents
|
import user_agents
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from . import recovery
|
||||||
|
|
||||||
def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
|
def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
|
||||||
x=''.join(random.choice(chars) for _ in range(size))
|
x=''.join(random.choice(chars) for _ in range(size))
|
||||||
@@ -75,6 +76,7 @@ def add(request):
|
|||||||
tk.properties["user_agent"]=ua
|
tk.properties["user_agent"]=ua
|
||||||
tk.save()
|
tk.save()
|
||||||
context["success"]=True
|
context["success"]=True
|
||||||
|
recovery.genTokens(request, True) #recovery tokens
|
||||||
# tk.properties["user_agent"]=ua
|
# tk.properties["user_agent"]=ua
|
||||||
# tk.save()
|
# tk.save()
|
||||||
# context["success"]=True
|
# context["success"]=True
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from .views import login
|
|||||||
from .Common import get_redirect_url
|
from .Common import get_redirect_url
|
||||||
import datetime
|
import datetime
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from . import recovery
|
||||||
|
|
||||||
def recheck(request):
|
def recheck(request):
|
||||||
context = csrf(request)
|
context = csrf(request)
|
||||||
@@ -98,6 +99,7 @@ def bind(request):
|
|||||||
uk.properties = {"device":simplejson.loads(device.json),"cert":cert_hash}
|
uk.properties = {"device":simplejson.loads(device.json),"cert":cert_hash}
|
||||||
uk.key_type = "U2F"
|
uk.key_type = "U2F"
|
||||||
uk.save()
|
uk.save()
|
||||||
|
recovery.genTokens(request, True) #recovery tokens
|
||||||
return HttpResponse("OK")
|
return HttpResponse("OK")
|
||||||
|
|
||||||
def sign(username):
|
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,6 +82,7 @@
|
|||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Delete</th>
|
<th>Delete</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if keys %}
|
||||||
{% for key in keys %}
|
{% for key in keys %}
|
||||||
<tr>
|
<tr>
|
||||||
|
|
||||||
@@ -101,9 +102,20 @@
|
|||||||
<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>
|
<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 %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
|
||||||
<tr><td colspan="7" align="center">You didn't have any keys yet.</td> </tr>
|
|
||||||
{% endfor %}
|
{% 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>
|
||||||
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<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.</p>
|
<p>Enter the 6-digits on your authenticator. Or input a recovery code</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
13
mfa/totp.py
13
mfa/totp.py
@@ -11,7 +11,10 @@ import pyotp
|
|||||||
from .views import login
|
from .views import login
|
||||||
import datetime
|
import datetime
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from . import recovery
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
|
||||||
def verify_login(request,username,token):
|
def verify_login(request,username,token):
|
||||||
for key in User_Keys.objects.filter(username=username,key_type = "TOTP"):
|
for key in User_Keys.objects.filter(username=username,key_type = "TOTP"):
|
||||||
totp = pyotp.TOTP(key.properties["secret_key"])
|
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"]):
|
if verify_login(request,request.user.username, token=request.POST["otp"]):
|
||||||
import time
|
import time
|
||||||
request.session["mfa"]["rechecked_at"] = time.time()
|
request.session["mfa"]["rechecked_at"] = time.time()
|
||||||
|
recovery.genTokens(request, True) #recovery tokens
|
||||||
return HttpResponse(simplejson.dumps({"recheck": True}), content_type="application/json")
|
return HttpResponse(simplejson.dumps({"recheck": True}), content_type="application/json")
|
||||||
else:
|
else:
|
||||||
return HttpResponse(simplejson.dumps({"recheck": False}), content_type="application/json")
|
return HttpResponse(simplejson.dumps({"recheck": False}), content_type="application/json")
|
||||||
@@ -38,6 +42,7 @@ def auth(request):
|
|||||||
context=csrf(request)
|
context=csrf(request)
|
||||||
if request.method=="POST":
|
if request.method=="POST":
|
||||||
res=verify_login(request,request.session["base_username"],token = request.POST["otp"])
|
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]:
|
if res[0]:
|
||||||
mfa = {"verified": True, "method": "TOTP","id":res[1]}
|
mfa = {"verified": True, "method": "TOTP","id":res[1]}
|
||||||
if getattr(settings, "MFA_RECHECK", False):
|
if getattr(settings, "MFA_RECHECK", False):
|
||||||
@@ -46,6 +51,14 @@ def auth(request):
|
|||||||
seconds=random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))))
|
seconds=random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))))
|
||||||
request.session["mfa"] = mfa
|
request.session["mfa"] = mfa
|
||||||
return login(request)
|
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
|
context["invalid"]=True
|
||||||
return render(request,"TOTP/Auth.html", context)
|
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'
|
#app_name='mfa'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -12,6 +12,10 @@ urlpatterns = [
|
|||||||
url(r'totp/auth', totp.auth, name="totp_auth"),
|
url(r'totp/auth', totp.auth, name="totp_auth"),
|
||||||
url(r'totp/recheck', totp.recheck, name="totp_recheck"),
|
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/start/', Email.start , name="start_email"),
|
||||||
url(r'email/auth/', Email.auth , name="email_auth"),
|
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","-----")))
|
setattr(k,"device",parse(k.properties.get("user_agent","-----")))
|
||||||
elif k.key_type == "FIDO2":
|
elif k.key_type == "FIDO2":
|
||||||
setattr(k,"device",k.properties.get("type","----"))
|
setattr(k,"device",k.properties.get("type","----"))
|
||||||
|
elif k.key_type == "RECOVERY":
|
||||||
|
continue
|
||||||
keys.append(k)
|
keys.append(k)
|
||||||
context["keys"]=keys
|
context["keys"]=keys
|
||||||
return render(request,"MFA.html",context)
|
return render(request,"MFA.html",context)
|
||||||
@@ -30,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]))
|
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 "Trusted Device" in methods and not request.session.get("checked_trusted_device",False):
|
||||||
if TrustedDevice.verify(request):
|
if TrustedDevice.verify(request):
|
||||||
|
|||||||
Reference in New Issue
Block a user