├── accounts ├── __init__.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── templates │ ├── mfa_auth_base.html │ ├── login.html │ ├── index.html │ └── register.html ├── tests.py ├── apps.py ├── models.py ├── urls.py ├── utils.py ├── admin.py └── views.py ├── mfa ├── ApproveLogin.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── __init__.py ├── tests.py ├── apps.py ├── templates │ ├── FIDO2 │ │ ├── Auth.html │ │ ├── Add.html │ │ └── recheck.html │ ├── U2F │ │ ├── Auth.html │ │ ├── Add.html │ │ └── recheck.html │ ├── Email │ │ ├── mfa_email_token_template.html │ │ ├── Auth.html │ │ ├── Add.html │ │ └── recheck.html │ ├── ApproveLogin │ │ └── Add.html │ ├── TrustedDevices │ │ ├── email.html │ │ ├── user-agent.html │ │ ├── Done.html │ │ ├── start.html │ │ └── Add.html │ ├── TOTP │ │ ├── Auth.html │ │ ├── recheck.html │ │ └── Add.html │ ├── modal.html │ ├── select_mfa_method.html │ ├── mfa_check.html │ └── MFA.html ├── admin.py ├── Common.py ├── middleware.py ├── helpers.py ├── models.py ├── static │ └── mfa │ │ ├── css │ │ └── bootstrap-toggle.min.css │ │ └── js │ │ ├── bootstrap-toggle.min.js │ │ ├── cbor.js │ │ ├── qrious.min.js │ │ ├── ua-parser.min.js │ │ └── u2f-api.js ├── urls.py ├── Email.py ├── totp.py ├── views.py ├── U2F.py ├── TrustedDevice.py └── FIDO2.py ├── django_mfa2_example ├── __init__.py ├── asgi.py ├── wsgi.py ├── urls.py └── settings.py ├── Procfile ├── db.sqlite3 ├── static ├── images │ └── logo.png └── css │ └── custom.css ├── Pipfile ├── requirements.txt ├── manage.py ├── templates └── base.html ├── README.md └── Pipfile.lock /accounts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mfa/ApproveLogin.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mfa/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /accounts/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_mfa2_example/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mfa/__init__.py: -------------------------------------------------------------------------------- 1 | __version__="2.1.2" 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn django_mfa2_example.wsgi --log-file - -------------------------------------------------------------------------------- /accounts/templates/mfa_auth_base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | -------------------------------------------------------------------------------- /db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sirneij/django_mfa2_example/HEAD/db.sqlite3 -------------------------------------------------------------------------------- /mfa/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /accounts/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sirneij/django_mfa2_example/HEAD/static/images/logo.png -------------------------------------------------------------------------------- /mfa/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | class myAppNameConfig(AppConfig): 3 | name = 'mfa' 4 | verbose_name = 'A Much Better Name' -------------------------------------------------------------------------------- /mfa/templates/FIDO2/Auth.html: -------------------------------------------------------------------------------- 1 | {% extends "mfa_auth_base.html" %} 2 | {% block content %} 3 | {% include 'FIDO2/recheck.html' with mode='auth' %} 4 | {% endblock %} -------------------------------------------------------------------------------- /mfa/templates/U2F/Auth.html: -------------------------------------------------------------------------------- 1 | {% extends "mfa_auth_base.html" %} 2 | {% block content %} 3 |
4 |
5 | {% include 'U2F/recheck.html' with mode='auth' %} 6 | {% endblock %} -------------------------------------------------------------------------------- /accounts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'accounts' 7 | -------------------------------------------------------------------------------- /accounts/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import AbstractUser 3 | 4 | class User(AbstractUser): 5 | display_name = models.CharField(max_length=32) 6 | 7 | -------------------------------------------------------------------------------- /mfa/templates/Email/mfa_email_token_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dear {{ username }},
6 | Your OTP is: {{ otp }} 7 | 8 | Thanks 9 | 10 | -------------------------------------------------------------------------------- /mfa/templates/ApproveLogin/Add.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /mfa/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from mfa.models import User_Keys 3 | 4 | 5 | @admin.register(User_Keys) 6 | class User_KeysAdmin(admin.ModelAdmin): 7 | list_display = ('username', 'added_on', 'key_type', 'owned_by_enterprise',) 8 | -------------------------------------------------------------------------------- /mfa/templates/TrustedDevices/email.html: -------------------------------------------------------------------------------- 1 |

Dear {{ request.user.last_name }}, {{ request.user.first_name }}

2 |

You requested the link to add a new trusted device, please follow the link below
3 | {{ HOST }}{% url 'mfa_add_new_trusted_device' %} 4 |

