Compare commits

..

8 Commits

Author SHA1 Message Date
Mohamed ElKalioby
7a0c77108f Updating ChangeLog 2022-06-16 20:16:03 +03:00
Mohamed ElKalioby
84b9d297d2 v2.5b1 2022-06-16 20:01:07 +03:00
Mohamed ElKalioby
1ebc5bfd2b Upgrading to FIDO2==1.0.0 2022-06-13 17:18:00 +03:00
Mohamed ElKalioby
d8b10bcdc1 WIP: Upgrading Fido2 2022-06-13 16:47:31 +03:00
Mohamed ElKalioby
f6d25d7a79 Updated CHANGELOG 2022-06-01 20:23:37 +03:00
Mohamed ElKalioby
1f4be15fc5 Updated README and CHANGLOG 2022-05-30 17:43:00 +03:00
Mohamed ElKalioby
6ea99ff931 Fix issue in case of not Safari on iOS 2022-05-30 17:25:57 +03:00
Ezra Rice
660d5ca8f8 BootStrap5 Update MFA.html 2022-05-24 09:58:31 -04:00
10 changed files with 40 additions and 54 deletions

View File

@@ -1,4 +1,12 @@
# Change Log # Change Log
## 2.5.0
* Bumped fido2 version to 1.0
* Fixed: issue in the 'Authorize' button don't show on Firefox and Chrome on iOS.
Note: It seems Firefox doesn't support WebAuthn on iOS
* Fixed: Support for bootstrap5
Thanks to @ezrajrice
## 2.4.0 ## 2.4.0
* Fixed: issue in the 'Authorize' button don't show on Safari Mobile. * Fixed: issue in the 'Authorize' button don't show on Safari Mobile.

View File

