Compare commits

..

9 Commits

Author SHA1 Message Date
Mohamed ElKalioby
19563fe2b8 Fixes #37 2021-02-26 13:55:49 +03:00
Mohamed ElKalioby
0e7c0911ca Merge branch 'master' of https://github.com/mkalioby/django-mfa2 2021-02-26 13:26:31 +03:00
Mohamed ElKalioby
ebfdaf7504 Merge branch 'TouchID' 2021-02-26 13:25:56 +03:00
Mohamed El-Kalioby
c78e600b37 Update CHANGELOG.md 2021-01-21 09:17:28 +03:00
Mohamed El-Kalioby
e1e931285f Update requirements.txt
Fix FIDO2 version in the requirements file
2021-01-21 09:13:52 +03:00
Mohamed El-Kalioby
2709e70f88 Removed Touch ID Beta statment 2021-01-20 17:11:08 +03:00
Mohamed El-Kalioby
25fe80d76d Merge branch 'master' of github.com:mkalioby/django-mfa2 2021-01-20 17:10:08 +03:00
Mohamed El-Kalioby
75cf1e6130 Update README.md 2021-01-18 19:57:54 +03:00
Mohamed El-Kalioby
911be9e106 Update README.md 2021-01-18 19:56:19 +03:00
8 changed files with 67 additions and 41 deletions

View File

@@ -1,5 +1,13 @@
# Change Log # Change Log
## 2.1.2
* Fixed: Getting timestamp on Python 3.7 as ("%s") is raising an exception
* Upgraded to FIDO 0.9.1
## 2.1.1
* Fixed: FIDO2 version in requirments.txt file.
## 2.1.0 ## 2.1.0
* Added Support for Touch ID for Mac OSx and iOS 14 on Safari * Added Support for Touch ID for Mac OSx and iOS 14 on Safari

View File

@@ -46,8 +46,8 @@ def auth(request):
uk = User_Keys.objects.get(username=request.session["base_username"], key_type="Email") uk = User_Keys.objects.get(username=request.session["base_username"], key_type="Email")
mfa = {"verified": True, "method": "Email","id":uk.id} mfa = {"verified": True, "method": "Email","id":uk.id}
if getattr(settings, "MFA_RECHECK", False): if getattr(settings, "MFA_RECHECK", False):
mfa["next_check"] = int((datetime.datetime.now() + datetime.timedelta( mfa["next_check"] = datetime.datetime.timestamp(datetime.datetime.now() + datetime.timedelta(
seconds = random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))).strftime("%s")) seconds = random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX)))
request.session["mfa"] = mfa request.session["mfa"] = mfa
from django.utils import timezone from django.utils import timezone

View File