5 | -------------------------------------------------------------------------------- /mfa/templates/Email/Auth.html: -------------------------------------------------------------------------------- 1 | {% extends "mfa_auth_base.html" %} 2 | {% block head %} 3 | 8 | {% endblock %} 9 | {% block content %} 10 |
11 |
12 | {% include "Email/recheck.html" with mode='auth' %} 13 | 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /mfa/templates/TOTP/Auth.html: -------------------------------------------------------------------------------- 1 | {% extends "mfa_auth_base.html" %} 2 | {% block head %} 3 | 8 | {% endblock %} 9 | {% block content %} 10 |
11 |
12 | {% include "TOTP/recheck.html" with mode='auth' %} 13 | 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | django = "*" 8 | whitenoise = "*" 9 | gunicorn = "*" 10 | python-decouple = "*" 11 | psycopg2 = "*" 12 | dj-database-url = "*" 13 | jsonfield = "*" 14 | python-jose = "*" 15 | django-mfa2 = "*" 16 | 17 | [dev-packages] 18 | 19 | [requires] 20 | python_version = "3.8" 21 | -------------------------------------------------------------------------------- /mfa/templates/TrustedDevices/user-agent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
Browser: {{ ua.browser.family }}
Version: {{ ua.browser.version_string }}
Device: {{ ua.device.brand }} / {{ ua.device.model }}
-------------------------------------------------------------------------------- /accounts/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | from django.contrib.auth import views as auth_views 4 | 5 | app_name = 'accounts' 6 | 7 | urlpatterns = [ 8 | path('', views.index, name='index'), 9 | path('login/', views.login, name='login'), 10 | path('register/', views.register, name='register'), 11 | path('logout/', auth_views.LogoutView.as_view(), name='logout'), 12 | ] 13 | -------------------------------------------------------------------------------- /mfa/Common.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.mail import EmailMessage 3 | 4 | def send(to,subject,body): 5 | from_email_address = settings.EMAIL_HOST_USER 6 | if '@' not in from_email_address: 7 | from_email_address = settings.DEFAULT_FROM_EMAIL 8 | From = "%s <%s>" % (settings.EMAIL_FROM, from_email_address) 9 | email = EmailMessage(subject,body,From,to) 10 | email.content_subtype = "html" 11 | return email.send(False) -------------------------------------------------------------------------------- /django_mfa2_example/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for django_mfa2_example project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_mfa2_example.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /django_mfa2_example/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_mfa2_example project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_mfa2_example.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.3.4 2 | cffi==1.14.5 3 | cryptography==3.4.7 4 | dj-database-url==0.5.0 5 | Django==3.2.3 6 | ecdsa==0.14.1 7 | fido2==0.9.1 8 | gunicorn==20.1.0 9 | jsonfield==3.1.0 10 | jsonLookup==0.9.0 11 | psycopg2==2.8.6 12 | pyasn1==0.4.8 13 | pycparser==2.20 14 | pyotp==2.6.0 15 | python-decouple==3.4 16 | python-jose==3.2.0 17 | python-u2flib-server==5.0.1 18 | pytz==2021.1 19 | rsa==4.7.2 20 | simplejson==3.17.2 21 | six==1.16.0 22 | sqlparse==0.4.1 23 | ua-parser==0.10.0 24 | user-agents==2.2.0 25 | whitenoise==5.2.0 26 | -------------------------------------------------------------------------------- /mfa/middleware.py: -------------------------------------------------------------------------------- 1 | import time 2 | from django.http import HttpResponseRedirect 3 | from django.core.urlresolvers import reverse 4 | from django.conf import settings 5 | def process(request): 6 | next_check=request.session.get('mfa',{}).get("next_check",False) 7 | if not next_check: return None 8 | now=int(time.time()) 9 | if now >= next_check: 10 | method=request.session["mfa"]["method"] 11 | path = request.META["PATH_INFO"] 12 | return HttpResponseRedirect(reverse(method+"_auth")+"?next=%s"%(settings.BASE_URL + path).replace("//", "/")) 13 | return None -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_mfa2_example.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /accounts/utils.py: -------------------------------------------------------------------------------- 1 | import six 2 | 3 | USERNAME_MAX_LENGTH = 32 4 | DISPLAY_NAME_MAX_LENGTH = 65 5 | 6 | def validate_username(username): 7 | if not isinstance(username, six.string_types): 8 | return False 9 | 10 | if len(username) > USERNAME_MAX_LENGTH: 11 | return False 12 | 13 | if not username.isalnum(): 14 | return False 15 | 16 | if not username.lower().startswith("cpe"): 17 | return False 18 | 19 | return True 20 | 21 | 22 | def validate_display_name(display_name): 23 | if not isinstance(display_name, six.string_types): 24 | return False 25 | 26 | if len(display_name) > DISPLAY_NAME_MAX_LENGTH: 27 | return False 28 | 29 | if not display_name.replace(' ', '').isalnum(): 30 | return False 31 | 32 | return True -------------------------------------------------------------------------------- /mfa/templates/modal.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mfa/templates/TrustedDevices/Done.html: -------------------------------------------------------------------------------- 1 | {% extends "mfa_auth_base.html" %} 2 | {% block head %} 3 | {% endblock %} 4 | {% block content %} 5 |
6 |
7 |
8 | 9 |
10 |
11 |
12 | Add Trusted Device 13 |
14 |
15 |
16 | Your device is now trusted, please try to login 17 |
18 | 19 |
20 | 24 |
25 |
26 |
27 | 28 | 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /mfa/templates/select_mfa_method.html: -------------------------------------------------------------------------------- 1 | {% extends "mfa_auth_base.html" %} 2 | {% block content %} 3 |
4 |
5 |
6 | 7 |
8 |
9 |
10 | Select Second Verification Method 11 |
12 | 26 |
27 |
28 |
29 | {% endblock %} -------------------------------------------------------------------------------- /mfa/templates/mfa_check.html: -------------------------------------------------------------------------------- 1 | 36 | {% include "modal.html" %} -------------------------------------------------------------------------------- /mfa/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.3 on 2021-05-26 09:05 2 | 3 | from django.db import migrations, models 4 | import jsonfield.fields 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='User_Keys', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('username', models.CharField(max_length=50)), 20 | ('properties', jsonfield.fields.JSONField(null=True)), 21 | ('added_on', models.DateTimeField(auto_now_add=True)), 22 | ('key_type', models.CharField(default='TOTP', max_length=25)), 23 | ('enabled', models.BooleanField(default=True)), 24 | ('expires', models.DateTimeField(blank=True, default=None, null=True)), 25 | ('last_used', models.DateTimeField(blank=True, default=None, null=True)), 26 | ('owned_by_enterprise', models.BooleanField(blank=True, default=None, null=True)), 27 | ], 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Django-mfa2 Example | {% block title %} {% endblock title %} 11 | 12 | 13 | 17 | 18 | 19 | 23 | 24 | 25 | 26 | {% block head %} {% endblock %} {% block css %} {% endblock css %} 27 | 28 | 29 | {% block content %} {% endblock content %} 30 | 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django_mfa2_example 2 | 3 | Fingerprint-based authentication and authorization system in Python (Django). This can be integrated with e-voting systems and other applications that should be very secure. 4 | 5 | A walk-through of this repository can be found on [dev.to](https://dev.to/) in this tutorial-like article [Fingerprint-based authentication and authorization in Python(Django) web applications]( 6 | https://dev.to/sirneij/fingerprint-based-authentication-and-authorization-in-python-django-web-applications-2c6l). 7 | This example application uses [Django-mfa2](https://github.com/mkalioby/django-mfa2) to implement a password-less fingerprint-based authentication and authorization system. It's live and can be accessed [here](https://django-mfa2-example.herokuapp.com/). 8 | ## Run locally 9 | 10 | - clone this report: 11 | ``` 12 | git clone https://github.com/Sirneij/django_mfa2_example.git 13 | ``` 14 | - create and activate virtual environment (I used `pipenv` but you can stick with `venv`, `virtualenv` or `poetry`): 15 | ``` 16 | pipenv shell 17 | pipenv install 18 | ``` 19 | - makemigrations and migrate: 20 | ``` 21 | python manage.py makemigrations 22 | python manage.py migrate 23 | ``` 24 | - optionally, createsuperuser: 25 | ``` 26 | python manage.py createsuperuser 27 | ``` 28 | -------------------------------------------------------------------------------- /django_mfa2_example/urls.py: -------------------------------------------------------------------------------- 1 | """django_mfa2_example URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.conf import settings 18 | from django.conf.urls.static import static 19 | from django.urls import path, include 20 | import mfa 21 | import mfa.TrustedDevice 22 | 23 | urlpatterns = [ 24 | path('admin/', admin.site.urls), 25 | path('mfa/', include('mfa.urls')), 26 | path('devices/add/', mfa.TrustedDevice.add,name="mfa_add_new_trusted_device"), 27 | path("", include('accounts.urls', namespace='accounts')) 28 | ] 29 | 30 | if settings.DEBUG: 31 | urlpatterns += static(settings.STATIC_URL, 32 | document_root=settings.STATIC_ROOT) -------------------------------------------------------------------------------- /static/css/custom.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Globals 3 | */ 4 | 5 | /* Custom default button */ 6 | .btn-secondary, 7 | .btn-secondary:hover, 8 | .btn-secondary:focus { 9 | color: #333; 10 | text-shadow: none; /* Prevent inheritance from `body` */ 11 | } 12 | 13 | .bd-placeholder-img { 14 | font-size: 1.125rem; 15 | text-anchor: middle; 16 | -webkit-user-select: none; 17 | -moz-user-select: none; 18 | user-select: none; 19 | } 20 | 21 | @media (min-width: 768px) { 22 | .bd-placeholder-img-lg { 23 | font-size: 3.5rem; 24 | } 25 | } 26 | /* 27 | * Base structure 28 | */ 29 | 30 | body { 31 | text-shadow: 0 0.05rem 0.1rem rgba(0, 0, 0, 0.5); 32 | box-shadow: inset 0 0 5rem rgba(0, 0, 0, 0.5); 33 | } 34 | 35 | .cover-container { 36 | max-width: 42em; 37 | } 38 | 39 | /* 40 | * Header 41 | */ 42 | 43 | .nav-masthead .nav-link { 44 | padding: 0.25rem 0; 45 | font-weight: 700; 46 | color: rgba(255, 255, 255, 0.5); 47 | background-color: transparent; 48 | border-bottom: 0.25rem solid transparent; 49 | } 50 | 51 | .nav-masthead .nav-link:hover, 52 | .nav-masthead .nav-link:focus { 53 | border-bottom-color: rgba(255, 255, 255, 0.25); 54 | } 55 | 56 | .nav-masthead .nav-link + .nav-link { 57 | margin-left: 1rem; 58 | } 59 | 60 | .nav-masthead .active { 61 | color: #fff; 62 | border-bottom-color: #fff; 63 | } 64 | -------------------------------------------------------------------------------- /mfa/helpers.py: -------------------------------------------------------------------------------- 1 | import pyotp 2 | from .models import * 3 | from . import TrustedDevice, U2F, FIDO2, totp 4 | import simplejson 5 | from django.shortcuts import HttpResponse 6 | from mfa.views import verify,goto 7 | def has_mfa(request,username): 8 | if User_Keys.objects.filter(username=username,enabled=1).count()>0: 9 | return verify(request, username) 10 | return False 11 | 12 | def is_mfa(request,ignore_methods=[]): 13 | if request.session.get("mfa",{}).get("verified",False): 14 | if not request.session.get("mfa",{}).get("method",None) in ignore_methods: 15 | return True 16 | return False 17 | 18 | def recheck(request): 19 | method=request.session.get("mfa",{}).get("method",None) 20 | if not method: 21 | return HttpResponse(simplejson.dumps({"res":False}),content_type="application/json") 22 | if method=="Trusted Device": 23 | return HttpResponse(simplejson.dumps({"res":TrustedDevice.verify(request)}),content_type="application/json") 24 | elif method=="U2F": 25 | return HttpResponse(simplejson.dumps({"html": U2F.recheck(request).content}), content_type="application/json") 26 | elif method == "FIDO2": 27 | return HttpResponse(simplejson.dumps({"html": FIDO2.recheck(request).content}), content_type="application/json") 28 | elif method=="TOTP": 29 | return HttpResponse(simplejson.dumps({"html": totp.recheck(request).content}), content_type="application/json") 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /mfa/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from jsonfield import JSONField 3 | from jose import jwt 4 | from django.conf import settings 5 | from jsonLookup import shasLookup, hasLookup 6 | JSONField.register_lookup(shasLookup) 7 | JSONField.register_lookup(hasLookup) 8 | 9 | 10 | class User_Keys(models.Model): 11 | username=models.CharField(max_length = 50) 12 | properties=JSONField(null = True) 13 | added_on=models.DateTimeField(auto_now_add = True) 14 | key_type=models.CharField(max_length = 25,default = "TOTP") 15 | enabled=models.BooleanField(default=True) 16 | expires=models.DateTimeField(null=True,default=None,blank=True) 17 | last_used=models.DateTimeField(null=True,default=None,blank=True) 18 | owned_by_enterprise=models.BooleanField(default=None,null=True,blank=True) 19 | 20 | def save(self, force_insert=False, force_update=False, using=None, update_fields=None): 21 | if self.key_type == "Trusted Device" and self.properties.get("signature","") == "": 22 | self.properties["signature"]= jwt.encode({"username": self.username, "key": self.properties["key"]}, settings.SECRET_KEY) 23 | super(User_Keys, self).save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields) 24 | 25 | def __unicode__(self): 26 | return "%s -- %s"%(self.username,self.key_type) 27 | 28 | def __str__(self): 29 | return self.__unicode__() 30 | 31 | class Meta: 32 | app_label='mfa' 33 | -------------------------------------------------------------------------------- /mfa/static/mfa/css/bootstrap-toggle.min.css: -------------------------------------------------------------------------------- 1 | /*! ======================================================================== 2 | * Bootstrap Toggle: bootstrap-toggle.css v2.2.0 3 | * http://www.bootstraptoggle.com 4 | * ======================================================================== 5 | * Copyright 2014 Min Hur, The New York Times Company 6 | * Licensed under MIT 7 | * ======================================================================== */ 8 | .checkbox label .toggle,.checkbox-inline .toggle{margin-left:-20px;margin-right:5px} 9 | .toggle{position:relative;overflow:hidden} 10 | .toggle input[type=checkbox]{display:none} 11 | .toggle-group{position:absolute;width:200%;top:0;bottom:0;left:0;transition:left .35s;-webkit-transition:left .35s;-moz-user-select:none;-webkit-user-select:none} 12 | .toggle.off .toggle-group{left:-100%} 13 | .toggle-on{position:absolute;top:0;bottom:0;left:0;right:50%;margin:0;border:0;border-radius:0} 14 | .toggle-off{position:absolute;top:0;bottom:0;left:50%;right:0;margin:0;border:0;border-radius:0} 15 | .toggle-handle{position:relative;margin:0 auto;padding-top:0;padding-bottom:0;height:100%;width:0;border-width:0 1px} 16 | .toggle.btn{min-width:59px;min-height:34px} 17 | .toggle-on.btn{padding-right:24px} 18 | .toggle-off.btn{padding-left:24px} 19 | .toggle.btn-lg{min-width:79px;min-height:45px} 20 | .toggle-on.btn-lg{padding-right:31px} 21 | .toggle-off.btn-lg{padding-left:31px} 22 | .toggle-handle.btn-lg{width:40px} 23 | .toggle.btn-sm{min-width:50px;min-height:30px} 24 | .toggle-on.btn-sm{padding-right:20px} 25 | .toggle-off.btn-sm{padding-left:20px} 26 | .toggle.btn-xs{min-width:35px;min-height:22px} 27 | .toggle-on.btn-xs{padding-right:12px} 28 | .toggle-off.btn-xs{padding-left:12px} -------------------------------------------------------------------------------- /mfa/templates/Email/Add.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block head %} 3 | {% endblock %} 4 | {% block content %} 5 |
6 |
7 |
8 |
9 |
10 | Activate Token by email 11 |
12 |
13 |
14 | {% csrf_token %} 15 | {% if invalid %} 16 |
17 | Sorry, The provided token is not valid. 18 |
19 | {% endif %} 20 | {% if quota %} 21 |
22 | {{ quota }} 23 |
24 | {% endif %} 25 |
26 |
27 |
28 |

Enter the code sent to your email.

29 |
30 |
31 |
32 |
33 |
34 |
35 | 36 | 37 | 38 | 39 |
40 |
41 |
42 | 43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | {% endblock %} -------------------------------------------------------------------------------- /mfa/templates/U2F/Add.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | {% block head %} 4 | 14 | 15 | 35 | {% endblock %} 36 | {% block content %} 37 |
38 |
39 |
40 |
41 |
42 |

Adding Security Key

43 |
44 |
45 |

Your secure Key should be flashing now, please press on button.

46 | 47 |
48 |
49 |
50 | {% include "modal.html" %} 51 | {% endblock %} -------------------------------------------------------------------------------- /accounts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import User 3 | from django.contrib.auth.admin import UserAdmin 4 | 5 | class CustomUserAdmin(UserAdmin): 6 | model = User 7 | readonly_fields = ['date_joined', ] 8 | actions = ['activate_users', ] 9 | list_display = ('username','display_name', 'email', 'first_name', 'last_name', 10 | 'is_staff',) 11 | 12 | def get_inline_instances(self, request, obj=None): 13 | if not obj: 14 | return list() 15 | return super(CustomUserAdmin, self).get_inline_instances(request, obj) 16 | 17 | def get_form(self, request, obj=None, **kwargs): 18 | form = super().get_form(request, obj, **kwargs) 19 | is_superuser = request.user.is_superuser 20 | disabled_fields = set() 21 | 22 | if not is_superuser: 23 | disabled_fields |= { 24 | 'username', 25 | 'is_superuser', 26 | } 27 | # Prevent non-superusers from editing their own permissions 28 | if ( 29 | not is_superuser 30 | and obj is not None 31 | and obj == request.user 32 | ): 33 | disabled_fields |= { 34 | 'is_staff', 35 | 'is_superuser', 36 | 'groups', 37 | 'user_permissions', 38 | } 39 | for f in disabled_fields: 40 | if f in form.base_fields: 41 | form.base_fields[f].disabled = True 42 | 43 | return form 44 | 45 | def activate_users(self, request, queryset): 46 | cannot = queryset.filter(is_active=False).update(is_active=True) 47 | self.message_user(request, 'Activated {} users.'.format(cannot)) 48 | activate_users.short_description = 'Activate Users' # type: ignore 49 | 50 | def get_actions(self, request): 51 | actions = super().get_actions(request) 52 | if not request.user.has_perm('auth.change_user'): 53 | del actions['activate_users'] 54 | return actions 55 | 56 | 57 | admin.site.register(User, CustomUserAdmin) 58 | -------------------------------------------------------------------------------- /mfa/urls.py: -------------------------------------------------------------------------------- 1 | from . import views,totp,U2F,TrustedDevice,helpers,FIDO2,Email 2 | #app_name='mfa' 3 | 4 | try: 5 | from django.urls import re_path as url 6 | except: 7 | from django.conf.urls import url 8 | urlpatterns = [ 9 | url(r'totp/start/', totp.start , name="start_new_otop"), 10 | url(r'totp/getToken', totp.getToken , name="get_new_otop"), 11 | url(r'totp/verify', totp.verify, name="verify_otop"), 12 | url(r'totp/auth', totp.auth, name="totp_auth"), 13 | url(r'totp/recheck', totp.recheck, name="totp_recheck"), 14 | 15 | url(r'email/start/', Email.start , name="start_email"), 16 | url(r'email/auth/', Email.auth , name="email_auth"), 17 | 18 | url(r'u2f/$', U2F.start, name="start_u2f"), 19 | url(r'u2f/bind', U2F.bind, name="bind_u2f"), 20 | url(r'u2f/auth', U2F.auth, name="u2f_auth"), 21 | url(r'u2f/process_recheck', U2F.process_recheck, name="u2f_recheck"), 22 | url(r'u2f/verify', U2F.verify, name="u2f_verify"), 23 | 24 | url(r'fido2/$', FIDO2.start, name="start_fido2"), 25 | url(r'fido2/auth', FIDO2.auth, name="fido2_auth"), 26 | url(r'fido2/begin_auth', FIDO2.authenticate_begin, name="fido2_begin_auth"), 27 | url(r'fido2/complete_auth', FIDO2.authenticate_complete, name="fido2_complete_auth"), 28 | url(r'fido2/begin_reg', FIDO2.begin_registeration, name="fido2_begin_reg"), 29 | url(r'fido2/complete_reg', FIDO2.complete_reg, name="fido2_complete_reg"), 30 | url(r'fido2/recheck', FIDO2.recheck, name="fido2_recheck"), 31 | 32 | 33 | url(r'td/$', TrustedDevice.start, name="start_td"), 34 | url(r'td/add', TrustedDevice.add, name="add_td"), 35 | url(r'td/send_link', TrustedDevice.send_email, name="td_sendemail"), 36 | url(r'td/get-ua', TrustedDevice.getUserAgent, name="td_get_useragent"), 37 | url(r'td/trust', TrustedDevice.trust_device, name="td_trust_device"), 38 | url(r'u2f/checkTrusted', TrustedDevice.checkTrusted, name="td_checkTrusted"), 39 | url(r'u2f/secure_device', TrustedDevice.getCookie, name="td_securedevice"), 40 | 41 | url(r'^$', views.index, name="mfa_home"), 42 | url(r'goto/(.*)', views.goto, name="mfa_goto"), 43 | url(r'selct_method', views.show_methods, name="mfa_methods_list"), 44 | url(r'recheck', helpers.recheck, name="mfa_recheck"), 45 | url(r'toggleKey', views.toggleKey, name="toggle_key"), 46 | url(r'delete', views.delKey, name="mfa_delKey"), 47 | url(r'reset', views.reset_cookie, name="mfa_reset_cookie"), 48 | 49 | ] 50 | # print(urlpatterns) -------------------------------------------------------------------------------- /accounts/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load static %} 4 | 5 | {% block css %} 6 | 60 | {% endblock css %} 61 | 62 | 63 | {% block title %} {{page_title}} {% endblock title %} 64 | 65 | 66 | {% block content %} 67 | 68 |
69 |
70 | {% csrf_token %} 71 | 78 |

Login

79 | 80 | {% if err %} 81 |

{{err}}

82 | {% endif %} 83 | 84 |
85 | 92 | 93 | 94 |
95 | 96 | 97 |

© 2021

98 |
99 |
100 | 101 | {% endblock content %} 102 | -------------------------------------------------------------------------------- /accounts/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load static %} 4 | 5 | {% block title %} {{page_title}} {% endblock title %} 6 | 7 | {% block css %} 8 | 9 | {% endblock css %} 10 | 11 | {% block content %} 12 | 13 |
14 |
15 |
16 |