@@ -12,7 +12,7 @@ A Django app that handles MFA, it supports TOTP, U2F, FIDO2 U2F (Web Authn), Ema
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.
![Android Fingerprint](https://cdn-images-1.medium.com/max/800/1*1FWkRE8D7NTA2Kn1DrPjPA.png) ![Andriod Fingerprint](https://cdn-images-1.medium.com/max/800/1*1FWkRE8D7NTA2Kn1DrPjPA.png)
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+),
@@ -75,7 +75,6 @@ Depends on
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_RESIDENT_KEY = None # Use Resident Key (Only supported in Chromimum based browsers)
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 owns security keys
@@ -188,6 +187,7 @@ function some_func() {
* [willingham](https://github.com/willingham) * [willingham](https://github.com/willingham)
* [AndreasDickow](https://github.com/AndreasDickow) * [AndreasDickow](https://github.com/AndreasDickow)
* [mnelson4](https://github.com/mnelson4) * [mnelson4](https://github.com/mnelson4)
* [ezrajrice](https://github.com/ezrajrice)
# Security contact information # Security contact information

View File

@@ -3,7 +3,6 @@ from django.http import HttpResponseRedirect
from django.urls import reverse from django.urls import reverse
from django.contrib.auth import authenticate,login,logout from django.contrib.auth import authenticate,login,logout
from django.contrib.auth.models import User from django.contrib.auth.models import User
def loginView(request): def loginView(request):
context={} context={}
if request.method=="POST": if request.method=="POST":

View File

@@ -142,10 +142,9 @@ MFA_QUICKLOGIN=True # Allow quick login for returning users by provide on
MFA_HIDE_DISABLE=('',) # 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).
MFA_REDIRECT_AFTER_REGISTRATION="registered" MFA_REDIRECT_AFTER_REGISTRATION="registered"
MFA_SUCCESS_REGISTRATION_MSG="Go to Home" MFA_SUCCESS_REGISTRATION_MSG="Go to Home"
MFA_RESIDENT_KEY = True
TOKEN_ISSUER_NAME="PROJECT_NAME" #TOTP Issuer name TOKEN_ISSUER_NAME="PROJECT_NAME" #TOTP Issuer name
U2F_APPID="https://localhost" #URL For U2F U2F_APPID="https://localhost" #URL For U2F
FIDO_SERVER_ID=u"localhost" # Server rp id for FIDO2, it the full domain of your project FIDO_SERVER_ID="localhost" # Server rp id for FIDO2, it the full domain of your project
FIDO_SERVER_NAME=u"TestApp" FIDO_SERVER_NAME="TestApp"

View File

@@ -45,10 +45,6 @@
</div> </div>
<button class="btn btn-primary btn-block" type="submit">Login</button> <button class="btn btn-primary btn-block" type="submit">Login</button>
<br/>
OR
<br/>
<a href="{% url 'fido2_auth' %}"><button class="btn btn-primary btn-block" type="button">Login By Security Key</button></a>
</form> </form>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,6 @@
from fido2.client import ClientData from fido2.client import Fido2Client
from fido2.server import Fido2Server, PublicKeyCredentialRpEntity from fido2.server import Fido2Server, PublicKeyCredentialRpEntity
from fido2.ctap2 import AttestationObject, AuthenticatorData from fido2.webauthn import AttestationObject, AuthenticatorData, CollectedClientData
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
@@ -11,7 +11,7 @@ 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.webauthn 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 .Common import get_redirect_url
@@ -28,7 +28,7 @@ def recheck(request):
def getServer(): def getServer():
"""Get Server Info from settings and returns a Fido2Server""" """Get Server Info from settings and returns a Fido2Server"""
rp = PublicKeyCredentialRpEntity(settings.FIDO_SERVER_ID, settings.FIDO_SERVER_NAME) rp = PublicKeyCredentialRpEntity(id=settings.FIDO_SERVER_ID, name=settings.FIDO_SERVER_NAME)
return Fido2Server(rp) return Fido2Server(rp)
@@ -39,7 +39,7 @@ def begin_registeration(request):
u'id': request.user.username.encode("utf8"), u'id': request.user.username.encode("utf8"),
u'name': (request.user.first_name + " " + request.user.last_name), u'name': (request.user.first_name + " " + request.user.last_name),
u'displayName': request.user.username, u'displayName': request.user.username,
}, getUserCredentials(request.user.username),resident_key=getattr(settings,'MFA_RESIDENT_KEY',None)) }, 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')
@@ -51,7 +51,7 @@ def complete_reg(request):
try: try:
data = cbor.decode(request.body) data = cbor.decode(request.body)
client_data = ClientData(data['clientDataJSON']) client_data = CollectedClientData(data['clientDataJSON'])
att_obj = AttestationObject((data['attestationObject'])) att_obj = AttestationObject((data['attestationObject']))
server = getServer() server = getServer()
auth_data = server.register_complete( auth_data = server.register_complete(
@@ -63,13 +63,13 @@ def complete_reg(request):
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, }
if data.get('userHandle'):
uk.properties["userHandle"] = data['userHandle']
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'}))
except Exception as exp: except Exception as exp:
import traceback
print(traceback.format_exc())
try: try:
from raven.contrib.django.raven_compat.models import client from raven.contrib.django.raven_compat.models import client
client.captureException() client.captureException()
@@ -99,9 +99,7 @@ def auth(request):
def authenticate_begin(request): def authenticate_begin(request):
server = getServer() server = getServer()
credentials=None credentials = getUserCredentials(request.session.get("base_username", request.user.username))
if not getattr(settings,'MFA_RESIDENT_KEY',None):
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")
@@ -111,26 +109,13 @@ def authenticate_begin(request):
def authenticate_complete(request): def authenticate_complete(request):
try: try:
credentials = [] credentials = []
data = cbor.decode(request.body) username = request.session.get("base_username", request.user.username)
if data.get("userHandle"):
keys = User_Keys.objects.filter(key_type="FIDO2", properties__icontains='"userHandle": "%s"'%data["userHandle"])
if keys.count()==1:
username = keys[0].username
request.session["base_username"]=username
request.session.update = 1
else:
username = request.session.get("base_username", request.user.username)
server = getServer() server = getServer()
credentials = getUserCredentials(username) credentials = getUserCredentials(username)
auth_data = AuthenticatorData(data['authenticatorData']) data = cbor.decode(request.body)
credential_id = data['credentialId'] credential_id = data['credentialId']
client_data = ClientData(data['clientDataJSON']) client_data = CollectedClientData(data['clientDataJSON'])
auth_data = AuthenticatorData(data['authenticatorData'])
signature = data['signature'] signature = data['signature']
try: try:
cred = server.authenticate_complete( cred = server.authenticate_complete(

View File

@@ -17,12 +17,10 @@
return navigator.credentials.create(options); return navigator.credentials.create(options);
}).then(function(attestation) { }).then(function(attestation) {
console.log(attestation)
return fetch('{% url 'fido2_complete_reg' %}', { return fetch('{% url 'fido2_complete_reg' %}', {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/cbor'}, headers: {'Content-Type': 'application/cbor'},
body: CBOR.encode({ body: CBOR.encode({
"userHandle":attestation.id,
"attestationObject": new Uint8Array(attestation.response.attestationObject), "attestationObject": new Uint8Array(attestation.response.attestationObject),
"clientDataJSON": new Uint8Array(attestation.response.clientDataJSON), "clientDataJSON": new Uint8Array(attestation.response.clientDataJSON),
}) })

View File

@@ -58,11 +58,8 @@
}).then(CBOR.decode).then(function(options) { }).then(CBOR.decode).then(function(options) {
console.log(options) console.log(options)
return navigator.credentials.get(options); return navigator.credentials.get(options);
}).then(function(assertion) { }).then(function(assertion) {
console.log(assertion)
res=CBOR.encode({ res=CBOR.encode({
"userHandle":assertion.id,
"credentialId": new Uint8Array(assertion.rawId), "credentialId": new Uint8Array(assertion.rawId),
"authenticatorData": new Uint8Array(assertion.response.authenticatorData), "authenticatorData": new Uint8Array(assertion.response.authenticatorData),
"clientDataJSON": new Uint8Array(assertion.response.clientDataJSON), "clientDataJSON": new Uint8Array(assertion.response.clientDataJSON),
@@ -108,7 +105,7 @@
$("#main_paragraph").html("FIDO2 must work under secure context") $("#main_paragraph").html("FIDO2 must work under secure context")
} else { } else {
ua=new UAParser().getResult() ua=new UAParser().getResult()
if (ua.browser.name == "Safari" || ua.browser.name == "Mobile Safari" ) if (ua.browser.name == "Safari" || ua.browser.name == "Mobile Safari" || ua.os.name == "iOS" || ua.os.name == "iPadOS")
$("#res").html("<button class='btn btn-success' onclick='authen()'>Authenticate...</button>") $("#res").html("<button class='btn btn-success' onclick='authen()'>Authenticate...</button>")
else else
authen() authen()

View File

@@ -47,24 +47,24 @@
<div class="row"> <div class="row">
<div align="center"> <div align="center">
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-success dropdown-toggle" data-toggle="dropdown"> <button class="btn btn-success dropdown-toggle" data-toggle="dropdown" data-bs-toggle="dropdown">
Add Method&nbsp;<span class="caret"></span> Add Method&nbsp;<span class="caret"></span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
{% if not 'TOTP' in UNALLOWED_AUTHEN_METHODS %} {% if not 'TOTP' in UNALLOWED_AUTHEN_METHODS %}
<li><a href="{% url 'start_new_otop' %}">Authenticator app</a></li> <li><a class="dropdown-item" href="{% url 'start_new_otop' %}">Authenticator app</a></li>
{% endif %} {% endif %}
{% if not 'Email' in UNALLOWED_AUTHEN_METHODS %} {% if not 'Email' in UNALLOWED_AUTHEN_METHODS %}
<li><a href="{% url 'start_email' %}">Email Token</a></li> <li><a class="dropdown-item" href="{% url 'start_email' %}">Email Token</a></li>
{% endif %} {% endif %}
{% if not 'U2F' in UNALLOWED_AUTHEN_METHODS %} {% if not 'U2F' in UNALLOWED_AUTHEN_METHODS %}
<li><a href="{% url 'start_u2f' %}">Security Key</a></li> <li><a class="dropdown-item" href="{% url 'start_u2f' %}">Security Key</a></li>
{% endif %} {% endif %}
{% if not 'FIDO2' in UNALLOWED_AUTHEN_METHODS %} {% if not 'FIDO2' in UNALLOWED_AUTHEN_METHODS %}
<li><a href="{% url 'start_fido2' %}">FIDO2 Security Key</a></li> <li><a class="dropdown-item" href="{% url 'start_fido2' %}">FIDO2 Security Key</a></li>
{% endif %} {% endif %}
{% if not 'Trusted_Devices' in UNALLOWED_AUTHEN_METHODS %} {% if not 'Trusted_Devices' in UNALLOWED_AUTHEN_METHODS %}
<li><a href="{% url 'start_td' %}">Trusted Device</a></li> <li><a class="dropdown-item" href="{% url 'start_td' %}">Trusted Device</a></li>
{% endif %} {% endif %}
</ul> </ul>
</div> </div>
@@ -108,4 +108,4 @@
</div> </div>
</div> </div>
{% include "modal.html" %} {% include "modal.html" %}
{% endblock %} {% endblock %}

View File

@@ -4,7 +4,7 @@ from setuptools import find_packages, setup
setup( setup(
name='django-mfa2', name='django-mfa2',
version='2.4.0', version='2.5.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",
@@ -24,14 +24,14 @@ setup(
'ua-parser', 'ua-parser',
'user-agents', 'user-agents',
'python-jose', 'python-jose',
'fido2 == 0.9.2', 'fido2 == 1.0.0',
'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",
"Environment :: Web Environment", "Environment :: Web Environment",
"Framework :: Django", "Framework :: Django",
"Framework :: Django :: 2.0", "Framework :: Django :: 2.0",
@@ -39,6 +39,8 @@ setup(
"Framework :: Django :: 2.2", "Framework :: Django :: 2.2",
"Framework :: Django :: 3.0", "Framework :: Django :: 3.0",
"Framework :: Django :: 3.1", "Framework :: Django :: 3.1",
"Framework :: Django :: 3.2",
"Framework :: Django :: 4.0",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Programming Language :: Python", "Programming Language :: Python",
@@ -47,6 +49,8 @@ setup(
"Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Libraries :: Python Modules",
] ]
) )