Compare commits

..

1 Commits

Author SHA1 Message Date
Mohamed El-Kalioby
b39fa1a99b Adding Touch ID to Django 1.8 2021-01-30 17:10:33 +03:00
32 changed files with 264 additions and 421 deletions

1
.github/FUNDING.yml vendored
View File

@@ -1 +0,0 @@
tidelift: "pypi/django-mfa2"

View File

@@ -1,11 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"

View File

@@ -1,46 +1,5 @@
# Change Log # Change Log
## 2.2.0 (Not released)
* Added: MFA_REDIRECT_AFTER_REGISTRATION settings parameter
## 2.1.2
* Fixed: Getting timestamp on Python 3.7 as ("%s") is raising an exception
* Upgraded to FIDO 0.9.1
## 2.1.1
* Fixed: FIDO2 version in requirments.txt file.
## 2.1.0
* Added Support for Touch ID for Mac OSx and iOS 14 on Safari
## 2.0.5
* Fixed issue in __version__
## 2.0.4
* Fixed: Closes #30
## 2.0.3
* Fixed: __version__ to show correct version
## 2.0.2
* Added: A missing migration
thnks to @swainn
## 2.0.1
* Fixed: issue in migration between Postgres and SQLite
thnks to @swainn and @willingham
## 2.0
* Dropped support to djangp-1.8 and Python 2.7
* Added: never-cache decorator
* Fixes to Make Email Method More Robust
* Addresses several structure and style issues with TOTP and Email dialogs
* Updated to fido2 0.8.1
Thanks to @swainn
## v1.9.1 ## v1.9.1
* Fixed: is_authenticated #13 * Fixed: is_authenticated #13
* Fixed: is_anonymous #6 * Fixed: is_anonymous #6

View File

