diff --git a/README.md b/README.md index 89fc2a8..dadccc8 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,10 @@ Depends on U2F_APPID="https://localhost" #URL For U2F FIDO_SERVER_ID=u"localehost" # Server rp id for FIDO2, it is the full domain of your project FIDO_SERVER_NAME=u"PROJECT_NAME" + MFA_FIDO2_RESIDENT_KEY = mfa.ResidentKey.DISCOURAGED # Resident Key allows a special User Handle + MFA_FIDO2_AUTHENTICATOR_ATTACHMENT = None # Let the user choose + MFA_FIDO2_USER_VERIFICATION = None # Verify User Presence + MFA_FIDO2_ATTESTATION_PREFERENCE = mfa.AttestationPreference.NONE ``` **Method Names** * U2F @@ -104,8 +108,10 @@ Depends on * Starting version 1.7.0, Key owners can be specified. * Starting version 2.2.0 * Added: `MFA_SUCCESS_REGISTRATION_MSG` & `MFA_REDIRECT_AFTER_REGISTRATION` - Start version 2.6.0 + * Starting version 2.6.0 * Added: `MFA_ALWAYS_GO_TO_LAST_METHOD`, `MFA_RENAME_METHODS`, `MFA_ENFORCE_RECOVERY_METHOD` & `RECOVERY_ITERATION` + * Starting version 2.7.0 + * Added: `MFA_FIDO2_RESIDENT_KEY`, `MFA_FIDO2_AUTHENTICATOR_ATTACHMENT`, `MFA_FIDO2_USER_VERIFICATION`, `MFA_FIDO2_ATTESTATION_PREFERENCE` 4. 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 diff --git a/example/example/settings.py b/example/example/settings.py index a341a6c..89bba98 100644 --- a/example/example/settings.py +++ b/example/example/settings.py @@ -13,6 +13,8 @@ https://docs.djangoproject.com/en/2.0/ref/settings/ import os from django.conf.global_settings import PASSWORD_HASHERS as DEFAULT_PASSWORD_HASHERS +import mfa + # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -145,6 +147,10 @@ MFA_REDIRECT_AFTER_REGISTRATION="registered" MFA_SUCCESS_REGISTRATION_MSG="Go to Home" MFA_ALWAYS_GO_TO_LAST_METHOD = True MFA_ENFORCE_RECOVERY_METHOD = True +MFA_FIDO2_RESIDENT_KEY = mfa.ResidentKey.REQUIRED # Resident Key allows a special User Handle +MFA_FIDO2_AUTHENTICATOR_ATTACHMENT = None # Let the user choose +MFA_FIDO2_USER_VERIFICATION = None # Verify User Presence +MFA_FIDO2_ATTESTATION_PREFERENCE = mfa.AttestationPreference.NONE MFA_RENAME_METHODS = {"RECOVERY":"Backup Codes","FIDO2":"Biometric Authentication"} PASSWORD_HASHERS = DEFAULT_PASSWORD_HASHERS #Comment if PASSWORD_HASHER already set PASSWORD_HASHERS += ['mfa.recovery.Hash'] diff --git a/example/example/templates/login.html b/example/example/templates/login.html index 139c77f..22a6ae8 100644 --- a/example/example/templates/login.html +++ b/example/example/templates/login.html @@ -44,7 +44,9 @@ - +
+ + @@ -56,7 +58,7 @@ - + {% include 'FIDO2/Auth_JS.html'%} diff --git a/mfa/FIDO2.py b/mfa/FIDO2.py index bd78ef0..ef1cd32 100644 --- a/mfa/FIDO2.py +++ b/mfa/FIDO2.py @@ -28,18 +28,25 @@ def recheck(request): def getServer(): """Get Server Info from settings and returns a Fido2Server""" + from mfa import AttestationPreference rp = PublicKeyCredentialRpEntity(id=settings.FIDO_SERVER_ID, name=settings.FIDO_SERVER_NAME) - return Fido2Server(rp) + attestation= getattr(settings,'MFA_FIDO2_ATTESTATION_PREFERENCE', AttestationPreference.NONE ) + return Fido2Server(rp,attestation=attestation) def begin_registeration(request): """Starts registering a new FIDO Device, called from API""" server = getServer() + from mfa import ResidentKey + resident_key = getattr(settings,'MFA_FIDO2_RESIDENT_KEY', ResidentKey.DISCOURAGED) + auth_attachment = getattr(settings,'MFA_FIDO2_AUTHENTICATOR_ATTACHMENT', None) + user_verification = getattr(settings,'MFA_FIDO2_USER_VERIFICATION', None) registration_data, state = server.register_begin({ u'id': request.user.username.encode("utf8"), u'name': (request.user.first_name + " " + request.user.last_name), u'displayName': request.user.username, - }, getUserCredentials(request.user.username)) + }, getUserCredentials(request.user.username),user_verification = user_verification, + resident_key_requirement = resident_key, authenticator_attachment = auth_attachment) request.session['fido_state'] = state return HttpResponse(cbor.encode(registration_data), content_type = 'application/octet-stream') @@ -67,6 +74,8 @@ def complete_reg(request): uk.properties = {"device": encoded, "type": att_obj.fmt, } uk.owned_by_enterprise = getattr(settings, "MFA_OWNED_BY_ENTERPRISE", False) uk.key_type = "FIDO2" + if auth_data.credential_data.credential_id: + uk.user_handle = auth_data.credential_data.credential_id uk.save() if getattr(settings, 'MFA_ENFORCE_RECOVERY_METHOD', False) and not User_Keys.objects.filter(key_type = "RECOVERY", username=request.user.username).exists(): request.session["mfa_reg"] = {"method":"FIDO2","name": getattr(settings, "MFA_RENAME_METHODS", {}).get("FIDO2", "FIDO2")} @@ -107,7 +116,14 @@ def auth(request): def authenticate_begin(request): server = getServer() - credentials = getUserCredentials(request.session.get("base_username", request.user.username)) + credentials=[] + username = None + if "base_username" in request.session: + username = request.session["base_username"] + if request.user.is_authenticated: + username = request.user.username + if username: + credentials = getUserCredentials(request.session.get("base_username", request.user.username)) auth_data, state = server.authenticate_begin(credentials) request.session['fido_state'] = state return HttpResponse(cbor.encode(auth_data), content_type = "application/octet-stream") @@ -117,11 +133,22 @@ def authenticate_begin(request): def authenticate_complete(request): try: credentials = [] - username = request.session.get("base_username", request.user.username) + username = None + keys = None + if "base_username" in request.session: + username = request.session["base_username"] + if request.user.is_authenticated: + username = request.user.username server = getServer() - credentials = getUserCredentials(username) data = cbor.decode(request.body) credential_id = data['credentialId'] + if credential_id and username is None: + keys = User_Keys.objects.filter(user_handle = credential_id) + if keys.exists(): + credentials=[AttestedCredentialData(websafe_decode(keys[0].properties["device"]))] + else: + credentials = getUserCredentials(username) + client_data = CollectedClientData(data['clientDataJSON']) auth_data = AuthenticatorData(data['authenticatorData']) signature = data['signature'] @@ -155,7 +182,8 @@ def authenticate_complete(request): content_type = "application/json") else: import random - keys = User_Keys.objects.filter(username = username, key_type = "FIDO2", enabled = 1) + if keys is None: + keys = User_Keys.objects.filter(username = username, key_type = "FIDO2", enabled = 1) for k in keys: if AttestedCredentialData(websafe_decode(k.properties["device"])).credential_id == cred.credential_id: k.last_used = timezone.now() @@ -170,7 +198,7 @@ def authenticate_complete(request): except: authenticated = request.user.is_authenticated() if not authenticated: - res = login(request) + res = login(request,k.username) if not "location" in res: return reset_cookie(request) return HttpResponse(simplejson.dumps({'status': "OK", "redirect": res["location"]}), content_type = "application/json") diff --git a/mfa/__init__.py b/mfa/__init__.py index d9d106a..8ccb53f 100644 --- a/mfa/__init__.py +++ b/mfa/__init__.py @@ -1 +1,5 @@ -__version__="2.2.0" +__version__="2.7.1" +from fido2.webauthn import ResidentKeyRequirement as ResidentKey +from fido2.webauthn import AuthenticatorAttachment as Attachment +from fido2.webauthn import UserVerificationRequirement as UserVerification +from fido2.webauthn import AttestationConveyancePreference as AttestationPreference \ No newline at end of file diff --git a/mfa/migrations/0012_user_keys_userhandle.py b/mfa/migrations/0012_user_keys_userhandle.py new file mode 100644 index 0000000..b074980 --- /dev/null +++ b/mfa/migrations/0012_user_keys_userhandle.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2 on 2022-10-16 14:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mfa', '0011_auto_20210530_0622'), + ] + operations = [ + migrations.AddField( + model_name='user_keys', + name='user_handle', + field=models.CharField(blank=True, default=None, max_length=255, null=True), + ), + ] diff --git a/mfa/models.py b/mfa/models.py index d4eff59..50736c4 100644 --- a/mfa/models.py +++ b/mfa/models.py @@ -16,6 +16,7 @@ class User_Keys(models.Model): expires=models.DateTimeField(null=True,default=None,blank=True) last_used=models.DateTimeField(null=True,default=None,blank=True) owned_by_enterprise=models.BooleanField(default=None,null=True,blank=True) + user_handle = models.CharField(default = None, null = True, blank = True, max_length = 255) def save(self, force_insert=False, force_update=False, using=None, update_fields=None): if self.key_type == "Trusted Device" and self.properties.get("signature","") == "": diff --git a/mfa/templates/FIDO2/Add.html b/mfa/templates/FIDO2/Add.html index 2b8071d..d401aac 100644 --- a/mfa/templates/FIDO2/Add.html +++ b/mfa/templates/FIDO2/Add.html @@ -12,7 +12,7 @@ } throw new Error('Error getting registration data!'); }).then(CBOR.decode).then(function(options) { - options.publicKey.attestation="direct" + //options.publicKey.attestation="direct" console.log(options) return navigator.credentials.create(options); diff --git a/mfa/templates/FIDO2/Auth_JS.html b/mfa/templates/FIDO2/Auth_JS.html new file mode 100644 index 0000000..e31d8ed --- /dev/null +++ b/mfa/templates/FIDO2/Auth_JS.html @@ -0,0 +1,71 @@ +{% load static %} + + + \ No newline at end of file diff --git a/mfa/templates/FIDO2/recheck.html b/mfa/templates/FIDO2/recheck.html index c44db3a..8d85a7b 100644 --- a/mfa/templates/FIDO2/recheck.html +++ b/mfa/templates/FIDO2/recheck.html @@ -1,6 +1,4 @@ {% load static %} - -
@@ -47,71 +45,4 @@
- + {% include 'FIDO2/Auth_JS.html' %} diff --git a/mfa/views.py b/mfa/views.py index 819dae5..b7cd3f0 100644 --- a/mfa/views.py +++ b/mfa/views.py @@ -60,11 +60,13 @@ def reset_cookie(request): response.delete_cookie("base_username") return response -def login(request): +def login(request,username = None): from django.contrib import auth from django.conf import settings callable_func = __get_callable_function__(settings.MFA_LOGIN_CALLBACK) - return callable_func(request,username=request.session["base_username"]) + if not username: + username = request.session["base_username"] + return callable_func(request,username=username) @login_required