Cover

17 | 32 |
33 |
34 | 35 |
36 | {% if request.user.is_authenticated %} 37 | 38 |

You are logged in!!!!

39 |

40 | You have successfully used Django-mfa2 biometric authentication to log 41 | in. Hurray! 42 |

43 |

44 | Learn more 49 |

50 | {% else %} 51 | 52 |

Cover your page.

53 |

54 | Cover is a one-page template for building simple and beautiful home 55 | pages. Download, edit the text, and add your own fullscreen background 56 | photo to make it your own. 57 |

58 |

59 | Learn more 64 |

65 | {% endif %} 66 |
67 | 68 | 75 |
76 | 77 | 78 | {% endblock content %} 79 | -------------------------------------------------------------------------------- /mfa/templates/Email/recheck.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | 18 |
19 |
20 |
21 | Email One Time Password 22 |
23 |
24 | 25 |
26 | 27 | 28 | {% csrf_token %} 29 | {% if invalid %} 30 |
31 | Sorry, The provided token is not valid. 32 |
33 | {% endif %} 34 | {% if quota %} 35 |
36 | {{ quota }} 37 |
38 | {% endif %} 39 |
40 |
41 |
42 |

Enter the code sent to your email.

43 |
44 |
45 | 46 |
47 |
48 |
49 |
50 | 51 | 52 | 53 | 54 | 55 |
56 |
57 | 58 |
59 | 60 | 61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | {% if request.session.mfa_methods|length > 1 %} 69 | Select Another Method 70 | {% endif %} 71 |
72 |
73 |
74 |
75 |
76 | 77 | -------------------------------------------------------------------------------- /mfa/templates/TOTP/recheck.html: -------------------------------------------------------------------------------- 1 | 16 |
17 | 18 |
19 |
20 |
21 | One Time Password 22 |
23 |
24 | 25 |
26 | 27 | 28 | {% csrf_token %} 29 | {% if invalid %} 30 |
31 | Sorry, The provided token is not valid. 32 |
33 | {% endif %} 34 | {% if quota %} 35 |
36 | {{ quota }} 37 |
38 | {% endif %} 39 |
40 |
41 |
42 |

