recovery codes

This commit is contained in:
Spitap
2022-08-20 11:58:25 +02:00
parent 0936ea2533
commit 3ac893ad50
11 changed files with 219 additions and 22 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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())

View File

@@ -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>

View 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 %}

View File

@@ -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>

View File

@@ -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)

View File

@@ -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"),

View File

@@ -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):