@@ -1,14 +1,7 @@
# django-mfa2 # django-mfa2
A Django app that handles MFA, it supports TOTP, U2F, FIDO2 U2F (Web Authn), Email Tokens , and Trusted Devices A Django app that handles MFA, it supports TOTP, U2F, FIDO2 U2F (Web Authn), Email Tokens , and Trusted Devices
### Pip Stats
[![PyPI version](https://badge.fury.io/py/django-mfa2.svg)](https://badge.fury.io/py/django-mfa2) [![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)
### Conda Stats
[![Conda Recipe](https://img.shields.io/badge/recipe-django--mfa2-green.svg)](https://anaconda.org/conda-forge/django-mfa2)
[![Conda Downloads](https://img.shields.io/conda/dn/conda-forge/django-mfa2.svg)](https://anaconda.org/conda-forge/django-mfa2)
[![Conda Version](https://img.shields.io/conda/vn/conda-forge/django-mfa2.svg)](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.
@@ -17,17 +10,17 @@ Web Authencation API (WebAuthn) is state-of-the art techology that is expected t
For FIDO2, the following are supported 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+), * **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+ ), * **Apple's Touch ID** (Chrome 70+ on Mac OS X ),
* **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)
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 ID on Macbooks (Chrome) and Fingerprint/Face/Iris/PIN on Andriod Phones.
Trusted device is a mode for the user to add a device that doesn't support security keys like Android without fingerprints or NFC. Trusted device is a mode for the user to add a device that doesn't support security keys like iOS and andriod without fingerprints or NFC.
**Note**: `U2F and FIDO2 can only be served under secure context (https)` **Note**: `U2F and FIDO2 can only be served under secure context (https)`
Package tested with Django 1.8, Django 2.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.1 on Python 2.7 and Python 3.5+ but it was not checked with any version in between but open for issues.
Depends on Depends on
@@ -36,23 +29,11 @@ Depends on
* ua-parser * ua-parser
* user-agents * user-agents
* python-jose * python-jose
* fido2==0.9.0 * fido2==0.7
# Installation # Installation
1. using pip 1. `pip install django-mfa2`
`pip install django-mfa2`
2. Using Conda forge
`conda config --add channels conda-forge`
`conda install django-mfa2`
For more info, see the conda-forge repo (https://github.com/conda-forge/django-mfa2-feedstock)
Thanks for [swainn](https://github.com/swainn) for adding package to conda-forge
# Usage
1. in your settings.py add the application to your installed apps 1. in your settings.py add the application to your installed apps
```python ```python
INSTALLED_APPS=( INSTALLED_APPS=(
@@ -68,13 +49,11 @@ Depends on
MFA_UNALLOWED_METHODS=() # Methods that shouldn't be allowed for the user MFA_UNALLOWED_METHODS=() # Methods that shouldn't be allowed for the user
MFA_LOGIN_CALLBACK="" # A function that should be called by username to login the user in session MFA_LOGIN_CALLBACK="" # A function that should be called by username to login the user in session
MFA_RECHECK=True # Allow random rechecking of the user MFA_RECHECK=True # Allow random rechecking of the user
MFA_REDIRECT_AFTER_REGISTRATION="mfa_home" # Allows Changing the page after successful registeration
MFA_SUCCESS_REGISTRATION_MSG = "Go to Security Home" # The text of the link
MFA_RECHECK_MIN=10 # Minimum interval in seconds MFA_RECHECK_MIN=10 # Minimum interval in seconds
MFA_RECHECK_MAX=30 # Maximum in seconds MFA_RECHECK_MAX=30 # Maximum in seconds
MFA_QUICKLOGIN=True # Allow quick login for returning users by provide only their 2FA MFA_QUICKLOGIN=True # Allow quick login for returning users by provide only their 2FA
MFA_HIDE_DISABLE=('FIDO2',) # Can the user disable his key (Added in 1.2.0). MFA_HIDE_DISABLE=('FIDO2',) # Can the user disable his key (Added in 1.2.0).
MFA_OWNED_BY_ENTERPRISE = FALSE # Who owns security keys MFA_OWNED_BY_ENTERPRISE = FALSE # Who ownes security keys
TOKEN_ISSUER_NAME="PROJECT_NAME" #TOTP Issuer name TOKEN_ISSUER_NAME="PROJECT_NAME" #TOTP Issuer name
@@ -93,8 +72,6 @@ Depends on
**Notes**: **Notes**:
* Starting version 1.1, ~~FIDO_LOGIN_URL~~ isn't required for FIDO2 anymore. * Starting version 1.1, ~~FIDO_LOGIN_URL~~ isn't required for FIDO2 anymore.
* Starting version 1.7.0, Key owners can be specified. * Starting version 1.7.0, Key owners can be specified.
* Starting version 2.2.0
* Added: `MFA_SUCCESS_REGISTRATION_MSG` & `MFA_REDIRECT_AFTER_REGISTRATION`
1. Break your login function 1. Break your login function
Usually your login function will check for username and password, log the user in if the username and password are correct and create the user session, to support mfa, this has to change Usually your login function will check for username and password, log the user in if the username and password are correct and create the user session, to support mfa, this has to change
@@ -179,10 +156,3 @@ function some_func() {
# Contributors # Contributors
* [mahmoodnasr](https://github.com/mahmoodnasr) * [mahmoodnasr](https://github.com/mahmoodnasr)
* [d3cline](https://github.com/d3cline) * [d3cline](https://github.com/d3cline)
* [swainn](https://github.com/swainn)
* [unramk](https://github.com/unramk)
* [willingham](https://github.com/willingham)
# Security contact information
To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure.

View File

@@ -9,15 +9,15 @@ Usually your login function will check for username and password, log the user i
* if user has mfa then redirect to mfa page * if user has mfa then redirect to mfa page
* if user doesn't have mfa then call your function to create the user session * if user doesn't have mfa then call your function to create the user session
```python <code>
def login(request): # this function handles the login form POST def login(request): # this function handles the login form POST
user = auth.authenticate(username=username, password=password) user = auth.authenticate(username=username, password=password)
if user is not None: # if the user object exist if user is not None: # if the user object exist
from mfa.helpers import has_mfa from mfa.helpers import has_mfa
res = has_mfa(username = username,request=request) # has_mfa returns false or HttpResponseRedirect res = has_mfa(username = username,request=request) # has_mfa returns false or HttpResponseRedirect
if res: if res:
return res return res
return log_user_in(request,username=user.username) return log_user_in(request,username=user.username)
#log_user_in is a function that handles creatung user session, it should be in the setting file as MFA_CALLBACK #log_user_in is a function that handles creatung user session, it should be in the setting file as MFA_CALLBACK
``` </code>

View File

@@ -27,8 +27,4 @@ def create_session(request,username):
def logoutView(request): def logoutView(request):
logout(request) logout(request)
return render(request,"logout.html",{}) return render(request,"logout.html",{})
def register(request):
if request.method == "GET":
return

View File

@@ -78,7 +78,7 @@ WSGI_APPLICATION = 'example.wsgi.application'
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'test_db', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
} }
} }
@@ -139,9 +139,7 @@ MFA_RECHECK=True # Allow random rechecking of the user
MFA_RECHECK_MIN=10 # Minimum interval in seconds MFA_RECHECK_MIN=10 # Minimum interval in seconds
MFA_RECHECK_MAX=30 # Maximum in seconds MFA_RECHECK_MAX=30 # Maximum in seconds
MFA_QUICKLOGIN=True # Allow quick login for returning users by provide only their 2FA MFA_QUICKLOGIN=True # Allow quick login for returning users by provide only their 2FA
MFA_HIDE_DISABLE=('',) # Can the user disable his key (Added in 1.2.0). MFA_HIDE_DISABLE=('FIDO2',) # Can the user disable his key (Added in 1.2.0).
MFA_REDIRECT_AFTER_REGISTRATION="registered"
MFA_SUCCESS_REGISTRATION_MSG="Go to Home"
TOKEN_ISSUER_NAME="PROJECT_NAME" #TOTP Issuer name TOKEN_ISSUER_NAME="PROJECT_NAME" #TOTP Issuer name

View File

@@ -12,9 +12,6 @@
</ol> </ol>
<!-- Page Content --> <!-- Page Content -->
{% if registered %}
<div class="alert alert-success">Registered Successfully</div>
{% endif %}
<h1>Welcome {{ request.user.username }}!</h1> <h1>Welcome {{ request.user.username }}!</h1>
<hr> <hr>

View File

@@ -17,6 +17,7 @@
<!-- Custom styles for this template--> <!-- Custom styles for this template-->
<link href="{% static 'css/sb-admin.css'%}" rel="stylesheet"> <link href="{% static 'css/sb-admin.css'%}" rel="stylesheet">
</head> </head>
<body class="bg-dark"> <body class="bg-dark">
@@ -28,6 +29,7 @@
{% if invalid %} {% if invalid %}
<div class="alert alert-danger">Invalid Username or password</div> <div class="alert alert-danger">Invalid Username or password</div>
{% endif %} {% endif %}
<form action="{% url 'login' %}" method="post">
{% csrf_token %} {% csrf_token %}
<div class="form-group"> <div class="form-group">
<div class="form-label-group"> <div class="form-label-group">
@@ -35,9 +37,14 @@
<label for="inputUsername">Username</label> <label for="inputUsername">Username</label>
</div> </div>
</div> </div>
<div class="form-group">
<div class="form-label-group">
<input type="password" id="inputPassword" name="password" class="form-control" placeholder="Password" required="required">
<label for="inputPassword">Password</label>
</div>
</div>
<button class="btn btn-primary btn-block" type="submit">Login</button>
<button class="btn btn-primary btn-block" type="button" onclick="authen()">Login</button>
</form> </form>
</div> </div>
</div> </div>
@@ -49,7 +56,7 @@
<!-- Core plugin JavaScript--> <!-- Core plugin JavaScript-->
<script src="{% static 'vendor/jquery-easing/jquery.easing.min.js'%}"></script> <script src="{% static 'vendor/jquery-easing/jquery.easing.min.js'%}"></script>
{% include 'FIDO2/login.html' %}
</body> </body>
</html> </html>

View File

@@ -22,6 +22,5 @@ urlpatterns = [
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"),
re_path('^$',views.home,name='home'), re_path('^$',views.home,name='home')
path('registered/',views.registered,name='registered')
] ]

View File

@@ -5,7 +5,3 @@ from django.contrib.auth.decorators import login_required
@login_required() @login_required()
def home(request): def home(request):
return render(request,"home.html",{}) return render(request,"home.html",{})
@login_required()
def registered(request):
return render(request,"home.html",{"registered":True})

View File

@@ -1,2 +1,2 @@
django >= 2.2 django==2.0
django_ssl django-sslserver

View File

@@ -1,19 +1,8 @@
from django.conf import settings from django.conf import settings
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
try:
from django.urls import reverse
except:
from django.core.urlresolver import reverse
def send(to,subject,body): def send(to,subject,body):
from_email_address = settings.EMAIL_HOST_USER From = "%s <%s>" % (settings.EMAIL_FROM, settings.EMAIL_HOST_USER)
if '@' not in from_email_address:
from_email_address = settings.DEFAULT_FROM_EMAIL
From = "%s <%s>" % (settings.EMAIL_FROM, from_email_address)
email = EmailMessage(subject,body,From,to) email = EmailMessage(subject,body,From,to)
email.content_subtype = "html" email.content_subtype = "html"
return email.send(False) return email.send(False)
def get_redirect_url():
return {"redirect_html": reverse(getattr(settings, 'MFA_REDIRECT_AFTER_REGISTRATION', 'mfa_home')),
"reg_success_msg":getattr(settings,"MFA_SUCCESS_REGISTRATION_MSG")}

View File

@@ -1,5 +1,4 @@
from django.shortcuts import render from django.shortcuts import render
from django.views.decorators.cache import never_cache
from django.template.context_processors import csrf from django.template.context_processors import csrf
import datetime,random import datetime,random
from random import randint from random import randint
@@ -7,23 +6,19 @@ from .models import *
#from django.template.context import RequestContext #from django.template.context import RequestContext
from .views import login from .views import login
from .Common import send from .Common import send
def sendEmail(request,username,secret): def sendEmail(request,username,secret):
"""Send Email to the user after rendering `mfa_email_token_template`"""
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
User = get_user_model() User = get_user_model()
key = getattr(User, 'USERNAME_FIELD', 'username') key = getattr(User, 'USERNAME_FIELD', 'username')
kwargs = {key: username} kwargs = {key: username}
user = User.objects.get(**kwargs) user = User.objects.get(**kwargs)
res=render(request,"mfa_email_token_template.html",{"request":request,"user":user,'otp':secret}) res=render(request,"mfa_email_token_template.html",{"request":request,"user":user,'otp':secret})
return send([user.email],"OTP", res.content.decode()) return send([user.email],"OTP", str(res.content))
@never_cache
def start(request): def start(request):
"""Start adding email as a 2nd factor"""
context = csrf(request) context = csrf(request)
if request.method == "POST": if request.method == "POST":
if request.session["email_secret"] == request.POST["otp"]: #if successful if request.session["email_secret"] == request.POST["otp"]:
uk=User_Keys() uk=User_Keys()
uk.username=request.user.username uk.username=request.user.username
uk.key_type="Email" uk.key_type="Email"
@@ -34,24 +29,22 @@ def start(request):
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
except: except:
from django.urls import reverse from django.urls import reverse
return HttpResponseRedirect(reverse(getattr(settings,'MFA_REDIRECT_AFTER_REGISTRATION','mfa_home'))) return HttpResponseRedirect(reverse('mfa_home'))
context["invalid"] = True context["invalid"] = True
else: else:
request.session["email_secret"] = str(randint(0,100000)) #generate a random integer request.session["email_secret"] = str(randint(0,100000))
if sendEmail(request, request.user.username, request.session["email_secret"]): if sendEmail(request, request.user.username, request.session["email_secret"]):
context["sent"] = True context["sent"] = True
return render(request,"Email/Add.html", context) return render(request,"Email/Add.html", context)
@never_cache
def auth(request): def auth(request):
"""Authenticating the user by email."""
context=csrf(request) context=csrf(request)
if request.method=="POST": if request.method=="POST":
if request.session["email_secret"]==request.POST["otp"].strip(): if request.session["email_secret"]==request.POST["otp"].strip():
uk = User_Keys.objects.get(username=request.session["base_username"], key_type="Email") uk = User_Keys.objects.get(username=request.session["base_username"], key_type="Email")
mfa = {"verified": True, "method": "Email","id":uk.id} mfa = {"verified": True, "method": "Email","id":uk.id}
if getattr(settings, "MFA_RECHECK", False): if getattr(settings, "MFA_RECHECK", False):
mfa["next_check"] = datetime.datetime.timestamp(datetime.datetime.now() + datetime.timedelta( mfa["next_check"] = int((datetime.datetime.now() + datetime.timedelta(
seconds = random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))) seconds = random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))).strftime("%s"))
request.session["mfa"] = mfa request.session["mfa"] = mfa
from django.utils import timezone from django.utils import timezone

View File

@@ -1,39 +1,32 @@
from fido2.client import ClientData from fido2.client import ClientData
from fido2.server import Fido2Server, PublicKeyCredentialRpEntity from fido2.server import Fido2Server, RelyingParty
from fido2.ctap2 import AttestationObject, AuthenticatorData from fido2.ctap2 import AttestationObject, AuthenticatorData
from django.template.context_processors import csrf from django.template.context_processors import csrf
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.shortcuts import render from django.shortcuts import render
# from django.template.context import RequestContext #from django.template.context import RequestContext
import simplejson import simplejson
from fido2 import cbor from fido2 import cbor
from django.http import HttpResponse from django.http import HttpResponse
from django.conf import settings from django.conf import settings
from .models import * from .models import *
from fido2.utils import websafe_decode, websafe_encode from fido2.utils import websafe_decode,websafe_encode
from fido2.ctap2 import AttestedCredentialData from fido2.ctap2 import AttestedCredentialData
from .views import login, reset_cookie from .views import login,reset_cookie
import datetime import datetime
from .Common import get_redirect_url
from django.utils import timezone from django.utils import timezone
def recheck(request): def recheck(request):
"""Starts FIDO2 recheck"""
context = csrf(request) context = csrf(request)
context["mode"] = "recheck" context["mode"]="recheck"
request.session["mfa_recheck"] = True request.session["mfa_recheck"]=True
return render(request, "FIDO2/recheck.html", context) return render(request,"FIDO2/recheck.html", context)
def getServer(): def getServer():
"""Get Server Info from settings and returns a Fido2Server""" rp = RelyingParty(settings.FIDO_SERVER_ID, settings.FIDO_SERVER_NAME)
rp = PublicKeyCredentialRpEntity(settings.FIDO_SERVER_ID, settings.FIDO_SERVER_NAME)
return Fido2Server(rp) return Fido2Server(rp)
def begin_registeration(request): def begin_registeration(request):
"""Starts registering a new FIDO Device, called from API"""
server = getServer() server = getServer()
registration_data, state = server.register_begin({ registration_data, state = server.register_begin({
u'id': request.user.username.encode("utf8"), u'id': request.user.username.encode("utf8"),
@@ -42,12 +35,9 @@ def begin_registeration(request):
}, getUserCredentials(request.user.username)) }, getUserCredentials(request.user.username))
request.session['fido_state'] = state request.session['fido_state'] = state
return HttpResponse(cbor.encode(registration_data), content_type = 'application/octet-stream') return HttpResponse(cbor.encode(registration_data),content_type='application/octet-stream')
@csrf_exempt @csrf_exempt
def complete_reg(request): def complete_reg(request):
"""Completes the registeration, called by API"""
try: try:
data = cbor.decode(request.body) data = cbor.decode(request.body)
@@ -60,10 +50,10 @@ def complete_reg(request):
att_obj att_obj
) )
encoded = websafe_encode(auth_data.credential_data) encoded = websafe_encode(auth_data.credential_data)
uk = User_Keys() uk=User_Keys()
uk.username = request.user.username uk.username = request.user.username
uk.properties = {"device": encoded, "type": att_obj.fmt, } uk.properties = {"device":encoded,"type":att_obj.fmt,}
uk.owned_by_enterprise = getattr(settings, "MFA_OWNED_BY_ENTERPRISE", False) uk.owned_by_enterprise=getattr(settings,"MFA_OWNED_BY_ENTERPRISE",False)
uk.key_type = "FIDO2" uk.key_type = "FIDO2"
uk.save() uk.save()
return HttpResponse(simplejson.dumps({'status': 'OK'})) return HttpResponse(simplejson.dumps({'status': 'OK'}))
@@ -73,16 +63,10 @@ def complete_reg(request):
client.captureException() client.captureException()
except: except:
pass pass
return HttpResponse(simplejson.dumps({'status': 'ERR', "message": "Error on server, please try again later"})) return HttpResponse(simplejson.dumps({'status': 'ERR',"message":"Error on server, please try again later"}))
def start(request): def start(request):
"""Start Registeration a new FIDO Token"""
context = csrf(request) context = csrf(request)
context.update(get_redirect_url()) return render(request,"FIDO2/Add.html", context)
context["mfa_invoke"] = True
return render(request, "FIDO2/Add.html", context)
def getUserCredentials(username): def getUserCredentials(username):
credentials = [] credentials = []
@@ -90,27 +74,24 @@ def getUserCredentials(username):
credentials.append(AttestedCredentialData(websafe_decode(uk.properties["device"]))) credentials.append(AttestedCredentialData(websafe_decode(uk.properties["device"])))
return credentials return credentials
def auth(request): def auth(request):
context = csrf(request) context=csrf(request)
return render(request, "FIDO2/Auth.html", context) return render(request,"FIDO2/Auth.html",context)
def authenticate_begin(request): def authenticate_begin(request):
server = getServer() server = getServer()
#credentials = getUserCredentials(request.session.get("base_username", request.user.username)) credentials=getUserCredentials(request.session.get("base_username",request.user.username))
auth_data, state = server.authenticate_begin() auth_data, state = server.authenticate_begin(credentials)
request.session['fido_state'] = state request.session['fido_state'] = state
return HttpResponse(cbor.encode(auth_data), content_type = "application/octet-stream") return HttpResponse(cbor.encode(auth_data),content_type="application/octet-stream")
@csrf_exempt @csrf_exempt
def authenticate_complete(request): def authenticate_complete(request):
try: try:
credentials = [] credentials = []
username = request.session.get("base_username", request.POST.get("username",request.user.username)) username=request.session.get("base_username",request.user.username)
server = getServer() server=getServer()
credentials = getUserCredentials(username) credentials=getUserCredentials(username)
data = cbor.decode(request.body) data = cbor.decode(request.body)
credential_id = data['credentialId'] credential_id = data['credentialId']
client_data = ClientData(data['clientDataJSON']) client_data = ClientData(data['clientDataJSON'])
@@ -126,8 +107,7 @@ def authenticate_complete(request):
signature signature
) )
except ValueError: except ValueError:
return HttpResponse(simplejson.dumps({'status': "ERR", return HttpResponse(simplejson.dumps({'status': "ERR", "message": "Wrong challenge received, make sure that this is your security and try again."}),
"message": "Wrong challenge received, make sure that this is your security and try again."}),
content_type = "application/json") content_type = "application/json")
except Exception as excep: except Exception as excep:
try: try:
@@ -139,34 +119,32 @@ def authenticate_complete(request):
"message": excep.message}), "message": excep.message}),
content_type = "application/json") content_type = "application/json")
if request.session.get("mfa_recheck", False): if request.session.get("mfa_recheck",False):
import time import time
request.session["mfa"]["rechecked_at"] = time.time() request.session["mfa"]["rechecked_at"]=time.time()
return HttpResponse(simplejson.dumps({'status': "OK"}), return HttpResponse(simplejson.dumps({'status': "OK"}),
content_type = "application/json") content_type="application/json")
else: else:
import random import random
keys = User_Keys.objects.filter(username = username, key_type = "FIDO2", enabled = 1) keys = User_Keys.objects.filter(username=username, key_type="FIDO2", enabled=1)
for k in keys: for k in keys:
if AttestedCredentialData(websafe_decode(k.properties["device"])).credential_id == cred.credential_id: if AttestedCredentialData(websafe_decode(k.properties["device"])).credential_id == cred.credential_id:
k.last_used = timezone.now() k.last_used = timezone.now()
k.save() k.save()
mfa = {"verified": True, "method": "FIDO2", 'id': k.id} mfa = {"verified": True, "method": "FIDO2",'id':k.id}
if getattr(settings, "MFA_RECHECK", False): if getattr(settings, "MFA_RECHECK", False):
mfa["next_check"] = datetime.datetime.timestamp((datetime.datetime.now() + datetime.timedelta( mfa["next_check"] = int((datetime.datetime.now()+ datetime.timedelta(
seconds = random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX)))) seconds=random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))).strftime("%s"))
request.session["mfa"] = mfa request.session["mfa"] = mfa
try: try:
authenticated = request.user.is_authenticated authenticated=request.user.is_authenticated
except: except:
authenticated = request.user.is_authenticated() authenticated = request.user.is_authenticated()
if not authenticated: if not authenticated:
res = login(request) res=login(request)
if not "location" in res: return reset_cookie(request) if not "location" in res: return reset_cookie(request)
return HttpResponse(simplejson.dumps({'status': "OK", "redirect": res["location"]}), return HttpResponse(simplejson.dumps({'status':"OK","redirect":res["location"]}),content_type="application/json")
content_type = "application/json")
return HttpResponse(simplejson.dumps({'status': "OK"}), return HttpResponse(simplejson.dumps({'status': "OK"}),
content_type = "application/json") content_type = "application/json")
except Exception as exp: except Exception as exp:
return HttpResponse(simplejson.dumps({'status': "ERR", "message": exp.message}), return HttpResponse(simplejson.dumps({'status': "ERR","message":exp.message}),content_type="application/json")
content_type = "application/json")

