+ Not me +
+ + {% endif %} +
please press the button on your security key to prove it is you.
+ {% if mode == "auth" %} + +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 @@ + + +
+ +Your broswer should ask you to confirm you indentity.
+ +please press the button on your security key to prove it is you.
+ {% if mode == "auth" %} + +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.
+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' %}
+
Allow access from mobile phone and tables.
+| Browser: | +{{ ua.browser.family }} | +
|---|---|
| Version: | +{{ ua.browser.version_string }} | +
| Device: | +{{ ua.device.brand }} / {{ ua.device.model }} | +
Your secure Key should be flashing now, please press on button.
+ +Your key should be flashing now, please press the button.
+ {% if mode == "auth" %} + +