diff --git a/mfa/Email.py b/mfa/Email.py index 010d6d5..7bbb26d 100644 --- a/mfa/Email.py +++ b/mfa/Email.py @@ -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: diff --git a/mfa/FIDO2.py b/mfa/FIDO2.py index dcdf9f2..e41c27c 100644 --- a/mfa/FIDO2.py +++ b/mfa/FIDO2.py @@ -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 diff --git a/mfa/TrustedDevice.py b/mfa/TrustedDevice.py index 94b2136..aac4f6c 100644 --- a/mfa/TrustedDevice.py +++ b/mfa/TrustedDevice.py @@ -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 diff --git a/mfa/U2F.py b/mfa/U2F.py index 0eb04f0..18387d2 100644 --- a/mfa/U2F.py +++ b/mfa/U2F.py @@ -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): diff --git a/mfa/recovery.py b/mfa/recovery.py new file mode 100644 index 0000000..6ddd09f --- /dev/null +++ b/mfa/recovery.py @@ -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()) \ No newline at end of file diff --git a/mfa/templates/MFA.html b/mfa/templates/MFA.html index 4940ad8..7a4d396 100644 --- a/mfa/templates/MFA.html +++ b/mfa/templates/MFA.html @@ -82,28 +82,40 @@ Status Delete - {% for key in keys %} - + {% if keys %} + {% for key in keys %} + - {{ key.key_type }} - {{ key.added_on }} - {{ key.expires }} - {% if key.device %}{{ key.device }}{% endif %} - {{ key.last_used }} - {% if key.key_type in HIDE_DISABLE %} - {% if key.enabled %}On{% else %} Off{% endif %} - {% else %} - - {% endif %} - {% if key.key_type in HIDE_DISABLE %} - ---- - {% else %} - + {{ key.key_type }} + {{ key.added_on }} + {{ key.expires }} + {% if key.device %}{{ key.device }}{% endif %} + {{ key.last_used }} + {% if key.key_type in HIDE_DISABLE %} + {% if key.enabled %}On{% else %} Off{% endif %} + {% else %} + {% endif %} - - {% empty %} + {% if key.key_type in HIDE_DISABLE %} + ---- + {% else %} + + {% endif %} + + {% endfor %} + + + RECOVERY + + + + + On + + + {% else %} You didn't have any keys yet. - {% endfor %} + {% endif %} diff --git a/mfa/templates/RECOVERY/Add.html b/mfa/templates/RECOVERY/Add.html new file mode 100644 index 0000000..09782a9 --- /dev/null +++ b/mfa/templates/RECOVERY/Add.html @@ -0,0 +1,88 @@ + +{% extends "base.html" %} +{% load static %} +{% block head %} + + + +{% endblock %} +{% block content %} +
+
+
+
+
+

Token List

+
+ +
+ +
+ +
+ +
+ +
+ + +
+ +
+
+{% include "modal.html" %} +{% endblock %} diff --git a/mfa/templates/TOTP/recheck.html b/mfa/templates/TOTP/recheck.html index e172b0e..84900da 100644 --- a/mfa/templates/TOTP/recheck.html +++ b/mfa/templates/TOTP/recheck.html @@ -40,7 +40,7 @@
-

Enter the 6-digits on your authenticator.

+

Enter the 6-digits on your authenticator. Or input a recovery code

diff --git a/mfa/totp.py b/mfa/totp.py index 76aa810..0f5d665 100644 --- a/mfa/totp.py +++ b/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) diff --git a/mfa/urls.py b/mfa/urls.py index 90e9432..e894f77 100644 --- a/mfa/urls.py +++ b/mfa/urls.py @@ -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"), diff --git a/mfa/views.py b/mfa/views.py index 9039363..9d74e12 100644 --- a/mfa/views.py +++ b/mfa/views.py @@ -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):