View File

@@ -57,9 +57,9 @@ def validate(request,username):
key.save() key.save()
mfa = {"verified": True, "method": "U2F","id":key.id} mfa = {"verified": True, "method": "U2F","id":key.id}
if getattr(settings, "MFA_RECHECK", False): if getattr(settings, "MFA_RECHECK", False):
mfa["next_check"] = datetime.datetime.timestamp((datetime.datetime.now() mfa["next_check"] = int((datetime.datetime.now()
+ datetime.timedelta( + datetime.timedelta(
seconds=random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX)))) seconds=random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))).strftime("%s"))
request.session["mfa"] = mfa request.session["mfa"] = mfa
return True return True
@@ -76,7 +76,6 @@ def start(request):
request.session['_u2f_enroll_'] = enroll.json request.session['_u2f_enroll_'] = enroll.json
context=csrf(request) context=csrf(request)
context["token"]=simplejson.dumps(enroll.data_for_client) context["token"]=simplejson.dumps(enroll.data_for_client)
context.update(get_redirect_url())
return render(request,"U2F/Add.html",context) return render(request,"U2F/Add.html",context)

View File

@@ -1 +1 @@
__version__="2.2.0b1" __version__="1.6.0"

View File

@@ -4,12 +4,6 @@ from __future__ import unicode_literals
from django.db import models, migrations from django.db import models, migrations
from django.conf import settings from django.conf import settings
def update_owned_by_enterprise(apps, schema_editor):
user_keys = apps.get_model('mfa', 'user_keys')
user_keys.objects.filter(key_type='FIDO2').update(owned_by_enterprise=getattr(settings,"MFA_OWNED_BY_ENTERPRISE",False))
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
@@ -22,5 +16,5 @@ class Migration(migrations.Migration):
name='owned_by_enterprise', name='owned_by_enterprise',
field=models.NullBooleanField(default=None), field=models.NullBooleanField(default=None),
), ),
migrations.RunPython(update_owned_by_enterprise) migrations.RunSQL("update mfa_user_keys set owned_by_enterprise = %s where key_type='FIDO2'"%(True if getattr(settings,"MFA_OWNED_BY_ENTERPRISE",False) else False ))
] ]

