From 9c126f06b577e52e8804ae0e4724e9f4d706cc7f Mon Sep 17 00:00:00 2001 From: Mohamed ElKalioby Date: Tue, 22 Jun 2021 17:12:12 +0300 Subject: [PATCH 01/22] Applied Black --- mfa/Common.py | 17 +- mfa/Email.py | 78 ++++++--- mfa/FIDO2.py | 156 +++++++++++------- mfa/TrustedDevice.py | 141 ++++++++++------ mfa/U2F.py | 120 +++++++++----- mfa/__init__.py | 2 +- mfa/apps.py | 6 +- mfa/helpers.py | 51 ++++-- mfa/middleware.py | 18 +- mfa/migrations/0001_initial.py | 21 ++- mfa/migrations/0002_user_keys_key_type.py | 8 +- mfa/migrations/0003_auto_20181114_2159.py | 6 +- mfa/migrations/0004_user_keys_enabled.py | 6 +- mfa/migrations/0005_auto_20181115_2014.py | 13 +- mfa/migrations/0006_trusted_devices.py | 28 ++-- mfa/migrations/0007_auto_20181230_1549.py | 8 +- mfa/migrations/0008_user_keys_last_used.py | 6 +- .../0009_user_keys_owned_by_enterprise.py | 14 +- mfa/migrations/0010_auto_20201110_0557.py | 8 +- mfa/migrations/0011_auto_20210530_0622.py | 6 +- mfa/models.py | 44 +++-- mfa/totp.py | 97 +++++++---- mfa/urls.py | 88 +++++----- mfa/views.py | 77 +++++---- 24 files changed, 630 insertions(+), 389 deletions(-) diff --git a/mfa/Common.py b/mfa/Common.py index 1504340..7d45373 100644 --- a/mfa/Common.py +++ b/mfa/Common.py @@ -1,19 +1,26 @@ from django.conf import settings from django.core.mail import EmailMessage + try: from django.urls import reverse except: from django.core.urlresolver import reverse -def send(to,subject,body): + +def send(to, subject, body): from_email_address = settings.EMAIL_HOST_USER - if '@' not in from_email_address: + if "@" not in from_email_address: from_email_address = settings.DEFAULT_FROM_EMAIL From = "%s <%s>" % (settings.EMAIL_FROM, from_email_address) - email = EmailMessage(subject,body,From,to) + email = EmailMessage(subject, body, From, to) email.content_subtype = "html" return email.send(False) + def get_redirect_url(): - return {"redirect_html": reverse(getattr(settings, 'MFA_REDIRECT_AFTER_REGISTRATION', 'mfa_home')), - "reg_success_msg":getattr(settings,"MFA_SUCCESS_REGISTRATION_MSG")} + return { + "redirect_html": reverse( + getattr(settings, "MFA_REDIRECT_AFTER_REGISTRATION", "mfa_home") + ), + "reg_success_msg": getattr(settings, "MFA_SUCCESS_REGISTRATION_MSG"), + } diff --git a/mfa/Email.py b/mfa/Email.py index 010d6d5..d5535cd 100644 --- a/mfa/Email.py +++ b/mfa/Email.py @@ -1,66 +1,94 @@ from django.shortcuts import render from django.views.decorators.cache import never_cache from django.template.context_processors import csrf -import datetime,random +import datetime, random from random import randint from .models import * -#from django.template.context import RequestContext + +# from django.template.context import RequestContext from .views import login from .Common import send -def sendEmail(request,username,secret): + +def sendEmail(request, username, secret): """Send Email to the user after rendering `mfa_email_token_template`""" from django.contrib.auth import get_user_model + User = get_user_model() - key = getattr(User, 'USERNAME_FIELD', 'username') + key = getattr(User, "USERNAME_FIELD", "username") kwargs = {key: username} user = User.objects.get(**kwargs) - res=render(request,"mfa_email_token_template.html",{"request":request,"user":user,'otp':secret}) - return send([user.email],"OTP", res.content.decode()) + res = render( + request, + "mfa_email_token_template.html", + {"request": request, "user": user, "otp": secret}, + ) + return send([user.email], "OTP", res.content.decode()) + @never_cache def start(request): """Start adding email as a 2nd factor""" context = csrf(request) if request.method == "POST": - if request.session["email_secret"] == request.POST["otp"]: #if successful - uk=User_Keys() - uk.username=request.user.username - uk.key_type="Email" - uk.enabled=1 + if request.session["email_secret"] == request.POST["otp"]: # if successful + uk = User_Keys() + uk.username = request.user.username + uk.key_type = "Email" + uk.enabled = 1 uk.save() from django.http import HttpResponseRedirect + try: from django.core.urlresolvers import reverse except: from django.urls import reverse - 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 else: - request.session["email_secret"] = str(randint(0,100000)) #generate a random integer + request.session["email_secret"] = str( + randint(0, 100000) + ) # generate a random integer if sendEmail(request, request.user.username, request.session["email_secret"]): context["sent"] = True - return render(request,"Email/Add.html", context) + return render(request, "Email/Add.html", context) + + @never_cache def auth(request): """Authenticating the user by email.""" - context=csrf(request) - if request.method=="POST": - if request.session["email_secret"]==request.POST["otp"].strip(): - uk = User_Keys.objects.get(username=request.session["base_username"], key_type="Email") - mfa = {"verified": True, "method": "Email","id":uk.id} + context = csrf(request) + if request.method == "POST": + if request.session["email_secret"] == request.POST["otp"].strip(): + uk = User_Keys.objects.get( + username=request.session["base_username"], key_type="Email" + ) + mfa = {"verified": True, "method": "Email", "id": uk.id} 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))) + 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 from django.utils import timezone - uk.last_used=timezone.now() + + uk.last_used = timezone.now() uk.save() return login(request) - context["invalid"]=True + context["invalid"] = True else: request.session["email_secret"] = str(randint(0, 100000)) - if sendEmail(request, request.session["base_username"], request.session["email_secret"]): + if sendEmail( + request, request.session["base_username"], request.session["email_secret"] + ): context["sent"] = True - return render(request,"Email/Auth.html", context) + return render(request, "Email/Auth.html", context) diff --git a/mfa/FIDO2.py b/mfa/FIDO2.py index 98a6e5e..2a51b4b 100644 --- a/mfa/FIDO2.py +++ b/mfa/FIDO2.py @@ -1,9 +1,12 @@ +import traceback + from fido2.client import ClientData from fido2.server import Fido2Server, PublicKeyCredentialRpEntity from fido2.ctap2 import AttestationObject, AuthenticatorData from django.template.context_processors import csrf from django.views.decorators.csrf import csrf_exempt from django.shortcuts import render + # from django.template.context import RequestContext import simplejson from fido2 import cbor @@ -35,14 +38,19 @@ def getServer(): def begin_registeration(request): """Starts registering a new FIDO Device, called from API""" server = getServer() - registration_data, state = server.register_begin({ - u'id': request.user.username.encode("utf8"), - u'name': (request.user.first_name + " " + request.user.last_name), - u'displayName': request.user.username, - }, getUserCredentials(request.user.username)) - request.session['fido_state'] = state + registration_data, state = server.register_begin( + { + u"id": request.user.username.encode("utf8"), + u"name": (request.user.first_name + " " + request.user.last_name), + u"displayName": request.user.username, + }, + getUserCredentials(request.user.username), + ) + request.session["fido_state"] = state - return HttpResponse(cbor.encode(registration_data), content_type = 'application/octet-stream') + return HttpResponse( + cbor.encode(registration_data), content_type="application/octet-stream" + ) @csrf_exempt @@ -51,29 +59,30 @@ def complete_reg(request): try: data = cbor.decode(request.body) - client_data = ClientData(data['clientDataJSON']) - att_obj = AttestationObject((data['attestationObject'])) + client_data = ClientData(data["clientDataJSON"]) + att_obj = AttestationObject((data["attestationObject"])) server = getServer() auth_data = server.register_complete( - request.session['fido_state'], - client_data, - att_obj + request.session["fido_state"], client_data, att_obj ) encoded = websafe_encode(auth_data.credential_data) uk = User_Keys() uk.username = request.user.username - uk.properties = {"device": encoded, "type": att_obj.fmt, } + uk.properties = { + "device": encoded, + "type": att_obj.fmt, + } uk.owned_by_enterprise = getattr(settings, "MFA_OWNED_BY_ENTERPRISE", False) uk.key_type = "FIDO2" uk.save() - return HttpResponse(simplejson.dumps({'status': 'OK'})) + return HttpResponse(simplejson.dumps({"status": "OK"})) except Exception as exp: - try: - from raven.contrib.django.raven_compat.models import client - client.captureException() - except: - pass - return HttpResponse(simplejson.dumps({'status': 'ERR', "message": "Error on server, please try again later"})) + print(traceback.format_exc()) + return HttpResponse( + simplejson.dumps( + {"status": "ERR", "message": "Error on server, please try again later"} + ) + ) def start(request): @@ -85,8 +94,10 @@ def start(request): def getUserCredentials(username): credentials = [] - for uk in User_Keys.objects.filter(username = username, key_type = "FIDO2"): - credentials.append(AttestedCredentialData(websafe_decode(uk.properties["device"]))) + for uk in User_Keys.objects.filter(username=username, key_type="FIDO2"): + credentials.append( + AttestedCredentialData(websafe_decode(uk.properties["device"])) + ) return credentials @@ -97,10 +108,12 @@ def auth(request): def authenticate_begin(request): server = getServer() - credentials = getUserCredentials(request.session.get("base_username", request.user.username)) + credentials = getUserCredentials( + request.session.get("base_username", request.user.username) + ) auth_data, state = server.authenticate_begin(credentials) - request.session['fido_state'] = state - return HttpResponse(cbor.encode(auth_data), content_type = "application/octet-stream") + request.session["fido_state"] = state + return HttpResponse(cbor.encode(auth_data), content_type="application/octet-stream") @csrf_exempt @@ -111,49 +124,71 @@ def authenticate_complete(request): server = getServer() credentials = getUserCredentials(username) data = cbor.decode(request.body) - credential_id = data['credentialId'] - client_data = ClientData(data['clientDataJSON']) - auth_data = AuthenticatorData(data['authenticatorData']) - signature = data['signature'] + credential_id = data["credentialId"] + client_data = ClientData(data["clientDataJSON"]) + auth_data = AuthenticatorData(data["authenticatorData"]) + signature = data["signature"] try: cred = server.authenticate_complete( - request.session.pop('fido_state'), + request.session.pop("fido_state"), credentials, credential_id, client_data, auth_data, - signature + signature, ) except ValueError: - return HttpResponse(simplejson.dumps({'status': "ERR", - "message": "Wrong challenge received, make sure that this is your security and try again."}), - content_type = "application/json") + return HttpResponse( + simplejson.dumps( + { + "status": "ERR", + "message": "Wrong challenge received, make sure that this is your security and try again.", + } + ), + content_type="application/json", + ) except Exception as excep: - try: - from raven.contrib.django.raven_compat.models import client - client.captureException() - except: - pass - return HttpResponse(simplejson.dumps({'status': "ERR", - "message": excep.message}), - content_type = "application/json") + print(traceback.format_exc()) + return HttpResponse( + simplejson.dumps({"status": "ERR", "message": excep.message}), + content_type="application/json", + ) if request.session.get("mfa_recheck", False): import time + request.session["mfa"]["rechecked_at"] = time.time() - return HttpResponse(simplejson.dumps({'status': "OK"}), - content_type = "application/json") + return HttpResponse( + simplejson.dumps({"status": "OK"}), content_type="application/json" + ) else: import random - keys = User_Keys.objects.filter(username = username, key_type = "FIDO2", enabled = 1) + + keys = User_Keys.objects.filter( + username=username, key_type="FIDO2", enabled=1 + ) for k in keys: - if AttestedCredentialData(websafe_decode(k.properties["device"])).credential_id == cred.credential_id: + if ( + AttestedCredentialData( + websafe_decode(k.properties["device"]) + ).credential_id + == cred.credential_id + ): k.last_used = timezone.now() k.save() - mfa = {"verified": True, "method": "FIDO2", 'id': k.id} + mfa = {"verified": True, "method": "FIDO2", "id": k.id} 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)))) + 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 try: authenticated = request.user.is_authenticated @@ -161,11 +196,20 @@ def authenticate_complete(request): authenticated = request.user.is_authenticated() if not authenticated: res = login(request) - if not "location" in res: return reset_cookie(request) - return HttpResponse(simplejson.dumps({'status': "OK", "redirect": res["location"]}), - content_type = "application/json") - return HttpResponse(simplejson.dumps({'status': "OK"}), - content_type = "application/json") + if not "location" in res: + return reset_cookie(request) + return HttpResponse( + simplejson.dumps( + {"status": "OK", "redirect": res["location"]} + ), + content_type="application/json", + ) + return HttpResponse( + simplejson.dumps({"status": "OK"}), + content_type="application/json", + ) except Exception as exp: - return HttpResponse(simplejson.dumps({'status': "ERR", "message": exp.message}), - content_type = "application/json") + return HttpResponse( + simplejson.dumps({"status": "ERR", "message": str(exp)}), + content_type="application/json", + ) diff --git a/mfa/TrustedDevice.py b/mfa/TrustedDevice.py index 94b2136..2b6dc66 100644 --- a/mfa/TrustedDevice.py +++ b/mfa/TrustedDevice.py @@ -8,127 +8,158 @@ from .models import * import user_agents from django.utils import timezone + def id_generator(size=6, chars=string.ascii_uppercase + string.digits): - x=''.join(random.choice(chars) for _ in range(size)) - if not User_Keys.objects.filter(properties__shas="$.key="+x).exists(): return x - else: return id_generator(size,chars) + x = "".join(random.choice(chars) for _ in range(size)) + if not User_Keys.objects.filter(properties__shas="$.key=" + x).exists(): + return x + else: + return id_generator(size, chars) + def getUserAgent(request): - id=id=request.session.get("td_id",None) + id = id = request.session.get("td_id", None) if id: - tk=User_Keys.objects.get(id=id) - if tk.properties.get("user_agent","")!="": + tk = User_Keys.objects.get(id=id) + if tk.properties.get("user_agent", "") != "": ua = user_agents.parse(tk.properties["user_agent"]) - res = render(None, "TrustedDevices/user-agent.html", context={"ua":ua}) + res = render(None, "TrustedDevices/user-agent.html", context={"ua": ua}) return HttpResponse(res) return HttpResponse("") + def trust_device(request): tk = User_Keys.objects.get(id=request.session["td_id"]) - tk.properties["status"]="trusted" + tk.properties["status"] = "trusted" tk.save() del request.session["td_id"] return HttpResponse("OK") + def checkTrusted(request): res = "" - id=request.session.get("td_id","") - if id!="": + id = request.session.get("td_id", "") + if id != "": try: tk = User_Keys.objects.get(id=id) - if tk.properties["status"] == "trusted": res = "OK" + if tk.properties["status"] == "trusted": + res = "OK" except: pass return HttpResponse(res) + def getCookie(request): tk = User_Keys.objects.get(id=request.session["td_id"]) if tk.properties["status"] == "trusted": - context={"added":True} - response = render(request,"TrustedDevices/Done.html", context) + context = {"added": True} + response = render(request, "TrustedDevices/Done.html", context) from datetime import datetime, timedelta + expires = datetime.now() + timedelta(days=180) - tk.expires=expires + tk.expires = expires tk.save() response.set_cookie("deviceid", tk.properties["signature"], expires=expires) return response + def add(request): - context=csrf(request) - if request.method=="GET": - return render(request,"TrustedDevices/Add.html",context) + context = csrf(request) + if request.method == "GET": + return render(request, "TrustedDevices/Add.html", context) else: - key=request.POST["key"].replace("-","").replace(" ","").upper() + key = request.POST["key"].replace("-", "").replace(" ", "").upper() context["username"] = request.POST["username"] context["key"] = request.POST["key"] - trusted_keys=User_Keys.objects.filter(username=request.POST["username"],properties__has="$.key="+key) - cookie=False + trusted_keys = User_Keys.objects.filter( + username=request.POST["username"], properties__has="$.key=" + key + ) + cookie = False if trusted_keys.exists(): - tk=trusted_keys[0] - request.session["td_id"]=tk.id - ua=request.META['HTTP_USER_AGENT'] - agent=user_agents.parse(ua) + tk = trusted_keys[0] + request.session["td_id"] = tk.id + ua = request.META["HTTP_USER_AGENT"] + agent = user_agents.parse(ua) if agent.is_pc: - context["invalid"]="This is a PC, it can't used as a trusted device." + context["invalid"] = "This is a PC, it can't used as a trusted device." else: - tk.properties["user_agent"]=ua + tk.properties["user_agent"] = ua tk.save() - context["success"]=True + context["success"] = True # tk.properties["user_agent"]=ua # tk.save() # context["success"]=True else: - context["invalid"]="The username or key is wrong, please check and try again." + context[ + "invalid" + ] = "The username or key is wrong, please check and try again." + + return render(request, "TrustedDevices/Add.html", context) - return render(request,"TrustedDevices/Add.html", context) def start(request): - if User_Keys.objects.filter(username=request.user.username,key_type="Trusted Device").count()>= 2: - return render(request,"TrustedDevices/start.html",{"not_allowed":True}) - td=None - if not request.session.get("td_id",None): - td=User_Keys() - td.username=request.user.username - td.properties={"key":id_generator(),"status":"adding"} - td.key_type="Trusted Device" + if ( + User_Keys.objects.filter( + username=request.user.username, key_type="Trusted Device" + ).count() + >= 2 + ): + return render(request, "TrustedDevices/start.html", {"not_allowed": True}) + td = None + if not request.session.get("td_id", None): + td = User_Keys() + td.username = request.user.username + td.properties = {"key": id_generator(), "status": "adding"} + td.key_type = "Trusted Device" td.save() - request.session["td_id"]=td.id + request.session["td_id"] = td.id try: - if td==None: td=User_Keys.objects.get(id=request.session["td_id"]) - context={"key":td.properties["key"]} + if td == None: + td = User_Keys.objects.get(id=request.session["td_id"]) + context = {"key": td.properties["key"]} except: del request.session["td_id"] return start(request) - return render(request,"TrustedDevices/start.html",context) + return render(request, "TrustedDevices/start.html", context) + def send_email(request): - body=render(request,"TrustedDevices/email.html",{}).content + body = render(request, "TrustedDevices/email.html", {}).content from .Common import send - e=request.user.email - if e=="": - e=request.session.get("user",{}).get("email","") - if e=="": + + e = request.user.email + if e == "": + e = request.session.get("user", {}).get("email", "") + if e == "": res = "User has no email on the system." - elif send([e],"Add Trusted Device Link",body): - res="Sent Successfully" + elif send([e], "Add Trusted Device Link", body): + res = "Sent Successfully" else: - res="Error occured, please try again later." + res = "Error occured, please try again later." return HttpResponse(res) def verify(request): - if request.COOKIES.get('deviceid',None): + if request.COOKIES.get("deviceid", None): from jose import jwt - json= jwt.decode(request.COOKIES.get('deviceid'),settings.SECRET_KEY) - if json["username"].lower()== request.session['base_username'].lower(): + + json = jwt.decode(request.COOKIES.get("deviceid"), settings.SECRET_KEY) + if json["username"].lower() == request.session["base_username"].lower(): try: - uk = User_Keys.objects.get(username=request.POST["username"].lower(), properties__has="$.key=" + json["key"]) + uk = User_Keys.objects.get( + username=request.POST["username"].lower(), + properties__has="$.key=" + json["key"], + ) if uk.enabled and uk.properties["status"] == "trusted": - uk.last_used=timezone.now() + uk.last_used = timezone.now() uk.save() - request.session["mfa"] = {"verified": True, "method": "Trusted Device","id":uk.id} + request.session["mfa"] = { + "verified": True, + "method": "Trusted Device", + "id": uk.id, + } return True except: return False diff --git a/mfa/U2F.py b/mfa/U2F.py index fe5ea19..2ad5f08 100644 --- a/mfa/U2F.py +++ b/mfa/U2F.py @@ -1,12 +1,16 @@ - -from u2flib_server.u2f import (begin_registration, begin_authentication, - complete_registration, complete_authentication) +from u2flib_server.u2f import ( + begin_registration, + begin_authentication, + complete_registration, + complete_authentication, +) from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.serialization import Encoding from django.shortcuts import render import simplejson -#from django.template.context import RequestContext + +# from django.template.context import RequestContext from django.template.context_processors import csrf from django.conf import settings from django.http import HttpResponse @@ -15,97 +19,127 @@ from .views import login import datetime from django.utils import timezone + def recheck(request): context = csrf(request) - context["mode"]="recheck" + context["mode"] = "recheck" s = sign(request.user.username) request.session["_u2f_challenge_"] = s[0] context["token"] = s[1] - request.session["mfa_recheck"]=True - return render(request,"U2F/recheck.html", context) + request.session["mfa_recheck"] = True + return render(request, "U2F/recheck.html", context) + def process_recheck(request): - x=validate(request,request.user.username) - if x==True: + x = validate(request, request.user.username) + if x == True: import time + request.session["mfa"]["rechecked_at"] = time.time() - return HttpResponse(simplejson.dumps({"recheck":True}),content_type="application/json") + return HttpResponse( + simplejson.dumps({"recheck": True}), content_type="application/json" + ) return x + def check_errors(request, data): if "errorCode" in data: - if data["errorCode"] == 0: return True + if data["errorCode"] == 0: + return True if data["errorCode"] == 4: return HttpResponse("Invalid Security Key") if data["errorCode"] == 1: return auth(request) return True -def validate(request,username): + + +def validate(request, username): import datetime, random data = simplejson.loads(request.POST["response"]) - res= check_errors(request,data) - if res!=True: + res = check_errors(request, data) + if res != True: return res - challenge = request.session.pop('_u2f_challenge_') + challenge = request.session.pop("_u2f_challenge_") device, c, t = complete_authentication(challenge, data, [settings.U2F_APPID]) - key=User_Keys.objects.get(username=username,properties__shas="$.device.publicKey=%s"%device["publicKey"]) - key.last_used=timezone.now() + key = User_Keys.objects.get( + username=username, + properties__shas="$.device.publicKey=%s" % device["publicKey"], + ) + key.last_used = timezone.now() key.save() - mfa = {"verified": True, "method": "U2F","id":key.id} + mfa = {"verified": True, "method": "U2F", "id": key.id} 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)))) + 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 True -def auth(request): - context=csrf(request) - s=sign(request.session["base_username"]) - request.session["_u2f_challenge_"]=s[0] - context["token"]=s[1] - return render(request,"U2F/Auth.html") +def auth(request): + context = csrf(request) + s = sign(request.session["base_username"]) + request.session["_u2f_challenge_"] = s[0] + context["token"] = s[1] + + return render(request, "U2F/Auth.html") + def start(request): enroll = begin_registration(settings.U2F_APPID, []) - request.session['_u2f_enroll_'] = enroll.json - context=csrf(request) - context["token"]=simplejson.dumps(enroll.data_for_client) + request.session["_u2f_enroll_"] = enroll.json + context = csrf(request) + context["token"] = simplejson.dumps(enroll.data_for_client) context.update(get_redirect_url()) - return render(request,"U2F/Add.html",context) + return render(request, "U2F/Add.html", context) def bind(request): import hashlib - enroll = request.session['_u2f_enroll_'] - data=simplejson.loads(request.POST["response"]) + + enroll = request.session["_u2f_enroll_"] + data = simplejson.loads(request.POST["response"]) device, cert = complete_registration(enroll, data, [settings.U2F_APPID]) cert = x509.load_der_x509_certificate(cert, default_backend()) - cert_hash=hashlib.md5(cert.public_bytes(Encoding.PEM)).hexdigest() - q=User_Keys.objects.filter(key_type="U2F", properties__icontains= cert_hash) + cert_hash = hashlib.md5(cert.public_bytes(Encoding.PEM)).hexdigest() + q = User_Keys.objects.filter(key_type="U2F", properties__icontains=cert_hash) if q.exists(): - return HttpResponse("This key is registered before, it can't be registered again.") - User_Keys.objects.filter(username=request.user.username,key_type="U2F").delete() + return HttpResponse( + "This key is registered before, it can't be registered again." + ) + User_Keys.objects.filter(username=request.user.username, key_type="U2F").delete() uk = User_Keys() uk.username = request.user.username uk.owned_by_enterprise = getattr(settings, "MFA_OWNED_BY_ENTERPRISE", False) - 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.save() return HttpResponse("OK") + def sign(username): - u2f_devices=[d.properties["device"] for d in User_Keys.objects.filter(username=username,key_type="U2F")] + u2f_devices = [ + d.properties["device"] + for d in User_Keys.objects.filter(username=username, key_type="U2F") + ] challenge = begin_authentication(settings.U2F_APPID, u2f_devices) - return [challenge.json,simplejson.dumps(challenge.data_for_client)] + return [challenge.json, simplejson.dumps(challenge.data_for_client)] + def verify(request): - x= validate(request,request.session["base_username"]) - if x==True: + x = validate(request, request.session["base_username"]) + if x == True: return login(request) - else: return x + else: + return x diff --git a/mfa/__init__.py b/mfa/__init__.py index d9d106a..8a124bf 100644 --- a/mfa/__init__.py +++ b/mfa/__init__.py @@ -1 +1 @@ -__version__="2.2.0" +__version__ = "2.2.0" diff --git a/mfa/apps.py b/mfa/apps.py index cb5ecca..f9816d4 100644 --- a/mfa/apps.py +++ b/mfa/apps.py @@ -1,4 +1,6 @@ from django.apps import AppConfig + + class myAppNameConfig(AppConfig): - name = 'mfa' - verbose_name = 'A Much Better Name' \ No newline at end of file + name = "mfa" + verbose_name = "A Much Better Name" diff --git a/mfa/helpers.py b/mfa/helpers.py index a61c769..e22e573 100644 --- a/mfa/helpers.py +++ b/mfa/helpers.py @@ -3,30 +3,45 @@ from .models import * from . import TrustedDevice, U2F, FIDO2, totp import simplejson from django.shortcuts import HttpResponse -from mfa.views import verify,goto -def has_mfa(request,username): - if User_Keys.objects.filter(username=username,enabled=1).count()>0: +from mfa.views import verify, goto + + +def has_mfa(request, username): + if User_Keys.objects.filter(username=username, enabled=1).count() > 0: return verify(request, username) return False -def is_mfa(request,ignore_methods=[]): - if request.session.get("mfa",{}).get("verified",False): - if not request.session.get("mfa",{}).get("method",None) in ignore_methods: + +def is_mfa(request, ignore_methods=[]): + if request.session.get("mfa", {}).get("verified", False): + if not request.session.get("mfa", {}).get("method", None) in ignore_methods: return True return False + def recheck(request): - method=request.session.get("mfa",{}).get("method",None) + method = request.session.get("mfa", {}).get("method", None) if not method: - return HttpResponse(simplejson.dumps({"res":False}),content_type="application/json") - if method=="Trusted Device": - return HttpResponse(simplejson.dumps({"res":TrustedDevice.verify(request)}),content_type="application/json") - elif method=="U2F": - return HttpResponse(simplejson.dumps({"html": U2F.recheck(request).content}), content_type="application/json") + return HttpResponse( + simplejson.dumps({"res": False}), content_type="application/json" + ) + if method == "Trusted Device": + return HttpResponse( + simplejson.dumps({"res": TrustedDevice.verify(request)}), + content_type="application/json", + ) + elif method == "U2F": + return HttpResponse( + simplejson.dumps({"html": U2F.recheck(request).content}), + content_type="application/json", + ) elif method == "FIDO2": - return HttpResponse(simplejson.dumps({"html": FIDO2.recheck(request).content}), content_type="application/json") - elif method=="TOTP": - return HttpResponse(simplejson.dumps({"html": totp.recheck(request).content}), content_type="application/json") - - - + return HttpResponse( + simplejson.dumps({"html": FIDO2.recheck(request).content}), + content_type="application/json", + ) + elif method == "TOTP": + return HttpResponse( + simplejson.dumps({"html": totp.recheck(request).content}), + content_type="application/json", + ) diff --git a/mfa/middleware.py b/mfa/middleware.py index 4923416..4acc51f 100644 --- a/mfa/middleware.py +++ b/mfa/middleware.py @@ -2,12 +2,18 @@ import time from django.http import HttpResponseRedirect from django.core.urlresolvers import reverse from django.conf import settings + + def process(request): - next_check=request.session.get('mfa',{}).get("next_check",False) - if not next_check: return None - now=int(time.time()) + next_check = request.session.get("mfa", {}).get("next_check", False) + if not next_check: + return None + now = int(time.time()) if now >= next_check: - method=request.session["mfa"]["method"] + method = request.session["mfa"]["method"] path = request.META["PATH_INFO"] - return HttpResponseRedirect(reverse(method+"_auth")+"?next=%s"%(settings.BASE_URL + path).replace("//", "/")) - return None \ No newline at end of file + return HttpResponseRedirect( + reverse(method + "_auth") + + "?next=%s" % (settings.BASE_URL + path).replace("//", "/") + ) + return None diff --git a/mfa/migrations/0001_initial.py b/mfa/migrations/0001_initial.py index c243f37..bce7c86 100644 --- a/mfa/migrations/0001_initial.py +++ b/mfa/migrations/0001_initial.py @@ -6,17 +6,24 @@ from django.db import models, migrations class Migration(migrations.Migration): - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='User_Keys', + name="User_Keys", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('username', models.CharField(max_length=50)), - ('secret_key', models.CharField(max_length=15)), - ('added_on', models.DateTimeField(auto_now_add=True)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("username", models.CharField(max_length=50)), + ("secret_key", models.CharField(max_length=15)), + ("added_on", models.DateTimeField(auto_now_add=True)), ], ), ] diff --git a/mfa/migrations/0002_user_keys_key_type.py b/mfa/migrations/0002_user_keys_key_type.py index 5cb4aef..6738181 100644 --- a/mfa/migrations/0002_user_keys_key_type.py +++ b/mfa/migrations/0002_user_keys_key_type.py @@ -7,13 +7,13 @@ from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ - ('mfa', '0001_initial'), + ("mfa", "0001_initial"), ] operations = [ migrations.AddField( - model_name='user_keys', - name='key_type', - field=models.CharField(default=b'TOTP', max_length=25), + model_name="user_keys", + name="key_type", + field=models.CharField(default=b"TOTP", max_length=25), ), ] diff --git a/mfa/migrations/0003_auto_20181114_2159.py b/mfa/migrations/0003_auto_20181114_2159.py index 49dfd5e..d9b4797 100644 --- a/mfa/migrations/0003_auto_20181114_2159.py +++ b/mfa/migrations/0003_auto_20181114_2159.py @@ -7,13 +7,13 @@ from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ - ('mfa', '0002_user_keys_key_type'), + ("mfa", "0002_user_keys_key_type"), ] operations = [ migrations.AlterField( - model_name='user_keys', - name='secret_key', + model_name="user_keys", + name="secret_key", field=models.CharField(max_length=32), ), ] diff --git a/mfa/migrations/0004_user_keys_enabled.py b/mfa/migrations/0004_user_keys_enabled.py index b0bcb78..372e265 100644 --- a/mfa/migrations/0004_user_keys_enabled.py +++ b/mfa/migrations/0004_user_keys_enabled.py @@ -7,13 +7,13 @@ from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ - ('mfa', '0003_auto_20181114_2159'), + ("mfa", "0003_auto_20181114_2159"), ] operations = [ migrations.AddField( - model_name='user_keys', - name='enabled', + model_name="user_keys", + name="enabled", field=models.BooleanField(default=True), ), ] diff --git a/mfa/migrations/0005_auto_20181115_2014.py b/mfa/migrations/0005_auto_20181115_2014.py index 8f9b76d..0084cb0 100644 --- a/mfa/migrations/0005_auto_20181115_2014.py +++ b/mfa/migrations/0005_auto_20181115_2014.py @@ -7,24 +7,25 @@ import jsonfield.fields def modify_json(apps, schema_editor): from django.conf import settings + if "mysql" in settings.DATABASES.get("default", {}).get("engine", ""): migrations.RunSQL("alter table mfa_user_keys modify column properties json;") class Migration(migrations.Migration): dependencies = [ - ('mfa', '0004_user_keys_enabled'), + ("mfa", "0004_user_keys_enabled"), ] operations = [ migrations.RemoveField( - model_name='user_keys', - name='secret_key', + model_name="user_keys", + name="secret_key", ), migrations.AddField( - model_name='user_keys', - name='properties', + model_name="user_keys", + name="properties", field=jsonfield.fields.JSONField(null=True), ), - migrations.RunPython(modify_json) + migrations.RunPython(modify_json), ] diff --git a/mfa/migrations/0006_trusted_devices.py b/mfa/migrations/0006_trusted_devices.py index de14a0e..3ae6504 100644 --- a/mfa/migrations/0006_trusted_devices.py +++ b/mfa/migrations/0006_trusted_devices.py @@ -7,21 +7,29 @@ from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ - ('mfa', '0005_auto_20181115_2014'), + ("mfa", "0005_auto_20181115_2014"), ] operations = [ migrations.CreateModel( - name='Trusted_Devices', + name="Trusted_Devices", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('signature', models.CharField(max_length=255)), - ('key', models.CharField(max_length=6)), - ('username', models.CharField(max_length=50)), - ('user_agent', models.CharField(max_length=255)), - ('status', models.CharField(default=b'adding', max_length=255)), - ('added_on', models.DateTimeField(auto_now_add=True)), - ('last_used', models.DateTimeField(default=None, null=True)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("signature", models.CharField(max_length=255)), + ("key", models.CharField(max_length=6)), + ("username", models.CharField(max_length=50)), + ("user_agent", models.CharField(max_length=255)), + ("status", models.CharField(default=b"adding", max_length=255)), + ("added_on", models.DateTimeField(auto_now_add=True)), + ("last_used", models.DateTimeField(default=None, null=True)), ], ), ] diff --git a/mfa/migrations/0007_auto_20181230_1549.py b/mfa/migrations/0007_auto_20181230_1549.py index 965bb9e..fe3350f 100644 --- a/mfa/migrations/0007_auto_20181230_1549.py +++ b/mfa/migrations/0007_auto_20181230_1549.py @@ -7,16 +7,16 @@ from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ - ('mfa', '0006_trusted_devices'), + ("mfa", "0006_trusted_devices"), ] operations = [ migrations.DeleteModel( - name='Trusted_Devices', + name="Trusted_Devices", ), migrations.AddField( - model_name='user_keys', - name='expires', + model_name="user_keys", + name="expires", field=models.DateTimeField(default=None, null=True, blank=True), ), ] diff --git a/mfa/migrations/0008_user_keys_last_used.py b/mfa/migrations/0008_user_keys_last_used.py index b555762..76d42f5 100644 --- a/mfa/migrations/0008_user_keys_last_used.py +++ b/mfa/migrations/0008_user_keys_last_used.py @@ -7,13 +7,13 @@ from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ - ('mfa', '0007_auto_20181230_1549'), + ("mfa", "0007_auto_20181230_1549"), ] operations = [ migrations.AddField( - model_name='user_keys', - name='last_used', + model_name="user_keys", + name="last_used", field=models.DateTimeField(default=None, null=True, blank=True), ), ] diff --git a/mfa/migrations/0009_user_keys_owned_by_enterprise.py b/mfa/migrations/0009_user_keys_owned_by_enterprise.py index 9185fcc..adf2bd0 100644 --- a/mfa/migrations/0009_user_keys_owned_by_enterprise.py +++ b/mfa/migrations/0009_user_keys_owned_by_enterprise.py @@ -6,21 +6,23 @@ from django.conf import settings def update_owned_by_enterprise(apps, schema_editor): - user_keys = apps.get_model('mfa', 'user_keys') - user_keys.objects.filter(key_type='FIDO2').update(owned_by_enterprise=getattr(settings,"MFA_OWNED_BY_ENTERPRISE",False)) + user_keys = apps.get_model("mfa", "user_keys") + user_keys.objects.filter(key_type="FIDO2").update( + owned_by_enterprise=getattr(settings, "MFA_OWNED_BY_ENTERPRISE", False) + ) class Migration(migrations.Migration): dependencies = [ - ('mfa', '0008_user_keys_last_used'), + ("mfa", "0008_user_keys_last_used"), ] operations = [ migrations.AddField( - model_name='user_keys', - name='owned_by_enterprise', + model_name="user_keys", + name="owned_by_enterprise", field=models.NullBooleanField(default=None), ), - migrations.RunPython(update_owned_by_enterprise) + migrations.RunPython(update_owned_by_enterprise), ] diff --git a/mfa/migrations/0010_auto_20201110_0557.py b/mfa/migrations/0010_auto_20201110_0557.py index 48190b6..de6913c 100644 --- a/mfa/migrations/0010_auto_20201110_0557.py +++ b/mfa/migrations/0010_auto_20201110_0557.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('mfa', '0009_user_keys_owned_by_enterprise'), + ("mfa", "0009_user_keys_owned_by_enterprise"), ] operations = [ migrations.AlterField( - model_name='user_keys', - name='key_type', - field=models.CharField(default='TOTP', max_length=25), + model_name="user_keys", + name="key_type", + field=models.CharField(default="TOTP", max_length=25), ), ] diff --git a/mfa/migrations/0011_auto_20210530_0622.py b/mfa/migrations/0011_auto_20210530_0622.py index 3d215c2..a270d66 100644 --- a/mfa/migrations/0011_auto_20210530_0622.py +++ b/mfa/migrations/0011_auto_20210530_0622.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('mfa', '0010_auto_20201110_0557'), + ("mfa", "0010_auto_20201110_0557"), ] operations = [ migrations.AlterField( - model_name='user_keys', - name='owned_by_enterprise', + model_name="user_keys", + name="owned_by_enterprise", field=models.BooleanField(blank=True, default=None, null=True), ), ] diff --git a/mfa/models.py b/mfa/models.py index d4eff59..b9dd673 100644 --- a/mfa/models.py +++ b/mfa/models.py @@ -2,31 +2,45 @@ from django.db import models from jsonfield import JSONField from jose import jwt from django.conf import settings -#from jsonLookup import shasLookup, hasLookup + +# from jsonLookup import shasLookup, hasLookup # JSONField.register_lookup(shasLookup) # JSONField.register_lookup(hasLookup) class User_Keys(models.Model): - username=models.CharField(max_length = 50) - properties=JSONField(null = True) - added_on=models.DateTimeField(auto_now_add = True) - key_type=models.CharField(max_length = 25,default = "TOTP") - enabled=models.BooleanField(default=True) - expires=models.DateTimeField(null=True,default=None,blank=True) - last_used=models.DateTimeField(null=True,default=None,blank=True) - owned_by_enterprise=models.BooleanField(default=None,null=True,blank=True) + username = models.CharField(max_length=50) + properties = JSONField(null=True) + added_on = models.DateTimeField(auto_now_add=True) + key_type = models.CharField(max_length=25, default="TOTP") + enabled = models.BooleanField(default=True) + expires = models.DateTimeField(null=True, default=None, blank=True) + last_used = models.DateTimeField(null=True, default=None, blank=True) + owned_by_enterprise = models.BooleanField(default=None, null=True, blank=True) - def save(self, force_insert=False, force_update=False, using=None, update_fields=None): - if self.key_type == "Trusted Device" and self.properties.get("signature","") == "": - self.properties["signature"]= jwt.encode({"username": self.username, "key": self.properties["key"]}, settings.SECRET_KEY) - super(User_Keys, self).save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields) + def save( + self, force_insert=False, force_update=False, using=None, update_fields=None + ): + if ( + self.key_type == "Trusted Device" + and self.properties.get("signature", "") == "" + ): + self.properties["signature"] = jwt.encode( + {"username": self.username, "key": self.properties["key"]}, + settings.SECRET_KEY, + ) + super(User_Keys, self).save( + force_insert=force_insert, + force_update=force_update, + using=using, + update_fields=update_fields, + ) def __unicode__(self): - return "%s -- %s"%(self.username,self.key_type) + return "%s -- %s" % (self.username, self.key_type) def __str__(self): return self.__unicode__() class Meta: - app_label='mfa' + app_label = "mfa" diff --git a/mfa/totp.py b/mfa/totp.py index 96617c7..2284ab1 100644 --- a/mfa/totp.py +++ b/mfa/totp.py @@ -11,66 +11,95 @@ from .views import login import datetime from django.utils import timezone import random -def verify_login(request,username,token): - for key in User_Keys.objects.filter(username=username,key_type = "TOTP"): + + +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"]) - if totp.verify(token,valid_window = 30): - key.last_used=timezone.now() + if totp.verify(token, valid_window=30): + key.last_used = timezone.now() key.save() - return [True,key.id] + return [True, key.id] return [False] + def recheck(request): context = csrf(request) - context["mode"]="recheck" + context["mode"] = "recheck" if request.method == "POST": - if verify_login(request,request.user.username, token=request.POST["otp"]): + if verify_login(request, request.user.username, token=request.POST["otp"]): import time + request.session["mfa"]["rechecked_at"] = time.time() - return HttpResponse(simplejson.dumps({"recheck": True}), content_type="application/json") + return HttpResponse( + simplejson.dumps({"recheck": True}), content_type="application/json" + ) else: - return HttpResponse(simplejson.dumps({"recheck": False}), content_type="application/json") - return render(request,"TOTP/recheck.html", context) + return HttpResponse( + simplejson.dumps({"recheck": False}), content_type="application/json" + ) + return render(request, "TOTP/recheck.html", context) + @never_cache def auth(request): - context=csrf(request) - if request.method=="POST": - res=verify_login(request,request.session["base_username"],token = request.POST["otp"]) + context = csrf(request) + if request.method == "POST": + res = verify_login( + request, request.session["base_username"], token=request.POST["otp"] + ) 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): - mfa["next_check"] = datetime.datetime.timestamp((datetime.datetime.now() - + datetime.timedelta( - seconds=random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX)))) + 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) - + context["invalid"] = True + return render(request, "TOTP/Auth.html", context) def getToken(request): - secret_key=pyotp.random_base32() + secret_key = pyotp.random_base32() totp = pyotp.TOTP(secret_key) - request.session["new_mfa_answer"]=totp.now() - return HttpResponse(simplejson.dumps({"qr":pyotp.totp.TOTP(secret_key).provisioning_uri(str(request.user.username), issuer_name = settings.TOKEN_ISSUER_NAME), - "secret_key": secret_key})) + request.session["new_mfa_answer"] = totp.now() + return HttpResponse( + simplejson.dumps( + { + "qr": pyotp.totp.TOTP(secret_key).provisioning_uri( + str(request.user.username), issuer_name=settings.TOKEN_ISSUER_NAME + ), + "secret_key": secret_key, + } + ) + ) + + def verify(request): - answer=request.GET["answer"] - secret_key=request.GET["key"] + answer = request.GET["answer"] + secret_key = request.GET["key"] totp = pyotp.TOTP(secret_key) - if totp.verify(answer,valid_window = 60): - uk=User_Keys() - uk.username=request.user.username - uk.properties={"secret_key":secret_key} - #uk.name="Authenticatior #%s"%User_Keys.objects.filter(username=user.username,type="TOTP") - uk.key_type="TOTP" + if totp.verify(answer, valid_window=60): + uk = User_Keys() + uk.username = request.user.username + uk.properties = {"secret_key": secret_key} + # uk.name="Authenticatior #%s"%User_Keys.objects.filter(username=user.username,type="TOTP") + uk.key_type = "TOTP" uk.save() return HttpResponse("Success") - else: return HttpResponse("Error") + else: + return HttpResponse("Error") + @never_cache def start(request): """Start Adding Time One Time Password (TOTP)""" - return render(request,"TOTP/Add.html",get_redirect_url()) + return render(request, "TOTP/Add.html", get_redirect_url()) diff --git a/mfa/urls.py b/mfa/urls.py index 90e9432..48513c2 100644 --- a/mfa/urls.py +++ b/mfa/urls.py @@ -1,50 +1,46 @@ -from . import views,totp,U2F,TrustedDevice,helpers,FIDO2,Email -#app_name='mfa' +from . import views, totp, U2F, TrustedDevice, helpers, FIDO2, Email + +# app_name='mfa' try: - from django.urls import re_path as url + from django.urls import re_path as url except: - from django.conf.urls import url + from django.conf.urls import url urlpatterns = [ - url(r'totp/start/', totp.start , name="start_new_otop"), - url(r'totp/getToken', totp.getToken , name="get_new_otop"), - url(r'totp/verify', totp.verify, name="verify_otop"), - url(r'totp/auth', totp.auth, name="totp_auth"), - url(r'totp/recheck', totp.recheck, name="totp_recheck"), - - url(r'email/start/', Email.start , name="start_email"), - url(r'email/auth/', Email.auth , name="email_auth"), - - url(r'u2f/$', U2F.start, name="start_u2f"), - url(r'u2f/bind', U2F.bind, name="bind_u2f"), - url(r'u2f/auth', U2F.auth, name="u2f_auth"), - url(r'u2f/process_recheck', U2F.process_recheck, name="u2f_recheck"), - url(r'u2f/verify', U2F.verify, name="u2f_verify"), - - url(r'fido2/$', FIDO2.start, name="start_fido2"), - url(r'fido2/auth', FIDO2.auth, name="fido2_auth"), - url(r'fido2/begin_auth', FIDO2.authenticate_begin, name="fido2_begin_auth"), - url(r'fido2/complete_auth', FIDO2.authenticate_complete, name="fido2_complete_auth"), - url(r'fido2/begin_reg', FIDO2.begin_registeration, name="fido2_begin_reg"), - url(r'fido2/complete_reg', FIDO2.complete_reg, name="fido2_complete_reg"), - url(r'fido2/recheck', FIDO2.recheck, name="fido2_recheck"), - - - url(r'td/$', TrustedDevice.start, name="start_td"), - url(r'td/add', TrustedDevice.add, name="add_td"), - url(r'td/send_link', TrustedDevice.send_email, name="td_sendemail"), - url(r'td/get-ua', TrustedDevice.getUserAgent, name="td_get_useragent"), - url(r'td/trust', TrustedDevice.trust_device, name="td_trust_device"), - url(r'u2f/checkTrusted', TrustedDevice.checkTrusted, name="td_checkTrusted"), - url(r'u2f/secure_device', TrustedDevice.getCookie, name="td_securedevice"), - - url(r'^$', views.index, name="mfa_home"), - url(r'goto/(.*)', views.goto, name="mfa_goto"), - url(r'selct_method', views.show_methods, name="mfa_methods_list"), - url(r'recheck', helpers.recheck, name="mfa_recheck"), - url(r'toggleKey', views.toggleKey, name="toggle_key"), - url(r'delete', views.delKey, name="mfa_delKey"), - url(r'reset', views.reset_cookie, name="mfa_reset_cookie"), - - ] -# print(urlpatterns) \ No newline at end of file + url(r"totp/start/", totp.start, name="start_new_otop"), + url(r"totp/getToken", totp.getToken, name="get_new_otop"), + url(r"totp/verify", totp.verify, name="verify_otop"), + url(r"totp/auth", totp.auth, name="totp_auth"), + url(r"totp/recheck", totp.recheck, name="totp_recheck"), + url(r"email/start/", Email.start, name="start_email"), + url(r"email/auth/", Email.auth, name="email_auth"), + url(r"u2f/$", U2F.start, name="start_u2f"), + url(r"u2f/bind", U2F.bind, name="bind_u2f"), + url(r"u2f/auth", U2F.auth, name="u2f_auth"), + url(r"u2f/process_recheck", U2F.process_recheck, name="u2f_recheck"), + url(r"u2f/verify", U2F.verify, name="u2f_verify"), + url(r"fido2/$", FIDO2.start, name="start_fido2"), + url(r"fido2/auth", FIDO2.auth, name="fido2_auth"), + url(r"fido2/begin_auth", FIDO2.authenticate_begin, name="fido2_begin_auth"), + url( + r"fido2/complete_auth", FIDO2.authenticate_complete, name="fido2_complete_auth" + ), + url(r"fido2/begin_reg", FIDO2.begin_registeration, name="fido2_begin_reg"), + url(r"fido2/complete_reg", FIDO2.complete_reg, name="fido2_complete_reg"), + url(r"fido2/recheck", FIDO2.recheck, name="fido2_recheck"), + url(r"td/$", TrustedDevice.start, name="start_td"), + url(r"td/add", TrustedDevice.add, name="add_td"), + url(r"td/send_link", TrustedDevice.send_email, name="td_sendemail"), + url(r"td/get-ua", TrustedDevice.getUserAgent, name="td_get_useragent"), + url(r"td/trust", TrustedDevice.trust_device, name="td_trust_device"), + url(r"u2f/checkTrusted", TrustedDevice.checkTrusted, name="td_checkTrusted"), + url(r"u2f/secure_device", TrustedDevice.getCookie, name="td_securedevice"), + url(r"^$", views.index, name="mfa_home"), + url(r"goto/(.*)", views.goto, name="mfa_goto"), + url(r"selct_method", views.show_methods, name="mfa_methods_list"), + url(r"recheck", helpers.recheck, name="mfa_recheck"), + url(r"toggleKey", views.toggleKey, name="toggle_key"), + url(r"delete", views.delKey, name="mfa_delKey"), + url(r"reset", views.reset_cookie, name="mfa_reset_cookie"), +] +# print(urlpatterns) diff --git a/mfa/views.py b/mfa/views.py index 9039363..3d3251e 100644 --- a/mfa/views.py +++ b/mfa/views.py @@ -1,6 +1,7 @@ from django.shortcuts import render -from django.http import HttpResponse,HttpResponseRedirect +from django.http import HttpResponse, HttpResponseRedirect from .models import * + try: from django.urls import reverse except: @@ -12,79 +13,94 @@ from . import TrustedDevice from django.contrib.auth.decorators import login_required from user_agents import parse + @login_required def index(request): - keys=[] - context={"keys":User_Keys.objects.filter(username=request.user.username),"UNALLOWED_AUTHEN_METHODS":settings.MFA_UNALLOWED_METHODS - ,"HIDE_DISABLE":getattr(settings,"MFA_HIDE_DISABLE",[])} + keys = [] + context = { + "keys": User_Keys.objects.filter(username=request.user.username), + "UNALLOWED_AUTHEN_METHODS": settings.MFA_UNALLOWED_METHODS, + "HIDE_DISABLE": getattr(settings, "MFA_HIDE_DISABLE", []), + } for k in context["keys"]: - if k.key_type =="Trusted Device" : - setattr(k,"device",parse(k.properties.get("user_agent","-----"))) + if k.key_type == "Trusted Device": + setattr(k, "device", parse(k.properties.get("user_agent", "-----"))) elif k.key_type == "FIDO2": - setattr(k,"device",k.properties.get("type","----")) + setattr(k, "device", k.properties.get("type", "----")) keys.append(k) - context["keys"]=keys - return render(request,"MFA.html",context) + context["keys"] = keys + return render(request, "MFA.html", context) -def verify(request,username): + +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])) + # request.session["base_password"] = password + keys = User_Keys.objects.filter(username=username, enabled=1) + methods = list(set([k.key_type for k in keys])) - 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): return login(request) methods.remove("Trusted Device") request.session["mfa_methods"] = methods - if len(methods)==1: - return HttpResponseRedirect(reverse(methods[0].lower()+"_auth")) + if len(methods) == 1: + return HttpResponseRedirect(reverse(methods[0].lower() + "_auth")) return show_methods(request) + def show_methods(request): - return render(request,"select_mfa_method.html", {}) + return render(request, "select_mfa_method.html", {}) + def reset_cookie(request): - response=HttpResponseRedirect(settings.LOGIN_URL) + response = HttpResponseRedirect(settings.LOGIN_URL) response.delete_cookie("base_username") return response + + def login(request): from django.contrib import auth from django.conf import settings + callable_func = __get_callable_function__(settings.MFA_LOGIN_CALLBACK) - return callable_func(request,username=request.session["base_username"]) + return callable_func(request, username=request.session["base_username"]) @login_required def delKey(request): - key=User_Keys.objects.get(id=request.GET["id"]) + key = User_Keys.objects.get(id=request.GET["id"]) if key.username == request.user.username: key.delete() return HttpResponse("Deleted Successfully") else: return HttpResponse("Error: You own this token so you can't delete it") + def __get_callable_function__(func_path): import importlib - if not '.' in func_path: + + if not "." in func_path: raise Exception("class Name should include modulename.classname") parsed_str = func_path.split(".") - module_name , func_name = ".".join(parsed_str[:-1]) , parsed_str[-1] + module_name, func_name = ".".join(parsed_str[:-1]), parsed_str[-1] imported_module = importlib.import_module(module_name) - callable_func = getattr(imported_module,func_name) + callable_func = getattr(imported_module, func_name) if not callable_func: raise Exception("Module does not have requested function") return callable_func + @login_required def toggleKey(request): - id=request.GET["id"] - q=User_Keys.objects.filter(username=request.user.username, id=id) - if q.count()==1: - key=q[0] + id = request.GET["id"] + q = User_Keys.objects.filter(username=request.user.username, id=id) + if q.count() == 1: + key = q[0] if not key.key_type in settings.MFA_HIDE_DISABLE: - key.enabled=not key.enabled + key.enabled = not key.enabled key.save() return HttpResponse("OK") else: @@ -92,5 +108,6 @@ def toggleKey(request): else: return HttpResponse("Error") -def goto(request,method): - return HttpResponseRedirect(reverse(method.lower()+"_auth")) + +def goto(request, method): + return HttpResponseRedirect(reverse(method.lower() + "_auth")) From 8911a4f0b612d7c25f2f23c5507b79a665b7b39c Mon Sep 17 00:00:00 2001 From: Mohamed ElKalioby Date: Tue, 22 Jun 2021 17:15:01 +0300 Subject: [PATCH 02/22] Add Code Style Black --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 77a147a..7d0401b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # django-mfa2 A Django app that handles MFA, it supports TOTP, U2F, FIDO2 U2F (Web Authn), Email Tokens , and Trusted Devices +![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg) ### Pip Stats [![PyPI version](https://badge.fury.io/py/django-mfa2.svg)](https://badge.fury.io/py/django-mfa2) [![Downloads Count](https://static.pepy.tech/personalized-badge/django-mfa2?period=total&units=international_system&left_color=black&right_color=green&left_text=Downloads)](https://pepy.tech/project/django-mfa2) From 007872bd8a7907fbd4de67fa37ebcd080689f59e Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Thu, 17 Jun 2021 10:56:49 +0200 Subject: [PATCH 03/22] rm empty files --- mfa/ApproveLogin.py | 0 mfa/admin.py | 3 --- mfa/tests.py | 3 --- 3 files changed, 6 deletions(-) delete mode 100644 mfa/ApproveLogin.py delete mode 100644 mfa/admin.py delete mode 100644 mfa/tests.py diff --git a/mfa/ApproveLogin.py b/mfa/ApproveLogin.py deleted file mode 100644 index e69de29..0000000 diff --git a/mfa/admin.py b/mfa/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/mfa/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/mfa/tests.py b/mfa/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/mfa/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. From d54cd20d9bc2430b1ded16231ba26a5a56b82e60 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Thu, 17 Jun 2021 10:21:04 +0200 Subject: [PATCH 04/22] rm commented code --- mfa/Email.py | 1 - mfa/FIDO2.py | 1 - mfa/TrustedDevice.py | 3 --- mfa/U2F.py | 1 - mfa/models.py | 4 ---- mfa/totp.py | 1 - mfa/urls.py | 4 +--- mfa/views.py | 1 - 8 files changed, 1 insertion(+), 15 deletions(-) diff --git a/mfa/Email.py b/mfa/Email.py index d5535cd..ba6caf1 100644 --- a/mfa/Email.py +++ b/mfa/Email.py @@ -5,7 +5,6 @@ import datetime, random from random import randint from .models import * -# from django.template.context import RequestContext from .views import login from .Common import send diff --git a/mfa/FIDO2.py b/mfa/FIDO2.py index 2a51b4b..70fa2a1 100644 --- a/mfa/FIDO2.py +++ b/mfa/FIDO2.py @@ -7,7 +7,6 @@ from django.template.context_processors import csrf from django.views.decorators.csrf import csrf_exempt from django.shortcuts import render -# from django.template.context import RequestContext import simplejson from fido2 import cbor from django.http import HttpResponse diff --git a/mfa/TrustedDevice.py b/mfa/TrustedDevice.py index 2b6dc66..e0fe3b8 100644 --- a/mfa/TrustedDevice.py +++ b/mfa/TrustedDevice.py @@ -87,9 +87,6 @@ def add(request): tk.properties["user_agent"] = ua tk.save() context["success"] = True - # tk.properties["user_agent"]=ua - # tk.save() - # context["success"]=True else: context[ diff --git a/mfa/U2F.py b/mfa/U2F.py index 2ad5f08..239fdba 100644 --- a/mfa/U2F.py +++ b/mfa/U2F.py @@ -10,7 +10,6 @@ from cryptography.hazmat.primitives.serialization import Encoding from django.shortcuts import render import simplejson -# from django.template.context import RequestContext from django.template.context_processors import csrf from django.conf import settings from django.http import HttpResponse diff --git a/mfa/models.py b/mfa/models.py index b9dd673..14b7c14 100644 --- a/mfa/models.py +++ b/mfa/models.py @@ -3,10 +3,6 @@ from jsonfield import JSONField from jose import jwt from django.conf import settings -# from jsonLookup import shasLookup, hasLookup -# JSONField.register_lookup(shasLookup) -# JSONField.register_lookup(hasLookup) - class User_Keys(models.Model): username = models.CharField(max_length=50) diff --git a/mfa/totp.py b/mfa/totp.py index 2284ab1..ff2973a 100644 --- a/mfa/totp.py +++ b/mfa/totp.py @@ -91,7 +91,6 @@ def verify(request): uk = User_Keys() uk.username = request.user.username uk.properties = {"secret_key": secret_key} - # uk.name="Authenticatior #%s"%User_Keys.objects.filter(username=user.username,type="TOTP") uk.key_type = "TOTP" uk.save() return HttpResponse("Success") diff --git a/mfa/urls.py b/mfa/urls.py index 48513c2..3c888e2 100644 --- a/mfa/urls.py +++ b/mfa/urls.py @@ -1,11 +1,10 @@ from . import views, totp, U2F, TrustedDevice, helpers, FIDO2, Email -# app_name='mfa' - try: from django.urls import re_path as url except: from django.conf.urls import url + urlpatterns = [ url(r"totp/start/", totp.start, name="start_new_otop"), url(r"totp/getToken", totp.getToken, name="get_new_otop"), @@ -43,4 +42,3 @@ urlpatterns = [ url(r"delete", views.delKey, name="mfa_delKey"), url(r"reset", views.reset_cookie, name="mfa_reset_cookie"), ] -# print(urlpatterns) diff --git a/mfa/views.py b/mfa/views.py index 3d3251e..f862ab9 100644 --- a/mfa/views.py +++ b/mfa/views.py @@ -34,7 +34,6 @@ def index(request): 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])) From 6b132683a7154bba6e5af73750093bcdedd0a933 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Wed, 23 Jun 2021 08:23:48 +0200 Subject: [PATCH 05/22] run black on example and setup.py --- example/example/auth.py | 34 +++++----- example/example/settings.py | 121 ++++++++++++++++++------------------ example/example/urls.py | 18 +++--- example/example/views.py | 7 ++- setup.py | 43 +++++++------ 5 files changed, 115 insertions(+), 108 deletions(-) diff --git a/example/example/auth.py b/example/example/auth.py index afd587b..6d6fda6 100644 --- a/example/example/auth.py +++ b/example/example/auth.py @@ -1,30 +1,36 @@ from django.shortcuts import render from django.http import HttpResponseRedirect from django.urls import reverse -from django.contrib.auth import authenticate,login,logout +from django.contrib.auth import authenticate, login, logout from django.contrib.auth.models import User + + def loginView(request): - context={} - if request.method=="POST": - username=request.POST["username"] - password=request.POST["password"] - user=authenticate(username=username,password=password) + context = {} + if request.method == "POST": + username = request.POST["username"] + password = request.POST["password"] + user = authenticate(username=username, password=password) if user: from mfa.helpers import has_mfa - res = has_mfa(username = username, request = request) # has_mfa returns false or HttpResponseRedirect + + res = has_mfa( + username=username, request=request + ) # has_mfa returns false or HttpResponseRedirect if res: return res - return create_session(request,user.username) - context["invalid"]=True + return create_session(request, user.username) + context["invalid"] = True return render(request, "login.html", context) -def create_session(request,username): - user=User.objects.get(username=username) - user.backend='django.contrib.auth.backends.ModelBackend' + +def create_session(request, username): + user = User.objects.get(username=username) + user.backend = "django.contrib.auth.backends.ModelBackend" login(request, user) - return HttpResponseRedirect(reverse('home')) + return HttpResponseRedirect(reverse("home")) def logoutView(request): logout(request) - return render(request,"logout.html",{}) \ No newline at end of file + return render(request, "logout.html", {}) diff --git a/example/example/settings.py b/example/example/settings.py index 37735a2..a3fe9f2 100644 --- a/example/example/settings.py +++ b/example/example/settings.py @@ -20,7 +20,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '#9)q!_i3@pr-^3oda(e^3$x!kq3b4f33#5l@+=+&vuz+p6gb3g' +SECRET_KEY = "#9)q!_i3@pr-^3oda(e^3$x!kq3b4f33#5l@+=+&vuz+p6gb3g" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -31,54 +31,54 @@ ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'mfa', - 'sslserver' + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "mfa", + "sslserver", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'example.urls' +ROOT_URLCONF = "example.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR ,'example','templates' )], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [os.path.join(BASE_DIR, "example", "templates")], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'example.wsgi.application' +WSGI_APPLICATION = "example.wsgi.application" # Database # https://docs.djangoproject.com/en/2.0/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'test_db', + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": "test_db", } } @@ -88,16 +88,16 @@ DATABASES = { AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] @@ -105,9 +105,9 @@ AUTH_PASSWORD_VALIDATORS = [ # Internationalization # https://docs.djangoproject.com/en/2.0/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -119,32 +119,33 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.0/howto/static-files/ -STATIC_URL = '/static/' -#STATIC_ROOT=(os.path.join(BASE_DIR,'static')) -STATICFILES_DIRS=[os.path.join(BASE_DIR,'static')] -LOGIN_URL="/auth/login" +STATIC_URL = "/static/" +# STATIC_ROOT=(os.path.join(BASE_DIR,'static')) +STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")] +LOGIN_URL = "/auth/login" -EMAIL_FROM='Test App' -EMAIL_HOST="smtp.gmail.com" -EMAIL_PORT=587 -EMAIL_HOST_USER="" -EMAIL_HOST_PASSWORD='' -EMAIL_USE_TLS=True +EMAIL_FROM = "Test App" +EMAIL_HOST = "smtp.gmail.com" +EMAIL_PORT = 587 +EMAIL_HOST_USER = "" +EMAIL_HOST_PASSWORD = "" +EMAIL_USE_TLS = True +MFA_UNALLOWED_METHODS = () # Methods that shouldn't be allowed for the user +MFA_LOGIN_CALLBACK = "example.auth.create_session" # A function that should be called by username to login the user in session +MFA_RECHECK = True # Allow random rechecking of the user +MFA_RECHECK_MIN = 10 # Minimum interval in seconds +MFA_RECHECK_MAX = 30 # Maximum in seconds +MFA_QUICKLOGIN = True # Allow quick login for returning users by provide only their 2FA +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" -MFA_UNALLOWED_METHODS=() # Methods that shouldn't be allowed for the user -MFA_LOGIN_CALLBACK="example.auth.create_session" # A function that should be called by username to login the user in session -MFA_RECHECK=True # Allow random rechecking of the user -MFA_RECHECK_MIN=10 # Minimum interval in seconds -MFA_RECHECK_MAX=30 # Maximum in seconds -MFA_QUICKLOGIN=True # Allow quick login for returning users by provide only their 2FA -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" +TOKEN_ISSUER_NAME = "PROJECT_NAME" # TOTP Issuer name -TOKEN_ISSUER_NAME="PROJECT_NAME" #TOTP Issuer name - -U2F_APPID="https://localhost" #URL For U2F -FIDO_SERVER_ID=u"localhost" # Server rp id for FIDO2, it the full domain of your project -FIDO_SERVER_NAME=u"PROJECT_NAME" +U2F_APPID = "https://localhost" # URL For U2F +FIDO_SERVER_ID = ( + u"localhost" # Server rp id for FIDO2, it the full domain of your project +) +FIDO_SERVER_NAME = u"PROJECT_NAME" diff --git a/example/example/urls.py b/example/example/urls.py index bd0dc4d..d8ce223 100644 --- a/example/example/urls.py +++ b/example/example/urls.py @@ -14,14 +14,14 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path,re_path,include -from . import views,auth -urlpatterns = [ - path('admin/', admin.site.urls), - path('mfa/', include('mfa.urls')), - path('auth/login',auth.loginView,name="login"), - path('auth/logout',auth.logoutView,name="logout"), +from django.urls import path, re_path, include +from . import views, auth - re_path('^$',views.home,name='home'), - path('registered/',views.registered,name='registered') +urlpatterns = [ + path("admin/", admin.site.urls), + path("mfa/", include("mfa.urls")), + path("auth/login", auth.loginView, name="login"), + path("auth/logout", auth.logoutView, name="logout"), + re_path("^$", views.home, name="home"), + path("registered/", views.registered, name="registered"), ] diff --git a/example/example/views.py b/example/example/views.py index 98e80e9..74b03ee 100644 --- a/example/example/views.py +++ b/example/example/views.py @@ -1,11 +1,12 @@ -from django.shortcuts import render from django.contrib.auth.decorators import login_required +from django.shortcuts import render @login_required() def home(request): - return render(request,"home.html",{}) + return render(request, "home.html", {}) + @login_required() def registered(request): - return render(request,"home.html",{"registered":True}) + return render(request, "home.html", {"registered": True}) diff --git a/setup.py b/setup.py index ae6cc81..dbb4b6a 100644 --- a/setup.py +++ b/setup.py @@ -3,33 +3,32 @@ from setuptools import find_packages, setup setup( - name='django-mfa2', - version='2.2.0', - description='Allows user to add 2FA to their accounts', + name="django-mfa2", + version="2.2.0", + description="Allows user to add 2FA to their accounts", long_description=open("README.md").read(), long_description_content_type="text/markdown", - - author='Mohamed El-Kalioby', - author_email = 'mkalioby@mkalioby.com', - url = 'https://github.com/mkalioby/django-mfa2/', - download_url='https://github.com/mkalioby/django-mfa2/', - license='MIT', + author="Mohamed El-Kalioby", + author_email="mkalioby@mkalioby.com", + url="https://github.com/mkalioby/django-mfa2/", + download_url="https://github.com/mkalioby/django-mfa2/", + license="MIT", packages=find_packages(), install_requires=[ - 'django >= 2.0', - 'jsonfield', - 'simplejson', - 'pyotp', - 'python-u2flib-server', - 'ua-parser', - 'user-agents', - 'python-jose', - 'fido2 == 0.9.1', - 'jsonLookup' - ], + "django >= 2.0", + "jsonfield", + "simplejson", + "pyotp", + "python-u2flib-server", + "ua-parser", + "user-agents", + "python-jose", + "fido2 == 0.9.1", + "jsonLookup", + ], python_requires=">=3.5", include_package_data=True, - zip_safe=False, # because we're including static files + zip_safe=False, # because we're including static files classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", @@ -48,5 +47,5 @@ setup( "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Topic :: Software Development :: Libraries :: Python Modules", -] + ], ) From 0945561136e9a00c098b517f7eb9fe1b97ed56fc Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Wed, 23 Jun 2021 08:26:35 +0200 Subject: [PATCH 06/22] avoid local imports --- example/example/auth.py | 3 +-- mfa/Email.py | 19 +++++++++---------- mfa/FIDO2.py | 6 ++---- mfa/TrustedDevice.py | 9 ++++----- mfa/U2F.py | 10 ++++------ mfa/totp.py | 4 ++-- mfa/views.py | 9 ++++----- 7 files changed, 26 insertions(+), 34 deletions(-) diff --git a/example/example/auth.py b/example/example/auth.py index 6d6fda6..3dc9b2f 100644 --- a/example/example/auth.py +++ b/example/example/auth.py @@ -3,6 +3,7 @@ from django.http import HttpResponseRedirect from django.urls import reverse from django.contrib.auth import authenticate, login, logout from django.contrib.auth.models import User +from mfa.helpers import has_mfa def loginView(request): @@ -12,8 +13,6 @@ def loginView(request): password = request.POST["password"] user = authenticate(username=username, password=password) if user: - from mfa.helpers import has_mfa - res = has_mfa( username=username, request=request ) # has_mfa returns false or HttpResponseRedirect diff --git a/mfa/Email.py b/mfa/Email.py index ba6caf1..5b7055f 100644 --- a/mfa/Email.py +++ b/mfa/Email.py @@ -1,6 +1,15 @@ +from django.contrib.auth import get_user_model +from django.http import HttpResponseRedirect from django.shortcuts import render from django.views.decorators.cache import never_cache from django.template.context_processors import csrf +from django.utils import timezone + +try: + from django.core.urlresolvers import reverse +except: + from django.urls import reverse + import datetime, random from random import randint from .models import * @@ -11,8 +20,6 @@ from .Common import send def sendEmail(request, username, secret): """Send Email to the user after rendering `mfa_email_token_template`""" - from django.contrib.auth import get_user_model - User = get_user_model() key = getattr(User, "USERNAME_FIELD", "username") kwargs = {key: username} @@ -36,12 +43,6 @@ def start(request): uk.key_type = "Email" uk.enabled = 1 uk.save() - from django.http import HttpResponseRedirect - - try: - from django.core.urlresolvers import reverse - except: - from django.urls import reverse return HttpResponseRedirect( reverse( getattr(settings, "MFA_REDIRECT_AFTER_REGISTRATION", "mfa_home") @@ -78,8 +79,6 @@ def auth(request): ) request.session["mfa"] = mfa - from django.utils import timezone - uk.last_used = timezone.now() uk.save() return login(request) diff --git a/mfa/FIDO2.py b/mfa/FIDO2.py index 70fa2a1..2ece302 100644 --- a/mfa/FIDO2.py +++ b/mfa/FIDO2.py @@ -1,3 +1,5 @@ +import random +import time import traceback from fido2.client import ClientData @@ -154,15 +156,11 @@ def authenticate_complete(request): ) if request.session.get("mfa_recheck", False): - import time - request.session["mfa"]["rechecked_at"] = time.time() return HttpResponse( simplejson.dumps({"status": "OK"}), content_type="application/json" ) else: - import random - keys = User_Keys.objects.filter( username=username, key_type="FIDO2", enabled=1 ) diff --git a/mfa/TrustedDevice.py b/mfa/TrustedDevice.py index e0fe3b8..85d3af4 100644 --- a/mfa/TrustedDevice.py +++ b/mfa/TrustedDevice.py @@ -1,5 +1,6 @@ import string import random +from datetime import datetime, timedelta from django.shortcuts import render from django.http import HttpResponse from django.template.context import RequestContext @@ -7,6 +8,9 @@ from django.template.context_processors import csrf from .models import * import user_agents from django.utils import timezone +from jose import jwt + +from .Common import send def id_generator(size=6, chars=string.ascii_uppercase + string.digits): @@ -55,7 +59,6 @@ def getCookie(request): if tk.properties["status"] == "trusted": context = {"added": True} response = render(request, "TrustedDevices/Done.html", context) - from datetime import datetime, timedelta expires = datetime.now() + timedelta(days=180) tk.expires = expires @@ -124,8 +127,6 @@ def start(request): def send_email(request): body = render(request, "TrustedDevices/email.html", {}).content - from .Common import send - e = request.user.email if e == "": e = request.session.get("user", {}).get("email", "") @@ -140,8 +141,6 @@ def send_email(request): def verify(request): if request.COOKIES.get("deviceid", None): - from jose import jwt - json = jwt.decode(request.COOKIES.get("deviceid"), settings.SECRET_KEY) if json["username"].lower() == request.session["base_username"].lower(): try: diff --git a/mfa/U2F.py b/mfa/U2F.py index 239fdba..0c78c91 100644 --- a/mfa/U2F.py +++ b/mfa/U2F.py @@ -1,3 +1,7 @@ +import datetime, random +import hashlib +import time + from u2flib_server.u2f import ( begin_registration, begin_authentication, @@ -32,8 +36,6 @@ def recheck(request): def process_recheck(request): x = validate(request, request.user.username) if x == True: - import time - request.session["mfa"]["rechecked_at"] = time.time() return HttpResponse( simplejson.dumps({"recheck": True}), content_type="application/json" @@ -53,8 +55,6 @@ def check_errors(request, data): def validate(request, username): - import datetime, random - data = simplejson.loads(request.POST["response"]) res = check_errors(request, data) @@ -105,8 +105,6 @@ def start(request): def bind(request): - import hashlib - enroll = request.session["_u2f_enroll_"] data = simplejson.loads(request.POST["response"]) device, cert = complete_registration(enroll, data, [settings.U2F_APPID]) diff --git a/mfa/totp.py b/mfa/totp.py index ff2973a..fccb61f 100644 --- a/mfa/totp.py +++ b/mfa/totp.py @@ -1,3 +1,5 @@ +import time + from django.shortcuts import render from django.views.decorators.cache import never_cache from django.http import HttpResponse @@ -28,8 +30,6 @@ def recheck(request): context["mode"] = "recheck" if request.method == "POST": if verify_login(request, request.user.username, token=request.POST["otp"]): - import time - request.session["mfa"]["rechecked_at"] = time.time() return HttpResponse( simplejson.dumps({"recheck": True}), content_type="application/json" diff --git a/mfa/views.py b/mfa/views.py index f862ab9..ea829cd 100644 --- a/mfa/views.py +++ b/mfa/views.py @@ -1,3 +1,7 @@ +import importlib + +from django.conf import settings +from django.contrib import auth from django.shortcuts import render from django.http import HttpResponse, HttpResponseRedirect from .models import * @@ -60,9 +64,6 @@ def reset_cookie(request): def login(request): - from django.contrib import auth - from django.conf import settings - callable_func = __get_callable_function__(settings.MFA_LOGIN_CALLBACK) return callable_func(request, username=request.session["base_username"]) @@ -78,8 +79,6 @@ def delKey(request): def __get_callable_function__(func_path): - import importlib - if not "." in func_path: raise Exception("class Name should include modulename.classname") From 174dba2878f53e378d48e31ec2f8ce0508e74c5d Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Wed, 23 Jun 2021 08:35:55 +0200 Subject: [PATCH 07/22] sort imports (isort) --- example/example/auth.py | 7 ++++--- example/example/urls.py | 5 +++-- mfa/Email.py | 13 +++++++------ mfa/FIDO2.py | 29 ++++++++++++++--------------- mfa/TrustedDevice.py | 9 +++++---- mfa/U2F.py | 26 +++++++++++++------------- mfa/helpers.py | 7 ++++--- mfa/middleware.py | 5 +++-- mfa/models.py | 6 +++--- mfa/totp.py | 23 ++++++++++++----------- mfa/urls.py | 4 ++-- mfa/views.py | 14 +++++++------- 12 files changed, 77 insertions(+), 71 deletions(-) diff --git a/example/example/auth.py b/example/example/auth.py index 3dc9b2f..63172cf 100644 --- a/example/example/auth.py +++ b/example/example/auth.py @@ -1,8 +1,9 @@ -from django.shortcuts import render -from django.http import HttpResponseRedirect -from django.urls import reverse from django.contrib.auth import authenticate, login, logout from django.contrib.auth.models import User +from django.http import HttpResponseRedirect +from django.shortcuts import render +from django.urls import reverse + from mfa.helpers import has_mfa diff --git a/example/example/urls.py b/example/example/urls.py index d8ce223..ba1418e 100644 --- a/example/example/urls.py +++ b/example/example/urls.py @@ -14,8 +14,9 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path, re_path, include -from . import views, auth +from django.urls import include, path, re_path + +from . import auth, views urlpatterns = [ path("admin/", admin.site.urls), diff --git a/mfa/Email.py b/mfa/Email.py index 5b7055f..7d52f6c 100644 --- a/mfa/Email.py +++ b/mfa/Email.py @@ -1,21 +1,22 @@ +import datetime +import random +from random import randint + from django.contrib.auth import get_user_model from django.http import HttpResponseRedirect from django.shortcuts import render -from django.views.decorators.cache import never_cache from django.template.context_processors import csrf from django.utils import timezone +from django.views.decorators.cache import never_cache try: from django.core.urlresolvers import reverse except: from django.urls import reverse -import datetime, random -from random import randint -from .models import * - -from .views import login from .Common import send +from .models import * +from .views import login def sendEmail(request, username, secret): diff --git a/mfa/FIDO2.py b/mfa/FIDO2.py index 2ece302..fa0fe1e 100644 --- a/mfa/FIDO2.py +++ b/mfa/FIDO2.py @@ -1,25 +1,24 @@ +import datetime import random import time import traceback -from fido2.client import ClientData -from fido2.server import Fido2Server, PublicKeyCredentialRpEntity -from fido2.ctap2 import AttestationObject, AuthenticatorData -from django.template.context_processors import csrf -from django.views.decorators.csrf import csrf_exempt -from django.shortcuts import render - import simplejson -from fido2 import cbor -from django.http import HttpResponse from django.conf import settings -from .models import * -from fido2.utils import websafe_decode, websafe_encode -from fido2.ctap2 import AttestedCredentialData -from .views import login, reset_cookie -import datetime -from .Common import get_redirect_url +from django.http import HttpResponse +from django.shortcuts import render +from django.template.context_processors import csrf from django.utils import timezone +from django.views.decorators.csrf import csrf_exempt +from fido2 import cbor +from fido2.client import ClientData +from fido2.ctap2 import AttestationObject, AttestedCredentialData, AuthenticatorData +from fido2.server import Fido2Server, PublicKeyCredentialRpEntity +from fido2.utils import websafe_decode, websafe_encode + +from .Common import get_redirect_url +from .models import * +from .views import login, reset_cookie def recheck(request): diff --git a/mfa/TrustedDevice.py b/mfa/TrustedDevice.py index 85d3af4..9e225cd 100644 --- a/mfa/TrustedDevice.py +++ b/mfa/TrustedDevice.py @@ -1,16 +1,17 @@ -import string import random +import string from datetime import datetime, timedelta -from django.shortcuts import render + +import user_agents from django.http import HttpResponse +from django.shortcuts import render from django.template.context import RequestContext from django.template.context_processors import csrf -from .models import * -import user_agents from django.utils import timezone from jose import jwt from .Common import send +from .models import * def id_generator(size=6, chars=string.ascii_uppercase + string.digits): diff --git a/mfa/U2F.py b/mfa/U2F.py index 0c78c91..4bc2b02 100644 --- a/mfa/U2F.py +++ b/mfa/U2F.py @@ -1,26 +1,26 @@ -import datetime, random +import datetime import hashlib +import random import time -from u2flib_server.u2f import ( - begin_registration, - begin_authentication, - complete_registration, - complete_authentication, -) +import simplejson from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.serialization import Encoding -from django.shortcuts import render -import simplejson - -from django.template.context_processors import csrf from django.conf import settings from django.http import HttpResponse +from django.shortcuts import render +from django.template.context_processors import csrf +from django.utils import timezone +from u2flib_server.u2f import ( + begin_authentication, + begin_registration, + complete_authentication, + complete_registration, +) + from .models import * from .views import login -import datetime -from django.utils import timezone def recheck(request): diff --git a/mfa/helpers.py b/mfa/helpers.py index e22e573..9c0f2b8 100644 --- a/mfa/helpers.py +++ b/mfa/helpers.py @@ -1,9 +1,10 @@ import pyotp -from .models import * -from . import TrustedDevice, U2F, FIDO2, totp import simplejson from django.shortcuts import HttpResponse -from mfa.views import verify, goto + +from . import FIDO2, U2F, TrustedDevice, totp +from .models import * +from .views import goto, verify def has_mfa(request, username): diff --git a/mfa/middleware.py b/mfa/middleware.py index 4acc51f..d61e956 100644 --- a/mfa/middleware.py +++ b/mfa/middleware.py @@ -1,7 +1,8 @@ import time -from django.http import HttpResponseRedirect -from django.core.urlresolvers import reverse + from django.conf import settings +from django.core.urlresolvers import reverse +from django.http import HttpResponseRedirect def process(request): diff --git a/mfa/models.py b/mfa/models.py index 14b7c14..ccc8ffc 100644 --- a/mfa/models.py +++ b/mfa/models.py @@ -1,7 +1,7 @@ -from django.db import models -from jsonfield import JSONField -from jose import jwt from django.conf import settings +from django.db import models +from jose import jwt +from jsonfield import JSONField class User_Keys(models.Model): diff --git a/mfa/totp.py b/mfa/totp.py index fccb61f..6400d7e 100644 --- a/mfa/totp.py +++ b/mfa/totp.py @@ -1,18 +1,19 @@ +import datetime +import random import time -from django.shortcuts import render -from django.views.decorators.cache import never_cache -from django.http import HttpResponse -from .models import * -from django.template.context_processors import csrf -import simplejson -from django.template.context import RequestContext -from django.conf import settings import pyotp -from .views import login -import datetime +import simplejson +from django.conf import settings +from django.http import HttpResponse +from django.shortcuts import render +from django.template.context import RequestContext +from django.template.context_processors import csrf from django.utils import timezone -import random +from django.views.decorators.cache import never_cache + +from .models import * +from .views import login def verify_login(request, username, token): diff --git a/mfa/urls.py b/mfa/urls.py index 3c888e2..5bdf1ae 100644 --- a/mfa/urls.py +++ b/mfa/urls.py @@ -1,10 +1,10 @@ -from . import views, totp, U2F, TrustedDevice, helpers, FIDO2, Email - try: from django.urls import re_path as url except: from django.conf.urls import url +from . import FIDO2, U2F, Email, TrustedDevice, helpers, totp, views + urlpatterns = [ url(r"totp/start/", totp.start, name="start_new_otop"), url(r"totp/getToken", totp.getToken, name="get_new_otop"), diff --git a/mfa/views.py b/mfa/views.py index ea829cd..7394a6e 100644 --- a/mfa/views.py +++ b/mfa/views.py @@ -2,20 +2,20 @@ import importlib from django.conf import settings from django.contrib import auth -from django.shortcuts import render +from django.contrib.auth.decorators import login_required from django.http import HttpResponse, HttpResponseRedirect -from .models import * +from django.shortcuts import render +from django.template.context import RequestContext +from django.template.context_processors import csrf +from user_agents import parse try: from django.urls import reverse except: from django.core.urlresolvers import reverse -from django.template.context_processors import csrf -from django.template.context import RequestContext -from django.conf import settings + from . import TrustedDevice -from django.contrib.auth.decorators import login_required -from user_agents import parse +from .models import * @login_required From 34fcca57c88e91d62b340af6f11602140a77741a Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Wed, 23 Jun 2021 08:39:36 +0200 Subject: [PATCH 08/22] avoid star imports --- mfa/Email.py | 3 ++- mfa/FIDO2.py | 2 +- mfa/TrustedDevice.py | 3 ++- mfa/U2F.py | 2 +- mfa/helpers.py | 2 +- mfa/totp.py | 2 +- mfa/views.py | 2 +- 7 files changed, 9 insertions(+), 7 deletions(-) diff --git a/mfa/Email.py b/mfa/Email.py index 7d52f6c..e7f055f 100644 --- a/mfa/Email.py +++ b/mfa/Email.py @@ -2,6 +2,7 @@ import datetime import random from random import randint +from django.conf import settings from django.contrib.auth import get_user_model from django.http import HttpResponseRedirect from django.shortcuts import render @@ -15,7 +16,7 @@ except: from django.urls import reverse from .Common import send -from .models import * +from .models import User_Keys from .views import login diff --git a/mfa/FIDO2.py b/mfa/FIDO2.py index fa0fe1e..9bb921f 100644 --- a/mfa/FIDO2.py +++ b/mfa/FIDO2.py @@ -17,7 +17,7 @@ from fido2.server import Fido2Server, PublicKeyCredentialRpEntity from fido2.utils import websafe_decode, websafe_encode from .Common import get_redirect_url -from .models import * +from .models import User_Keys from .views import login, reset_cookie diff --git a/mfa/TrustedDevice.py b/mfa/TrustedDevice.py index 9e225cd..780fa99 100644 --- a/mfa/TrustedDevice.py +++ b/mfa/TrustedDevice.py @@ -3,6 +3,7 @@ import string from datetime import datetime, timedelta import user_agents +from django.conf import settings from django.http import HttpResponse from django.shortcuts import render from django.template.context import RequestContext @@ -11,7 +12,7 @@ from django.utils import timezone from jose import jwt from .Common import send -from .models import * +from .models import User_Keys def id_generator(size=6, chars=string.ascii_uppercase + string.digits): diff --git a/mfa/U2F.py b/mfa/U2F.py index 4bc2b02..4a24d61 100644 --- a/mfa/U2F.py +++ b/mfa/U2F.py @@ -19,7 +19,7 @@ from u2flib_server.u2f import ( complete_registration, ) -from .models import * +from .models import User_Keys from .views import login diff --git a/mfa/helpers.py b/mfa/helpers.py index 9c0f2b8..c2f9640 100644 --- a/mfa/helpers.py +++ b/mfa/helpers.py @@ -3,7 +3,7 @@ import simplejson from django.shortcuts import HttpResponse from . import FIDO2, U2F, TrustedDevice, totp -from .models import * +from .models import User_Keys from .views import goto, verify diff --git a/mfa/totp.py b/mfa/totp.py index 6400d7e..8e90842 100644 --- a/mfa/totp.py +++ b/mfa/totp.py @@ -12,7 +12,7 @@ from django.template.context_processors import csrf from django.utils import timezone from django.views.decorators.cache import never_cache -from .models import * +from .models import User_Keys from .views import login diff --git a/mfa/views.py b/mfa/views.py index 7394a6e..ffa7b2d 100644 --- a/mfa/views.py +++ b/mfa/views.py @@ -15,7 +15,7 @@ except: from django.core.urlresolvers import reverse from . import TrustedDevice -from .models import * +from .models import User_Keys @login_required From 84f93444a3963afc80798d1741275dfcd52e2c22 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Wed, 23 Jun 2021 08:43:09 +0200 Subject: [PATCH 09/22] fix unused imports --- mfa/TrustedDevice.py | 1 - mfa/U2F.py | 1 + mfa/helpers.py | 3 +-- mfa/totp.py | 2 +- mfa/views.py | 3 --- 5 files changed, 3 insertions(+), 7 deletions(-) diff --git a/mfa/TrustedDevice.py b/mfa/TrustedDevice.py index 780fa99..127ee19 100644 --- a/mfa/TrustedDevice.py +++ b/mfa/TrustedDevice.py @@ -6,7 +6,6 @@ import user_agents from django.conf import settings from django.http import HttpResponse from django.shortcuts import render -from django.template.context import RequestContext from django.template.context_processors import csrf from django.utils import timezone from jose import jwt diff --git a/mfa/U2F.py b/mfa/U2F.py index 4a24d61..c6d8e0e 100644 --- a/mfa/U2F.py +++ b/mfa/U2F.py @@ -19,6 +19,7 @@ from u2flib_server.u2f import ( complete_registration, ) +from .Common import get_redirect_url from .models import User_Keys from .views import login diff --git a/mfa/helpers.py b/mfa/helpers.py index c2f9640..fed0457 100644 --- a/mfa/helpers.py +++ b/mfa/helpers.py @@ -1,10 +1,9 @@ -import pyotp import simplejson from django.shortcuts import HttpResponse from . import FIDO2, U2F, TrustedDevice, totp from .models import User_Keys -from .views import goto, verify +from .views import verify def has_mfa(request, username): diff --git a/mfa/totp.py b/mfa/totp.py index 8e90842..72288c3 100644 --- a/mfa/totp.py +++ b/mfa/totp.py @@ -7,11 +7,11 @@ import simplejson from django.conf import settings from django.http import HttpResponse from django.shortcuts import render -from django.template.context import RequestContext from django.template.context_processors import csrf from django.utils import timezone from django.views.decorators.cache import never_cache +from .Common import get_redirect_url from .models import User_Keys from .views import login diff --git a/mfa/views.py b/mfa/views.py index ffa7b2d..7827adf 100644 --- a/mfa/views.py +++ b/mfa/views.py @@ -1,12 +1,9 @@ import importlib from django.conf import settings -from django.contrib import auth from django.contrib.auth.decorators import login_required from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import render -from django.template.context import RequestContext -from django.template.context_processors import csrf from user_agents import parse try: From b18dfe2bb662a247ace39d30e09c0150b463aa47 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Thu, 17 Jun 2021 09:00:54 +0200 Subject: [PATCH 10/22] strip whitespace in docs --- CHANGELOG.md | 14 +++++++------- EXAMPLE.md | 2 +- README.md | 32 ++++++++++++++++---------------- docs/change_login.md | 6 +++--- docs/installation.md | 16 ++++++++-------- mkdocs.yml | 2 +- 6 files changed, 36 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1afe5f2..a810f67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ ## 2.1.1 * Fixed: FIDO2 version in requirements.txt file. - + ## 2.1.0 * Added Support for Touch ID for Mac OSx and iOS 14 on Safari @@ -26,27 +26,27 @@ * Fixed: __version__ to show correct version ## 2.0.2 - * Added: A missing migration + * Added: A missing migration thnks to @swainn ## 2.0.1 * Fixed: issue in migration between Postgres and SQLite - thnks to @swainn and @willingham + thnks to @swainn and @willingham ## 2.0 * Dropped support to djangp-1.8 and Python 2.7 * Added: never-cache decorator - * Fixes to Make Email Method More Robust + * Fixes to Make Email Method More Robust * Addresses several structure and style issues with TOTP and Email dialogs * Updated to fido2 0.8.1 - + Thanks to @swainn ## v1.9.1 * Fixed: is_authenticated #13 * Fixed: is_anonymous #6 - - thanks to @d3cline, + + thanks to @d3cline, ## v1.7 * Better Error Management diff --git a/EXAMPLE.md b/EXAMPLE.md index b38b93a..bd38a7e 100644 --- a/EXAMPLE.md +++ b/EXAMPLE.md @@ -5,4 +5,4 @@ 1. activate env `source venv/bin/activate` 1. install requirements `pip install -r requirements.txt` 1. migrate `python manage.py migrate` -1. create super user 'python manage.py createsuperuser' \ No newline at end of file +1. create super user 'python manage.py createsuperuser' \ No newline at end of file diff --git a/README.md b/README.md index 7d0401b..369921a 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,9 @@ A Django app that handles MFA, it supports TOTP, U2F, FIDO2 U2F (Web Authn), Ema [![Downloads Count](https://static.pepy.tech/personalized-badge/django-mfa2?period=total&units=international_system&left_color=black&right_color=green&left_text=Downloads)](https://pepy.tech/project/django-mfa2) ### Conda Stats -[![Conda Recipe](https://img.shields.io/badge/recipe-django--mfa2-green.svg)](https://anaconda.org/conda-forge/django-mfa2) -[![Conda Downloads](https://img.shields.io/conda/dn/conda-forge/django-mfa2.svg)](https://anaconda.org/conda-forge/django-mfa2) -[![Conda Version](https://img.shields.io/conda/vn/conda-forge/django-mfa2.svg)](https://anaconda.org/conda-forge/django-mfa2) +[![Conda Recipe](https://img.shields.io/badge/recipe-django--mfa2-green.svg)](https://anaconda.org/conda-forge/django-mfa2) +[![Conda Downloads](https://img.shields.io/conda/dn/conda-forge/django-mfa2.svg)](https://anaconda.org/conda-forge/django-mfa2) +[![Conda Version](https://img.shields.io/conda/vn/conda-forge/django-mfa2.svg)](https://anaconda.org/conda-forge/django-mfa2) Web Authencation API (WebAuthn) is state-of-the art techology that is expected to replace passwords. @@ -40,17 +40,17 @@ Depends on * fido2==0.9.0 # Installation -1. using pip +1. using pip `pip install django-mfa2` -2. Using Conda forge - +2. Using Conda forge + `conda config --add channels conda-forge` - + `conda install django-mfa2` - + For more info, see the conda-forge repo (https://github.com/conda-forge/django-mfa2-feedstock) - + Thanks for [swainn](https://github.com/swainn) for adding package to conda-forge # Usage @@ -65,7 +65,7 @@ Depends on `python manage.py collectstatic` 1. Add the following settings to your file - ```python + ```python 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 @@ -75,7 +75,7 @@ Depends on MFA_RECHECK_MAX=30 # Maximum in seconds 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 + MFA_OWNED_BY_ENTERPRISE = FALSE # Who owns security keys TOKEN_ISSUER_NAME="PROJECT_NAME" #TOTP Issuer name @@ -90,7 +90,7 @@ Depends on * TOTP * Trusted_Devices * Email - + **Notes**: * Starting version 1.1, ~~FIDO_LOGIN_URL~~ isn't required for FIDO2 anymore. * Starting version 1.7.0, Key owners can be specified. @@ -99,7 +99,7 @@ Depends on 1. Break your login function Usually your login function will check for username and password, log the user in if the username and password are correct and create the user session, to support mfa, this has to change - + * authenticate the user * if username and password are correct , check if the user has mfa or not * if user has mfa then redirect to mfa page @@ -107,17 +107,17 @@ Depends on ```python def login(request): # this function handles the login form POST - user = auth.authenticate(username=username, password=password) + user = auth.authenticate(username=username, password=password) if user is not None: # if the user object exist from mfa.helpers import has_mfa res = has_mfa(username = username,request=request) # has_mfa returns false or HttpResponseRedirect if res: return res - return log_user_in(request,username=user.username) + return log_user_in(request,username=user.username) #log_user_in is a function that handles creatung user session, it should be in the setting file as MFA_CALLBACK ``` 1. Add mfa to urls.py - ```python + ```python import mfa import mfa.TrustedDevice urls_patterns= [ diff --git a/docs/change_login.md b/docs/change_login.md index d1425e6..ed4d7af 100644 --- a/docs/change_login.md +++ b/docs/change_login.md @@ -3,7 +3,7 @@ ## Break your login function Usually your login function will check for username and password, log the user in if the username and password are correct and create the user session, to support mfa, this has to change - + * authenticate the user * if username and password are correct , check if the user has mfa or not * if user has mfa then redirect to mfa page @@ -11,13 +11,13 @@ Usually your login function will check for username and password, log the user i ```python def login(request): # this function handles the login form POST - user = auth.authenticate(username=username, password=password) + user = auth.authenticate(username=username, password=password) if user is not None: # if the user object exist from mfa.helpers import has_mfa res = has_mfa(username = username,request=request) # has_mfa returns false or HttpResponseRedirect if res: return res - return log_user_in(request,username=user.username) + return log_user_in(request,username=user.username) #log_user_in is a function that handles creatung user session, it should be in the setting file as MFA_CALLBACK ``` diff --git a/docs/installation.md b/docs/installation.md index 0859b72..eb4b098 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,5 +1,5 @@ # Installation & Configuration -1. Install the package +1. Install the package ```sh pip install django-mfa2 ``` @@ -17,27 +17,27 @@ MFA_RECHECK=True # Allow random rechecking of the user MFA_RECHECK_MIN=10 # Minimum interval in seconds MFA_RECHECK_MAX=30 # Maximum in seconds - MFA_QUICKLOGIN=True # Allow quick login for returning users by provide only their 2FA - + MFA_QUICKLOGIN=True # Allow quick login for returning users by provide only their 2FA + TOKEN_ISSUER_NAME="PROJECT_NAME" #TOTP Issuer name - + U2F_APPID="https://localhost" #URL For U2 FIDO_SERVER_ID=u"localehost" # Server rp id for FIDO2, it the full domain of your project FIDO_SERVER_NAME=u"PROJECT_NAME" FIDO_LOGIN_URL=BASE_URL ``` - + **Method Names** * U2F * FIDO2 * TOTP * Trusted_Devices * Email - + **Note**: Starting version 1.1, ~~FIDO_LOGIN_URL~~ isn't required for FIDO2 anymore. - + 1. Add mfa to urls.py - + ```python import mfa import mfa.TrustedDevice diff --git a/mkdocs.yml b/mkdocs.yml index a4f0f2c..9040671 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,7 +2,7 @@ site_name: MkLorum nav: - Home: index.md - Installation: installation.md - - Code Changes: change_login.md + - Code Changes: change_login.md theme: readthedocs markdown_extensions: - fenced_code From e8ce96c40485bb2704e2e0cc317e287f81935cfd Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Wed, 23 Jun 2021 08:07:32 +0200 Subject: [PATCH 11/22] fix typos --- CHANGELOG.md | 2 +- README.md | 2 +- docs/installation.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a810f67..8219064 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2.2.0 * Added: MFA_REDIRECT_AFTER_REGISTRATION settings parameter - * Fixed: Deprecation error for NULBooleanField + * Fixed: Deprecation error for NullBooleanField ## 2.1.2 * Fixed: Getting timestamp on Python 3.7 as ("%s") is raising an exception diff --git a/README.md b/README.md index 369921a..3c80637 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ Depends on '....', ] ``` -1. Provide `mfa_auth_base.html` in your templaes with block called 'head' and 'content' +1. Provide `mfa_auth_base.html` in your templates with block called 'head' and 'content' The template will be included during the user login. If you will use Email Token method, then you have to provide template named `mfa_email_token_template.html` that will content the format of the email with parameter named `user` and `otp`. 1. To match the look and feel of your project, MFA includes `base.html` but it needs blocks named `head` & `content` to added its content to it. diff --git a/docs/installation.md b/docs/installation.md index eb4b098..33ef8a3 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -48,7 +48,7 @@ '....', ] ``` -1. Provide `mfa_auth_base.html` in your templaes with block called 'head' and 'content' +1. Provide `mfa_auth_base.html` in your templates with block called 'head' and 'content' The template will be included during the user login. If you will use Email Token method, then you have to provide template named `mfa_email_token_template.html` that will content the format of the email with parameter named `user` and `otp`. 1. To match the look and feel of your project, MFA includes `base.html` but it needs blocks named `head` & `content` to added its content to it. From ec16539c34b4bd00d3f5880a7173a5eb1ac9380e Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Wed, 23 Jun 2021 08:09:38 +0200 Subject: [PATCH 12/22] cleanup code examples in README --- README.md | 64 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 3c80637..482b1ab 100644 --- a/README.md +++ b/README.md @@ -106,27 +106,29 @@ Depends on * if user doesn't have mfa then call your function to create the user session ```python - def login(request): # this function handles the login form POST + from mfa.helpers import has_mfa + + def login(request): # this function handles the login form POST user = auth.authenticate(username=username, password=password) - if user is not None: # if the user object exist - from mfa.helpers import has_mfa - res = has_mfa(username = username,request=request) # has_mfa returns false or HttpResponseRedirect + if user is not None: # if the user object exist + res = has_mfa(username=username, request=request) # has_mfa returns false or HttpResponseRedirect if res: return res - return log_user_in(request,username=user.username) - #log_user_in is a function that handles creatung user session, it should be in the setting file as MFA_CALLBACK + return log_user_in(request, username=user.username) + # log_user_in is a function that handles creating user session, it should be in the setting file as MFA_CALLBACK ``` 1. Add mfa to urls.py ```python import mfa import mfa.TrustedDevice - urls_patterns= [ - '...', - url(r'^mfa/', include('mfa.urls')), - url(r'devices/add$', mfa.TrustedDevice.add,name="mfa_add_new_trusted_device"), # This short link to add new trusted device - '....', - ] - ``` + + urls_patterns = [ + '...', + url(r'^mfa/', include('mfa.urls')), + url(r'devices/add$', mfa.TrustedDevice.add,name="mfa_add_new_trusted_device"), # This short link to add new trusted device + '....', + ] + ``` 1. Provide `mfa_auth_base.html` in your templates with block called 'head' and 'content' The template will be included during the user login. If you will use Email Token method, then you have to provide template named `mfa_email_token_template.html` that will content the format of the email with parameter named `user` and `otp`. @@ -140,40 +142,42 @@ For Example, See https://github.com/mkalioby/AutoDeploy/commit/5f1d94b1804e0aa33 To be able to go passwordless for returning users, create a cookie named 'base_username' containing username as shown in snippet below ```python - response = render(request, 'Dashboard.html', context)) - if request.session.get("mfa",{}).get("verified",False) and getattr(settings,"MFA_QUICKLOGIN",False): - if request.session["mfa"]["method"]!="Trusted Device": - response.set_cookie("base_username", request.user.username, path="/",max_age = 15*24*60*60) - return response +response = render(request, 'Dashboard.html', context)) +if request.session.get("mfa", {}).get("verified", False) and getattr(settings, "MFA_QUICKLOGIN", False): + if request.session["mfa"]["method"] != "Trusted Device": + response.set_cookie("base_username", request.user.username, path="/", max_age=15 * 24 * 60 * 60) +return response ``` Second, update the GET part of your login view ```python - if "mfa" in settings.INSTALLED_APPS and getattr(settings,"MFA_QUICKLOGIN",False) and request.COOKIES.get('base_username'): - username=request.COOKIES.get('base_username') - from mfa.helpers import has_mfa - res = has_mfa(username = username,request=request,) - if res: return res - ## continue and return the form. +from mfa.helpers import has_mfa + +if "mfa" in settings.INSTALLED_APPS and getattr(settings, "MFA_QUICKLOGIN", False) and request.COOKIES.get('base_username'): + username=request.COOKIES.get('base_username') + res = has_mfa(username=username, request=request) + if res: + return res + # continue and return the form. ``` # Checking MFA on Client Side Sometimes you like to verify that the user is still there so simple you can ask django-mfa2 to check that for you ```html - {% include 'mfa_check.html' %} +{% include 'mfa_check.html' %} ``` ````js function success_func() { - //logic if mfa check succeeds + // logic if mfa check succeeds } function fail_func() { - //logic if mfa check fails + // logic if mfa check fails } function some_func() { - recheck_mfa(success_func,fail_func,MUST_BE_MFA) - //MUST_BE_MFA true or false, if the user must has with MFA - } + recheck_mfa(success_func, fail_func, MUST_BE_MFA) + // MUST_BE_MFA true or false, if the user must has with MFA +} ```` From 714fb68a6581f48a8fa9b478941a5054a9094b42 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Thu, 17 Jun 2021 09:22:17 +0200 Subject: [PATCH 13/22] rm python2 compat code --- mfa/models.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/mfa/models.py b/mfa/models.py index ccc8ffc..e770a3b 100644 --- a/mfa/models.py +++ b/mfa/models.py @@ -25,18 +25,15 @@ class User_Keys(models.Model): {"username": self.username, "key": self.properties["key"]}, settings.SECRET_KEY, ) - super(User_Keys, self).save( + super().save( force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields, ) - def __unicode__(self): - return "%s -- %s" % (self.username, self.key_type) - def __str__(self): - return self.__unicode__() + return "%s -- %s" % (self.username, self.key_type) class Meta: app_label = "mfa" From 3d133b3fff29852724f8412ea5edfb1cdb8089e8 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Thu, 17 Jun 2021 11:02:31 +0200 Subject: [PATCH 14/22] use *args, **kwargs with super() --- mfa/models.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/mfa/models.py b/mfa/models.py index e770a3b..811f2f3 100644 --- a/mfa/models.py +++ b/mfa/models.py @@ -14,9 +14,7 @@ class User_Keys(models.Model): last_used = models.DateTimeField(null=True, default=None, blank=True) owned_by_enterprise = models.BooleanField(default=None, null=True, blank=True) - def save( - self, force_insert=False, force_update=False, using=None, update_fields=None - ): + def save(self, *args, **kwargs): if ( self.key_type == "Trusted Device" and self.properties.get("signature", "") == "" @@ -25,12 +23,7 @@ class User_Keys(models.Model): {"username": self.username, "key": self.properties["key"]}, settings.SECRET_KEY, ) - super().save( - force_insert=force_insert, - force_update=force_update, - using=using, - update_fields=update_fields, - ) + super().save(*args, **kwargs) def __str__(self): return "%s -- %s" % (self.username, self.key_type) From f654debb9821a12fa71e51a6a6580bd75b26372d Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Wed, 23 Jun 2021 08:54:13 +0200 Subject: [PATCH 15/22] rm compat for is_authenticated was removed in django 2.0 https://docs.djangoproject.com/en/3.1/releases/1.10/#user-is-auth-anon-deprecation --- mfa/FIDO2.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/mfa/FIDO2.py b/mfa/FIDO2.py index 9bb921f..945c727 100644 --- a/mfa/FIDO2.py +++ b/mfa/FIDO2.py @@ -186,11 +186,7 @@ def authenticate_complete(request): ) ) request.session["mfa"] = mfa - try: - authenticated = request.user.is_authenticated - except: - authenticated = request.user.is_authenticated() - if not authenticated: + if not request.user.is_authenticated: res = login(request) if not "location" in res: return reset_cookie(request) From daece24c6d0d0c3ec5055796bf76442a7b6e595c Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Thu, 17 Jun 2021 10:43:19 +0200 Subject: [PATCH 16/22] use simplified URL routing syntax was introduced in django 2.0 --- example/example/urls.py | 8 ++--- mfa/urls.py | 73 ++++++++++++++++++++--------------------- 2 files changed, 39 insertions(+), 42 deletions(-) diff --git a/example/example/urls.py b/example/example/urls.py index ba1418e..6c0e019 100644 --- a/example/example/urls.py +++ b/example/example/urls.py @@ -14,15 +14,15 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import include, path, re_path +from django.urls import include, path from . import auth, views urlpatterns = [ path("admin/", admin.site.urls), path("mfa/", include("mfa.urls")), - path("auth/login", auth.loginView, name="login"), - path("auth/logout", auth.logoutView, name="logout"), - re_path("^$", views.home, name="home"), + path("auth/login/", auth.loginView, name="login"), + path("auth/logout/", auth.logoutView, name="logout"), + path("", views.home, name="home"), path("registered/", views.registered, name="registered"), ] diff --git a/mfa/urls.py b/mfa/urls.py index 5bdf1ae..8c51473 100644 --- a/mfa/urls.py +++ b/mfa/urls.py @@ -1,44 +1,41 @@ -try: - from django.urls import re_path as url -except: - from django.conf.urls import url +from django.urls import path from . import FIDO2, U2F, Email, TrustedDevice, helpers, totp, views urlpatterns = [ - url(r"totp/start/", totp.start, name="start_new_otop"), - url(r"totp/getToken", totp.getToken, name="get_new_otop"), - url(r"totp/verify", totp.verify, name="verify_otop"), - url(r"totp/auth", totp.auth, name="totp_auth"), - url(r"totp/recheck", totp.recheck, name="totp_recheck"), - url(r"email/start/", Email.start, name="start_email"), - url(r"email/auth/", Email.auth, name="email_auth"), - url(r"u2f/$", U2F.start, name="start_u2f"), - url(r"u2f/bind", U2F.bind, name="bind_u2f"), - url(r"u2f/auth", U2F.auth, name="u2f_auth"), - url(r"u2f/process_recheck", U2F.process_recheck, name="u2f_recheck"), - url(r"u2f/verify", U2F.verify, name="u2f_verify"), - url(r"fido2/$", FIDO2.start, name="start_fido2"), - url(r"fido2/auth", FIDO2.auth, name="fido2_auth"), - url(r"fido2/begin_auth", FIDO2.authenticate_begin, name="fido2_begin_auth"), - url( - r"fido2/complete_auth", FIDO2.authenticate_complete, name="fido2_complete_auth" + path("totp/start/", totp.start, name="start_new_otop"), + path("totp/getToken/", totp.getToken, name="get_new_otop"), + path("totp/verify/", totp.verify, name="verify_otop"), + path("totp/auth/", totp.auth, name="totp_auth"), + path("totp/recheck/", totp.recheck, name="totp_recheck"), + path("email/start/", Email.start, name="start_email"), + path("email/auth/", Email.auth, name="email_auth"), + path("u2f/", U2F.start, name="start_u2f"), + path("u2f/bind/", U2F.bind, name="bind_u2f"), + path("u2f/auth/", U2F.auth, name="u2f_auth"), + path("u2f/process_recheck/", U2F.process_recheck, name="u2f_recheck"), + path("u2f/verify/", U2F.verify, name="u2f_verify"), + path("fido2/", FIDO2.start, name="start_fido2"), + path("fido2/auth/", FIDO2.auth, name="fido2_auth"), + path("fido2/begin_auth/", FIDO2.authenticate_begin, name="fido2_begin_auth"), + path( + "fido2/complete_auth/", FIDO2.authenticate_complete, name="fido2_complete_auth" ), - url(r"fido2/begin_reg", FIDO2.begin_registeration, name="fido2_begin_reg"), - url(r"fido2/complete_reg", FIDO2.complete_reg, name="fido2_complete_reg"), - url(r"fido2/recheck", FIDO2.recheck, name="fido2_recheck"), - url(r"td/$", TrustedDevice.start, name="start_td"), - url(r"td/add", TrustedDevice.add, name="add_td"), - url(r"td/send_link", TrustedDevice.send_email, name="td_sendemail"), - url(r"td/get-ua", TrustedDevice.getUserAgent, name="td_get_useragent"), - url(r"td/trust", TrustedDevice.trust_device, name="td_trust_device"), - url(r"u2f/checkTrusted", TrustedDevice.checkTrusted, name="td_checkTrusted"), - url(r"u2f/secure_device", TrustedDevice.getCookie, name="td_securedevice"), - url(r"^$", views.index, name="mfa_home"), - url(r"goto/(.*)", views.goto, name="mfa_goto"), - url(r"selct_method", views.show_methods, name="mfa_methods_list"), - url(r"recheck", helpers.recheck, name="mfa_recheck"), - url(r"toggleKey", views.toggleKey, name="toggle_key"), - url(r"delete", views.delKey, name="mfa_delKey"), - url(r"reset", views.reset_cookie, name="mfa_reset_cookie"), + path("fido2/begin_reg/", FIDO2.begin_registeration, name="fido2_begin_reg"), + path("fido2/complete_reg/", FIDO2.complete_reg, name="fido2_complete_reg"), + path("fido2/recheck/", FIDO2.recheck, name="fido2_recheck"), + path("td/", TrustedDevice.start, name="start_td"), + path("td/add/", TrustedDevice.add, name="add_td"), + path("td/send_link/", TrustedDevice.send_email, name="td_sendemail"), + path("td/get-ua/", TrustedDevice.getUserAgent, name="td_get_useragent"), + path("td/trust/", TrustedDevice.trust_device, name="td_trust_device"), + path("u2f/checkTrusted/", TrustedDevice.checkTrusted, name="td_checkTrusted"), + path("u2f/secure_device", TrustedDevice.getCookie, name="td_securedevice"), + path("", views.index, name="mfa_home"), + path("goto//", views.goto, name="mfa_goto"), + path("selct_method/", views.show_methods, name="mfa_methods_list"), + path("recheck/", helpers.recheck, name="mfa_recheck"), + path("toggleKey/", views.toggleKey, name="toggle_key"), + path("delete/", views.delKey, name="mfa_delKey"), + path("reset/", views.reset_cookie, name="mfa_reset_cookie"), ] From 81675207d3d34098e9e7e4d7c431e838a911535a Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Wed, 23 Jun 2021 08:46:45 +0200 Subject: [PATCH 17/22] rm urlresolvers compat has been removed in django 2.0 https://docs.djangoproject.com/en/1.10/internals/deprecation/#deprecation-removed-in-2-0 --- mfa/Common.py | 6 +----- mfa/Email.py | 6 +----- mfa/middleware.py | 2 +- mfa/views.py | 6 +----- 4 files changed, 4 insertions(+), 16 deletions(-) diff --git a/mfa/Common.py b/mfa/Common.py index 7d45373..07575b1 100644 --- a/mfa/Common.py +++ b/mfa/Common.py @@ -1,10 +1,6 @@ from django.conf import settings from django.core.mail import EmailMessage - -try: - from django.urls import reverse -except: - from django.core.urlresolver import reverse +from django.urls import reverse def send(to, subject, body): diff --git a/mfa/Email.py b/mfa/Email.py index e7f055f..c337009 100644 --- a/mfa/Email.py +++ b/mfa/Email.py @@ -7,14 +7,10 @@ from django.contrib.auth import get_user_model from django.http import HttpResponseRedirect from django.shortcuts import render from django.template.context_processors import csrf +from django.urls import reverse from django.utils import timezone from django.views.decorators.cache import never_cache -try: - from django.core.urlresolvers import reverse -except: - from django.urls import reverse - from .Common import send from .models import User_Keys from .views import login diff --git a/mfa/middleware.py b/mfa/middleware.py index d61e956..818222e 100644 --- a/mfa/middleware.py +++ b/mfa/middleware.py @@ -1,8 +1,8 @@ import time from django.conf import settings -from django.core.urlresolvers import reverse from django.http import HttpResponseRedirect +from django.urls import reverse def process(request): diff --git a/mfa/views.py b/mfa/views.py index 7827adf..d9d9fbd 100644 --- a/mfa/views.py +++ b/mfa/views.py @@ -4,13 +4,9 @@ from django.conf import settings from django.contrib.auth.decorators import login_required from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import render +from django.urls import reverse from user_agents import parse -try: - from django.urls import reverse -except: - from django.core.urlresolvers import reverse - from . import TrustedDevice from .models import User_Keys From f4d8934ef5fae03b48e2eb7204d63d36b100362d Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Thu, 17 Jun 2021 09:20:02 +0200 Subject: [PATCH 18/22] use JsonResponse --- mfa/FIDO2.py | 50 +++++++++++++++----------------------------------- mfa/U2F.py | 6 ++---- mfa/helpers.py | 27 ++++++--------------------- mfa/totp.py | 27 ++++++++++----------------- 4 files changed, 33 insertions(+), 77 deletions(-) diff --git a/mfa/FIDO2.py b/mfa/FIDO2.py index 945c727..a595eaa 100644 --- a/mfa/FIDO2.py +++ b/mfa/FIDO2.py @@ -3,9 +3,8 @@ import random import time import traceback -import simplejson from django.conf import settings -from django.http import HttpResponse +from django.http import HttpResponse, JsonResponse from django.shortcuts import render from django.template.context_processors import csrf from django.utils import timezone @@ -75,13 +74,11 @@ def complete_reg(request): uk.owned_by_enterprise = getattr(settings, "MFA_OWNED_BY_ENTERPRISE", False) uk.key_type = "FIDO2" uk.save() - return HttpResponse(simplejson.dumps({"status": "OK"})) + return JsonResponse({"status": "OK"}) except Exception as exp: print(traceback.format_exc()) - return HttpResponse( - simplejson.dumps( - {"status": "ERR", "message": "Error on server, please try again later"} - ) + return JsonResponse( + {"status": "ERR", "message": "Error on server, please try again later"} ) @@ -138,27 +135,19 @@ def authenticate_complete(request): signature, ) except ValueError: - return HttpResponse( - simplejson.dumps( - { - "status": "ERR", - "message": "Wrong challenge received, make sure that this is your security and try again.", - } - ), - content_type="application/json", + return JsonResponse( + { + "status": "ERR", + "message": "Wrong challenge received, make sure that this is your security and try again.", + } ) except Exception as excep: print(traceback.format_exc()) - return HttpResponse( - simplejson.dumps({"status": "ERR", "message": excep.message}), - content_type="application/json", - ) + return JsonResponse({"status": "ERR", "message": excep.message}) if request.session.get("mfa_recheck", False): request.session["mfa"]["rechecked_at"] = time.time() - return HttpResponse( - simplejson.dumps({"status": "OK"}), content_type="application/json" - ) + return JsonResponse({"status": "OK"}) else: keys = User_Keys.objects.filter( username=username, key_type="FIDO2", enabled=1 @@ -190,18 +179,9 @@ def authenticate_complete(request): res = login(request) if not "location" in res: return reset_cookie(request) - return HttpResponse( - simplejson.dumps( - {"status": "OK", "redirect": res["location"]} - ), - content_type="application/json", + return JsonResponse( + {"status": "OK", "redirect": res["location"]} ) - return HttpResponse( - simplejson.dumps({"status": "OK"}), - content_type="application/json", - ) + return JsonResponse({"status": "OK"}) except Exception as exp: - return HttpResponse( - simplejson.dumps({"status": "ERR", "message": str(exp)}), - content_type="application/json", - ) + return JsonResponse({"status": "ERR", "message": str(exp)}) diff --git a/mfa/U2F.py b/mfa/U2F.py index c6d8e0e..627c374 100644 --- a/mfa/U2F.py +++ b/mfa/U2F.py @@ -8,7 +8,7 @@ from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.serialization import Encoding from django.conf import settings -from django.http import HttpResponse +from django.http import HttpResponse, JsonResponse from django.shortcuts import render from django.template.context_processors import csrf from django.utils import timezone @@ -38,9 +38,7 @@ def process_recheck(request): x = validate(request, request.user.username) if x == True: request.session["mfa"]["rechecked_at"] = time.time() - return HttpResponse( - simplejson.dumps({"recheck": True}), content_type="application/json" - ) + return JsonResponse({"recheck": True}) return x diff --git a/mfa/helpers.py b/mfa/helpers.py index fed0457..b87dc57 100644 --- a/mfa/helpers.py +++ b/mfa/helpers.py @@ -1,5 +1,4 @@ -import simplejson -from django.shortcuts import HttpResponse +from django.http import JsonResponse from . import FIDO2, U2F, TrustedDevice, totp from .models import User_Keys @@ -22,26 +21,12 @@ def is_mfa(request, ignore_methods=[]): def recheck(request): method = request.session.get("mfa", {}).get("method", None) if not method: - return HttpResponse( - simplejson.dumps({"res": False}), content_type="application/json" - ) + return JsonResponse({"res": False}) if method == "Trusted Device": - return HttpResponse( - simplejson.dumps({"res": TrustedDevice.verify(request)}), - content_type="application/json", - ) + return JsonResponse({"res": TrustedDevice.verify(request)}) elif method == "U2F": - return HttpResponse( - simplejson.dumps({"html": U2F.recheck(request).content}), - content_type="application/json", - ) + return JsonResponse({"html": U2F.recheck(request).content}) elif method == "FIDO2": - return HttpResponse( - simplejson.dumps({"html": FIDO2.recheck(request).content}), - content_type="application/json", - ) + return JsonResponse({"html": FIDO2.recheck(request).content}) elif method == "TOTP": - return HttpResponse( - simplejson.dumps({"html": totp.recheck(request).content}), - content_type="application/json", - ) + return JsonResponse({"html": totp.recheck(request).content}) diff --git a/mfa/totp.py b/mfa/totp.py index 72288c3..7235086 100644 --- a/mfa/totp.py +++ b/mfa/totp.py @@ -3,9 +3,8 @@ import random import time import pyotp -import simplejson from django.conf import settings -from django.http import HttpResponse +from django.http import HttpResponse, JsonResponse from django.shortcuts import render from django.template.context_processors import csrf from django.utils import timezone @@ -32,13 +31,9 @@ def recheck(request): if request.method == "POST": if verify_login(request, request.user.username, token=request.POST["otp"]): request.session["mfa"]["rechecked_at"] = time.time() - return HttpResponse( - simplejson.dumps({"recheck": True}), content_type="application/json" - ) + return JsonResponse({"recheck": True}) else: - return HttpResponse( - simplejson.dumps({"recheck": False}), content_type="application/json" - ) + return JsonResponse({"recheck": False}) return render(request, "TOTP/recheck.html", context) @@ -72,15 +67,13 @@ def getToken(request): secret_key = pyotp.random_base32() totp = pyotp.TOTP(secret_key) request.session["new_mfa_answer"] = totp.now() - return HttpResponse( - simplejson.dumps( - { - "qr": pyotp.totp.TOTP(secret_key).provisioning_uri( - str(request.user.username), issuer_name=settings.TOKEN_ISSUER_NAME - ), - "secret_key": secret_key, - } - ) + return JsonResponse( + { + "qr": pyotp.totp.TOTP(secret_key).provisioning_uri( + str(request.user.username), issuer_name=settings.TOKEN_ISSUER_NAME + ), + "secret_key": secret_key, + } ) From ba4e7f9a1755897dc734e1fd711e37011bb9202f Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Thu, 17 Jun 2021 10:35:34 +0200 Subject: [PATCH 19/22] lint: fix boolean expressions --- mfa/FIDO2.py | 2 +- mfa/TrustedDevice.py | 2 +- mfa/U2F.py | 6 +++--- mfa/views.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/mfa/FIDO2.py b/mfa/FIDO2.py index a595eaa..cb29b2b 100644 --- a/mfa/FIDO2.py +++ b/mfa/FIDO2.py @@ -177,7 +177,7 @@ def authenticate_complete(request): request.session["mfa"] = mfa if not request.user.is_authenticated: res = login(request) - if not "location" in res: + if "location" not in res: return reset_cookie(request) return JsonResponse( {"status": "OK", "redirect": res["location"]} diff --git a/mfa/TrustedDevice.py b/mfa/TrustedDevice.py index 127ee19..c0cc1fd 100644 --- a/mfa/TrustedDevice.py +++ b/mfa/TrustedDevice.py @@ -117,7 +117,7 @@ def start(request): td.save() request.session["td_id"] = td.id try: - if td == None: + if td is None: td = User_Keys.objects.get(id=request.session["td_id"]) context = {"key": td.properties["key"]} except: diff --git a/mfa/U2F.py b/mfa/U2F.py index 627c374..545cbf7 100644 --- a/mfa/U2F.py +++ b/mfa/U2F.py @@ -36,7 +36,7 @@ def recheck(request): def process_recheck(request): x = validate(request, request.user.username) - if x == True: + if x is True: request.session["mfa"]["rechecked_at"] = time.time() return JsonResponse({"recheck": True}) return x @@ -57,7 +57,7 @@ def validate(request, username): data = simplejson.loads(request.POST["response"]) res = check_errors(request, data) - if res != True: + if res is not True: return res challenge = request.session.pop("_u2f_challenge_") @@ -135,7 +135,7 @@ def sign(username): def verify(request): x = validate(request, request.session["base_username"]) - if x == True: + if x is True: return login(request) else: return x diff --git a/mfa/views.py b/mfa/views.py index d9d9fbd..85d3d83 100644 --- a/mfa/views.py +++ b/mfa/views.py @@ -72,7 +72,7 @@ def delKey(request): def __get_callable_function__(func_path): - if not "." in func_path: + if "." not in func_path: raise Exception("class Name should include modulename.classname") parsed_str = func_path.split(".") @@ -90,7 +90,7 @@ def toggleKey(request): q = User_Keys.objects.filter(username=request.user.username, id=id) if q.count() == 1: key = q[0] - if not key.key_type in settings.MFA_HIDE_DISABLE: + if key.key_type not in settings.MFA_HIDE_DISABLE: key.enabled = not key.enabled key.save() return HttpResponse("OK") From 68e257d60eaa35451bb50d4346fa227f507ea29f Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Thu, 17 Jun 2021 11:02:20 +0200 Subject: [PATCH 20/22] rename model to UserKey --- mfa/Email.py | 6 ++--- mfa/FIDO2.py | 8 +++---- mfa/TrustedDevice.py | 22 +++++++++---------- mfa/U2F.py | 12 +++++----- mfa/helpers.py | 4 ++-- .../0012_rename_user_keys_userkey.py | 17 ++++++++++++++ mfa/models.py | 2 +- mfa/totp.py | 6 ++--- mfa/views.py | 10 ++++----- 9 files changed, 52 insertions(+), 35 deletions(-) create mode 100644 mfa/migrations/0012_rename_user_keys_userkey.py diff --git a/mfa/Email.py b/mfa/Email.py index c337009..2fcac08 100644 --- a/mfa/Email.py +++ b/mfa/Email.py @@ -12,7 +12,7 @@ from django.utils import timezone from django.views.decorators.cache import never_cache from .Common import send -from .models import User_Keys +from .models import UserKey from .views import login @@ -36,7 +36,7 @@ def start(request): context = csrf(request) if request.method == "POST": if request.session["email_secret"] == request.POST["otp"]: # if successful - uk = User_Keys() + uk = UserKey() uk.username = request.user.username uk.key_type = "Email" uk.enabled = 1 @@ -62,7 +62,7 @@ def auth(request): context = csrf(request) if request.method == "POST": if request.session["email_secret"] == request.POST["otp"].strip(): - uk = User_Keys.objects.get( + uk = UserKey.objects.get( username=request.session["base_username"], key_type="Email" ) mfa = {"verified": True, "method": "Email", "id": uk.id} diff --git a/mfa/FIDO2.py b/mfa/FIDO2.py index cb29b2b..59e22b3 100644 --- a/mfa/FIDO2.py +++ b/mfa/FIDO2.py @@ -16,7 +16,7 @@ from fido2.server import Fido2Server, PublicKeyCredentialRpEntity from fido2.utils import websafe_decode, websafe_encode from .Common import get_redirect_url -from .models import User_Keys +from .models import UserKey from .views import login, reset_cookie @@ -65,7 +65,7 @@ def complete_reg(request): request.session["fido_state"], client_data, att_obj ) encoded = websafe_encode(auth_data.credential_data) - uk = User_Keys() + uk = UserKey() uk.username = request.user.username uk.properties = { "device": encoded, @@ -91,7 +91,7 @@ def start(request): def getUserCredentials(username): credentials = [] - for uk in User_Keys.objects.filter(username=username, key_type="FIDO2"): + for uk in UserKey.objects.filter(username=username, key_type="FIDO2"): credentials.append( AttestedCredentialData(websafe_decode(uk.properties["device"])) ) @@ -149,7 +149,7 @@ def authenticate_complete(request): request.session["mfa"]["rechecked_at"] = time.time() return JsonResponse({"status": "OK"}) else: - keys = User_Keys.objects.filter( + keys = UserKey.objects.filter( username=username, key_type="FIDO2", enabled=1 ) for k in keys: diff --git a/mfa/TrustedDevice.py b/mfa/TrustedDevice.py index c0cc1fd..548bce8 100644 --- a/mfa/TrustedDevice.py +++ b/mfa/TrustedDevice.py @@ -11,12 +11,12 @@ from django.utils import timezone from jose import jwt from .Common import send -from .models import User_Keys +from .models import UserKey def id_generator(size=6, chars=string.ascii_uppercase + string.digits): x = "".join(random.choice(chars) for _ in range(size)) - if not User_Keys.objects.filter(properties__shas="$.key=" + x).exists(): + if not UserKey.objects.filter(properties__shas="$.key=" + x).exists(): return x else: return id_generator(size, chars) @@ -25,7 +25,7 @@ def id_generator(size=6, chars=string.ascii_uppercase + string.digits): def getUserAgent(request): id = id = request.session.get("td_id", None) if id: - tk = User_Keys.objects.get(id=id) + tk = UserKey.objects.get(id=id) if tk.properties.get("user_agent", "") != "": ua = user_agents.parse(tk.properties["user_agent"]) res = render(None, "TrustedDevices/user-agent.html", context={"ua": ua}) @@ -34,7 +34,7 @@ def getUserAgent(request): def trust_device(request): - tk = User_Keys.objects.get(id=request.session["td_id"]) + tk = UserKey.objects.get(id=request.session["td_id"]) tk.properties["status"] = "trusted" tk.save() del request.session["td_id"] @@ -46,7 +46,7 @@ def checkTrusted(request): id = request.session.get("td_id", "") if id != "": try: - tk = User_Keys.objects.get(id=id) + tk = UserKey.objects.get(id=id) if tk.properties["status"] == "trusted": res = "OK" except: @@ -55,7 +55,7 @@ def checkTrusted(request): def getCookie(request): - tk = User_Keys.objects.get(id=request.session["td_id"]) + tk = UserKey.objects.get(id=request.session["td_id"]) if tk.properties["status"] == "trusted": context = {"added": True} @@ -76,7 +76,7 @@ def add(request): key = request.POST["key"].replace("-", "").replace(" ", "").upper() context["username"] = request.POST["username"] context["key"] = request.POST["key"] - trusted_keys = User_Keys.objects.filter( + trusted_keys = UserKey.objects.filter( username=request.POST["username"], properties__has="$.key=" + key ) cookie = False @@ -102,7 +102,7 @@ def add(request): def start(request): if ( - User_Keys.objects.filter( + UserKey.objects.filter( username=request.user.username, key_type="Trusted Device" ).count() >= 2 @@ -110,7 +110,7 @@ def start(request): return render(request, "TrustedDevices/start.html", {"not_allowed": True}) td = None if not request.session.get("td_id", None): - td = User_Keys() + td = UserKey() td.username = request.user.username td.properties = {"key": id_generator(), "status": "adding"} td.key_type = "Trusted Device" @@ -118,7 +118,7 @@ def start(request): request.session["td_id"] = td.id try: if td is None: - td = User_Keys.objects.get(id=request.session["td_id"]) + td = UserKey.objects.get(id=request.session["td_id"]) context = {"key": td.properties["key"]} except: del request.session["td_id"] @@ -145,7 +145,7 @@ def verify(request): json = jwt.decode(request.COOKIES.get("deviceid"), settings.SECRET_KEY) if json["username"].lower() == request.session["base_username"].lower(): try: - uk = User_Keys.objects.get( + uk = UserKey.objects.get( username=request.POST["username"].lower(), properties__has="$.key=" + json["key"], ) diff --git a/mfa/U2F.py b/mfa/U2F.py index 545cbf7..6e9980f 100644 --- a/mfa/U2F.py +++ b/mfa/U2F.py @@ -20,7 +20,7 @@ from u2flib_server.u2f import ( ) from .Common import get_redirect_url -from .models import User_Keys +from .models import UserKey from .views import login @@ -63,7 +63,7 @@ def validate(request, username): challenge = request.session.pop("_u2f_challenge_") device, c, t = complete_authentication(challenge, data, [settings.U2F_APPID]) - key = User_Keys.objects.get( + key = UserKey.objects.get( username=username, properties__shas="$.device.publicKey=%s" % device["publicKey"], ) @@ -109,13 +109,13 @@ def bind(request): device, cert = complete_registration(enroll, data, [settings.U2F_APPID]) cert = x509.load_der_x509_certificate(cert, default_backend()) cert_hash = hashlib.md5(cert.public_bytes(Encoding.PEM)).hexdigest() - q = User_Keys.objects.filter(key_type="U2F", properties__icontains=cert_hash) + q = UserKey.objects.filter(key_type="U2F", properties__icontains=cert_hash) if q.exists(): return HttpResponse( "This key is registered before, it can't be registered again." ) - User_Keys.objects.filter(username=request.user.username, key_type="U2F").delete() - uk = User_Keys() + UserKey.objects.filter(username=request.user.username, key_type="U2F").delete() + uk = UserKey() uk.username = request.user.username uk.owned_by_enterprise = getattr(settings, "MFA_OWNED_BY_ENTERPRISE", False) uk.properties = {"device": simplejson.loads(device.json), "cert": cert_hash} @@ -127,7 +127,7 @@ def bind(request): def sign(username): u2f_devices = [ d.properties["device"] - for d in User_Keys.objects.filter(username=username, key_type="U2F") + for d in UserKey.objects.filter(username=username, key_type="U2F") ] challenge = begin_authentication(settings.U2F_APPID, u2f_devices) return [challenge.json, simplejson.dumps(challenge.data_for_client)] diff --git a/mfa/helpers.py b/mfa/helpers.py index b87dc57..18fee3c 100644 --- a/mfa/helpers.py +++ b/mfa/helpers.py @@ -1,12 +1,12 @@ from django.http import JsonResponse from . import FIDO2, U2F, TrustedDevice, totp -from .models import User_Keys +from .models import UserKey from .views import verify def has_mfa(request, username): - if User_Keys.objects.filter(username=username, enabled=1).count() > 0: + if UserKey.objects.filter(username=username, enabled=1).count() > 0: return verify(request, username) return False diff --git a/mfa/migrations/0012_rename_user_keys_userkey.py b/mfa/migrations/0012_rename_user_keys_userkey.py new file mode 100644 index 0000000..ddf4889 --- /dev/null +++ b/mfa/migrations/0012_rename_user_keys_userkey.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.4 on 2021-06-23 07:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('mfa', '0011_auto_20210530_0622'), + ] + + operations = [ + migrations.RenameModel( + old_name='User_Keys', + new_name='UserKey', + ), + ] diff --git a/mfa/models.py b/mfa/models.py index 811f2f3..30ced21 100644 --- a/mfa/models.py +++ b/mfa/models.py @@ -4,7 +4,7 @@ from jose import jwt from jsonfield import JSONField -class User_Keys(models.Model): +class UserKey(models.Model): username = models.CharField(max_length=50) properties = JSONField(null=True) added_on = models.DateTimeField(auto_now_add=True) diff --git a/mfa/totp.py b/mfa/totp.py index 7235086..efa8c6c 100644 --- a/mfa/totp.py +++ b/mfa/totp.py @@ -11,12 +11,12 @@ from django.utils import timezone from django.views.decorators.cache import never_cache from .Common import get_redirect_url -from .models import User_Keys +from .models import UserKey from .views import login def verify_login(request, username, token): - for key in User_Keys.objects.filter(username=username, key_type="TOTP"): + for key in UserKey.objects.filter(username=username, key_type="TOTP"): totp = pyotp.TOTP(key.properties["secret_key"]) if totp.verify(token, valid_window=30): key.last_used = timezone.now() @@ -82,7 +82,7 @@ def verify(request): secret_key = request.GET["key"] totp = pyotp.TOTP(secret_key) if totp.verify(answer, valid_window=60): - uk = User_Keys() + uk = UserKey() uk.username = request.user.username uk.properties = {"secret_key": secret_key} uk.key_type = "TOTP" diff --git a/mfa/views.py b/mfa/views.py index 85d3d83..2f770ac 100644 --- a/mfa/views.py +++ b/mfa/views.py @@ -8,14 +8,14 @@ from django.urls import reverse from user_agents import parse from . import TrustedDevice -from .models import User_Keys +from .models import UserKey @login_required def index(request): keys = [] context = { - "keys": User_Keys.objects.filter(username=request.user.username), + "keys": UserKey.objects.filter(username=request.user.username), "UNALLOWED_AUTHEN_METHODS": settings.MFA_UNALLOWED_METHODS, "HIDE_DISABLE": getattr(settings, "MFA_HIDE_DISABLE", []), } @@ -31,7 +31,7 @@ def index(request): def verify(request, username): request.session["base_username"] = username - keys = User_Keys.objects.filter(username=username, enabled=1) + keys = UserKey.objects.filter(username=username, enabled=1) methods = list(set([k.key_type for k in keys])) if "Trusted Device" in methods and not request.session.get( @@ -63,7 +63,7 @@ def login(request): @login_required def delKey(request): - key = User_Keys.objects.get(id=request.GET["id"]) + key = UserKey.objects.get(id=request.GET["id"]) if key.username == request.user.username: key.delete() return HttpResponse("Deleted Successfully") @@ -87,7 +87,7 @@ def __get_callable_function__(func_path): @login_required def toggleKey(request): id = request.GET["id"] - q = User_Keys.objects.filter(username=request.user.username, id=id) + q = UserKey.objects.filter(username=request.user.username, id=id) if q.count() == 1: key = q[0] if key.key_type not in settings.MFA_HIDE_DISABLE: From 62bb50307ea09cebbf0425583dd6b6cf9dee5983 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Wed, 23 Jun 2021 09:19:21 +0200 Subject: [PATCH 21/22] use underscores for python functions --- mfa/Email.py | 6 +++--- mfa/FIDO2.py | 18 +++++++++--------- mfa/TrustedDevice.py | 6 +++--- mfa/urls.py | 12 ++++++------ mfa/views.py | 4 ++-- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/mfa/Email.py b/mfa/Email.py index 2fcac08..4826360 100644 --- a/mfa/Email.py +++ b/mfa/Email.py @@ -16,7 +16,7 @@ from .models import UserKey from .views import login -def sendEmail(request, username, secret): +def send_email(request, username, secret): """Send Email to the user after rendering `mfa_email_token_template`""" User = get_user_model() key = getattr(User, "USERNAME_FIELD", "username") @@ -51,7 +51,7 @@ def start(request): request.session["email_secret"] = str( randint(0, 100000) ) # generate a random integer - if sendEmail(request, request.user.username, request.session["email_secret"]): + if send_email(request, request.user.username, request.session["email_secret"]): context["sent"] = True return render(request, "Email/Add.html", context) @@ -83,7 +83,7 @@ def auth(request): context["invalid"] = True else: request.session["email_secret"] = str(randint(0, 100000)) - if sendEmail( + if send_email( request, request.session["base_username"], request.session["email_secret"] ): context["sent"] = True diff --git a/mfa/FIDO2.py b/mfa/FIDO2.py index 59e22b3..19a9b1c 100644 --- a/mfa/FIDO2.py +++ b/mfa/FIDO2.py @@ -28,7 +28,7 @@ def recheck(request): return render(request, "FIDO2/recheck.html", context) -def getServer(): +def get_server(): """Get Server Info from settings and returns a Fido2Server""" rp = PublicKeyCredentialRpEntity(settings.FIDO_SERVER_ID, settings.FIDO_SERVER_NAME) return Fido2Server(rp) @@ -36,14 +36,14 @@ def getServer(): def begin_registeration(request): """Starts registering a new FIDO Device, called from API""" - server = getServer() + server = get_server() registration_data, state = server.register_begin( { u"id": request.user.username.encode("utf8"), u"name": (request.user.first_name + " " + request.user.last_name), u"displayName": request.user.username, }, - getUserCredentials(request.user.username), + get_user_credentials(request.user.username), ) request.session["fido_state"] = state @@ -60,7 +60,7 @@ def complete_reg(request): client_data = ClientData(data["clientDataJSON"]) att_obj = AttestationObject((data["attestationObject"])) - server = getServer() + server = get_server() auth_data = server.register_complete( request.session["fido_state"], client_data, att_obj ) @@ -89,7 +89,7 @@ def start(request): return render(request, "FIDO2/Add.html", context) -def getUserCredentials(username): +def get_user_credentials(username): credentials = [] for uk in UserKey.objects.filter(username=username, key_type="FIDO2"): credentials.append( @@ -104,8 +104,8 @@ def auth(request): def authenticate_begin(request): - server = getServer() - credentials = getUserCredentials( + server = get_server() + credentials = get_user_credentials( request.session.get("base_username", request.user.username) ) auth_data, state = server.authenticate_begin(credentials) @@ -118,8 +118,8 @@ def authenticate_complete(request): try: credentials = [] username = request.session.get("base_username", request.user.username) - server = getServer() - credentials = getUserCredentials(username) + server = get_server() + credentials = get_user_credentials(username) data = cbor.decode(request.body) credential_id = data["credentialId"] client_data = ClientData(data["clientDataJSON"]) diff --git a/mfa/TrustedDevice.py b/mfa/TrustedDevice.py index 548bce8..1a9d76f 100644 --- a/mfa/TrustedDevice.py +++ b/mfa/TrustedDevice.py @@ -22,7 +22,7 @@ def id_generator(size=6, chars=string.ascii_uppercase + string.digits): return id_generator(size, chars) -def getUserAgent(request): +def get_user_agent(request): id = id = request.session.get("td_id", None) if id: tk = UserKey.objects.get(id=id) @@ -41,7 +41,7 @@ def trust_device(request): return HttpResponse("OK") -def checkTrusted(request): +def check_trusted(request): res = "" id = request.session.get("td_id", "") if id != "": @@ -54,7 +54,7 @@ def checkTrusted(request): return HttpResponse(res) -def getCookie(request): +def get_cookie(request): tk = UserKey.objects.get(id=request.session["td_id"]) if tk.properties["status"] == "trusted": diff --git a/mfa/urls.py b/mfa/urls.py index 8c51473..2aaab82 100644 --- a/mfa/urls.py +++ b/mfa/urls.py @@ -4,7 +4,7 @@ from . import FIDO2, U2F, Email, TrustedDevice, helpers, totp, views urlpatterns = [ path("totp/start/", totp.start, name="start_new_otop"), - path("totp/getToken/", totp.getToken, name="get_new_otop"), + path("totp/getToken/", totp.get_token, name="get_new_otop"), path("totp/verify/", totp.verify, name="verify_otop"), path("totp/auth/", totp.auth, name="totp_auth"), path("totp/recheck/", totp.recheck, name="totp_recheck"), @@ -27,15 +27,15 @@ urlpatterns = [ path("td/", TrustedDevice.start, name="start_td"), path("td/add/", TrustedDevice.add, name="add_td"), path("td/send_link/", TrustedDevice.send_email, name="td_sendemail"), - path("td/get-ua/", TrustedDevice.getUserAgent, name="td_get_useragent"), + path("td/get-ua/", TrustedDevice.get_user_agent, name="td_get_useragent"), path("td/trust/", TrustedDevice.trust_device, name="td_trust_device"), - path("u2f/checkTrusted/", TrustedDevice.checkTrusted, name="td_checkTrusted"), - path("u2f/secure_device", TrustedDevice.getCookie, name="td_securedevice"), + path("u2f/checkTrusted/", TrustedDevice.check_trusted, name="td_checkTrusted"), + path("u2f/secure_device", TrustedDevice.get_cookie, name="td_securedevice"), path("", views.index, name="mfa_home"), path("goto//", views.goto, name="mfa_goto"), path("selct_method/", views.show_methods, name="mfa_methods_list"), path("recheck/", helpers.recheck, name="mfa_recheck"), - path("toggleKey/", views.toggleKey, name="toggle_key"), - path("delete/", views.delKey, name="mfa_delKey"), + path("toggleKey/", views.toggle_key, name="toggle_key"), + path("delete/", views.del_key, name="mfa_delKey"), path("reset/", views.reset_cookie, name="mfa_reset_cookie"), ] diff --git a/mfa/views.py b/mfa/views.py index 2f770ac..da9450f 100644 --- a/mfa/views.py +++ b/mfa/views.py @@ -62,7 +62,7 @@ def login(request): @login_required -def delKey(request): +def del_key(request): key = UserKey.objects.get(id=request.GET["id"]) if key.username == request.user.username: key.delete() @@ -85,7 +85,7 @@ def __get_callable_function__(func_path): @login_required -def toggleKey(request): +def toggle_key(request): id = request.GET["id"] q = UserKey.objects.filter(username=request.user.username, id=id) if q.count() == 1: From bb88f680a0bdc7e3e770cdf3fd33f1fb4cb2950c Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Wed, 23 Jun 2021 09:22:26 +0200 Subject: [PATCH 22/22] run black in CI --- .github/workflows/main.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..08a0392 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,13 @@ +on: [push] +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: python + run: | + sudo apt-get install python3 + - name: lint + run: | + pip install black + black --check --exclude migrations mfa/ example/ setup.py