@@ -4,28 +4,31 @@ from fido2.ctap2 import AttestationObject, AuthenticatorData
from django.template.context_processors import csrf from django.template.context_processors import csrf
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.shortcuts import render from django.shortcuts import render
#from django.template.context import RequestContext # from django.template.context import RequestContext
import simplejson import simplejson
from fido2 import cbor from fido2 import cbor
from django.http import HttpResponse from django.http import HttpResponse
from django.conf import settings from django.conf import settings
from .models import * from .models import *
from fido2.utils import websafe_decode,websafe_encode from fido2.utils import websafe_decode, websafe_encode
from fido2.ctap2 import AttestedCredentialData from fido2.ctap2 import AttestedCredentialData
from .views import login,reset_cookie from .views import login, reset_cookie
import datetime import datetime
from django.utils import timezone from django.utils import timezone
def recheck(request): def recheck(request):
context = csrf(request) context = csrf(request)
context["mode"]="recheck" context["mode"] = "recheck"
request.session["mfa_recheck"]=True request.session["mfa_recheck"] = True
return render(request,"FIDO2/recheck.html", context) return render(request, "FIDO2/recheck.html", context)
def getServer(): def getServer():
rp = PublicKeyCredentialRpEntity(settings.FIDO_SERVER_ID, settings.FIDO_SERVER_NAME) rp = PublicKeyCredentialRpEntity(settings.FIDO_SERVER_ID, settings.FIDO_SERVER_NAME)
return Fido2Server(rp) return Fido2Server(rp)
def begin_registeration(request): def begin_registeration(request):
server = getServer() server = getServer()
registration_data, state = server.register_begin({ registration_data, state = server.register_begin({
@@ -35,7 +38,9 @@ def begin_registeration(request):
}, getUserCredentials(request.user.username)) }, getUserCredentials(request.user.username))
request.session['fido_state'] = state 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 @csrf_exempt
def complete_reg(request): def complete_reg(request):
try: try:
@@ -50,10 +55,10 @@ def complete_reg(request):
att_obj att_obj
) )
encoded = websafe_encode(auth_data.credential_data) encoded = websafe_encode(auth_data.credential_data)
uk=User_Keys() uk = User_Keys()
uk.username = request.user.username 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.owned_by_enterprise = getattr(settings, "MFA_OWNED_BY_ENTERPRISE", False)
uk.key_type = "FIDO2" uk.key_type = "FIDO2"
uk.save() uk.save()
return HttpResponse(simplejson.dumps({'status': 'OK'})) return HttpResponse(simplejson.dumps({'status': 'OK'}))
@@ -63,10 +68,13 @@ def complete_reg(request):
client.captureException() client.captureException()
except: except:
pass pass
return HttpResponse(simplejson.dumps({'status': 'ERR',"message":"Error on server, please try again later"})) return HttpResponse(simplejson.dumps({'status': 'ERR', "message": "Error on server, please try again later"}))
def start(request): def start(request):
context = csrf(request) context = csrf(request)
return render(request,"FIDO2/Add.html", context) return render(request, "FIDO2/Add.html", context)
def getUserCredentials(username): def getUserCredentials(username):
credentials = [] credentials = []
@@ -74,24 +82,27 @@ def getUserCredentials(username):
credentials.append(AttestedCredentialData(websafe_decode(uk.properties["device"]))) credentials.append(AttestedCredentialData(websafe_decode(uk.properties["device"])))
return credentials return credentials
def auth(request): def auth(request):
context=csrf(request) context = csrf(request)
return render(request,"FIDO2/Auth.html",context) return render(request, "FIDO2/Auth.html", context)
def authenticate_begin(request): def authenticate_begin(request):
server = getServer() 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) auth_data, state = server.authenticate_begin(credentials)
request.session['fido_state'] = state request.session['fido_state'] = state
return HttpResponse(cbor.encode(auth_data),content_type="application/octet-stream") return HttpResponse(cbor.encode(auth_data), content_type = "application/octet-stream")
@csrf_exempt @csrf_exempt
def authenticate_complete(request): def authenticate_complete(request):
try: try:
credentials = [] credentials = []
username=request.session.get("base_username",request.user.username) username = request.session.get("base_username", request.user.username)
server=getServer() server = getServer()
credentials=getUserCredentials(username) credentials = getUserCredentials(username)
data = cbor.decode(request.body) data = cbor.decode(request.body)
credential_id = data['credentialId'] credential_id = data['credentialId']
client_data = ClientData(data['clientDataJSON']) client_data = ClientData(data['clientDataJSON'])
@@ -107,7 +118,8 @@ def authenticate_complete(request):
signature signature
) )
except ValueError: except ValueError:
return HttpResponse(simplejson.dumps({'status': "ERR", "message": "Wrong challenge received, make sure that this is your security and try again."}), return HttpResponse(simplejson.dumps({'status': "ERR",
"message": "Wrong challenge received, make sure that this is your security and try again."}),
content_type = "application/json") content_type = "application/json")
except Exception as excep: except Exception as excep:
try: try:
@@ -119,32 +131,34 @@ def authenticate_complete(request):
"message": excep.message}), "message": excep.message}),
content_type = "application/json") content_type = "application/json")
if request.session.get("mfa_recheck",False): if request.session.get("mfa_recheck", False):
import time import time
request.session["mfa"]["rechecked_at"]=time.time() request.session["mfa"]["rechecked_at"] = time.time()
return HttpResponse(simplejson.dumps({'status': "OK"}), return HttpResponse(simplejson.dumps({'status': "OK"}),
content_type="application/json") content_type = "application/json")
else: else:
import random 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: 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.last_used = timezone.now()
k.save() k.save()
mfa = {"verified": True, "method": "FIDO2",'id':k.id} mfa = {"verified": True, "method": "FIDO2", 'id': k.id}
if getattr(settings, "MFA_RECHECK", False): if getattr(settings, "MFA_RECHECK", False):
mfa["next_check"] = int((datetime.datetime.now()+ datetime.timedelta( mfa["next_check"] = datetime.datetime.timestamp((datetime.datetime.now() + datetime.timedelta(
seconds=random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))).strftime("%s")) seconds = random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))))
request.session["mfa"] = mfa request.session["mfa"] = mfa
try: try:
authenticated=request.user.is_authenticated authenticated = request.user.is_authenticated
except: except:
authenticated = request.user.is_authenticated() authenticated = request.user.is_authenticated()
if not authenticated: if not authenticated:
res=login(request) res = login(request)
if not "location" in res: return reset_cookie(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", "redirect": res["location"]}),
content_type = "application/json")
return HttpResponse(simplejson.dumps({'status': "OK"}), return HttpResponse(simplejson.dumps({'status': "OK"}),
content_type = "application/json") content_type = "application/json")
except Exception as exp: except Exception as exp:
return HttpResponse(simplejson.dumps({'status': "ERR","message":exp.message}),content_type="application/json") return HttpResponse(simplejson.dumps({'status': "ERR", "message": exp.message}),
content_type = "application/json")