View File

@@ -1,18 +0,0 @@
# Generated by Django 2.0 on 2020-11-10 05:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mfa', '0009_user_keys_owned_by_enterprise'),
]
operations = [
migrations.AlterField(
model_name='user_keys',
name='key_type',
field=models.CharField(default='TOTP', max_length=25),
),
]

View File

@@ -2,10 +2,8 @@ from django.db import models
from jsonfield import JSONField from jsonfield import JSONField
from jose import jwt from jose import jwt
from django.conf import settings from django.conf import settings
#from jsonLookup import shasLookup, hasLookup from jsonLookup import shasLookup
# JSONField.register_lookup(shasLookup) JSONField.register_lookup(shasLookup)
# JSONField.register_lookup(hasLookup)
class User_Keys(models.Model): class User_Keys(models.Model):
username=models.CharField(max_length = 50) username=models.CharField(max_length = 50)
@@ -21,12 +19,9 @@ class User_Keys(models.Model):
if self.key_type == "Trusted Device" and self.properties.get("signature","") == "": if self.key_type == "Trusted Device" and self.properties.get("signature","") == "":
self.properties["signature"]= jwt.encode({"username": self.username, "key": self.properties["key"]}, settings.SECRET_KEY) self.properties["signature"]= jwt.encode({"username": self.username, "key": self.properties["key"]}, settings.SECRET_KEY)
super(User_Keys, self).save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields) super(User_Keys, self).save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
def __unicode__(self): def __unicode__(self):
return "%s -- %s"%(self.username,self.key_type) return "%s -- %s"%(self.username,self.key_type)
def __str__(self): def __str__(self):
return self.__unicode__() return self.__unicode__()
class Meta: class Meta:
app_label='mfa' app_label='mfa'