Enter the 6-digits on your authenticator.

43 |
44 |
45 | 46 |
47 |
48 |
49 |
50 | 51 | 52 | 53 | 54 | 55 |
56 |
57 | 58 |
59 | 60 | 61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | {% if request.session.mfa_methods|length > 1 %} 69 | Select Another Method 70 | {% endif %} 71 |
72 |
73 |
74 |
75 |
76 | 77 | -------------------------------------------------------------------------------- /mfa/Email.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.views.decorators.cache import never_cache 3 | from django.template.context_processors import csrf 4 | import datetime,random 5 | from random import randint 6 | from .models import * 7 | #from django.template.context import RequestContext 8 | from .views import login 9 | from .Common import send 10 | def sendEmail(request,username,secret): 11 | from django.contrib.auth import get_user_model 12 | User = get_user_model() 13 | key = getattr(User, 'USERNAME_FIELD', 'username') 14 | kwargs = {key: username} 15 | user = User.objects.get(**kwargs) 16 | res=render(request,"mfa_email_token_template.html",{"request":request,"user":user,'otp':secret}) 17 | return send([user.email],"OTP", res.content.decode()) 18 | 19 | @never_cache 20 | def start(request): 21 | context = csrf(request) 22 | if request.method == "POST": 23 | if request.session["email_secret"] == request.POST["otp"]: 24 | uk=User_Keys() 25 | uk.username=request.user.username 26 | uk.key_type="Email" 27 | uk.enabled=1 28 | uk.save() 29 | from django.http import HttpResponseRedirect 30 | try: 31 | from django.core.urlresolvers import reverse 32 | except: 33 | from django.urls import reverse 34 | return HttpResponseRedirect(reverse('mfa_home')) 35 | context["invalid"] = True 36 | else: 37 | request.session["email_secret"] = str(randint(0,100000)) 38 | if sendEmail(request, request.user.username, request.session["email_secret"]): 39 | context["sent"] = True 40 | return render(request,"Email/Add.html", context) 41 | @never_cache 42 | def auth(request): 43 | context=csrf(request) 44 | if request.method=="POST": 45 | if request.session["email_secret"]==request.POST["otp"].strip(): 46 | uk = User_Keys.objects.get(username=request.session["base_username"], key_type="Email") 47 | mfa = {"verified": True, "method": "Email","id":uk.id} 48 | if getattr(settings, "MFA_RECHECK", False): 49 | mfa["next_check"] = datetime.datetime.timestamp(datetime.datetime.now() + datetime.timedelta( 50 | seconds = random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))) 51 | request.session["mfa"] = mfa 52 | 53 | from django.utils import timezone 54 | uk.last_used=timezone.now() 55 | uk.save() 56 | return login(request) 57 | context["invalid"]=True 58 | else: 59 | request.session["email_secret"] = str(randint(0, 100000)) 60 | if sendEmail(request, request.session["base_username"], request.session["email_secret"]): 61 | context["sent"] = True 62 | return render(request,"Email/Auth.html", context) 63 | -------------------------------------------------------------------------------- /accounts/templates/register.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | 4 | {% block title %} {{page_title}} {% endblock title %} 5 | 6 | 7 | {% load static %} 8 | 9 | {% block css %} 10 | 64 | {% endblock css %} 65 | 66 | {% block content %} 67 | 68 |
69 |
70 | {% csrf_token %} 71 | 78 |

