Compare commits

..

13 Commits
v2.6.0 ... v2.8

Author SHA1 Message Date
Mohamed ElKalioby
be3cf69956 Merge branch 'master' of https://github.com/mkalioby/django-mfa2 2022-12-19 14:55:48 +03:00
Mohamed ElKalioby
98b9fce1d2 Support for Django4.0+ JSONField 2022-12-19 14:55:37 +03:00
Mohamed ElKalioby
17ef0f4b1e #70 closed 2022-12-19 14:23:32 +03:00
Mohamed El-Kalioby
669fef84fd Link to django-passkeys 2022-11-05 10:13:55 +03:00
Mohamed El-Kalioby
25be381ca9 Merge remote-tracking branch 'origin/v2.6.1' 2022-10-16 16:18:45 +03:00
Mohamed El-Kalioby
bdb4de3375 Update README.md 2022-10-14 19:46:49 +02:00
Mohamed ElKalioby
ab89a204bb Added Works with PassKeys Logo 2022-10-14 15:17:07 +03:00
Mohamed ElKalioby
caaa059d5b Add PassKey image 2022-10-14 15:15:24 +03:00
Mohamed ElKalioby
2d7b80bf5a Fixing CVE-2022-42731 2022-10-10 17:35:26 +03:00
Mohamed ElKalioby
8dba66b7b2 Merge branch 'CVE-2022-42731' 2022-10-10 17:21:31 +03:00
Mohamed ElKalioby
54db5a513b Fixing CVE-2022-42731 2022-10-10 17:20:47 +03:00
Mohamed El-Kalioby
4903967c23 Released v2.6.0 2022-10-01 12:45:18 +03:00
Mohamed El-Kalioby
cb2149acf3 Merged v2.6.0 2022-10-01 12:41:15 +03:00
10 changed files with 114 additions and 26 deletions

View File

@@ -1,5 +1,21 @@
# Change Log
## 2.6.0 (dev)
## 2.8.0
* Support For Django 4.0+ JSONField
* Removed jsonfield package from requirements
## 2.7.0
* Fixed #70
* Add QR Code for trusted device link
* Better formatting for trusted device start page.
## 2.6.1
* Fix: CVE-2022-42731: related to the possibility of registration replay attack.
Thanks to 'SSE (Secure Systems Engineering)'
## 2.5.1
* Fix: CVE-2022-42731: related to the possibility of registration replay attack.
Thanks to 'SSE (Secure Systems Engineering)'
## 2.6.0
* Adding Backup Recovery Codes (Recovery) as a method.
Thanks to @Spitfireap for work, and @peterthomassen for guidance.
* Added: `RECOVERY_ITERATION` to set the number of iteration when hashing recovery token

View File