View File

@@ -2,50 +2,54 @@
{% block head %} {% block head %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<br/> <br/>
<br/> <br/>
<div class="container">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong> Activate Token by email</strong> <strong> Activate Token by email</strong>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<FORM METHOD="POST" ACTION="{% url 'start_email' %}" Id="formLogin" onSubmit="" name="FrontPage_Form1"> <FORM METHOD="POST" ACTION="{% url 'start_email' %}" Id="formLogin" onSubmit="" name="FrontPage_Form1">
{% csrf_token %}
{% if invalid %}
<div class="alert alert-danger"> {% csrf_token %}
Sorry, The provided token is not valid. {% if invalid %}
</div> <div class="alert alert-danger">
{% endif %} Sorry, The provided token is not valid.
{% if quota %}
<div class="alert alert-warning">
{{ quota }}
</div>
{% endif %}
<fieldset>
<div class="row">
<div class="col-sm-12 col-md-12">
<p>Enter the code sent to your email.</p>
</div>
</div> </div>
<div class="row"> {% endif %}
<div class="col-sm-12 col-md-12"> {% if quota %}
<div class="form-group"> <div class="alert alert-warning">
<div class="input-group"> {{ quota }}
<span class="input-group-addon">
<i class="glyphicon glyphicon-lock"></i>
</span>
<input class="form-control" size="6" MaxLength="6" value="" placeholder="e.g 55552" name="otp" type="text" id="otp" autofocus>
</div>
</div>
<div class="form-group">
<input type="submit" class="btn btn-lg btn-success btn-block" value="Verify">
</div>
</div>
</div> </div>
</fieldset> {% endif %}
<fieldset>
<div class="row">
<div class="col-sm-12 col-md-12">
<p>Enter the 6-digits sent to your email.</p>
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12">
<div class="form-group">
<div class="input-group">
<span class="input-group-addon">
<i class="glyphicon glyphicon-lock"></i>
</span>
<input class="form-control" size="6" MaxLength="6" value="" placeholder="e.g 55552" name="otp" type="text" id="otp" autofocus>
</div>
</div>
<div class="form-group">
<input type="submit" class="btn btn-lg btn-success btn-block" value="Verify">
</div>
</div>
</fieldset>
</FORM> </FORM>
</div> </div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@@ -39,7 +39,7 @@
<fieldset> <fieldset>
<div class="row"> <div class="row">
<div class="col-sm-12 col-md-12"> <div class="col-sm-12 col-md-12">
<p>Enter the code sent to your email.</p> <p>Enter the 6-digits sent to your email.</p>
</div> </div>
</div> </div>

View File

@@ -32,7 +32,7 @@
}).then(function (res) }).then(function (res)
{ {
if (res["status"] =='OK') if (res["status"] =='OK')
$("#res").html("<div class='alert alert-success'>Registered Successfully, <a href='{{redirect_html}}'> {{reg_success_msg}}</a></div>") $("#res").html("<div class='alert alert-success'>Registered Successfully, <a href='{% url 'mfa_home' %}'> Go to Security Home</a></div>")
else else
$("#res").html("<div class='alert alert-danger'>Registeration Failed as " + res["message"] + ", <a href='javascript:void(0)' onclick='begin_reg()'> try again or <a href='{% url 'mfa_home' %}'> Go to Security Home</a></div>") $("#res").html("<div class='alert alert-danger'>Registeration Failed as " + res["message"] + ", <a href='javascript:void(0)' onclick='begin_reg()'> try again or <a href='{% url 'mfa_home' %}'> Go to Security Home</a></div>")
@@ -42,10 +42,10 @@
}) })
} }
$(document).ready(function (){ $(document).ready(function (){
ua=new UAParser().getResult() ua=new UAParser()
if (ua.browser.name == "Safari") if (ua.getBrowser().name == "Safari")
{ {
$("#res").html("<button class='btn btn-success' onclick='begin_reg()'>Start...</button>") $("#res").html("<button class='btn btn-primary' onclick='begin_reg()'>Start...</button>")
} }
else else
{ {
@@ -66,7 +66,7 @@
<div class="panel-body"> <div class="panel-body">
<div class="row alert alert-pr" id="res" align="center"> <div class="row alert alert-pr" id="res">
<p style="color: green">Your browser should ask you to confirm you identity.</p> <p style="color: green">Your browser should ask you to confirm you identity.</p>
</div> </div>

View File

@@ -1,71 +0,0 @@
{% load static %}
<script type="application/javascript" src="{% static 'mfa/js/cbor.js' %}"></script>
<script type="application/javascript" src="{% static 'mfa/js/ua-parser.min.js' %}"></script>
<script type="text/javascript">
function authen()
{
fetch('{% url 'fido2_begin_auth' %}', {
method: 'GET',
}).then(function(response) {
if(response.ok) return response.arrayBuffer();
throw new Error('No credential available to authenticate!');
}).then(CBOR.decode).then(function(options) {
console.log(options)
return navigator.credentials.get(options);
}).then(function(assertion) {
res=CBOR.encode({
"credentialId": new Uint8Array(assertion.rawId),
"authenticatorData": new Uint8Array(assertion.response.authenticatorData),
"clientDataJSON": new Uint8Array(assertion.response.clientDataJSON),
"signature": new Uint8Array(assertion.response.signature)
});
return fetch('{% url 'fido2_complete_auth' %}', {
method: 'POST',
headers: {'Content-Type': 'application/cbor'},
body:res,
}).then(function (response) {if (response.ok) return res = response.json()}).then(function (res) {
if (res.status=="OK")
{
$("#msgdiv").addClass("alert alert-success").removeClass("alert-danger")
$("#msgdiv").html("Verified....please wait")
{% if mode == "auth" %}
window.location.href=res.redirect;
{% elif mode == "recheck" %}
mfa_success_function();
{% endif %}
}
else {
$("#msgdiv").addClass("alert alert-danger").removeClass("alert-success")
$("#msgdiv").html("Verification Failed as " + res.message + ", <a href='javascript:void(0)' onclick='authen())'> try again</a> or <a href='javascript:void(0)' onclick='history.back()'> Go Back</a>")
{% if mode == "auth" %}
{% elif mode == "recheck" %}
mfa_failed_function();
{% endif %}
}
})
})
}
$(document).ready(function () {
if (location.protocol != 'https:') {
$("#main_paragraph").addClass("alert alert-danger")
$("#main_paragraph").html("FIDO2 must work under secure context")
} else {
{% if mfa_invoke %}
ua=new UAParser().getResult()
if (ua.browser.name == "Safari")
$("#res").html("<button class='btn btn-success' onclick='authen()'>Authenticate...</button>")
else
authen()
{% endif %}
}
});
</script>

View File

@@ -1 +0,0 @@
{% include 'FIDO2/fido2_auth.html' %}

View File

@@ -1,4 +1,6 @@
{% load static %}
<script type="application/javascript" src="{% static 'mfa/js/cbor.js' %}"></script>
<script type="application/javascript" src="{% static 'mfa/js/us-parser.min.js' %}"></script>
<div class="row"> <div class="row">
<div class="col-sm-10 col-sm-offset-1 col-xs-12 col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2"> <div class="col-sm-10 col-sm-offset-1 col-xs-12 col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
@@ -45,4 +47,71 @@
</div> </div>
</div> </div>
{% include 'FIDO2/fido2_auth.html' %} <script type="text/javascript">
function authen()
{
fetch('{% url 'fido2_begin_auth' %}', {
method: 'GET',
}).then(function(response) {
if(response.ok) return response.arrayBuffer();
throw new Error('No credential available to authenticate!');
}).then(CBOR.decode).then(function(options) {
console.log(options)
return navigator.credentials.get(options);
}).then(function(assertion) {
res=CBOR.encode({
"credentialId": new Uint8Array(assertion.rawId),
"authenticatorData": new Uint8Array(assertion.response.authenticatorData),
"clientDataJSON": new Uint8Array(assertion.response.clientDataJSON),
"signature": new Uint8Array(assertion.response.signature)
});
return fetch('{% url 'fido2_complete_auth' %}', {
method: 'POST',
headers: {'Content-Type': 'application/cbor'},
body:res,
}).then(function (response) {if (response.ok) return res = response.json()}).then(function (res) {
if (res.status=="OK")
{
$("#msgdiv").addClass("alert alert-success").removeClass("alert-danger")
$("#msgdiv").html("Verified....please wait")
{% if mode == "auth" %}
window.location.href=res.redirect;
{% elif mode == "recheck" %}
mfa_success_function();
{% endif %}
}
else {
$("#msgdiv").addClass("alert alert-danger").removeClass("alert-success")
$("#msgdiv").html("Verification Failed as " + res.message + ", <a href='javascript:void(0)' onclick='authen())'> try again</a> or <a href='javascript:void(0)' onclick='history.back()'> Go Back</a>")
{% if mode == "auth" %}
{% elif mode == "recheck" %}
mfa_failed_function();
{% endif %}
}
})
})
}
$(document).ready(function () {
if (location.protocol != 'https:') {
$("#main_paragraph").addClass("alert alert-danger")
$("#main_paragraph").html("FIDO2 must work under secure context")
} else {
ua=UAParser()
if (ua.getBrowser().name == "Safari")
$("#res").html("<button class='btn btn-success' onclick='authen()'>Authenticate...</button>")
else
authen()
}
});
</script>

View File

@@ -66,7 +66,7 @@
{% endif %} {% endif %}
</ul> </ul>
</div> </div>
</div>
<br/> <br/>
<br/> <br/>
<table class="table table-striped"> <table class="table table-striped">

View File

@@ -2,7 +2,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %} {% load static %}
{% block head %} {% block head %}
<style> <style>
#two-factor-steps { #two-factor-steps {
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 3px; border-radius: 3px;
@@ -12,8 +12,8 @@
margin: 0px; margin: 0px;
} }
</style> </style>
<script src="{% static 'mfa/js/qrious.min.js' %}" type="text/javascript"></script> <script src="{% static 'mfa/js/qrious.min.js' %}" type="text/javascript"></script>
<script type="text/javascript"> <script type="text/javascript">
var key=""; var key="";
$(document).ready(function addToken() { $(document).ready(function addToken() {
$.ajax({ $.ajax({
@@ -43,7 +43,7 @@
else else
{ {
alert("Your authenticator is added successfully.") alert("Your authenticator is added successfully.")
window.location.href="{{ redirect_html }}" window.location.href="{% url 'mfa_home' %}"
} }
} }
}) })
@@ -61,49 +61,49 @@
</script> </script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<br/> <br/>
<br/> <br/>
<div class="container"> <div class="container">
<div class="col-md-6 col-md-offset-3" id="two-factor-steps"> <div class="col-md-6 col-md-offset-3" id="two-factor-steps">
<div class="row" align="center"> <div class="row" align="center">
<h4>Adding Authenticator</h4> <h4>Adding Authenticator</h4>
</div> </div>
<div class="row"> <div class="row">
<p>Scan the image below with the two-factor authentication app on your <a href="javascript:void(0)" onclick="showTOTP()">phone/PC</a>. If you cant use a barcode, <p>Scan the image below with the two-factor authentication app on your <a href="javascript:void(0)" onclick="showTOTP()">phone/PC</a> phone/PC. If you cant use a barcode,
<a href="javascript:void(0)" onclick="showKey()">enter this text</a> instead. </p> <a href="javascript:void(0)" onclick="showKey()">enter this text</a> instead. </p>
</div> </div>
<div class="row"> <div class="row">
<div align="center" style="display: none" id="second_step"> <div align="center" style="display: none" id="second_step">
<img id="qr"/> <img id="qr"/>
</div> </div>
<div class="row"> <div class="row">
<p><b>Enter the six-digit code from the application</b></p> <p><b>Enter the six-digit code from the application</b></p>
<p style="color: #333333;font-size: 10px">After scanning the barcode image, the app will display a six-digit code that you can enter below. </p> <p style="color: #333333;font-size: 10px">After scanning the barcode image, the app will display a six-digit code that you can enter below. </p>
</div> </div>
<div class="row"> <div class="row">
<input style="display: inline;width: 95%" maxlength="6" size="6" class="form-control" id="answer" placeholder="e.g 785481"/> <input style="display: inline;width: 95%" maxlength="6" size="6" class="form-control" id="answer" placeholder="e.g 785481"/>
</div>
<div class="row">
<div class="col-md-6" style="padding-left: 0px">
<button class="btn btn-success" onclick="verify()">Enable</button>
</div> </div>
<div class="row" style="padding-top: 10px;"> <div class="col-md-6" align="right" style="padding-right: 30px">
<div class="col-md-6" style="padding-left: 0px"> <a href="{% url 'mfa_home' %}"><button class="btn btn-default">Cancel</button></a>
<button class="btn btn-success" onclick="verify()">Enable</button>
</div>
<div class="col-md-6" align="right" style="padding-right: 30px">
<a href="{% url 'mfa_home' %}"><button class="btn btn-default">Cancel</button></a>
</div>
</div> </div>
</div> </div>
</div>
</div> </div>
</div> </div>
{% include "modal.html" %} {% include "modal.html" %}
{% endblock %} {% endblock %}

View File

@@ -24,7 +24,7 @@
if (data == "OK") if (data == "OK")
{ {
alert("Your device is added successfully.") alert("Your device is added successfully.")
window.location.href="{{ redirect_html }}" window.location.href="{% url 'mfa_home' %}"
} }
} }
}) })

