recovery code hashing
This commit is contained in:
@@ -67,6 +67,7 @@ Depends on
|
||||
3. Add the following settings to your file
|
||||
|
||||
```python
|
||||
from django.conf.global_settings import PASSWORD_HASHERS as DEFAULT_PASSWORD_HASHERS #Preferably at the same place where you import your other modules
|
||||
MFA_UNALLOWED_METHODS=() # Methods that shouldn't be allowed for the user
|
||||
MFA_LOGIN_CALLBACK="" # A function that should be called by username to login the user in session
|
||||
MFA_RECHECK=True # Allow random rechecking of the user
|
||||
@@ -77,6 +78,9 @@ Depends on
|
||||
MFA_QUICKLOGIN=True # Allow quick login for returning users by provide only their 2FA
|
||||
MFA_HIDE_DISABLE=('FIDO2',) # Can the user disable his key (Added in 1.2.0).
|
||||
MFA_OWNED_BY_ENTERPRISE = FALSE # Who owns security keys
|
||||
PASSWORD_HASHERS = DEFAULT_PASSWORD_HASHERS #Comment if PASSWORD_HASHER already set
|
||||
PASSWORD_HASHERS += ['mfa.recovery.Hash']
|
||||
RECOVERY_ITERATION = 350000 #Number of iteration for recovery code, higher is more secure, but uses more resources for generation and check...
|
||||
|
||||
TOKEN_ISSUER_NAME="PROJECT_NAME" #TOTP Issuer name
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/2.0/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
from django.conf.global_settings import PASSWORD_HASHERS as DEFAULT_PASSWORD_HASHERS
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
@@ -142,7 +143,9 @@ MFA_QUICKLOGIN=True # Allow quick login for returning users by provide on
|
||||
MFA_HIDE_DISABLE=('',) # Can the user disable his key (Added in 1.2.0).
|
||||
MFA_REDIRECT_AFTER_REGISTRATION="registered"
|
||||
MFA_SUCCESS_REGISTRATION_MSG="Go to Home"
|
||||
|
||||
PASSWORD_HASHERS = DEFAULT_PASSWORD_HASHERS #Comment if PASSWORD_HASHER already set
|
||||
PASSWORD_HASHERS += ['mfa.recovery.Hash']
|
||||
RECOVERY_ITERATION = 1 #Number of iteration for recovery code, higher is more secure, but uses more resources for generation and check...
|
||||
TOKEN_ISSUER_NAME="PROJECT_NAME" #TOTP Issuer name
|
||||
|
||||
U2F_APPID="https://localhost" #URL For U2F
|
||||
|
||||
@@ -7,7 +7,6 @@ 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`"""
|
||||
@@ -35,7 +34,6 @@ 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,7 +16,6 @@ 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):
|
||||
@@ -67,7 +66,6 @@ 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,7 +7,6 @@ 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))
|
||||
@@ -76,7 +75,6 @@ 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,7 +15,6 @@ 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)
|
||||
@@ -99,7 +98,6 @@ 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):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.shortcuts import render
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.template.context_processors import csrf
|
||||
from django.contrib.auth.hashers import make_password, PBKDF2PasswordHasher
|
||||
from django.http import HttpResponse
|
||||
from .Common import get_redirect_url
|
||||
from .models import *
|
||||
@@ -9,6 +10,9 @@ import random
|
||||
import string
|
||||
import datetime
|
||||
|
||||
class Hash(PBKDF2PasswordHasher):
|
||||
algorithm = 'pbkdf2_sha256_custom'
|
||||
iterations = settings.RECOVERY_ITERATION
|
||||
|
||||
def token_left(request, username=None):
|
||||
if not username and request:
|
||||
@@ -32,46 +36,50 @@ def delTokens(request):
|
||||
def randomGen(n):
|
||||
return ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(n))
|
||||
|
||||
|
||||
@never_cache
|
||||
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
|
||||
newKeys = []
|
||||
salt = randomGen(15)
|
||||
hashedKeys = []
|
||||
clearKeys = []
|
||||
for i in range(5):
|
||||
token = randomGen(5) + "-" + randomGen(5)
|
||||
newKeys.append(token)
|
||||
hashedToken = make_password(token, salt, 'pbkdf2_sha256_custom')
|
||||
hashedKeys.append(hashedToken)
|
||||
clearKeys.append(token)
|
||||
uk=User_Keys()
|
||||
uk.username = request.user.username
|
||||
uk.properties={"secret_keys":newKeys, "enabled":[True for j in range(5)]}
|
||||
uk.properties={"secret_keys":hashedKeys, "enabled":[True for j in range(5)], "salt":salt}
|
||||
uk.key_type="RECOVERY"
|
||||
uk.enabled = False
|
||||
uk.save()
|
||||
return HttpResponse("Success")
|
||||
return HttpResponse(simplejson.dumps({"keys":clearKeys}))
|
||||
|
||||
|
||||
def verify_login(request, username,token):
|
||||
for key in User_Keys.objects.filter(username=username, key_type = "RECOVERY"):
|
||||
secret_keys = key.properties["secret_keys"]
|
||||
salt = key.properties["salt"]
|
||||
hashedToken = make_password(token, salt, "pbkdf2_sha256_custom")
|
||||
for i in range(len(secret_keys)):
|
||||
if token == secret_keys[i] and key.properties["enabled"][i]:
|
||||
if hashedToken == secret_keys[i] and key.properties["enabled"][i]:
|
||||
key.properties["enabled"][i] = False
|
||||
key.save()
|
||||
lastToken = False
|
||||
if token_left(None, username) == 0:
|
||||
lastToken = True
|
||||
return [True, key.id, lastToken]
|
||||
return [True, key.id, token_left(None, username) == 0]
|
||||
return [False]
|
||||
|
||||
def getTokens(request):
|
||||
tokens = []
|
||||
enable = []
|
||||
def getTokenLeft(request):
|
||||
enable = 0
|
||||
for key in User_Keys.objects.filter(username=request.user.username, key_type = "RECOVERY"):
|
||||
secret_keys = key.properties["secret_keys"]
|
||||
for i in range(len(secret_keys)):
|
||||
tokens.append(secret_keys[i])
|
||||
enable.append(key.properties["enabled"][i])
|
||||
return HttpResponse(simplejson.dumps({"keys":tokens, "enable":enable}))
|
||||
tokenStatus = key.properties["enabled"]
|
||||
for i in range(len(tokenStatus)):
|
||||
enable += 1
|
||||
return HttpResponse(simplejson.dumps({"left":enable}))
|
||||
|
||||
@never_cache
|
||||
def auth(request):
|
||||
@@ -79,7 +87,7 @@ def auth(request):
|
||||
context=csrf(request)
|
||||
if request.method=="POST":
|
||||
tokenLength = len(request.POST["otp"])
|
||||
if tokenLength == 10 and "RECOVERY" not in settings.MFA_UNALLOWED_METHODS:
|
||||
if tokenLength == 11 and "RECOVERY" not in settings.MFA_UNALLOWED_METHODS:
|
||||
#Backup code check
|
||||
resBackup=verify_login(request, request.session["base_username"], token=request.POST["otp"])
|
||||
if resBackup[0]:
|
||||
|
||||
@@ -25,39 +25,36 @@
|
||||
</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>"
|
||||
}
|
||||
else
|
||||
{
|
||||
htmlkey +="<pre>" +data.keys[i] + "</pre>"
|
||||
}
|
||||
};
|
||||
document.getElementById('tokens').innerHTML = htmlkey
|
||||
$(document).ready(function checkTokenLeft() {
|
||||
$.ajax({"url":"{% url 'get_recovery_token_left' %}", dataType:"JSON",
|
||||
success:function (data) {
|
||||
tokenLeft = data.left
|
||||
let html
|
||||
if (tokenLeft == 0) {
|
||||
html = "<h6>You don't have any backup code left, please generate new ones !</h6>"
|
||||
|
||||
}
|
||||
})
|
||||
};
|
||||
else {
|
||||
html = "<p>You still have "+tokenLeft+" backup code left."
|
||||
}
|
||||
document.getElementById('tokens').innerHTML = html
|
||||
}})
|
||||
});
|
||||
function confirmRegenerateTokens() {
|
||||
htmlModal = "<h6>Caution! you can only note these token now, else you will need to generate new ones.</h6><button onclick='regenerateTokens()' class='btn btn-success'>Regenerate</button>"
|
||||
$("#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>")
|
||||
$("#modal-body").html(htmlModal)
|
||||
$("#popUpModal").modal('show')
|
||||
}
|
||||
function regenerateTokens() {
|
||||
$.ajax({
|
||||
"url":"{% url 'regen_recovery_tokens' %}",
|
||||
"url":"{% url 'regen_recovery_tokens' %}", dataType:"JSON",
|
||||
success:function (data) {
|
||||
console.warn("ksfvkjs")
|
||||
listToken()
|
||||
let htmlkey="";
|
||||
for (let i = 0; i < data.keys.length; i++) {
|
||||
htmlkey +="<pre>" +data.keys[i] + "</pre>"
|
||||
}
|
||||
document.getElementById('tokens').innerHTML = htmlkey
|
||||
$("#popUpModal").modal('hide')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -22,10 +22,11 @@
|
||||
})
|
||||
}
|
||||
function tryToAuth() {
|
||||
if ($("#otp").val().length == 6) {
|
||||
const otp_length = $("#otp").val().length
|
||||
if (otp_length == 6) {
|
||||
document.getElementById("formLogin").submit();
|
||||
}
|
||||
else if ($("#otp").val().length == 10) {
|
||||
else if (otp_length == 11) {
|
||||
const form = document.getElementById("formLogin");
|
||||
form.setAttribute("ACTION", "{% url 'recovery_auth' %}")
|
||||
form.submit();
|
||||
@@ -70,7 +71,7 @@
|
||||
<span class="input-group-addon input-group-text">
|
||||
<i class="glyphicon glyphicon-lock bi bi-lock"></i>
|
||||
</span>
|
||||
<input class="form-control" size="6" MaxLength="10" value="" placeholder="e.g 55552" name="otp" type="text" id="otp" autofocus>
|
||||
<input class="form-control" size="6" MaxLength="11" value="" placeholder="e.g 55552" name="otp" type="text" id="otp" autofocus>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,6 @@ import pyotp
|
||||
from .views import login
|
||||
import datetime
|
||||
from django.utils import timezone
|
||||
from . import recovery
|
||||
import random
|
||||
|
||||
|
||||
@@ -31,7 +30,6 @@ 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")
|
||||
|
||||
@@ -13,7 +13,7 @@ urlpatterns = [
|
||||
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/getTokenLeft', recovery.getTokenLeft, name="get_recovery_token_left"),
|
||||
url(r'recovery/genTokens', recovery.genTokens, name="regen_recovery_tokens"),
|
||||
url(r'recovery/auth', recovery.auth, name="recovery_auth"),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user