Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
972ccf4888 | ||
|
|
044f52da6a | ||
|
|
b1a167ad66 | ||
|
|
70dd692299 | ||
|
|
a1855d3998 | ||
|
|
16537baa6f | ||
|
|
e76feeb06b | ||
|
|
e0335ac4a7 | ||
|
|
54f5eb212a | ||
|
|
c53b4d1e1a | ||
|
|
0075f84b29 | ||
|
|
6ae4c2508c | ||
|
|
41256cc76d | ||
| b94faa1916 | |||
|
|
be3cf69956 | ||
|
|
98b9fce1d2 | ||
|
|
17ef0f4b1e | ||
|
|
669fef84fd | ||
|
|
25be381ca9 | ||
|
|
bdb4de3375 | ||
|
|
ab89a204bb | ||
|
|
caaa059d5b |
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,5 +1,9 @@
|
|||||||
.idea
|
|
||||||
example/venv
|
example/venv
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
@@ -105,3 +109,6 @@ venv.bak/
|
|||||||
# mypy
|
# mypy
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
example/test_db
|
example/test_db
|
||||||
|
|
||||||
|
# OS related
|
||||||
|
.DS_Store
|
||||||
|
|||||||
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,4 +1,17 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
## 2.9.0
|
||||||
|
* Fix a typo,
|
||||||
|
|
||||||
|
Thanks to @jkirkcaldy
|
||||||
|
|
||||||
|
## 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
|
## 2.6.1
|
||||||
* Fix: CVE-2022-42731: related to the possibility of registration replay attack.
|
* Fix: CVE-2022-42731: related to the possibility of registration replay attack.
|
||||||
Thanks to 'SSE (Secure Systems Engineering)'
|
Thanks to 'SSE (Secure Systems Engineering)'
|
||||||
|
|||||||
51
README.md
51
README.md
@@ -1,6 +1,7 @@
|
|||||||
# django-mfa2
|
# django-mfa2
|
||||||
A Django app that handles MFA, it supports TOTP, U2F, FIDO2 U2F (Web Authn), Email Tokens , Trusted Devices and backup codes.
|
A Django app that handles MFA, it supports TOTP, U2F, FIDO2 U2F (Web Authn), Email Tokens , Trusted Devices and backup codes.
|
||||||
|
|
||||||
|
[](https://fidoalliance.org/passkeys/)
|
||||||
### Pip Stats
|
### Pip Stats
|
||||||
[](https://badge.fury.io/py/django-mfa2)
|
[](https://badge.fury.io/py/django-mfa2)
|
||||||
[](https://pepy.tech/project/django-mfa2)
|
[](https://pepy.tech/project/django-mfa2)
|
||||||
@@ -10,6 +11,7 @@ A Django app that handles MFA, it supports TOTP, U2F, FIDO2 U2F (Web Authn), Ema
|
|||||||
[](https://anaconda.org/conda-forge/django-mfa2)
|
[](https://anaconda.org/conda-forge/django-mfa2)
|
||||||
[](https://anaconda.org/conda-forge/django-mfa2)
|
[](https://anaconda.org/conda-forge/django-mfa2)
|
||||||
|
|
||||||
|
|
||||||
Web Authencation API (WebAuthn) is state-of-the art techology that is expected to replace passwords.
|
Web Authencation API (WebAuthn) is state-of-the art techology that is expected to replace passwords.
|
||||||
|
|
||||||

|

|
||||||
@@ -21,7 +23,9 @@ For FIDO2, the following are supported
|
|||||||
* **android-safetynet** (Chrome 70+, Firefox 68+)
|
* **android-safetynet** (Chrome 70+, Firefox 68+)
|
||||||
* **NFC devices using PCSC** (Not Tested, but as supported in fido2)
|
* **NFC devices using PCSC** (Not Tested, but as supported in fido2)
|
||||||
* **Soft Tokens**
|
* **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.
|
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 +35,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.
|
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
|
Depends on
|
||||||
|
|
||||||
* pyotp
|
* pyotp
|
||||||
@@ -42,8 +48,12 @@ Depends on
|
|||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
1. using pip
|
1. using pip
|
||||||
|
* For Django >= 4.0
|
||||||
|
|
||||||
`pip install django-mfa2`
|
`pip install django-mfa2`
|
||||||
|
* For Django < 4.0
|
||||||
|
|
||||||
|
`pip install django-mfa2 jsonfield`
|
||||||
2. Using Conda forge
|
2. Using Conda forge
|
||||||
|
|
||||||
`conda config --add channels conda-forge`
|
`conda config --add channels conda-forge`
|
||||||
@@ -187,6 +197,43 @@ function some_func() {
|
|||||||
|
|
||||||
````
|
````
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
|
||||||
|
We use `pytest` and several pytest plugins, especially the `pytest-django` and `pytest-cov` plugins that provide Django fixtures, and test coverage analysis.
|
||||||
|
|
||||||
|
In the root folder, `pytest.ini` contains configurations for running the tests, `requirements_testing.txt` contains the python packages required for running tests, and the folder `tests` contains the actual test files.
|
||||||
|
|
||||||
|
To run the tests, install the packages in requirements and requirements_testing.txt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt -r requirements_testing.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
then simply run pytest
|
||||||
|
```
|
||||||
|
pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
to generate the coverage html pages:
|
||||||
|
|
||||||
|
```
|
||||||
|
pytest --cov=. --cov-report html -v
|
||||||
|
```
|
||||||
|
|
||||||
|
the coverage html files will be generated in the `htmlcov` folder
|
||||||
|
|
||||||
|
We use `tox` to test the package against different isolated environments. `tox.ini` contains the configurations for tox. To run the tests in the environments defined in the `tox.ini` file, make sure the python package tox is installed:
|
||||||
|
|
||||||
|
```
|
||||||
|
pip install tox
|
||||||
|
```
|
||||||
|
|
||||||
|
then run tox in the project root:
|
||||||
|
|
||||||
|
```
|
||||||
|
tox
|
||||||
|
```
|
||||||
|
|
||||||
# Contributors
|
# Contributors
|
||||||
* [mahmoodnasr](https://github.com/mahmoodnasr)
|
* [mahmoodnasr](https://github.com/mahmoodnasr)
|
||||||
* [d3cline](https://github.com/d3cline)
|
* [d3cline](https://github.com/d3cline)
|
||||||
@@ -198,6 +245,8 @@ function some_func() {
|
|||||||
* [ezrajrice](https://github.com/ezrajrice)
|
* [ezrajrice](https://github.com/ezrajrice)
|
||||||
* [Spitfireap](https://github.com/Spitfireap)
|
* [Spitfireap](https://github.com/Spitfireap)
|
||||||
* [peterthomassen](https://github.com/peterthomassen)
|
* [peterthomassen](https://github.com/peterthomassen)
|
||||||
|
* [oussjarrousse](https://github.com/oussjarrousse)
|
||||||
|
* [jkirkcaldy](https://github.com/jkirkcaldy)
|
||||||
|
|
||||||
|
|
||||||
# Security contact information
|
# Security contact information
|
||||||
|
|||||||
@@ -16,12 +16,13 @@ Including another URLconf
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path,re_path,include
|
from django.urls import path,re_path,include
|
||||||
from . import views,auth
|
from . import views,auth
|
||||||
|
from mfa import TrustedDevice
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('mfa/', include('mfa.urls')),
|
path('mfa/', include('mfa.urls')),
|
||||||
path('auth/login',auth.loginView,name="login"),
|
path('auth/login',auth.loginView,name="login"),
|
||||||
path('auth/logout',auth.logoutView,name="logout"),
|
path('auth/logout',auth.logoutView,name="logout"),
|
||||||
|
path('devices/add/', TrustedDevice.add,name="add_trusted_device"),
|
||||||
re_path('^$',views.home,name='home'),
|
re_path('^$',views.home,name='home'),
|
||||||
path('registered/',views.registered,name='registered')
|
path('registered/',views.registered,name='registered')
|
||||||
]
|
]
|
||||||
|
|||||||
BIN
img/Works with PassKeys-black.png
Normal file
BIN
img/Works with PassKeys-black.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
@@ -7,10 +7,11 @@ from django.template.context_processors import csrf
|
|||||||
from .models import *
|
from .models import *
|
||||||
import user_agents
|
import user_agents
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
|
def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
|
||||||
x=''.join(random.choice(chars) for _ in range(size))
|
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)
|
else: return id_generator(size,chars)
|
||||||
|
|
||||||
def getUserAgent(request):
|
def getUserAgent(request):
|
||||||
@@ -57,12 +58,13 @@ def getCookie(request):
|
|||||||
def add(request):
|
def add(request):
|
||||||
context=csrf(request)
|
context=csrf(request)
|
||||||
if request.method=="GET":
|
if request.method=="GET":
|
||||||
|
context.update({"username":request.GET.get('u',''),"key":request.GET.get('k','')})
|
||||||
return render(request,"TrustedDevices/Add.html",context)
|
return render(request,"TrustedDevices/Add.html",context)
|
||||||
else:
|
else:
|
||||||
key=request.POST["key"].replace("-","").replace(" ","").upper()
|
key=request.POST["key"].replace("-","").replace(" ","").upper()
|
||||||
context["username"] = request.POST["username"]
|
context["username"] = request.POST["username"]
|
||||||
context["key"] = request.POST["key"]
|
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
|
cookie=False
|
||||||
if trusted_keys.exists():
|
if trusted_keys.exists():
|
||||||
tk=trusted_keys[0]
|
tk=trusted_keys[0]
|
||||||
@@ -97,7 +99,7 @@ def start(request):
|
|||||||
request.session["td_id"]=td.id
|
request.session["td_id"]=td.id
|
||||||
try:
|
try:
|
||||||
if td==None: td=User_Keys.objects.get(id=request.session["td_id"])
|
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:
|
except:
|
||||||
del request.session["td_id"]
|
del request.session["td_id"]
|
||||||
return start(request)
|
return start(request)
|
||||||
@@ -124,12 +126,14 @@ def verify(request):
|
|||||||
json= jwt.decode(request.COOKIES.get('deviceid'),settings.SECRET_KEY)
|
json= jwt.decode(request.COOKIES.get('deviceid'),settings.SECRET_KEY)
|
||||||
if json["username"].lower()== request.session['base_username'].lower():
|
if json["username"].lower()== request.session['base_username'].lower():
|
||||||
try:
|
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":
|
if uk.enabled and uk.properties["status"] == "trusted":
|
||||||
uk.last_used=timezone.now()
|
uk.last_used=timezone.now()
|
||||||
uk.save()
|
uk.save()
|
||||||
request.session["mfa"] = {"verified": True, "method": "Trusted Device","id":uk.id}
|
request.session["mfa"] = {"verified": True, "method": "Trusted Device","id":uk.id}
|
||||||
return True
|
return True
|
||||||
except:
|
except:
|
||||||
|
import traceback
|
||||||
|
print(traceback.format_exc())
|
||||||
return False
|
return False
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
class myAppNameConfig(AppConfig):
|
class myAppNameConfig(AppConfig):
|
||||||
name = 'mfa'
|
name = 'mfa'
|
||||||
verbose_name = 'A Much Better Name'
|
verbose_name = 'Django MFA2'
|
||||||
@@ -2,7 +2,14 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models, migrations
|
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):
|
def modify_json(apps, schema_editor):
|
||||||
@@ -24,7 +31,7 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='user_keys',
|
model_name='user_keys',
|
||||||
name='properties',
|
name='properties',
|
||||||
field=jsonfield.fields.JSONField(null=True),
|
field=JSONField(null=True),
|
||||||
),
|
),
|
||||||
migrations.RunPython(modify_json)
|
migrations.RunPython(modify_json)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
try:
|
||||||
|
from django.db.models import JSONField
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
try:
|
||||||
from jsonfield import JSONField
|
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 jose import jwt
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
#from jsonLookup import shasLookup, hasLookup
|
#from jsonLookup import shasLookup, hasLookup
|
||||||
|
|||||||
@@ -116,7 +116,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr><td colspan="7" align="center">You didn't have any keys yet.</td> </tr>
|
<tr><td colspan="7" align="center">You don't have any keys yet.</td> </tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
{% block head %}
|
{% block head %}
|
||||||
|
<script src="{% static 'mfa/js/qrious.min.js' %}" type="text/javascript"></script>
|
||||||
<style>
|
<style>
|
||||||
#two-factor-steps {
|
#two-factor-steps {
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
@@ -12,6 +14,12 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<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() {
|
function sendEmail() {
|
||||||
$("#modal-title").html("Send Link")
|
$("#modal-title").html("Send Link")
|
||||||
$("#modal-body").html("Sending Email, Please wait....");
|
$("#modal-body").html("Sending Email, Please wait....");
|
||||||
@@ -82,21 +90,52 @@
|
|||||||
{% if not_allowed %}
|
{% if not_allowed %}
|
||||||
<div class="alert alert-danger">You can't add any more devices, you need to remove previously trusted devices first.</div>
|
<div class="alert alert-danger">You can't add any more devices, you need to remove previously trusted devices first.</div>
|
||||||
{% else %}
|
{% 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>
|
<h5>Steps:</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h5>Using Camera</h5>
|
||||||
<ol>
|
<ol>
|
||||||
<li>Using your mobile/table, open Chrome/Firefox.</li>
|
<li>Using your mobile/table, open Chrome/Firefox.</li>
|
||||||
<li>Go to <b>{{ HOST }}{{ BASE_URL }}devices/add</b> <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> </li>
|
||||||
<li>Enter your username & following 6 digits<br/>
|
<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>
|
<span style="font-size: 16px;font-weight: bold; margin-left: 50px">{{ key|slice:":3" }} - {{ key|slice:"3:" }}</span>
|
||||||
</li>
|
</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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
{% include "modal.html" %}
|
{% include "modal.html" %}
|
||||||
{% include 'mfa_check.html' %}
|
{% include 'mfa_check.html' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
109
pytest.ini
Normal file
109
pytest.ini
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
[pytest]
|
||||||
|
# Searching
|
||||||
|
python_files = test_*
|
||||||
|
python_classes = Tests*
|
||||||
|
python_functions = test_*
|
||||||
|
|
||||||
|
env_files =
|
||||||
|
.env
|
||||||
|
|
||||||
|
# do not search for tests in these folders
|
||||||
|
norecursedirs = .vscode .tox docs example img mfa venv .coverage django_mfa2.egg-info
|
||||||
|
|
||||||
|
# Add folder to PYTHONPATH
|
||||||
|
# requires pytest >= 7.0.0
|
||||||
|
pythonpath = .
|
||||||
|
|
||||||
|
|
||||||
|
# https://pytest-django.readthedocs.io/en/latest/usage.html
|
||||||
|
DJANGO_SETTINGS_MODULE = tests.settings
|
||||||
|
|
||||||
|
|
||||||
|
# do not override the debug mode (True/False) set in the django settings module
|
||||||
|
# https://pytest-django.readthedocs.io/en/latest/usage.html#additional-pytest-ini-settings
|
||||||
|
django_debug_mode = keep
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# set env variables
|
||||||
|
# https://tech.serhatteker.com/post/2020-02/test-env-vars-in-python/
|
||||||
|
# https://github.com/pytest-dev/pytest-env
|
||||||
|
; env =
|
||||||
|
; KEY=value
|
||||||
|
|
||||||
|
|
||||||
|
addopts =
|
||||||
|
# verbose
|
||||||
|
-v
|
||||||
|
# more verbosity
|
||||||
|
# -vv
|
||||||
|
# Don't show warnings
|
||||||
|
# -p no:warnings
|
||||||
|
# generates coverage report
|
||||||
|
# note that enabling pytest coverage will cause debugging pytest to fail on pycharm
|
||||||
|
# add the --no-cov to the pytest configuration on pycharm to allow for debugging pytest
|
||||||
|
--cov=./mfa
|
||||||
|
# surpress generating converage if one or more tests failed
|
||||||
|
; --no-cov-on-fail
|
||||||
|
# do not run migrations => faster test initialization
|
||||||
|
# --nomigrations
|
||||||
|
# Show hypthesis statistics whereever hypothesis was used
|
||||||
|
# ignore these tests/files when looking for tests
|
||||||
|
#--ignore=
|
||||||
|
# black
|
||||||
|
# --black
|
||||||
|
--hypothesis-show-statistics
|
||||||
|
# Add --reuse-db if you want to speed up tests by reusing the database between test runs.
|
||||||
|
#--reuse-db
|
||||||
|
|
||||||
|
|
||||||
|
# Define additional pytest markers so that using them in test will not trigger warnings
|
||||||
|
# To show the help line use: % pytest --marker
|
||||||
|
# To run pytest on a specifc marker use: pytest -m mark
|
||||||
|
# to run pytestt on several markers use quotation and logic operators as in:
|
||||||
|
# pytest -m "mark1 and mark2"
|
||||||
|
# pytest -m "mark1 or mark2"
|
||||||
|
# pytest -m "mark1 and not mark2"
|
||||||
|
markers =
|
||||||
|
API: tests of server api functions whether it is exposed as REST API or otherwise
|
||||||
|
BLACK_BOX: Black Box tests
|
||||||
|
WHITE_BOX: White Box tests
|
||||||
|
ENVIRONMENT: tests for the environment
|
||||||
|
CONFIGURATION: tests related configurations
|
||||||
|
LOGGING: tests related to logging
|
||||||
|
UNIT: Unit tests
|
||||||
|
INTEGRATION: Integration testing
|
||||||
|
UTILS: tests for utilities
|
||||||
|
FOCUS: tests under the microscope... under the spotlight... in focus
|
||||||
|
FUNC: functional teesting
|
||||||
|
REGRESSION: tests for fixed bugs
|
||||||
|
|
||||||
|
DJANGO: tests related to DJANGO
|
||||||
|
|
||||||
|
HTTP_REQUEST: tests of functions that handles HTTP REQUESTS
|
||||||
|
HTTP_GET: tests of functions that handles HTTP_GET_REQUESTS
|
||||||
|
HTTP_POST: tests of functions that handles HTTP_POST_REQUESTS
|
||||||
|
AUTH: tests related to user authentication
|
||||||
|
SQL_DB: tests related to the sql database
|
||||||
|
|
||||||
|
CLI: tests related to flask-cli
|
||||||
|
SERVER: tests for the server
|
||||||
|
|
||||||
|
API_V1: API related tests
|
||||||
|
|
||||||
|
PRIVILEGED_USER: tests for privileged users
|
||||||
|
NON_PRIVILEGED_USER: tests for non-privileged users
|
||||||
|
PERMISSIONS: tests related to permissions
|
||||||
|
|
||||||
|
ANNONYMOUS_USER: tests for non-authenticated users
|
||||||
|
AUTHENTICATED_USER: tests for authenticated users
|
||||||
|
|
||||||
|
ENDPOINTS: tests for endpoints (API nodes)
|
||||||
|
SERIALIZERS: tests for serializers
|
||||||
|
VIEWS: tests for DRF viewsets
|
||||||
|
FILTERS: tests for DRF filters
|
||||||
|
MODELS: tests for models
|
||||||
|
VALIDATORS: tests for validators
|
||||||
|
|
||||||
|
ERROR_HANDLING: tests for error handling
|
||||||
|
SECURITY: tests for security
|
||||||
@@ -6,5 +6,5 @@ python-u2flib-server
|
|||||||
ua-parser
|
ua-parser
|
||||||
user-agents
|
user-agents
|
||||||
python-jose
|
python-jose
|
||||||
fido2 == 1.0.0
|
fido2==1.1.2
|
||||||
jsonLookup
|
jsonLookup
|
||||||
|
|||||||
10
requirements_testing.txt
Normal file
10
requirements_testing.txt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
tox
|
||||||
|
pytest>=7.0.0
|
||||||
|
pytest-xdist
|
||||||
|
pytest-cov # Test coverage
|
||||||
|
pytest-dotenv # plugin to load environment from .env file
|
||||||
|
pytest-env # plugin to allow passing environment variable to pytest environmentt
|
||||||
|
pytest-mock # plugin that provides a mocker fixture which is a thin-wrapper around the patching API provided by the mock package
|
||||||
|
hypothesis # plugin that helps in automatize generating random values instead of static values in pyttests
|
||||||
|
pytest-django # pytest support for Django
|
||||||
|
validators # a package containing several validator functions
|
||||||
11
setup.py
11
setup.py
@@ -4,7 +4,7 @@ from setuptools import find_packages, setup
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='django-mfa2',
|
name='django-mfa2',
|
||||||
version='2.6.1',
|
version='2.8.0',
|
||||||
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",
|
||||||
@@ -14,24 +14,23 @@ setup(
|
|||||||
url = 'https://github.com/mkalioby/django-mfa2/',
|
url = 'https://github.com/mkalioby/django-mfa2/',
|
||||||
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(exclude=("tests",)),
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'django >= 2.0',
|
'django >= 2.0',
|
||||||
'jsonfield',
|
|
||||||
'simplejson',
|
'simplejson',
|
||||||
'pyotp',
|
'pyotp',
|
||||||
'python-u2flib-server',
|
'python-u2flib-server',
|
||||||
'ua-parser',
|
'ua-parser',
|
||||||
'user-agents',
|
'user-agents',
|
||||||
'python-jose',
|
'python-jose',
|
||||||
'fido2 == 1.0.0',
|
'fido2 == 1.1.2',
|
||||||
'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 :: 5 - Production/Stable",
|
||||||
|
#"Development Status :: 4 - Beta",
|
||||||
"Environment :: Web Environment",
|
"Environment :: Web Environment",
|
||||||
"Framework :: Django",
|
"Framework :: Django",
|
||||||
"Framework :: Django :: 2.0",
|
"Framework :: Django :: 2.0",
|
||||||
@@ -41,6 +40,7 @@ setup(
|
|||||||
"Framework :: Django :: 3.1",
|
"Framework :: Django :: 3.1",
|
||||||
"Framework :: Django :: 3.2",
|
"Framework :: Django :: 3.2",
|
||||||
"Framework :: Django :: 4.0",
|
"Framework :: Django :: 4.0",
|
||||||
|
"Framework :: Django :: 4.1",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
"Programming Language :: Python",
|
"Programming Language :: Python",
|
||||||
@@ -51,6 +51,7 @@ setup(
|
|||||||
"Programming Language :: Python :: 3.8",
|
"Programming Language :: Python :: 3.8",
|
||||||
"Programming Language :: Python :: 3.9",
|
"Programming Language :: Python :: 3.9",
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
19
tests/conftest.py
Normal file
19
tests/conftest.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
# @pytest.fixture
|
||||||
|
# def api_request(rf):
|
||||||
|
# request = rf.get('/url')
|
||||||
|
# # Modify the request object as needed (e.g., set user, add data)
|
||||||
|
# return request
|
||||||
|
|
||||||
|
# @pytest.fixture
|
||||||
|
# def create_test_model(db):
|
||||||
|
# def make_model(**kwargs):
|
||||||
|
# return MyModel.objects.create(**kwargs)
|
||||||
|
# return make_model
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def authenticated_user(client, django_user_model):
|
||||||
|
user = django_user_model.objects.create_user(username='test', password='123')
|
||||||
|
client.login(username='test', password='123')
|
||||||
|
return user
|
||||||
51
tests/settings.py
Normal file
51
tests/settings.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
SECRET_KEY = 'fake-key-for-testing'
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'mfa'
|
||||||
|
]
|
||||||
|
ROOT_URLCONF="mfa.urls"
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': ':memory:',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [
|
||||||
|
os.path.join(BASE_DIR ,'mfa','templates' ),
|
||||||
|
os.path.join(BASE_DIR ,'tests','templates' )
|
||||||
|
],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.debug',
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
MFA_UNALLOWED_METHODS = []
|
||||||
11
tests/templates/base.html
Normal file
11
tests/templates/base.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
{% block head %}
|
||||||
|
{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
29
tests/test_views.py
Normal file
29
tests/test_views.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import pytest
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
@pytest.mark.VIEWS
|
||||||
|
@pytest.mark.DJANGO
|
||||||
|
@pytest.mark.ANNONYMOUS_USER
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_index_unauthenticated(client):
|
||||||
|
url = reverse("mfa_home")
|
||||||
|
response = client.get(url)
|
||||||
|
assert response is not None
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert response.url=="/accounts/login/?next=/"
|
||||||
|
|
||||||
|
@pytest.mark.VIEWS
|
||||||
|
@pytest.mark.AUTHENTICATED_USER
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_index_authenticated(client, authenticated_user):
|
||||||
|
url = reverse("mfa_home")
|
||||||
|
response = client.get(url)
|
||||||
|
assert response is not None
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert isinstance(response.templates, list)
|
||||||
|
assert len(response.templates) == 4
|
||||||
|
for template in response.templates:
|
||||||
|
assert template.name in ["modal.html", "base.html", "mfa_base.html", "MFA.html"]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
52
tox.ini
Normal file
52
tox.ini
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Tox (https://tox.readthedocs.io/) is a tool for running tests
|
||||||
|
# in multiple virtualenvs. This configuration file will run the
|
||||||
|
# test suite on all supported python versions. To use it, "pip install tox"
|
||||||
|
# and then run "tox" from this directory.
|
||||||
|
#
|
||||||
|
# See also https://tox.readthedocs.io/en/latest/config.html for more
|
||||||
|
# configuration options.
|
||||||
|
|
||||||
|
[tox]
|
||||||
|
# Choose your Python versions. They have to be available
|
||||||
|
# on the system the tests are run on.
|
||||||
|
# comma separated
|
||||||
|
envlist =
|
||||||
|
python{3.6,3.7,3.8,3.9}-django2.2
|
||||||
|
python{3.6,3.7,3.8,3.9}-django3
|
||||||
|
python{3.8,3.9,3.10,3.11}-django4
|
||||||
|
python{3.8,3.9,3.10,3.11}-django5
|
||||||
|
|
||||||
|
# Tell tox to not require a setup.py file
|
||||||
|
;skipsdist = True
|
||||||
|
|
||||||
|
isolated_build = True
|
||||||
|
|
||||||
|
[testenv]
|
||||||
|
# https://tox.wiki/en/latest/example/basic.html#using-a-different-default-pypi-url
|
||||||
|
;setenv =
|
||||||
|
; PIP_INDEX_URL = https://pypi.my-alternative-index.org
|
||||||
|
|
||||||
|
# https://tech.serhatteker.com/post/2020-02/test-env-vars-in-python/
|
||||||
|
;setenv =
|
||||||
|
; NAME=value
|
||||||
|
|
||||||
|
# https://tox.wiki/en/latest/example/basic.html#passing-down-environment-variables
|
||||||
|
# passenv = ENV_VAR_NAME
|
||||||
|
|
||||||
|
# https://tox.wiki/en/latest/example/pytest.html#extended-example-change-dir-before-test-and-use-per-virtualenv-tempdir
|
||||||
|
;changedir = tests
|
||||||
|
|
||||||
|
deps =
|
||||||
|
django22: django>=2.2,<2.3
|
||||||
|
django32: django>=3.2,<3.3
|
||||||
|
django40: django>=4.0,<4.1
|
||||||
|
django41: django>=4.1,<4.2
|
||||||
|
django42: django>=4.2,<4.3
|
||||||
|
django50: django==5.0
|
||||||
|
-rrequirements.txt
|
||||||
|
-rrequirements_testing.txt
|
||||||
|
|
||||||
|
# https://tox.wiki/en/latest/example/basic.html#ignoring-a-command-exit-code
|
||||||
|
commands =
|
||||||
|
; pytest --junitxml=report.xml
|
||||||
|
pytest {posargs}
|
||||||
Reference in New Issue
Block a user