View File

@@ -1,5 +1,4 @@
from django.shortcuts import render from django.shortcuts import render
from django.views.decorators.cache import never_cache
from django.http import HttpResponse from django.http import HttpResponse
from .models import * from .models import *
from django.template.context_processors import csrf from django.template.context_processors import csrf
@@ -32,7 +31,6 @@ def recheck(request):
return HttpResponse(simplejson.dumps({"recheck": False}), content_type="application/json") return HttpResponse(simplejson.dumps({"recheck": False}), content_type="application/json")
return render(request,"TOTP/recheck.html", context) return render(request,"TOTP/recheck.html", context)
@never_cache
def auth(request): def auth(request):
context=csrf(request) context=csrf(request)
if request.method=="POST": if request.method=="POST":
@@ -40,9 +38,9 @@ def auth(request):
if res[0]: if res[0]:
mfa = {"verified": True, "method": "TOTP","id":res[1]} mfa = {"verified": True, "method": "TOTP","id":res[1]}
if getattr(settings, "MFA_RECHECK", False): if getattr(settings, "MFA_RECHECK", False):
mfa["next_check"] = datetime.datetime.timestamp((datetime.datetime.now() mfa["next_check"] = int((datetime.datetime.now()
+ datetime.timedelta( + datetime.timedelta(
seconds=random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX)))) seconds=random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))).strftime("%s"))
request.session["mfa"] = mfa request.session["mfa"] = mfa
return login(request) return login(request)
context["invalid"]=True context["invalid"]=True
@@ -70,7 +68,5 @@ def verify(request):
return HttpResponse("Success") return HttpResponse("Success")
else: return HttpResponse("Error") else: return HttpResponse("Error")
@never_cache
def start(request): def start(request):
"""Start Adding Time One Time Password (TOTP)""" return render(request,"TOTP/Add.html",{})
return render(request,"TOTP/Add.html",get_redirect_url())