Register

79 | 80 | {% if error %} 81 |

{{error}}

82 | {% endif %} 83 | 84 |
85 | 92 | 93 | 94 |
95 |
96 | 103 | 104 | 105 |
106 | 107 | 112 |

© 2021

113 |
114 |
115 | 116 | {% endblock content %} 117 | -------------------------------------------------------------------------------- /accounts/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect 2 | from django.urls import reverse 3 | from django.contrib import auth 4 | from django.utils import timezone 5 | from django.conf import settings 6 | from .models import User 7 | from . import utils 8 | 9 | def login_user_in(request, username): 10 | user=User.objects.get(username=username) 11 | user.backend='django.contrib.auth.backends.ModelBackend' 12 | auth.login(request, user) 13 | if "redirect" in request.POST: 14 | return redirect(request.POST["redirect"]) 15 | else: 16 | return redirect(reverse('accounts:index')) 17 | 18 | def login(request): 19 | if request.method == "POST": 20 | username = request.POST.get('username').replace('/', '') 21 | user = User.objects.filter(username=username).first() 22 | err="" 23 | if user is not None: 24 | if user.is_active: 25 | if "mfa" in settings.INSTALLED_APPS: 26 | from mfa.helpers import has_mfa 27 | res = has_mfa(request,username=username) 28 | if res: return res 29 | return login_user_in(request, username) 30 | else: 31 | err="This student is NOT activated yet." 32 | else: 33 | err="No student with such matriculation number exists." 34 | return render(request, 'login.html', {"err":err}) 35 | else: 36 | return render(request, 'login.html') 37 | 38 | def register(request): 39 | if request.method == "POST": 40 | error = '' 41 | username = request.POST.get('username').replace('/', '') 42 | display_name = request.POST.get('display-name') 43 | if not utils.validate_username(username): 44 | error = 'Invalid matriculation number' 45 | return render(request, 'register.html', context = {'page_title': "Register", 'error': error}) 46 | if not utils.validate_display_name(display_name): 47 | error = 'Invalid display name' 48 | return render(request, 'register.html', context = {'page_title': "Register", 'error': error}) 49 | if User.objects.filter(username=username).exists(): 50 | error = 'Student already exists.' 51 | return render(request, 'register.html', context = {'page_title': "Register", 'error': error}) 52 | else: 53 | u = User.objects.create(first_name = display_name, password='none', is_superuser=False, username=username, last_name='', display_name=display_name, email='none', is_staff=False, is_active=True,date_joined=timezone.now()) 54 | u.backend = 'django.contrib.auth.backends.ModelBackend' 55 | auth.login(request,u) 56 | return redirect(reverse('start_fido2')) 57 | else: 58 | return render(request, 'register.html', context = {'page_title': "Register"}) 59 | 60 | def index(request): 61 | return render(request, 'index.html', {"page_title": "Welcome home"}) -------------------------------------------------------------------------------- /accounts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.3 on 2021-05-26 09:05 2 | 3 | import django.contrib.auth.models 4 | import django.contrib.auth.validators 5 | from django.db import migrations, models 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ('auth', '0012_alter_user_first_name_max_length'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='User', 20 | fields=[ 21 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('password', models.CharField(max_length=128, verbose_name='password')), 23 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 24 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 25 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), 26 | ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), 27 | ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), 28 | ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), 29 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 30 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 31 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 32 | ('display_name', models.CharField(max_length=32)), 33 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), 34 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), 35 | ], 36 | options={ 37 | 'verbose_name': 'user', 38 | 'verbose_name_plural': 'users', 39 | 'abstract': False, 40 | }, 41 | managers=[ 42 | ('objects', django.contrib.auth.models.UserManager()), 43 | ], 44 | ), 45 | ] 46 | -------------------------------------------------------------------------------- /mfa/totp.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.views.decorators.cache import never_cache 3 | from django.http import HttpResponse 4 | from .models import * 5 | from django.template.context_processors import csrf 6 | import simplejson 7 | from django.template.context import RequestContext 8 | from django.conf import settings 9 | import pyotp 10 | from .views import login 11 | import datetime 12 | from django.utils import timezone 13 | import random 14 | def verify_login(request,username,token): 15 | for key in User_Keys.objects.filter(username=username,key_type = "TOTP"): 16 | totp = pyotp.TOTP(key.properties["secret_key"]) 17 | if totp.verify(token,valid_window = 30): 18 | key.last_used=timezone.now() 19 | key.save() 20 | return [True,key.id] 21 | return [False] 22 | 23 | def recheck(request): 24 | context = csrf(request) 25 | context["mode"]="recheck" 26 | if request.method == "POST": 27 | if verify_login(request,request.user.username, token=request.POST["otp"]): 28 | import time 29 | request.session["mfa"]["rechecked_at"] = time.time() 30 | return HttpResponse(simplejson.dumps({"recheck": True}), content_type="application/json") 31 | else: 32 | return HttpResponse(simplejson.dumps({"recheck": False}), content_type="application/json") 33 | return render(request,"TOTP/recheck.html", context) 34 | 35 | @never_cache 36 | def auth(request): 37 | context=csrf(request) 38 | if request.method=="POST": 39 | res=verify_login(request,request.session["base_username"],token = request.POST["otp"]) 40 | if res[0]: 41 | mfa = {"verified": True, "method": "TOTP","id":res[1]} 42 | if getattr(settings, "MFA_RECHECK", False): 43 | mfa["next_check"] = datetime.datetime.timestamp((datetime.datetime.now() 44 | + datetime.timedelta( 45 | seconds=random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX)))) 46 | request.session["mfa"] = mfa 47 | return login(request) 48 | context["invalid"]=True 49 | return render(request,"TOTP/Auth.html", context) 50 | 51 | 52 | 53 | def getToken(request): 54 | secret_key=pyotp.random_base32() 55 | totp = pyotp.TOTP(secret_key) 56 | request.session["new_mfa_answer"]=totp.now() 57 | return HttpResponse(simplejson.dumps({"qr":pyotp.totp.TOTP(secret_key).provisioning_uri(str(request.user.username), issuer_name = settings.TOKEN_ISSUER_NAME), 58 | "secret_key": secret_key})) 59 | def verify(request): 60 | answer=request.GET["answer"] 61 | secret_key=request.GET["key"] 62 | totp = pyotp.TOTP(secret_key) 63 | if totp.verify(answer,valid_window = 60): 64 | uk=User_Keys() 65 | uk.username=request.user.username 66 | uk.properties={"secret_key":secret_key} 67 | #uk.name="Authenticatior #%s"%User_Keys.objects.filter(username=user.username,type="TOTP") 68 | uk.key_type="TOTP" 69 | uk.save() 70 | return HttpResponse("Success") 71 | else: return HttpResponse("Error") 72 | 73 | @never_cache 74 | def start(request): 75 | return render(request,"TOTP/Add.html",{}) 76 | -------------------------------------------------------------------------------- /mfa/templates/FIDO2/Add.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% load static %} {% block head %} 2 | 6 | 10 | 75 | 76 | {% endblock %} {% block content %} 77 |
78 |
79 |
80 |
81 |
82 | FIDO2 Security Key 83 |
84 |
85 |
86 |

