Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23377abfa6 | ||
|
|
ba9dfc4d36 | ||
|
|
ef8287666c | ||
|
|
600ef2421a | ||
|
|
4fbe88b90f | ||
|
|
a48ae253d6 | ||
|
|
19563fe2b8 | ||
|
|
0e7c0911ca | ||
|
|
ebfdaf7504 | ||
|
|
c78e600b37 | ||
|
|
e1e931285f | ||
|
|
2709e70f88 | ||
|
|
25fe80d76d | ||
|
|
ba76f842bb | ||
|
|
87e83b3bbe | ||
|
|
a94ad50b93 | ||
|
|
75cf1e6130 | ||
|
|
911be9e106 | ||
|
|
a577c80cc3 | ||
|
|
66cf103bca | ||
|
|
53f936a2c6 | ||
|
|
6f4c3e0486 | ||
|
|
b66ae97a0d | ||
|
|
2ced125e9f | ||
|
|
fb2f52c8ea | ||
|
|
91d66e6525 | ||
|
|
8fe7531bf4 | ||
|
|
b2764880a8 | ||
|
|
0668e1a34a | ||
|
|
4e063a2268 | ||
|
|
049b174a20 | ||
|
|
becfc9e238 | ||
|
|
dda82e5e2e | ||
|
|
9455c501d4 | ||
|
|
4a772ee626 | ||
|
|
5b97d5aa6d | ||
|
|
f3483868ed | ||
|
|
f5218bdbdb | ||
|
|
d9bc0dd5fd |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
tidelift: "pypi/django-mfa2"
|
||||||
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# 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"
|
||||||
29
CHANGELOG.md
29
CHANGELOG.md
@@ -1,5 +1,34 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
|
## 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
|
## 2.0
|
||||||
* Dropped support to djangp-1.8 and Python 2.7
|
* Dropped support to djangp-1.8 and Python 2.7
|
||||||
* Added: never-cache decorator
|
* Added: never-cache decorator
|
||||||
|
|||||||
38
README.md
38
README.md
@@ -1,7 +1,14 @@
|
|||||||
# 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
|
||||||
[](https://badge.fury.io/py/django-mfa2)
|
[](https://badge.fury.io/py/django-mfa2)
|
||||||
|
[](https://pepy.tech/project/django-mfa2)
|
||||||
|
|
||||||
|
### Conda Stats
|
||||||
|
[](https://anaconda.org/conda-forge/django-mfa2)
|
||||||
|
[](https://anaconda.org/conda-forge/django-mfa2)
|
||||||
|
[](https://anaconda.org/conda-forge/django-mfa2)
|
||||||
|
|
||||||
Web Authencation API (WebAuthn) is state-of-the art techology that is expected to replace passwords.
|
Web Authencation API (WebAuthn) is state-of-the art techology that is expected to replace passwords.
|
||||||
|
|
||||||
@@ -10,17 +17,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** (Chrome 70+ on Mac OS X ),
|
* **Apple's Touch ID/Face ID** (Chrome 70+ on Mac OS X, Safari on macOS Big Sur, Safari on iOS 14.0+ ),
|
||||||
* **android-safetynet** (Chrome 70+, Firefox 68+)
|
* **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 ID on Macbooks (Chrome) and Fingerprint/Face/Iris/PIN on Andriod Phones.
|
In English :), It allows you to verify the user by security keys on PC, Laptops or Mobiles, Windows Hello (Fingerprint, PIN) on Windows 10 Build 1903+ (May 2019 Update) Touch/Face ID on Macbooks (Chrome, Safari), Touch/Face ID on iPhone and iPad and Fingerprint/Face/Iris/PIN on Android Phones.
|
||||||
|
|
||||||
Trusted device is a mode for the user to add a device that doesn't support security keys like iOS and andriod without fingerprints or NFC.
|
Trusted device is a mode for the user to add a device that doesn't support security keys like Android without fingerprints or NFC.
|
||||||
|
|
||||||
**Note**: `U2F and FIDO2 can only be served under secure context (https)`
|
**Note**: `U2F and FIDO2 can only be served under secure context (https)`
|
||||||
|
|
||||||
Package tested with Django 1.8, Django 2.1 on Python 2.7 and Python 3.5+ but it was not checked with any version in between but open for issues.
|
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.
|
||||||
|
|
||||||
Depends on
|
Depends on
|
||||||
|
|
||||||
@@ -29,11 +36,23 @@ Depends on
|
|||||||
* ua-parser
|
* ua-parser
|
||||||
* user-agents
|
* user-agents
|
||||||
* python-jose
|
* python-jose
|
||||||
* fido2==0.7
|
* fido2==0.9.0
|
||||||
|
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
1. `pip install django-mfa2`
|
1. using pip
|
||||||
|
|
||||||
|
`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=(
|
||||||
@@ -158,3 +177,8 @@ function some_func() {
|
|||||||
* [d3cline](https://github.com/d3cline)
|
* [d3cline](https://github.com/d3cline)
|
||||||
* [swainn](https://github.com/swainn)
|
* [swainn](https://github.com/swainn)
|
||||||
* [unramk](https://github.com/unramk)
|
* [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.
|
||||||
|
|||||||
@@ -77,8 +77,10 @@ WSGI_APPLICATION = 'example.wsgi.application'
|
|||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
'ENGINE': 'django.db.backends.mysql',
|
||||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
'NAME': 'mfa',
|
||||||
|
'USER': 'root',
|
||||||
|
'PASSWORD': 'password',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +141,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=('FIDO2',) # Can the user disable his key (Added in 1.2.0).
|
MFA_HIDE_DISABLE=() # Can the user disable his key (Added in 1.2.0).
|
||||||
|
|
||||||
TOKEN_ISSUER_NAME="PROJECT_NAME" #TOTP Issuer name
|
TOKEN_ISSUER_NAME="PROJECT_NAME" #TOTP Issuer name
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,11 @@ Including another URLconf
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path,re_path,include
|
from django.urls import path,re_path,include
|
||||||
from . import views,auth
|
from . import views,auth
|
||||||
|
import mfa
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('mfa/', include('mfa.urls')),
|
path('mfa/', include('mfa.urls')),
|
||||||
|
path('devices/add', mfa.TrustedDevice.add,name="mfa_add_new_trusted_device"),
|
||||||
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"),
|
||||||
|
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ def auth(request):
|
|||||||
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"] = int((datetime.datetime.now() + datetime.timedelta(
|
mfa["next_check"] = datetime.datetime.timestamp(datetime.datetime.now() + datetime.timedelta(
|
||||||
seconds = random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))).strftime("%s"))
|
seconds = random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX)))
|
||||||
request.session["mfa"] = mfa
|
request.session["mfa"] = mfa
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|||||||
76
mfa/FIDO2.py
76
mfa/FIDO2.py
@@ -4,28 +4,31 @@ 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 django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
def recheck(request):
|
def recheck(request):
|
||||||
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():
|
||||||
rp = PublicKeyCredentialRpEntity(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):
|
||||||
server = getServer()
|
server = getServer()
|
||||||
registration_data, state = server.register_begin({
|
registration_data, state = server.register_begin({
|
||||||
@@ -35,7 +38,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):
|
||||||
try:
|
try:
|
||||||
@@ -50,10 +55,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'}))
|
||||||
@@ -63,10 +68,13 @@ 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):
|
||||||
context = csrf(request)
|
context = csrf(request)
|
||||||
return render(request,"FIDO2/Add.html", context)
|
return render(request, "FIDO2/Add.html", context)
|
||||||
|
|
||||||
|
|
||||||
def getUserCredentials(username):
|
def getUserCredentials(username):
|
||||||
credentials = []
|
credentials = []
|
||||||
@@ -74,24 +82,27 @@ 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(credentials)
|
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.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'])
|
||||||
@@ -107,7 +118,8 @@ def authenticate_complete(request):
|
|||||||
signature
|
signature
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return HttpResponse(simplejson.dumps({'status': "ERR", "message": "Wrong challenge received, make sure that this is your security and try again."}),
|
return HttpResponse(simplejson.dumps({'status': "ERR",
|
||||||
|
"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:
|
||||||
@@ -119,32 +131,34 @@ 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"] = int((datetime.datetime.now()+ datetime.timedelta(
|
mfa["next_check"] = datetime.datetime.timestamp((datetime.datetime.now() + datetime.timedelta(
|
||||||
seconds=random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))).strftime("%s"))
|
seconds = random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))))
|
||||||
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"]}),content_type="application/json")
|
return HttpResponse(simplejson.dumps({'status': "OK", "redirect": res["location"]}),
|
||||||
|
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}),content_type="application/json")
|
return HttpResponse(simplejson.dumps({'status': "ERR", "message": exp.message}),
|
||||||
|
content_type = "application/json")
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import string
|
|||||||
import random
|
import random
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.template.context import RequestContext
|
|
||||||
from django.template.context_processors import csrf
|
from django.template.context_processors import csrf
|
||||||
from .models import *
|
from .models import *
|
||||||
import user_agents
|
import user_agents
|
||||||
@@ -10,7 +9,7 @@ from django.utils import timezone
|
|||||||
|
|
||||||
def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
|
def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
|
||||||
x=''.join(random.choice(chars) for _ in range(size))
|
x=''.join(random.choice(chars) for _ in range(size))
|
||||||
if not User_Keys.objects.filter(properties__shas="$.key="+x).exists(): return x
|
if not User_Keys.objects.filter(properties__icontains=x, key_type="Trusted Device").exists(): return x
|
||||||
else: return id_generator(size,chars)
|
else: return id_generator(size,chars)
|
||||||
|
|
||||||
def getUserAgent(request):
|
def getUserAgent(request):
|
||||||
@@ -19,6 +18,7 @@ def getUserAgent(request):
|
|||||||
tk=User_Keys.objects.get(id=id)
|
tk=User_Keys.objects.get(id=id)
|
||||||
if tk.properties.get("user_agent","")!="":
|
if tk.properties.get("user_agent","")!="":
|
||||||
ua = user_agents.parse(tk.properties["user_agent"])
|
ua = user_agents.parse(tk.properties["user_agent"])
|
||||||
|
print(ua.os)
|
||||||
res = render(None, "TrustedDevices/user-agent.html", context={"ua":ua})
|
res = render(None, "TrustedDevices/user-agent.html", context={"ua":ua})
|
||||||
return HttpResponse(res)
|
return HttpResponse(res)
|
||||||
return HttpResponse("")
|
return HttpResponse("")
|
||||||
@@ -62,13 +62,14 @@ def add(request):
|
|||||||
key=request.POST["key"].replace("-","").replace(" ","").upper()
|
key=request.POST["key"].replace("-","").replace(" ","").upper()
|
||||||
context["username"] = request.POST["username"]
|
context["username"] = request.POST["username"]
|
||||||
context["key"] = request.POST["key"]
|
context["key"] = request.POST["key"]
|
||||||
trusted_keys=User_Keys.objects.filter(username=request.POST["username"],properties__has="$.key="+key)
|
trusted_keys=User_Keys.objects.filter(username=request.POST["username"],properties__iregex=rf'{key}')
|
||||||
cookie=False
|
cookie=False
|
||||||
if trusted_keys.exists():
|
if trusted_keys.exists():
|
||||||
tk=trusted_keys[0]
|
tk=trusted_keys[0]
|
||||||
request.session["td_id"]=tk.id
|
request.session["td_id"]=tk.id
|
||||||
ua=request.META['HTTP_USER_AGENT']
|
ua=request.META['HTTP_USER_AGENT']
|
||||||
agent=user_agents.parse(ua)
|
agent=user_agents.parse(ua)
|
||||||
|
print(agent.os)
|
||||||
if agent.is_pc:
|
if agent.is_pc:
|
||||||
context["invalid"]="This is a PC, it can't used as a trusted device."
|
context["invalid"]="This is a PC, it can't used as a trusted device."
|
||||||
else:
|
else:
|
||||||
@@ -124,7 +125,7 @@ def verify(request):
|
|||||||
json= jwt.decode(request.COOKIES.get('deviceid'),settings.SECRET_KEY)
|
json= jwt.decode(request.COOKIES.get('deviceid'),settings.SECRET_KEY)
|
||||||
if json["username"].lower()== request.session['base_username'].lower():
|
if json["username"].lower()== request.session['base_username'].lower():
|
||||||
try:
|
try:
|
||||||
uk = User_Keys.objects.get(username=request.POST["username"].lower(), properties__has="$.key=" + json["key"])
|
uk = User_Keys.objects.get(username=request.POST["username"].lower(), properties__properties__iregex=rf'{json["key"]}')
|
||||||
if uk.enabled and uk.properties["status"] == "trusted":
|
if uk.enabled and uk.properties["status"] == "trusted":
|
||||||
uk.last_used=timezone.now()
|
uk.last_used=timezone.now()
|
||||||
uk.save()
|
uk.save()
|
||||||
|
|||||||
@@ -52,14 +52,14 @@ def validate(request,username):
|
|||||||
challenge = request.session.pop('_u2f_challenge_')
|
challenge = request.session.pop('_u2f_challenge_')
|
||||||
device, c, t = complete_authentication(challenge, data, [settings.U2F_APPID])
|
device, c, t = complete_authentication(challenge, data, [settings.U2F_APPID])
|
||||||
|
|
||||||
key=User_Keys.objects.get(username=username,properties__shas="$.device.publicKey=%s"%device["publicKey"])
|
key = User_Keys.objects.get(username=username,key_type = "U2F", properties__iregex=rf'{device["publicKey"]}')
|
||||||
key.last_used=timezone.now()
|
key.last_used=timezone.now()
|
||||||
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"] = int((datetime.datetime.now()
|
mfa["next_check"] = datetime.datetime.timestamp((datetime.datetime.now()
|
||||||
+ datetime.timedelta(
|
+ datetime.timedelta(
|
||||||
seconds=random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))).strftime("%s"))
|
seconds=random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))))
|
||||||
request.session["mfa"] = mfa
|
request.session["mfa"] = mfa
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ def auth(request):
|
|||||||
request.session["_u2f_challenge_"]=s[0]
|
request.session["_u2f_challenge_"]=s[0]
|
||||||
context["token"]=s[1]
|
context["token"]=s[1]
|
||||||
|
|
||||||
return render(request,"U2F/Auth.html")
|
return render(request,"U2F/Auth.html",context)
|
||||||
|
|
||||||
def start(request):
|
def start(request):
|
||||||
enroll = begin_registration(settings.U2F_APPID, [])
|
enroll = begin_registration(settings.U2F_APPID, [])
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__="1.6.0"
|
__version__="2.1.2"
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ 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 = [
|
||||||
@@ -16,5 +22,5 @@ class Migration(migrations.Migration):
|
|||||||
name='owned_by_enterprise',
|
name='owned_by_enterprise',
|
||||||
field=models.NullBooleanField(default=None),
|
field=models.NullBooleanField(default=None),
|
||||||
),
|
),
|
||||||
migrations.RunSQL("update mfa_user_keys set owned_by_enterprise = %s where key_type='FIDO2'"%(1 if getattr(settings,"MFA_OWNED_BY_ENTERPRISE",False) else 0 ))
|
migrations.RunPython(update_owned_by_enterprise)
|
||||||
]
|
]
|
||||||
|
|||||||
18
mfa/migrations/0010_auto_20201110_0557.py
Normal file
18
mfa/migrations/0010_auto_20201110_0557.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -2,8 +2,7 @@ from django.db import models
|
|||||||
from jsonfield import JSONField
|
from jsonfield import JSONField
|
||||||
from jose import jwt
|
from jose import jwt
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from jsonLookup import shasLookup
|
|
||||||
JSONField.register_lookup(shasLookup)
|
|
||||||
|
|
||||||
class User_Keys(models.Model):
|
class User_Keys(models.Model):
|
||||||
username=models.CharField(max_length = 50)
|
username=models.CharField(max_length = 50)
|
||||||
@@ -19,9 +18,12 @@ 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'
|
||||||
9
mfa/static/mfa/js/ua-parser.min.js
vendored
Normal file
9
mfa/static/mfa/js/ua-parser.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -2,6 +2,7 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<script type="application/javascript" src="{% static 'mfa/js/cbor.js'%}"></script>
|
<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="application/javascript">
|
<script type="application/javascript">
|
||||||
function begin_reg(){
|
function begin_reg(){
|
||||||
fetch('{% url 'fido2_begin_reg' %}',{}).then(function(response) {
|
fetch('{% url 'fido2_begin_reg' %}',{}).then(function(response) {
|
||||||
@@ -40,7 +41,17 @@
|
|||||||
$("#res").html("<div class='alert alert-danger'>Registeration Failed as " +reason +", <a href='javascript:void(0)' onclick='begin_reg()'> try again </a> or <a href='{% url 'mfa_home' %}'> Go to Security Home</a></div>")
|
$("#res").html("<div class='alert alert-danger'>Registeration Failed as " +reason +", <a href='javascript:void(0)' onclick='begin_reg()'> try again </a> or <a href='{% url 'mfa_home' %}'> Go to Security Home</a></div>")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
$(document).ready(setTimeout(begin_reg,500))
|
$(document).ready(function (){
|
||||||
|
ua=new UAParser().getResult()
|
||||||
|
if (ua.browser.name == "Safari")
|
||||||
|
{
|
||||||
|
$("#res").html("<button class='btn btn-success' onclick='begin_reg()'>Start...</button>")
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setTimeout(begin_reg, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -55,8 +66,8 @@
|
|||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
|
||||||
|
|
||||||
<div class="row alert alert-pr" id="res">
|
<div class="row alert alert-pr" id="res" align="center">
|
||||||
<p style="color: green">Your broswer should ask you to confirm you indentity.</p>
|
<p style="color: green">Your browser should ask you to confirm you identity.</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
<script type="application/javascript" src="{% static 'mfa/js/cbor.js' %}"></script>
|
<script type="application/javascript" src="{% static 'mfa/js/cbor.js' %}"></script>
|
||||||
|
<script type="application/javascript" src="{% static 'mfa/js/ua-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">
|
||||||
@@ -17,7 +18,9 @@
|
|||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p style="color: green">please press the button on your security key to prove it is you.</p>
|
<div id="res">
|
||||||
|
<p style="color: green">please press the button on your security key to prove it is you.</p>
|
||||||
|
</div>
|
||||||
<div id="msgdiv"></div>
|
<div id="msgdiv"></div>
|
||||||
{% if mode == "auth" %}
|
{% if mode == "auth" %}
|
||||||
<form id="u2f_login" action="{% url 'fido2_complete_auth' %}" method="post" enctype="multipart/form-data">
|
<form id="u2f_login" action="{% url 'fido2_complete_auth' %}" method="post" enctype="multipart/form-data">
|
||||||
@@ -101,7 +104,11 @@
|
|||||||
$("#main_paragraph").addClass("alert alert-danger")
|
$("#main_paragraph").addClass("alert alert-danger")
|
||||||
$("#main_paragraph").html("FIDO2 must work under secure context")
|
$("#main_paragraph").html("FIDO2 must work under secure context")
|
||||||
} else {
|
} else {
|
||||||
authen()
|
ua=new UAParser().getResult()
|
||||||
|
if (ua.browser.name == "Safari")
|
||||||
|
$("#res").html("<button class='btn btn-success' onclick='authen()'>Authenticate...</button>")
|
||||||
|
else
|
||||||
|
authen()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -40,9 +40,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"] = int((datetime.datetime.now()
|
mfa["next_check"] = datetime.datetime.timestamp((datetime.datetime.now()
|
||||||
+ datetime.timedelta(
|
+ datetime.timedelta(
|
||||||
seconds=random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))).strftime("%s"))
|
seconds=random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))))
|
||||||
request.session["mfa"] = mfa
|
request.session["mfa"] = mfa
|
||||||
return login(request)
|
return login(request)
|
||||||
context["invalid"]=True
|
context["invalid"]=True
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
from . import views,totp,U2F,TrustedDevice,helpers,FIDO2,Email
|
from . import views,totp,U2F,TrustedDevice,helpers,FIDO2,Email
|
||||||
#app_name='mfa'
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from django.urls import re_path as url
|
from django.urls import re_path as url
|
||||||
except:
|
except:
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'totp/start/', totp.start , name="start_new_otop"),
|
url(r'totp/start/', totp.start , name="start_new_otop"),
|
||||||
url(r'totp/getToken', totp.getToken , name="get_new_otop"),
|
url(r'totp/getToken', totp.getToken , name="get_new_otop"),
|
||||||
@@ -39,6 +40,7 @@ urlpatterns = [
|
|||||||
url(r'u2f/secure_device', TrustedDevice.getCookie, name="td_securedevice"),
|
url(r'u2f/secure_device', TrustedDevice.getCookie, name="td_securedevice"),
|
||||||
|
|
||||||
url(r'^$', views.index, name="mfa_home"),
|
url(r'^$', views.index, name="mfa_home"),
|
||||||
|
url(r'devices/add$', TrustedDevice.add,name="mfa_add_new_trusted_device"),
|
||||||
url(r'goto/(.*)', views.goto, name="mfa_goto"),
|
url(r'goto/(.*)', views.goto, name="mfa_goto"),
|
||||||
url(r'selct_method', views.show_methods, name="mfa_methods_list"),
|
url(r'selct_method', views.show_methods, name="mfa_methods_list"),
|
||||||
url(r'recheck', helpers.recheck, name="mfa_recheck"),
|
url(r'recheck', helpers.recheck, name="mfa_recheck"),
|
||||||
|
|||||||
@@ -6,5 +6,5 @@ python-u2flib-server
|
|||||||
ua-parser
|
ua-parser
|
||||||
user-agents
|
user-agents
|
||||||
python-jose
|
python-jose
|
||||||
fido2 == 0.8.1
|
fido2 == 0.9.0
|
||||||
jsonLookup
|
jsonLookup
|
||||||
|
|||||||
13
setup.py
13
setup.py
@@ -4,7 +4,7 @@ from setuptools import find_packages, setup
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='django-mfa2',
|
name='django-mfa2',
|
||||||
version='2.0.0',
|
version='2.2.0b1',
|
||||||
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 >= 1.7',
|
'django >= 2.0',
|
||||||
'jsonfield',
|
'jsonfield',
|
||||||
'simplejson',
|
'simplejson',
|
||||||
'pyotp',
|
'pyotp',
|
||||||
@@ -24,14 +24,15 @@ setup(
|
|||||||
'ua-parser',
|
'ua-parser',
|
||||||
'user-agents',
|
'user-agents',
|
||||||
'python-jose',
|
'python-jose',
|
||||||
'fido2 == 0.8.1',
|
'fido2 == 0.9.1',
|
||||||
'jsonLookup'
|
# 'jsonLookup'
|
||||||
],
|
],
|
||||||
python_requires=">=3.5",
|
python_requires=">=3.5",
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
zip_safe=False, # because we're including static files
|
zip_safe=False, # because we're including static files
|
||||||
classifiers=[
|
classifiers=[
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 4 - Beta",
|
||||||
|
#"Development Status :: 5 - Production/Stable",
|
||||||
"Environment :: Web Environment",
|
"Environment :: Web Environment",
|
||||||
"Framework :: Django",
|
"Framework :: Django",
|
||||||
"Framework :: Django :: 1.11",
|
"Framework :: Django :: 1.11",
|
||||||
|
|||||||
Reference in New Issue
Block a user