@@ -1,6 +1,7 @@
# django-mfa2
A Django app that handles MFA, it supports TOTP, U2F, FIDO2 U2F (Web Authn), Email Tokens , Trusted Devices and backup codes.
[![Works with PassKeys](https://github.com/mkalioby/django-mfa2/raw/master/img/Works%20with%20PassKeys-black.png)](https://fidoalliance.org/passkeys/)
### Pip Stats
[![PyPI version](https://badge.fury.io/py/django-mfa2.svg)](https://badge.fury.io/py/django-mfa2)
[![Downloads Count](https://static.pepy.tech/personalized-badge/django-mfa2?period=total&units=international_system&left_color=black&right_color=green&left_text=Downloads)](https://pepy.tech/project/django-mfa2)
@@ -16,12 +17,14 @@ Web Authencation API (WebAuthn) is state-of-the art techology that is expected t
For FIDO2, the following are supported
* **security keys** (Firefox 60+, Chrome 67+, Edge 18+, Safari 13 on Mac OS, Chrome on Andriod, Safari on iOS 13.3+),
* **Windows Hello** (Firefox 67+, Chrome 72+ , Edge) ,
* **Windows Hello** (Firefox 67+, Chrome 72+ , Edge),
* **Apple's Touch ID/Face ID** (Chrome 70+ on Mac OS X, Safari on macOS Big Sur, Safari on iOS 14.0+ ),
* **android-safetynet** (Chrome 70+, Firefox 68+)
* **NFC devices using PCSC** (Not Tested, but as supported in fido2)
* **Soft Tokens**
* [krypt.co](https://krypt.co/): Login by a notification on your phone.
* ~~[krypt.co](https://krypt.co/): Login by a notification on your phone.~~
**Update**: Dec 2022, krypt.co has been killed by Google for Passkeys.
In English :), It allows you to verify the user by security keys on PC, Laptops or Mobiles, Windows Hello (Fingerprint, PIN) on Windows 10 Build 1903+ (May 2019 Update) Touch/Face ID on Macbooks (Chrome, Safari), Touch/Face ID on iPhone and iPad and Fingerprint/Face/Iris/PIN on Android Phones.
@@ -31,6 +34,8 @@ Trusted device is a mode for the user to add a device that doesn't support secur
Package tested with Django 1.8, Django 2.2 on Python 2.7 and Python 3.5+ but it was not checked with any version in between but open for issues.
If you just need WebAuthn and Passkeys, you can use **[django-passkeys](https://github.com/mkalioby/django-passkeys)**, which is a slim-down of this app and much easier to integrate.
Depends on
* pyotp
@@ -42,8 +47,12 @@ Depends on
# Installation
1. using pip
* For Django >= 4.0
`pip install django-mfa2`
* For Django < 4.0
`pip install django-mfa2 jsonfield`
2. Using Conda forge
`conda config --add channels conda-forge`
@@ -196,6 +205,8 @@ function some_func() {
* [AndreasDickow](https://github.com/AndreasDickow)
* [mnelson4](https://github.com/mnelson4)
* [ezrajrice](https://github.com/ezrajrice)
* [Spitfireap](https://github.com/Spitfireap)
* [peterthomassen](https://github.com/peterthomassen)
# Security contact information

View File

@@ -16,12 +16,13 @@ Including another URLconf
from django.contrib import admin
from django.urls import path,re_path,include
from . import views,auth
from mfa import TrustedDevice
urlpatterns = [
path('admin/', admin.site.urls),
path('mfa/', include('mfa.urls')),
path('auth/login',auth.loginView,name="login"),
path('auth/logout',auth.logoutView,name="logout"),
path('devices/add/', TrustedDevice.add,name="add_trusted_device"),
re_path('^$',views.home,name='home'),
path('registered/',views.registered,name='registered')
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -16,7 +16,7 @@ from .views import login, reset_cookie
import datetime
from .Common import get_redirect_url
from django.utils import timezone
from django.http import JsonResponse
def recheck(request):
"""Starts FIDO2 recheck"""
@@ -49,13 +49,15 @@ def begin_registeration(request):
def complete_reg(request):
"""Completes the registeration, called by API"""
try:
if not "fido_state" in request.session:
return JsonResponse({'status': 'ERR', "message": "FIDO Status can't be found, please try again"})
data = cbor.decode(request.body)
client_data = CollectedClientData(data['clientDataJSON'])
att_obj = AttestationObject((data['attestationObject']))
server = getServer()
auth_data = server.register_complete(
request.session['fido_state'],
request.session.pop('fido_state'),
client_data,
att_obj
)
@@ -79,7 +81,7 @@ def complete_reg(request):
client.captureException()
except:
pass
return HttpResponse(simplejson.dumps({'status': 'ERR', "message": "Error on server, please try again later"}))
return JsonResponse({'status': 'ERR', "message": "Error on server, please try again later"})
def start(request):

View File

@@ -7,10 +7,11 @@ from django.template.context_processors import csrf
from .models import *
import user_agents
from django.utils import timezone
from django.urls import reverse
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
if not User_Keys.objects.filter(properties__icontains='"key": "%s"'%x).exists(): return x
else: return id_generator(size,chars)
def getUserAgent(request):
@@ -57,12 +58,13 @@ def getCookie(request):
def add(request):
context=csrf(request)
if request.method=="GET":
context.update({"username":request.GET.get('u',''),"key":request.GET.get('k','')})
return render(request,"TrustedDevices/Add.html",context)
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)
trusted_keys=User_Keys.objects.filter(username=request.POST["username"],properties__icontains='"key": "%s"'%key)
cookie=False
if trusted_keys.exists():
tk=trusted_keys[0]
@@ -97,7 +99,7 @@ def start(request):
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"]}
context={"key":td.properties["key"],"url":request.scheme+"://"+request.get_host() + reverse('add_trusted_device')}
except:
del request.session["td_id"]
return start(request)
@@ -124,12 +126,14 @@ def verify(request):
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"])
uk = User_Keys.objects.get(username=request.POST["username"].lower(), properties__icontains='"key": "%s"'%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","id":uk.id}
return True
except:
import traceback
print(traceback.format_exc())
return False
return False

View File

@@ -2,7 +2,14 @@
from __future__ import unicode_literals
from django.db import models, migrations
import jsonfield.fields
try:
from django.db.models import JSONField
except ImportError:
try:
from jsonfield.fields import JSONField
except ImportError:
raise ImportError("Can't find a JSONField implementation, please install jsonfield if django < 4.0")
def modify_json(apps, schema_editor):
@@ -24,7 +31,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='user_keys',
name='properties',
field=jsonfield.fields.JSONField(null=True),
field=JSONField(null=True),
),
migrations.RunPython(modify_json)
]

View File

@@ -1,5 +1,12 @@
from django.db import models
from jsonfield import JSONField
try:
from django.db.models import JSONField
except ModuleNotFoundError:
try:
from jsonfield import JSONField
except ModuleNotFoundError:
raise ModuleNotFoundError("Can't find a JSONField implementation, please install jsonfield if django < 4.0")
from jose import jwt
from django.conf import settings
#from jsonLookup import shasLookup, hasLookup

View File

@@ -1,5 +1,7 @@
{% extends "base.html" %}
{% load static %}
{% block head %}
<script src="{% static 'mfa/js/qrious.min.js' %}" type="text/javascript"></script>
<style>
#two-factor-steps {
border: 1px solid #ccc;
@@ -12,6 +14,12 @@
</style>
<script type="text/javascript">
$(document).ready(function (){
var qr = new QRious({
element: document.getElementById('qr'),
value: "{{ url }}?u={{ request.user.username }}&k={{ key }}"
});
})
function sendEmail() {
$("#modal-title").html("Send Link")
$("#modal-body").html("Sending Email, Please wait....");
@@ -82,21 +90,52 @@
{% if not_allowed %}
<div class="alert alert-danger">You can't add any more devices, you need to remove previously trusted devices first.</div>
{% else %}
<p style="color: green">Allow access from mobile phone and tables.</p>
<p style="color: green">Allow access from mobile phone and tables.</p><br/>
<br/>
</div>
<div class="row">
<h5>Steps:</h5>
</div>
<div class="row">
<div class="col-md-6">
<h5>Using Camera</h5>
<ol>
<li>Using your mobile/table, open Chrome/Firefox.</li>
<li>Go to <b>{{ HOST }}{{ BASE_URL }}devices/add</b>&nbsp;&nbsp;<a href="javascript:void(0)" onclick="sendEmail()" title="Send to my email"><i class="fas fa-paper-plane"></i></a></li>
<li>Scan the following barcode <br/>
<img id="qr"/> <br/>
</li>
<li>Confirm the consent and submit form.</li>
</ol>
</div>
<div class="col-md-6">
<h5>Manual</h5>
<ol>
<li>Using your mobile/table, open Chrome/Firefox.</li>
<li>Go to <b>{{ url }}</b>&nbsp;&nbsp;</li>
<li>Enter your username & following 6 digits<br/>
<span style="font-size: 16px;font-weight: bold; margin-left: 50px">{{ key|slice:":3" }} - {{ key|slice:"3:" }}</span>
</li>
<li>This window will ask to confirm the device.</li>
<li>Confirm the consent and submit form.</li>
</div>
</div>
<div class="row">
<div class="col-md-8 offset-2">
This window will ask to confirm the device.
</div>
</div>
</ol>
{% endif %}
</div>
</div>
</div>
<br/>
<br/>
<br/>
<br/>
{% include "modal.html" %}
{% include 'mfa_check.html' %}
{% endblock %}

View File

@@ -4,7 +4,7 @@ from setuptools import find_packages, setup
setup(
name='django-mfa2',
version='2.5.0',
version='2.8.0',
description='Allows user to add 2FA to their accounts',
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
@@ -17,7 +17,6 @@ setup(
packages=find_packages(),
install_requires=[
'django >= 2.0',
'jsonfield',
'simplejson',
'pyotp',
'python-u2flib-server',
@@ -25,13 +24,13 @@ setup(
'user-agents',
'python-jose',
'fido2 == 1.0.0',
'jsonLookup'
],
python_requires=">=3.5",
include_package_data=True,
zip_safe=False, # because we're including static files
classifiers=[
"Development Status :: 5 - Production/Stable",
#"Development Status :: 4 - Beta",
"Environment :: Web Environment",
"Framework :: Django",
"Framework :: Django :: 2.0",
@@ -41,6 +40,7 @@ setup(
"Framework :: Django :: 3.1",
"Framework :: Django :: 3.2",
"Framework :: Django :: 4.0",
"Framework :: Django :: 4.1",
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Programming Language :: Python",
@@ -51,6 +51,7 @@ setup(
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Topic :: Software Development :: Libraries :: Python Modules",
]
)