87 | Your browser should ask you to confirm you identity. 88 |

89 |
90 |
91 |
92 | {% include "modal.html" %} {% endblock %} 93 |
94 | -------------------------------------------------------------------------------- /mfa/templates/TrustedDevices/start.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block head %} 3 | 13 | 14 | 71 | {% endblock %} 72 | {% block content %} 73 |
74 |
75 |
76 |
77 |
78 |

Add Trusted Device

79 |
80 | 81 |
82 | {% if not_allowed %} 83 |
You can't add any more devices, you need to remove previously trusted devices first.
84 | {% else %} 85 |

Allow access from mobile phone and tables.

86 |
Steps:
87 |
    88 |
  1. Using your mobile/table, open Chrome/Firefox.
  2. 89 |
  3. Go to {{ HOST }}{{ BASE_URL }}devices/add  
  4. 90 |
  5. Enter your username & following 6 digits
    91 | {{ key|slice:":3" }} - {{ key|slice:"3:" }} 92 |
  6. 93 |
  7. This window will ask to confirm the device.
  8. 94 | 95 |
96 | {% endif %} 97 |
98 |
99 |
100 | {% include "modal.html" %} 101 | {% include 'mfa_check.html' %} 102 | {% endblock %} -------------------------------------------------------------------------------- /mfa/templates/U2F/recheck.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 |
3 | 4 |
5 |
6 |
7 | Security Key 8 |
9 |
10 | 11 |
12 |
13 |