View File

@@ -57,9 +57,9 @@ def validate(request,username):
key.save() key.save()
mfa = {"verified": True, "method": "U2F","id":key.id} mfa = {"verified": True, "method": "U2F","id":key.id}
if getattr(settings, "MFA_RECHECK", False): if getattr(settings, "MFA_RECHECK", False):
mfa["next_check"] = int((datetime.datetime.now() mfa["next_check"] = datetime.datetime.timestamp((datetime.datetime.now()
+ datetime.timedelta( + datetime.timedelta(
seconds=random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))).strftime("%s")) seconds=random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))))
request.session["mfa"] = mfa request.session["mfa"] = mfa
return True return True

View File

@@ -6,6 +6,7 @@ from jsonLookup import shasLookup, hasLookup
JSONField.register_lookup(shasLookup) JSONField.register_lookup(shasLookup)
JSONField.register_lookup(hasLookup) JSONField.register_lookup(hasLookup)
class User_Keys(models.Model): class User_Keys(models.Model):
username=models.CharField(max_length = 50) username=models.CharField(max_length = 50)
properties=JSONField(null = True) properties=JSONField(null = True)
@@ -20,9 +21,12 @@ class User_Keys(models.Model):
if self.key_type == "Trusted Device" and self.properties.get("signature","") == "": 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) 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) super(User_Keys, self).save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
def __unicode__(self): def __unicode__(self):
return "%s -- %s"%(self.username,self.key_type) return "%s -- %s"%(self.username,self.key_type)
def __str__(self): def __str__(self):
return self.__unicode__() return self.__unicode__()
class Meta: class Meta:
app_label='mfa' app_label='mfa'

View File

@@ -40,9 +40,9 @@ def auth(request):
if res[0]: 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): if getattr(settings, "MFA_RECHECK", False):
mfa["next_check"] = int((datetime.datetime.now() mfa["next_check"] = datetime.datetime.timestamp((datetime.datetime.now()
+ datetime.timedelta( + datetime.timedelta(
seconds=random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))).strftime("%s")) seconds=random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))))
request.session["mfa"] = mfa request.session["mfa"] = mfa
return login(request) return login(request)
context["invalid"]=True context["invalid"]=True

View File

@@ -6,5 +6,5 @@ python-u2flib-server
ua-parser ua-parser
user-agents user-agents
python-jose python-jose
fido2 == 0.8.1 fido2 == 0.9.0
jsonLookup jsonLookup

View File

@@ -4,7 +4,7 @@ from setuptools import find_packages, setup
setup( setup(
name='django-mfa2', name='django-mfa2',
version='2.1.0', version='2.1.2b1',
description='Allows user to add 2FA to their accounts', description='Allows user to add 2FA to their accounts',
long_description=open("README.md").read(), long_description=open("README.md").read(),
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
@@ -24,14 +24,14 @@ setup(
'ua-parser', 'ua-parser',
'user-agents', 'user-agents',
'python-jose', 'python-jose',
'fido2 == 0.9', 'fido2 == 0.9.1',
'jsonLookup' 'jsonLookup'
], ],
python_requires=">=3.5", python_requires=">=3.5",
include_package_data=True, include_package_data=True,
zip_safe=False, # because we're including static files zip_safe=False, # because we're including static files
classifiers=[ classifiers=[
"Development Status :: 5 - Production/Stable", "Development Status :: 4 - Beta",
"Environment :: Web Environment", "Environment :: Web Environment",
"Framework :: Django", "Framework :: Django",
"Framework :: Django :: 1.11", "Framework :: Django :: 1.11",