diff --git a/mfa/ApproveLogin.py b/mfa/ApproveLogin.py new file mode 100644 index 0000000..e69de29 diff --git a/mfa/FIDO2.py b/mfa/FIDO2.py new file mode 100644 index 0000000..65ed2e4 --- /dev/null +++ b/mfa/FIDO2.py @@ -0,0 +1,119 @@ +from fido2.client import ClientData +from fido2.server import Fido2Server, RelyingParty +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_to_response +from django.template.context import RequestContext +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 +import datetime +from django.utils import timezone + +def recheck(request): + context = csrf(request) + context["mode"]="recheck" + return render_to_response("FIDO2/recheck.html", context, context_instance=RequestContext(request)) + + +def getServer(): + rp = RelyingParty(settings.FIDO_SERVER_ID, settings.FIDO_SERVER_NAME) + return Fido2Server(rp) +def begin_registeration(request): + 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 + + return HttpResponse(cbor.dumps(registration_data),content_type='application/octet-stream') +@csrf_exempt +def complete_reg(request): + try: + data = cbor.loads(request.body)[0] + + 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 + ) + print att_obj.fmt + encoded = websafe_encode(auth_data.credential_data) + uk=User_Keys() + uk.username = request.user.username + uk.properties = {"device":encoded,"type":att_obj.fmt,} + uk.key_type = "FIDO2" + uk.save() + return HttpResponse(simplejson.dumps({'status': 'OK'})) + except Exception as exp: + from raven.contrib.django.raven_compat.models import client + import traceback + client.captureException() + print traceback.format_exc() + return HttpResponse(simplejson.dumps({'status': 'ERR',"message":"Error on server, please try again later"})) +def start(request): + context = csrf(request) + return render_to_response("FIDO2/Add.html", context, RequestContext(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"]))) + return credentials + +def auth(request): + context=csrf(request) + return render_to_response("FIDO2/Auth.html",context,context_instance=RequestContext(request)) + +def authenticate_begin(request): + server = getServer() + 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.dumps(auth_data),content_type="application/octet-stream") + +@csrf_exempt +def authenticate_complete(request): + credentials = [] + username=request.session.get("base_username",request.user.username) + server=getServer() + credentials=getUserCredentials(username) + data = cbor.loads(request.body)[0] + credential_id = data['credentialId'] + client_data = ClientData(data['clientDataJSON']) + auth_data = AuthenticatorData(data['authenticatorData']) + signature = data['signature'] + + cred = server.authenticate_complete( + request.session.pop('fido_state'), + credentials, + credential_id, + client_data, + auth_data, + signature + ) + keys = User_Keys.objects.filter(username=username, key_type="FIDO2",enabled=1) + import random + for k in keys: + if AttestedCredentialData(websafe_decode(k.properties["device"])).credential_id == cred.credential_id: + k.last_used = timezone.now() + k.save() + mfa = {"verified": True, "method": "FIDO2"} + if getattr(settings, "MFA_RECHECK", False): + mfa["next_check"] = int((datetime.datetime.now()+ datetime.timedelta( + seconds=random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))).strftime("%s")) + request.session["mfa"] = mfa + login(request) + return HttpResponse(simplejson.dumps({'status':"OK","redirect":settings.FIDO_LOGIN_URL}),content_type="application/json") + return HttpResponse(simplejson.dumps({'status': "err"}),content_type="application/json") diff --git a/mfa/TrustedDevice.py b/mfa/TrustedDevice.py new file mode 100644 index 0000000..6c1b570 --- /dev/null +++ b/mfa/TrustedDevice.py @@ -0,0 +1,130 @@ +import string +import random +from django.shortcuts import render_to_response,render +from django.http import HttpResponse +from django.template.context import RequestContext +from django.template.context_processors import csrf +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) + +def getUserAgent(request): + id=id=request.session.get("td_id",None) + if id: + 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}) + return HttpResponse(res) + return HttpResponse("") + +def trust_device(request): + tk = User_Keys.objects.get(id=request.session["td_id"]) + 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!="": + try: + tk = User_Keys.objects.get(id=id) + 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_to_response("TrustedDevices/Done.html", context, context_instance=RequestContext(request)) + from datetime import datetime, timedelta + expires = datetime.now() + timedelta(days=180) + 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_to_response("TrustedDevices/Add.html",context,context_instance=RequestContext(request)) + else: + 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 + 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) + if agent.is_pc: + context["invalid"]="This is a PC, it can't used as a trusted device." + else: + tk.properties["user_agent"]=ua + tk.save() + 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." + + return render_to_response("TrustedDevices/Add.html", context, context_instance=RequestContext(request)) + +def start(request): + if User_Keys.objects.filter(username=request.user.username,key_type="Trusted Device").count()>= 2: + return render_to_response("TrustedDevices/start.html",{"not_allowed":True},context_instance=RequestContext(request)) + 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 + try: + 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_to_response("TrustedDevices/start.html",context,context_instance=RequestContext(request)) + +def send_email(request): + body=render(request,"TrustedDevices/email.html",{}).content + from Registry_app.Common import send + if send(request.user.email,"Add Trusted Device Link",body,delay=False): + res="Sent Successfully" + else: + res="Error occured, please try again later." + return HttpResponse(res) + + +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: + 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.save() + request.session["mfa"] = {"verified": True, "method": "Trusted Device"} + return True + except: + return False + return False diff --git a/mfa/U2F.py b/mfa/U2F.py new file mode 100644 index 0000000..3ffb845 --- /dev/null +++ b/mfa/U2F.py @@ -0,0 +1,106 @@ + +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_to_response +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 +from.models import * +from .views import login +from django.utils import timezone + +def recheck(request): + context = csrf(request) + 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_to_response("U2F/recheck.html", context, context_instance=RequestContext(request)) + +def process_recheck(request): + x=validate(request,request.user.username) + if x==True: + 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"] == 4: + return HttpResponse("Invalid Security Key") + if data["errorCode"] == 1: + return auth(request) + return True +def validate(request,username): + import datetime, random + + data = simplejson.loads(request.POST["response"]) + print "Checking Errors" + res= check_errors(request,data) + if res!=True: + return res + print "Checking Challenge" + challenge = request.session.pop('_u2f_challenge_') + device, c, t = complete_authentication(challenge, data, [settings.U2F_APPID]) + print device + 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"} + if getattr(settings, "MFA_RECHECK", False): + mfa["next_check"] = int((datetime.datetime.now() + + datetime.timedelta( + seconds=random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))).strftime("%s")) + 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_to_response("U2F/Auth.html",context,context_instance = RequestContext(request)) + +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) + return render_to_response("U2F/Add.html",context,RequestContext(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]) + 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) + 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() + uk.username = request.user.username + 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")] + challenge = begin_authentication(settings.U2F_APPID, u2f_devices) + return [challenge.json,simplejson.dumps(challenge.data_for_client)] + +def verify(request): + x= validate(request,request.session["base_username"]) + if x==True: + return login(request) + else: return x diff --git a/mfa/__init__.py b/mfa/__init__.py new file mode 100644 index 0000000..6a75453 --- /dev/null +++ b/mfa/__init__.py @@ -0,0 +1 @@ +import urls \ No newline at end of file diff --git a/mfa/admin.py b/mfa/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/mfa/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/mfa/helpers.py b/mfa/helpers.py new file mode 100644 index 0000000..f9e0c09 --- /dev/null +++ b/mfa/helpers.py @@ -0,0 +1,34 @@ +import pyotp +from .models import * +import TrustedDevice +import U2F, FIDO2 +import 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: + 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: + return True + return False + +def recheck(request): + 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") + 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") + + + diff --git a/mfa/middleware.py b/mfa/middleware.py new file mode 100644 index 0000000..4923416 --- /dev/null +++ b/mfa/middleware.py @@ -0,0 +1,13 @@ +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()) + if now >= next_check: + 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 diff --git a/mfa/migrations/0001_initial.py b/mfa/migrations/0001_initial.py new file mode 100644 index 0000000..c243f37 --- /dev/null +++ b/mfa/migrations/0001_initial.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + 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)), + ], + ), + ] diff --git a/mfa/migrations/0002_user_keys_key_type.py b/mfa/migrations/0002_user_keys_key_type.py new file mode 100644 index 0000000..5cb4aef --- /dev/null +++ b/mfa/migrations/0002_user_keys_key_type.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('mfa', '0001_initial'), + ] + + operations = [ + migrations.AddField( + 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 new file mode 100644 index 0000000..49dfd5e --- /dev/null +++ b/mfa/migrations/0003_auto_20181114_2159.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('mfa', '0002_user_keys_key_type'), + ] + + operations = [ + migrations.AlterField( + 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 new file mode 100644 index 0000000..b0bcb78 --- /dev/null +++ b/mfa/migrations/0004_user_keys_enabled.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('mfa', '0003_auto_20181114_2159'), + ] + + operations = [ + migrations.AddField( + 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 new file mode 100644 index 0000000..fb912d3 --- /dev/null +++ b/mfa/migrations/0005_auto_20181115_2014.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import jsonfield.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('mfa', '0004_user_keys_enabled'), + ] + + operations = [ + migrations.RemoveField( + model_name='user_keys', + name='secret_key', + ), + migrations.AddField( + model_name='user_keys', + name='properties', + field=jsonfield.fields.JSONField(null=True), + ), + migrations.RunSQL("alter table mfa_user_keys modify column properties json;") + ] diff --git a/mfa/migrations/0006_trusted_devices.py b/mfa/migrations/0006_trusted_devices.py new file mode 100644 index 0000000..de14a0e --- /dev/null +++ b/mfa/migrations/0006_trusted_devices.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('mfa', '0005_auto_20181115_2014'), + ] + + operations = [ + migrations.CreateModel( + 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)), + ], + ), + ] diff --git a/mfa/migrations/0007_auto_20181230_1549.py b/mfa/migrations/0007_auto_20181230_1549.py new file mode 100644 index 0000000..965bb9e --- /dev/null +++ b/mfa/migrations/0007_auto_20181230_1549.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('mfa', '0006_trusted_devices'), + ] + + operations = [ + migrations.DeleteModel( + name='Trusted_Devices', + ), + migrations.AddField( + 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 new file mode 100644 index 0000000..b555762 --- /dev/null +++ b/mfa/migrations/0008_user_keys_last_used.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('mfa', '0007_auto_20181230_1549'), + ] + + operations = [ + migrations.AddField( + model_name='user_keys', + name='last_used', + field=models.DateTimeField(default=None, null=True, blank=True), + ), + ] diff --git a/mfa/migrations/__init__.py b/mfa/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mfa/models.py b/mfa/models.py new file mode 100644 index 0000000..69508d0 --- /dev/null +++ b/mfa/models.py @@ -0,0 +1,21 @@ +from django.db import models +from jsonfield import JSONField +from jose import jwt +from django.conf import settings +from jsonLookup import hasLookup,shasLookup +JSONField.register_lookup(hasLookup) +JSONField.register_lookup(shasLookup) + +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) + 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) + diff --git a/mfa/templates/ApproveLogin/Add.html b/mfa/templates/ApproveLogin/Add.html new file mode 100644 index 0000000..566549b --- /dev/null +++ b/mfa/templates/ApproveLogin/Add.html @@ -0,0 +1,10 @@ + + + + + Title + + + + + \ No newline at end of file diff --git a/mfa/templates/FIDO2/Add.html b/mfa/templates/FIDO2/Add.html new file mode 100644 index 0000000..c5f942e --- /dev/null +++ b/mfa/templates/FIDO2/Add.html @@ -0,0 +1,62 @@ +{% extends "base.html" %} +{% block head %} + + + +{% endblock %} +{% block content %} +
+
+
+ FIDO2 Security Key +
+
+ + +
+

Your broswer should ask you to confirm you indentity.

+ +
+
+
+ {% include "modal.html" %} +{% endblock %} \ No newline at end of file diff --git a/mfa/templates/FIDO2/Auth.html b/mfa/templates/FIDO2/Auth.html new file mode 100644 index 0000000..e8839b3 --- /dev/null +++ b/mfa/templates/FIDO2/Auth.html @@ -0,0 +1,4 @@ +{% extends "login_base.html" %} +{% block form %} +{% include 'FIDO2/recheck.html' with mode='auth' %} +{% endblock %} \ No newline at end of file diff --git a/mfa/templates/FIDO2/recheck.html b/mfa/templates/FIDO2/recheck.html new file mode 100644 index 0000000..7ccbbdf --- /dev/null +++ b/mfa/templates/FIDO2/recheck.html @@ -0,0 +1,103 @@ + +
+ +
+
+
+ Security Key +
+
+ +
+
+ {% if mode == "auth" %} + Welcome back {{ request.session.base_username }}
+ Not me +
+ + {% endif %} +

please press the button on your security key to prove it is you.

+ {% if mode == "auth" %} +
+ {% elif mode == "recheck" %} + + {% endif %} + {% csrf_token %} + +
+
+
+
+ +
+
+ + {% if request.session.mfa_methods|length > 1 %} + Select Another Method + {% endif %} +
+
+
+
+
+
+ + \ No newline at end of file diff --git a/mfa/templates/MFA.html b/mfa/templates/MFA.html new file mode 100644 index 0000000..a36a78f --- /dev/null +++ b/mfa/templates/MFA.html @@ -0,0 +1,95 @@ +{% extends "base.html" %} +{% block head %} + + + + +{% endblock %} +{% block content %} +
+
+
+
+ + +
+ +
+
+ + + + + + + + + + + + {% for key in keys %} + + + + + + + + + + + {% empty %} + + {% endfor %} +
TypeDate AddedExpires OnDeviceLast UsedStatusDelete
{{ key.key_type }}{{ key.added_on }}{{ key.expires }}{% if key.device %}{{ key.device }}{% endif %}{{ key.last_used }}
You didn't have any keys yet.
+
+
+ {% include "modal.html" %} +{% endblock %} \ No newline at end of file diff --git a/mfa/templates/TOTP/Add.html b/mfa/templates/TOTP/Add.html new file mode 100644 index 0000000..a7892d7 --- /dev/null +++ b/mfa/templates/TOTP/Add.html @@ -0,0 +1,105 @@ +{% extends "base.html" %} +{% block head %} + + + +{% endblock %} +{% block content %} +
+
+
+

Adding Authenticator

+
+
+ +

Scan the image below with the two-factor authentication app on your phone/PC phone/PC. If you can’t use a barcode, + enter this text instead.

+
+ +
+ + +
+ +

Enter the six-digit code from the application

+

After scanning the barcode image, the app will display a six-digit code that you can enter below.

+ +
+
+ + + + +
+
+
+ +
+
+ +
+
+
+ +
+
+ {% include "modal.html" %} +{% endblock %} \ No newline at end of file diff --git a/mfa/templates/TOTP/recheck.html b/mfa/templates/TOTP/recheck.html new file mode 100644 index 0000000..87c5a7c --- /dev/null +++ b/mfa/templates/TOTP/recheck.html @@ -0,0 +1,76 @@ + +
+ +
+
+
+ One Time Password +
+
+ +
+ + + {% csrf_token %} + {% if invalid %} +
+ Sorry, The provided token is not valid. +
+ {% endif %} + {% if quota %} +
+ {{ quota }} +
+ {% endif %} +
+
+
+

Enter the 6-digits on your authenticator.

+
+
+ +
+
+
+
+ + + + + +
+
+ +
+ + +
+
+
+
+
+
+
+ {% if request.session.mfa_methods|length > 1 %} + Select Another Method + {% endif %} +
+
+
+
+
+
diff --git a/mfa/templates/TOTP/verify.html b/mfa/templates/TOTP/verify.html new file mode 100644 index 0000000..f171aca --- /dev/null +++ b/mfa/templates/TOTP/verify.html @@ -0,0 +1,13 @@ +{% extends "login_base.html" %} +{% block head %} + +{% endblock %} +{% block form %} + +{% include "TOTP/recheck.html" with mode='auth' %} + + {% endblock %} diff --git a/mfa/templates/TrustedDevices/Add.html b/mfa/templates/TrustedDevices/Add.html new file mode 100644 index 0000000..e288672 --- /dev/null +++ b/mfa/templates/TrustedDevices/Add.html @@ -0,0 +1,123 @@ +{% extends "login_base.html" %} +{% block head %} + +{% endblock %} +{% block form %} +
+ +
+
+
+ Add Trusted Device +
+
+ {% if success %} +
+ Please check your PC window, to continue the process. +
+ {% elif added %} +
+ Your device is now trusted, please try to login +
+ + {% else %} +
Please make sure you are not in private (incognito) mode
+
+ {% csrf_token %} + {% if invalid %} +
+ {{ invalid }} +
+ {% endif %} + {% if quota %} +
+ {{ quota }} +
+ {% endif %} +
+
+
+{# #} +
+
+
+
+
+
+ + + + + +
+
+
+
+ + + + +
+
+
+ + I confirm that this device is mine and it is only used by me. + +
+ {% comment %} +
+
+ + + + + +
+
+ {% endcomment %} +
+ +
+
+
+
+
+ {% endif %} +
+ +
+
+
+ + + {% endblock %} diff --git a/mfa/templates/TrustedDevices/Done.html b/mfa/templates/TrustedDevices/Done.html new file mode 100644 index 0000000..201b3db --- /dev/null +++ b/mfa/templates/TrustedDevices/Done.html @@ -0,0 +1,27 @@ +{% extends "login_base.html" %} +{% block head %} +{% endblock %} +{% block form %} +
+ +
+
+
+ Add Trusted Device +
+
+
+ Your device is now trusted, please try to login +
+ +
+ +
+
+
+ + + {% endblock %} diff --git a/mfa/templates/TrustedDevices/email.html b/mfa/templates/TrustedDevices/email.html new file mode 100644 index 0000000..9eb8d32 --- /dev/null +++ b/mfa/templates/TrustedDevices/email.html @@ -0,0 +1,4 @@ +

Dear {{ request.user.last_name }}, {{ request.user.first_name }}

+

You requested the link to add a new trusted device, please follow the link below
+{{ HOST }}{% url 'mfa_add_new_trusted_device' %} +

diff --git a/mfa/templates/TrustedDevices/start.html b/mfa/templates/TrustedDevices/start.html new file mode 100644 index 0000000..1ad6d07 --- /dev/null +++ b/mfa/templates/TrustedDevices/start.html @@ -0,0 +1,100 @@ +{% extends "base.html" %} +{% block head %} + + + +{% endblock %} +{% block content %} +
+
+
+

Add Trusted Device

+
+ +
+ {% if not_allowed %} +
You can't add any more devices, you need to remove previously trusted devices first.
+ {% else %} +

Allow access from mobile phone and tables.

+
Steps:
+
    +
  1. Using your mobile/table, open Chrome/Firefox.
  2. +
  3. Go to {{ HOST }}{{ BASE_URL }}devices/add  
  4. +
  5. Enter your username & following 6 digits
    + {{ key|slice:":3" }} - {{ key|slice:"3:" }} +
  6. +
  7. This window will ask to confirm the device.
  8. + +
+ {% endif %} +
+
+
+ {% include "modal.html" %} + {% include 'mfa_check.html' %} +{% endblock %} \ No newline at end of file diff --git a/mfa/templates/TrustedDevices/user-agent.html b/mfa/templates/TrustedDevices/user-agent.html new file mode 100644 index 0000000..58c98d7 --- /dev/null +++ b/mfa/templates/TrustedDevices/user-agent.html @@ -0,0 +1,14 @@ + + + + + + + + + + + + + +
Browser: {{ ua.browser.family }}
Version: {{ ua.browser.version_string }}
Device: {{ ua.device.brand }} / {{ ua.device.model }}
\ No newline at end of file diff --git a/mfa/templates/U2F/Add.html b/mfa/templates/U2F/Add.html new file mode 100644 index 0000000..e08d89e --- /dev/null +++ b/mfa/templates/U2F/Add.html @@ -0,0 +1,48 @@ +{% extends "base.html" %} +{% block head %} + + + +{% endblock %} +{% block content %} +
+
+
+

Adding Security Key

+
+
+

Your secure Key should be flashing now, please press on button.

+ +
+
+
+ {% include "modal.html" %} +{% endblock %} \ No newline at end of file diff --git a/mfa/templates/U2F/Auth.html b/mfa/templates/U2F/Auth.html new file mode 100644 index 0000000..6a8ac5c --- /dev/null +++ b/mfa/templates/U2F/Auth.html @@ -0,0 +1,4 @@ +{% extends "login_base.html" %} +{% block form %} +{% include 'U2F/recheck.html' with mode='auth' %} +{% endblock %} \ No newline at end of file diff --git a/mfa/templates/U2F/recheck.html b/mfa/templates/U2F/recheck.html new file mode 100644 index 0000000..e055078 --- /dev/null +++ b/mfa/templates/U2F/recheck.html @@ -0,0 +1,97 @@ +
+ +
+
+
+ Security Key +
+
+ +
+
+

Your key should be flashing now, please press the button.

+ {% if mode == "auth" %} +
+ {% elif mode == "recheck" %} + + {% endif %} + {% csrf_token %} + +
+
+
+
+ +
+
+ + {% if request.session.mfa_methods|length > 1 %} + Select Another Method + {% endif %} +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/mfa/templates/mfa_check.html b/mfa/templates/mfa_check.html new file mode 100644 index 0000000..a7077b7 --- /dev/null +++ b/mfa/templates/mfa_check.html @@ -0,0 +1,36 @@ + +{% include "modal.html" %} \ No newline at end of file diff --git a/mfa/templates/select_mfa_method.html b/mfa/templates/select_mfa_method.html new file mode 100644 index 0000000..82c9a03 --- /dev/null +++ b/mfa/templates/select_mfa_method.html @@ -0,0 +1,26 @@ +{% extends "login_base.html" %} +{% block form %} +
+ +
+
+
+ Select Second Verification Method +
+ +
+
+
+{% endblock %} \ No newline at end of file diff --git a/mfa/tests.py b/mfa/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/mfa/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/mfa/totp.py b/mfa/totp.py new file mode 100644 index 0000000..2d5c837 --- /dev/null +++ b/mfa/totp.py @@ -0,0 +1,70 @@ +from django.shortcuts import render,render_to_response +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 +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"): + totp = pyotp.TOTP(key.properties["secret_key"]) + if totp.verify(token,valid_window = 30): + key.last_used=timezone.now() + key.save() + mfa = {"verified": True, "method": "TOTP"} + if getattr(settings, "MFA_RECHECK", False): + mfa["next_check"] = int((datetime.datetime.now() + + datetime.timedelta( + seconds=random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))).strftime("%s")) + request.session["mfa"] = mfa + return True + return False + +def recheck(request): + context = csrf(request) + context["mode"]="recheck" + if request.method == "POST": + if verify_login(request,request.user.username, token=request.POST["otp"]): + return HttpResponse(simplejson.dumps({"recheck": True}), content_type="application/json") + else: + return HttpResponse(simplejson.dumps({"recheck": False}), content_type="application/json") + return render_to_response("TOTP/recheck.html", context, context_instance=RequestContext(request)) + +def auth(request): + context=csrf(request) + if request.method=="POST": + if verify_login(request,request.session["base_username"],token = request.POST["otp"]): + return login(request) + context["invalid"]=True + return render_to_response("TOTP/verify.html", context, context_instance = RequestContext(request)) + + + +def getToken(request): + secret_key=pyotp.random_base32() + totp = pyotp.TOTP(secret_key) + print "Answer is", totp.now() + 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"] + 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" + uk.save() + return HttpResponse("Success") + else: return HttpResponse("Error") + +def start(request): + return render_to_response("TOTP/Add.html",{},context_instance = RequestContext(request )) diff --git a/mfa/urls.py b/mfa/urls.py new file mode 100644 index 0000000..20404f9 --- /dev/null +++ b/mfa/urls.py @@ -0,0 +1,45 @@ +from django.conf.urls import url +import views,totp,U2F,TrustedDevice,helpers,FIDO2 + +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'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'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'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"), + + ] \ No newline at end of file diff --git a/mfa/views.py b/mfa/views.py new file mode 100644 index 0000000..00c4452 --- /dev/null +++ b/mfa/views.py @@ -0,0 +1,85 @@ +from django.shortcuts import render,render_to_response +from django.http import HttpResponse,HttpResponseRedirect +from .models import * +from django.core.urlresolvers import reverse +from django.template.context_processors import csrf +from django.template.context import RequestContext +from django.conf import settings +import TrustedDevice +from user_agents import parse +def index(request): + keys=[] + context={"keys":User_Keys.objects.filter(username=request.user.username),"UNALLOWED_AUTHEN_METHODS":settings.MFA_UNALLOWED_METHODS} + for k in context["keys"]: + 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","----")) + keys.append(k) + context["keys"]=keys + return render_to_response("MFA.html",context,context_instance=RequestContext(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])) + print methods + 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")) + return show_methods(request) + +def show_methods(request): + return render_to_response("select_mfa_method.html", {}, context_instance = RequestContext(request)) + +def reset_cookie(request): + response=HttpResponseRedirect(settings.BASE_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"]) + +def delKey(request): + 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: + raise Exception("class Name should include modulename.classname") + + parsed_str = func_path.split(".") + 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) + if not callable_func: + raise Exception("Module does not have requested function") + return callable_func + +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] + key.enabled=not key.enabled + key.save() + return HttpResponse("OK") + else: + return HttpResponse("Error") + +def goto(request,method): + return HttpResponseRedirect(reverse(method.lower()+"_auth")) + +