Your key should be flashing now, please press the button.

14 | {% if mode == "auth" %} 15 |
16 | {% elif mode == "recheck" %} 17 | 18 | {% endif %} 19 | {% csrf_token %} 20 | 21 |
22 |
23 |
24 |
25 | 26 |
27 |
28 | 29 | {% if request.session.mfa_methods|length > 1 %} 30 | Select Another Method 31 | {% endif %} 32 |
33 |
34 |
35 |
36 |
37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /mfa/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.http import HttpResponse,HttpResponseRedirect 3 | from .models import * 4 | try: 5 | from django.urls import reverse 6 | except: 7 | from django.core.urlresolvers import reverse 8 | from django.template.context_processors import csrf 9 | from django.template.context import RequestContext 10 | from django.conf import settings 11 | from . import TrustedDevice 12 | from django.contrib.auth.decorators import login_required 13 | from user_agents import parse 14 | 15 | @login_required 16 | def index(request): 17 | keys=[] 18 | context={"keys":User_Keys.objects.filter(username=request.user.username),"UNALLOWED_AUTHEN_METHODS":settings.MFA_UNALLOWED_METHODS 19 | ,"HIDE_DISABLE":getattr(settings,"MFA_HIDE_DISABLE",[])} 20 | for k in context["keys"]: 21 | if k.key_type =="Trusted Device" : 22 | setattr(k,"device",parse(k.properties.get("user_agent","-----"))) 23 | elif k.key_type == "FIDO2": 24 | setattr(k,"device",k.properties.get("type","----")) 25 | keys.append(k) 26 | context["keys"]=keys 27 | return render(request,"MFA.html",context) 28 | 29 | def verify(request,username): 30 | request.session["base_username"] = username 31 | #request.session["base_password"] = password 32 | keys=User_Keys.objects.filter(username=username,enabled=1) 33 | methods=list(set([k.key_type for k in keys])) 34 | 35 | if "Trusted Device" in methods and not request.session.get("checked_trusted_device",False): 36 | if TrustedDevice.verify(request): 37 | return login(request) 38 | methods.remove("Trusted Device") 39 | request.session["mfa_methods"] = methods 40 | if len(methods)==1: 41 | return HttpResponseRedirect(reverse(methods[0].lower()+"_auth")) 42 | return show_methods(request) 43 | 44 | def show_methods(request): 45 | return render(request,"select_mfa_method.html", {}) 46 | 47 | def reset_cookie(request): 48 | response=HttpResponseRedirect(settings.LOGIN_URL) 49 | response.delete_cookie("base_username") 50 | return response 51 | def login(request): 52 | from django.contrib import auth 53 | from django.conf import settings 54 | callable_func = __get_callable_function__(settings.MFA_LOGIN_CALLBACK) 55 | return callable_func(request,username=request.session["base_username"]) 56 | 57 | 58 | @login_required 59 | def delKey(request): 60 | key=User_Keys.objects.get(id=request.GET["id"]) 61 | if key.username == request.user.username: 62 | key.delete() 63 | return HttpResponse("Deleted Successfully") 64 | else: 65 | return HttpResponse("Error: You own this token so you can't delete it") 66 | 67 | def __get_callable_function__(func_path): 68 | import importlib 69 | if not '.' in func_path: 70 | raise Exception("class Name should include modulename.classname") 71 | 72 | parsed_str = func_path.split(".") 73 | module_name , func_name = ".".join(parsed_str[:-1]) , parsed_str[-1] 74 | imported_module = importlib.import_module(module_name) 75 | callable_func = getattr(imported_module,func_name) 76 | if not callable_func: 77 | raise Exception("Module does not have requested function") 78 | return callable_func 79 | 80 | @login_required 81 | def toggleKey(request): 82 | id=request.GET["id"] 83 | q=User_Keys.objects.filter(username=request.user.username, id=id) 84 | if q.count()==1: 85 | key=q[0] 86 | if not key.key_type in settings.MFA_HIDE_DISABLE: 87 | key.enabled=not key.enabled 88 | key.save() 89 | return HttpResponse("OK") 90 | else: 91 | return HttpResponse("You can't change this method.") 92 | else: 93 | return HttpResponse("Error") 94 | 95 | def goto(request,method): 96 | return HttpResponseRedirect(reverse(method.lower()+"_auth")) 97 | -------------------------------------------------------------------------------- /mfa/static/mfa/js/bootstrap-toggle.min.js: -------------------------------------------------------------------------------- 1 | /*! ======================================================================== 2 | * Bootstrap Toggle: bootstrap-toggle.js v2.2.0 3 | * http://www.bootstraptoggle.com 4 | * ======================================================================== 5 | * Copyright 2014 Min Hur, The New York Times Company 6 | * Licensed under MIT 7 | * ======================================================================== */ 8 | +function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.toggle"),f="object"==typeof b&&b;e||d.data("bs.toggle",e=new c(this,f)),"string"==typeof b&&e[b]&&e[b]()})}var c=function(b,c){this.$element=a(b),this.options=a.extend({},this.defaults(),c),this.render()};c.VERSION="2.2.0",c.DEFAULTS={on:"On",off:"Off",onstyle:"primary",offstyle:"default",size:"normal",style:"",width:null,height:null},c.prototype.defaults=function(){return{on:this.$element.attr("data-on")||c.DEFAULTS.on,off:this.$element.attr("data-off")||c.DEFAULTS.off,onstyle:this.$element.attr("data-onstyle")||c.DEFAULTS.onstyle,offstyle:this.$element.attr("data-offstyle")||c.DEFAULTS.offstyle,size:this.$element.attr("data-size")||c.DEFAULTS.size,style:this.$element.attr("data-style")||c.DEFAULTS.style,width:this.$element.attr("data-width")||c.DEFAULTS.width,height:this.$element.attr("data-height")||c.DEFAULTS.height}},c.prototype.render=function(){this._onstyle="btn-"+this.options.onstyle,this._offstyle="btn-"+this.options.offstyle;var b="large"===this.options.size?"btn-lg":"small"===this.options.size?"btn-sm":"mini"===this.options.size?"btn-xs":"",c=a('