Compare commits

..

8 Commits
0.9 ... v1.0.4

Author SHA1 Message Date
Mohamed El-Kalioby
2af838bfd3 Updated README file 2019-01-24 19:44:24 +03:00
Mohamed ElKalioby
ce6a224818 New PyPi version 2019-01-23 16:16:28 +03:00
Mohamed El-Kalioby
5e699aa66a Added Image 2019-01-23 16:13:11 +03:00
Mohamed El-Kalioby
bed70d7c22 Update README.md 2019-01-23 16:00:21 +03:00
Mohamed ElKalioby
ad4ee7f4d2 Version 1 is released 2019-01-23 15:52:18 +03:00
Mohamed ElKalioby
7efd3ccc38 Finalzed Python 3 Support 2019-01-23 15:34:47 +03:00
Mohamed ElKalioby
6efd643022 Python3 Support 2019-01-23 10:42:50 +03:00
Mohamed El-Kalioby
b199c86993 Update README.md 2019-01-22 18:56:08 +03:00
15 changed files with 97 additions and 37 deletions

View File

@@ -1,12 +1,21 @@
# django-mfa2 # django-mfa2
A Django app that handles MFA, it supports TOTP, U2F, FIDO2 U2F (Web Authn), Email Tokens , and Trusted Devices A Django app that handles MFA, it supports TOTP, U2F, FIDO2 U2F (Web Authn), Email Tokens , and Trusted Devices
[![PyPI version](https://badge.fury.io/py/django-mfa2.svg)](https://badge.fury.io/py/django-mfa2)
Web Authencation API (WebAuthn) is state-of-the art techology that is expected to replace passwords.
![Andriod Fingerprint](https://cdn-images-1.medium.com/max/800/1*1FWkRE8D7NTA2Kn1DrPjPA.png)
For FIDO2, both security keys and android-safetynet are supported. For FIDO2, both security keys and android-safetynet are supported.
In English :), It allows you to verify the user by security keys on PC, Laptops and Fingerprint/PIN on Andriod Phones.
Trusted device is a mode for the user to add a device that doesn't support security keys like iOS and andriod without fingerprints or NFC. Trusted device is a mode for the user to add a device that doesn't support security keys like iOS and andriod without fingerprints or NFC.
`**Note**: U2F and FIDO2 can only be served under secure context (https)` **Note**: `U2F and FIDO2 can only be served under secure context (https)`
Package tested with Django 1.8, Django 2.1 on Python 2.7 and Python 3.5+ but it was not checked with any version in between but open for issues.
Depends on Depends on
@@ -37,11 +46,11 @@ Depends on
MFA_RECHECK_MAX=30 # Maximum 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="MDL" #TOTP Issuer name TOKEN_ISSUER_NAME="PROJECT_NAME" #TOTP Issuer name
U2F_APPID="https://localhost" #URL For U2F U2F_APPID="https://localhost" #URL For U2F
FIDO_SERVER_ID=u"localhost" # Server rp id for FIDO2 FIDO_SERVER_ID=u"localehost" # Server rp id for FIDO2, it the full domain of your project
FIDO_SERVER_NAME=u"MDL" FIDO_SERVER_NAME=u"PROJECT_NAME"
FIDO_LOGIN_URL=BASE_URL FIDO_LOGIN_URL=BASE_URL
``` ```
**Method Names** **Method Names**
@@ -77,7 +86,7 @@ Depends on
import mfa.TrustedDevice import mfa.TrustedDevice
urls_patterns= [ urls_patterns= [
'...', '...',
url(r'^mfa/', include(mfa.urls)), 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 url(r'devices/add$', mfa.TrustedDevice.add,name="mfa_add_new_trusted_device"), # This short link to add new trusted device
'....', '....',
] ]
@@ -87,6 +96,6 @@ Depends on
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`. 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. 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.
1. Somewhere in your app, add a link to 'mfa_home' 1. Somewhere in your app, add a link to 'mfa_home'
```<l><a href="{% url 'mfa_home' %}">Security</a> </l>``` ```<li><a href="{% url 'mfa_home' %}">Security</a> </li>```
For Example, See https://github.com/mkalioby/AutoDeploy/commit/5f1d94b1804e0aa33c79e9e8530ce849d9eb78cc in AutDeploy Project For Example, See https://github.com/mkalioby/AutoDeploy/commit/5f1d94b1804e0aa33c79e9e8530ce849d9eb78cc in AutDeploy Project

8
mfa/Common.py Normal file
View File

@@ -0,0 +1,8 @@
from django.conf import settings
from django.core.mail import EmailMessage
def send(to,subject,body):
From = "%s <%s>" % (settings.EMAIL_FROM, settings.EMAIL_HOST_USER)
email = EmailMessage(subject,body,From,to)
email.content_subtype = "html"
return email.send(False)

View File

@@ -5,19 +5,13 @@ from random import randint
from .models import * from .models import *
from django.template.context import RequestContext from django.template.context import RequestContext
from .views import login from .views import login
from .Common import send
def sendEmail(request,username,secret): def sendEmail(request,username,secret):
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
User = get_user_model() User = get_user_model()
user=User.objects.get(username=username) user=User.objects.get(username=username)
print secret
res=render_to_response("mfa_email_token_template.html",{"request":request,"user":user,'otp':secret}) res=render_to_response("mfa_email_token_template.html",{"request":request,"user":user,'otp':secret})
from django.conf import settings return send([user.email],"OTP", res.content)
from django.core.mail import EmailMessage
From = "%s <%s>" % (settings.EMAIL_FROM, settings.EMAIL_HOST_USER)
email = EmailMessage("OTP",res.content,From,[user.email] )
email.content_subtype = "html"
return email.send(False)
def start(request): def start(request):
context = csrf(request) context = csrf(request)

View File

@@ -12,7 +12,7 @@ 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 from .views import login
import datetime import datetime
from django.utils import timezone from django.utils import timezone

View File

@@ -105,8 +105,8 @@ def start(request):
def send_email(request): def send_email(request):
body=render(request,"TrustedDevices/email.html",{}).content body=render(request,"TrustedDevices/email.html",{}).content
from Registry_app.Common import send from .Common import send
if send(request.user.email,"Add Trusted Device Link",body,delay=False): if send([request.user.email],"Add Trusted Device Link",body):
res="Sent Successfully" res="Sent Successfully"
else: else:
res="Error occured, please try again later." res="Error occured, please try again later."

View File

@@ -10,7 +10,7 @@ from django.template.context import RequestContext
from django.template.context_processors import csrf from django.template.context_processors import csrf
from django.conf import settings from django.conf import settings
from django.http import HttpResponse from django.http import HttpResponse
from.models import * from .models import *
from .views import login from .views import login
from django.utils import timezone from django.utils import timezone
@@ -41,14 +41,14 @@ def validate(request,username):
import datetime, random import datetime, random
data = simplejson.loads(request.POST["response"]) data = simplejson.loads(request.POST["response"])
print "Checking Errors"
res= check_errors(request,data) res= check_errors(request,data)
if res!=True: if res!=True:
return res return res
print "Checking Challenge"
challenge = request.session.pop('_u2f_challenge_') challenge = request.session.pop('_u2f_challenge_')
device, c, t = complete_authentication(challenge, data, [settings.U2F_APPID]) 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=User_Keys.objects.get(username=username,properties__shas="$.device.publicKey=%s"%device["publicKey"])
key.last_used=timezone.now() key.last_used=timezone.now()
key.save() key.save()

View File

@@ -1 +1,10 @@
import urls # @property
# def urls():
# import django
# if django.VERSION < (1, 9):
# from .mfa_urls import url_patterns
# return url_patterns, 'mfa', ''
# else:
# from .mfa_urls import url_patterns
# return url_patterns,'mfa'
#

4
mfa/apps.py Normal file
View File

@@ -0,0 +1,4 @@
from django.apps import AppConfig
class myAppNameConfig(AppConfig):
name = 'mfa'
verbose_name = 'A Much Better Name'

View File

@@ -1,8 +1,6 @@
import pyotp import pyotp
from .models import * from .models import *
import TrustedDevice from . import TrustedDevice, U2F, FIDO2, totp
import U2F, FIDO2
import totp
import simplejson import simplejson
from django.shortcuts import HttpResponse from django.shortcuts import HttpResponse
from mfa.views import verify,goto from mfa.views import verify,goto

View File

@@ -5,8 +5,13 @@ from django.db import models, migrations
import jsonfield.fields import jsonfield.fields
class Migration(migrations.Migration): 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 = [ dependencies = [
('mfa', '0004_user_keys_enabled'), ('mfa', '0004_user_keys_enabled'),
] ]
@@ -21,5 +26,5 @@ class Migration(migrations.Migration):
name='properties', name='properties',
field=jsonfield.fields.JSONField(null=True), field=jsonfield.fields.JSONField(null=True),
), ),
migrations.RunSQL("alter table mfa_user_keys modify column properties json;") migrations.RunPython(modify_json)
] ]

View File

@@ -2,8 +2,7 @@ from django.db import models
from jsonfield import JSONField from jsonfield import JSONField
from jose import jwt from jose import jwt
from django.conf import settings from django.conf import settings
from jsonLookup import hasLookup,shasLookup from jsonLookup import shasLookup
JSONField.register_lookup(hasLookup)
JSONField.register_lookup(shasLookup) JSONField.register_lookup(shasLookup)
class User_Keys(models.Model): class User_Keys(models.Model):
@@ -19,3 +18,5 @@ class User_Keys(models.Model):
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)
class Meta:
app_label='mfa'

View File

@@ -1,6 +1,7 @@
from django.conf.urls import url from django.conf.urls import url
from . import views,totp,U2F,TrustedDevice,helpers,FIDO2,Email
from . import views,totp,U2F,TrustedDevice,helpers,FIDO2,Email
app_name='mfa'
urlpatterns = [ urlpatterns = [
url(r'totp/start/', totp.start , name="start_new_otop"), url(r'totp/start/', totp.start , name="start_new_otop"),
url(r'totp/getToken', totp.getToken , name="get_new_otop"), url(r'totp/getToken', totp.getToken , name="get_new_otop"),

View File

@@ -1,11 +1,14 @@
from django.shortcuts import render,render_to_response from django.shortcuts import render,render_to_response
from django.http import HttpResponse,HttpResponseRedirect from django.http import HttpResponse,HttpResponseRedirect
from .models import * from .models import *
from django.core.urlresolvers import reverse try:
from django.urls import reverse
except:
from django.core.urlresolvers import reverse
from django.template.context_processors import csrf from django.template.context_processors import csrf
from django.template.context import RequestContext from django.template.context import RequestContext
from django.conf import settings from django.conf import settings
import TrustedDevice from . import TrustedDevice
from user_agents import parse from user_agents import parse
def index(request): def index(request):
keys=[] keys=[]
@@ -24,7 +27,7 @@ def verify(request,username):
#request.session["base_password"] = password #request.session["base_password"] = password
keys=User_Keys.objects.filter(username=username,enabled=1) keys=User_Keys.objects.filter(username=username,enabled=1)
methods=list(set([k.key_type for k in keys])) 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 "Trusted Device" in methods and not request.session.get("checked_trusted_device",False):
if TrustedDevice.verify(request): if TrustedDevice.verify(request):
return login(request) return login(request)

2
setup.cfg Normal file
View File

@@ -0,0 +1,2 @@
[bdist_wheel]
universal = 1

View File

@@ -4,17 +4,20 @@ from setuptools import find_packages, setup
setup( setup(
name='django-mfa2', name='django-mfa2',
version='0.9.0', version='1.0.4',
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_content_type="text/markdown",
author='Mohamed El-Kalioby', author='Mohamed El-Kalioby',
author_email = 'mkalioby@mkalioby.com', author_email = 'mkalioby@mkalioby.com',
url = 'https://github.com/mkalioby/django-mfa2/', url = 'https://github.com/mkalioby/django-mfa2/',
long_description=open('README.md').read(),
download_url='https://github.com/mkalioby/django-mfa2/', download_url='https://github.com/mkalioby/django-mfa2/',
license='MIT', license='MIT',
packages=find_packages(), packages=find_packages(),
install_requires=[ install_requires=[
'Django>=1.7', 'django >= 1.7',
'jsonfield', 'jsonfield',
'simplejson', 'simplejson',
'pyotp', 'pyotp',
@@ -22,9 +25,32 @@ setup(
'ua-parser', 'ua-parser',
'user-agents', 'user-agents',
'python-jose', 'python-jose',
'fido2==0.5' 'fido2 == 0.5',
'jsonLookup' 'jsonLookup'
], ],
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*",
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=[
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
"Framework :: Django",
"Framework :: Django :: 1.7",
"Framework :: Django :: 1.8",
"Framework :: Django :: 1.9",
"Framework :: Django :: 1.10",
"Framework :: Django :: 1.11",
"Framework :: Django :: 2.0",
"Framework :: Django :: 2.1",
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Topic :: Software Development :: Libraries :: Python Modules",
],
) )