Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2af838bfd3 | ||
|
|
ce6a224818 | ||
|
|
5e699aa66a | ||
|
|
bed70d7c22 | ||
|
|
ad4ee7f4d2 | ||
|
|
7efd3ccc38 | ||
|
|
6efd643022 | ||
|
|
b199c86993 |
21
README.md
21
README.md
@@ -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
|
||||||
|
|
||||||
|
[](https://badge.fury.io/py/django-mfa2)
|
||||||
|
|
||||||
|
Web Authencation API (WebAuthn) is state-of-the art techology that is expected to replace passwords.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
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
8
mfa/Common.py
Normal 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)
|
||||||
10
mfa/Email.py
10
mfa/Email.py
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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."
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
4
mfa/apps.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
class myAppNameConfig(AppConfig):
|
||||||
|
name = 'mfa'
|
||||||
|
verbose_name = 'A Much Better Name'
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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'
|
||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
34
setup.py
34
setup.py
@@ -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",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user