View File

@@ -1,10 +1,11 @@
django >= 2.0 django >= 1.7
jsonfield jsonfield
simplejson simplejson
pyotp pyotp
python-u2flib-server python-u2flib-server
ua-parser ua-parser
user-agents user-agents
python-jose python-jose
fido2 == 0.9.1 fido2 == 0.7
jsonLookup jsonLookup

View File

@@ -4,7 +4,7 @@ from setuptools import find_packages, setup
setup( setup(
name='django-mfa2', name='django-mfa2',
version='2.2.0b2 ', version='1.10.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",
@@ -16,7 +16,7 @@ setup(
license='MIT', license='MIT',
packages=find_packages(), packages=find_packages(),
install_requires=[ install_requires=[
'django >= 2.0', 'django >= 1.7',
'jsonfield', 'jsonfield',
'simplejson', 'simplejson',
'pyotp', 'pyotp',
@@ -24,27 +24,32 @@ setup(
'ua-parser', 'ua-parser',
'user-agents', 'user-agents',
'python-jose', 'python-jose',
'fido2 == 0.9.1', 'fido2 == 0.9',
'jsonLookup' 'jsonLookup'
], ],
python_requires=">=3.5", python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*",
include_package_data=True, include_package_data=True,
zip_safe=False, # because we're including static files zip_safe=False, # because we're including static files
classifiers=[ classifiers=[
"Development Status :: 4 - Beta", "Development Status :: 5 - Production/Stable",
"Environment :: Web Environment", "Environment :: Web Environment",
"Framework :: Django", "Framework :: Django",
"Framework :: Django :: 1.7",
"Framework :: Django :: 1.8",
"Framework :: Django :: 1.9",
"Framework :: Django :: 1.10",
"Framework :: Django :: 1.11",
"Framework :: Django :: 2.0", "Framework :: Django :: 2.0",
"Framework :: Django :: 2.1", "Framework :: Django :: 2.1",
"Framework :: Django :: 2.2",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Programming Language :: Python", "Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Libraries :: Python Modules",
] ]
) )