From f610de0d4a34369f272e75aa0f76651603e8d5d0 Mon Sep 17 00:00:00 2001 From: Christian Thieme Date: Tue, 5 Nov 2024 15:47:29 +0100 Subject: [PATCH 1/9] Moved tests into the passkeys package Necessary adjustments: - authentication tests should not use a view from the example project but the authentication backend - the authentication backend should not throw exceptions, all form validation must be done by the LoginForm class, if login fails the backend should simply return None, for configuration errors use logging - fixed a possible exception in delKey, improved the logic in delKey and toggleKey --- example/test_app/test_settings.py | 136 --------------- example/test_app/tests/__init__.py | 0 .../test_app/tests/test_current_platform.py | 32 ---- example/test_app/tests/test_fido.py | 142 --------------- example/test_app/tests/test_passkeys.py | 40 ----- example/test_app/tests/test_views.py | 46 ----- passkeys/FIDO2.py | 59 ++++--- passkeys/backend.py | 29 ++-- passkeys/models.py | 10 +- passkeys/tests/__init__.py | 4 + passkeys/tests/backend.py | 21 +++ passkeys/tests/fido.py | 163 ++++++++++++++++++ passkeys/tests/platform.py | 49 ++++++ .../tests}/soft_webauthn.py | 0 passkeys/tests/views.py | 95 ++++++++++ passkeys/views.py | 46 +++-- tox.ini | 4 +- 17 files changed, 418 insertions(+), 458 deletions(-) delete mode 100644 example/test_app/test_settings.py delete mode 100644 example/test_app/tests/__init__.py delete mode 100644 example/test_app/tests/test_current_platform.py delete mode 100644 example/test_app/tests/test_fido.py delete mode 100644 example/test_app/tests/test_passkeys.py delete mode 100644 example/test_app/tests/test_views.py create mode 100644 passkeys/tests/__init__.py create mode 100644 passkeys/tests/backend.py create mode 100644 passkeys/tests/fido.py create mode 100644 passkeys/tests/platform.py rename {example/test_app => passkeys/tests}/soft_webauthn.py (100%) create mode 100644 passkeys/tests/views.py diff --git a/example/test_app/test_settings.py b/example/test_app/test_settings.py deleted file mode 100644 index a5189b6..0000000 --- a/example/test_app/test_settings.py +++ /dev/null @@ -1,136 +0,0 @@ -""" -Django settings for example project. - -Generated by 'django-admin startproject' using Django 2.0. - -For more information on this file, see -https://docs.djangoproject.com/en/2.0/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/2.0/ref/settings/ -""" - -import os -from django.conf.global_settings import PASSWORD_HASHERS as DEFAULT_PASSWORD_HASHERS - -import passkeys - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '#9)q!_i3@pr-^3oda(e^3$x!kq3b4f33#5l@+=+&vuz+p6gb3g' - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = ['*'] - - -# Application definition - -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'test_app', - 'passkeys', - 'sslserver' -] - -MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', -] - -ROOT_URLCONF = 'test_app.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR ,'example','templates' )], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'test_app.wsgi.application' - - -# Database -# https://docs.djangoproject.com/en/2.0/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'test_db', - } -} - - -# Password validation -# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] - - -# Internationalization -# https://docs.djangoproject.com/en/2.0/topics/i18n/ - -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/2.0/howto/static-files/ - -STATIC_URL = '/static/' -#STATIC_ROOT=(os.path.join(BASE_DIR,'static')) -STATICFILES_DIRS=[os.path.join(BASE_DIR,'static')] -LOGIN_URL="/auth/login" - -AUTHENTICATION_BACKENDS = ['passkeys.backend.PasskeyModelBackend'] - -FIDO_SERVER_ID="testserver" # Server rp id for FIDO2, it the full domain of your project -FIDO_SERVER_NAME="TestApp" -KEY_ATTACHMENT = passkeys.Attachment.PLATFORM -CSRF_TRUSTED_ORIGINS = ['http://localhost:8000','https://localhost:8000'] diff --git a/example/test_app/tests/__init__.py b/example/test_app/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/example/test_app/tests/test_current_platform.py b/example/test_app/tests/test_current_platform.py deleted file mode 100644 index 5988f38..0000000 --- a/example/test_app/tests/test_current_platform.py +++ /dev/null @@ -1,32 +0,0 @@ -from django.test import TestCase, RequestFactory - -from passkeys.FIDO2 import get_current_platform - - -class TestCurrentPlatform(TestCase): - def setUp(self) -> None: - self.request_factory = RequestFactory() - # if not getattr(self, "assertEquals", None): - # self.assertEquals = self.assertEqual - - def check_platform(self,user_agent, platform): - request = self.request_factory.get('/', HTTP_USER_AGENT=user_agent) - self.assertEqual(get_current_platform(request), platform) - - def test_mac(self): - self.check_platform("Mozilla/5.0 (Macintosh; Intel Mac OS X 13_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15","Apple") - def test_ios(self): - self.check_platform("Mozilla/5.0 (Macintosh; Intel Mac OS X 13_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15","Apple") - def test_ipad(self): - self.check_platform("Mozilla/5.0 (iPad; CPU OS 16_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Mobile/15E148 Safari/604.1","Apple") - - def test_chrome_mac(self): - self.check_platform("Mozilla/5.0 (Macintosh; Intel Mac OS X 13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36","Chrome on Apple") - - def test_chrome_windows(self): - self.check_platform("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36","Microsoft") - - def test_android(self): - self.check_platform("Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.130 Mobile Safari/537.36","Google") - self.check_platform("Mozilla/5.0 (Linux; Android 10; SM-A205U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.130 Mobile Safari/537.36","Google") - self.check_platform("Mozilla/5.0 (Linux; Android 10; LM-Q720) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.130 Mobile Safari/537.36","Google") diff --git a/example/test_app/tests/test_fido.py b/example/test_app/tests/test_fido.py deleted file mode 100644 index b929db4..0000000 --- a/example/test_app/tests/test_fido.py +++ /dev/null @@ -1,142 +0,0 @@ -import json -from base64 import urlsafe_b64encode -from importlib import import_module - -from django.http import HttpRequest -from django.test import RequestFactory,TransactionTestCase, Client -from django.urls import reverse - -from django.conf import settings -from test_app.soft_webauthn import SoftWebauthnDevice - -from passkeys.models import UserPasskey - - -def get_server_id(request): - return request.META["SERVER_NAME"] + "1" - -def get_server_name(request): - return "MySite" - -class test_fido(TransactionTestCase): - def setUp(self) -> None: - - from django.contrib.auth import get_user_model - self.user_model = get_user_model() - if self.user_model.objects.filter(username="test").count()==0: - self.user = self.user_model.objects.create_user(username="test",password="test") - else: - self.user = self.user_model.objects.get(username="test") - self.client = Client() - settings.SESSION_ENGINE = 'django.contrib.sessions.backends.file' - engine = import_module(settings.SESSION_ENGINE) - #settings.SESSION_FILE_PATH = "/" - store = engine.SessionStore() - store.save(must_create=True) - self.session = store - self.client.cookies["sessionid"] = store.session_key - - self.client.post("/auth/login", {"username": "test", "password": "test", 'passkeys': ''}) - self.factory = RequestFactory() - - - def test_key_reg(self): - self.client.post('auth/login',{"usernaame":"test","password":"test","passkeys":""}) - r = self.client.get(reverse('passkeys:reg_begin')) - self.assertEqual(r.status_code, 200) - j = json.loads(r.content) - j['publicKey']['challenge'] = j['publicKey']['challenge'].encode("ascii") - s = SoftWebauthnDevice() - res = s.create(j, "https://" + j["publicKey"]["rp"]["id"]) - res["key_name"]="testKey" - u = reverse('passkeys:reg_complete') - r = self.client.post(u, data=json.dumps(res),headers={"USER_AGENT":""}, HTTP_USER_AGENT="", content_type="application/json") - try: - j = json.loads(r.content) - except Exception: - raise AssertionError("Failed to get the required JSON after reg_completed") - self.assertTrue("status" in j) - - self.assertEqual(j["status"], "OK") - self.assertEqual(UserPasskey.objects.latest('id').name, "testKey") - return s - - - def test_auto_key_name(self): - r = self.client.get(reverse('passkeys:reg_begin')) - self.assertEqual(r.status_code, 200) - j = json.loads(r.content) - j['publicKey']['challenge'] = j['publicKey']['challenge'].encode("ascii") - s = SoftWebauthnDevice() - res = s.create(j, "https://" + j["publicKey"]["rp"]["id"]) - u = reverse('passkeys:reg_complete') - r = self.client.post(u, data=json.dumps(res), HTTP_USER_AGENT="Mozilla/5.0 (Macintosh; Intel Mac OS X 13_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15", content_type="application/json") - try: - j = json.loads(r.content) - except Exception: - raise AssertionError("Failed to get the required JSON after reg_completed") - self.assertTrue("status" in j) - self.assertEqual(j["status"], "OK") - self.assertEqual(UserPasskey.objects.latest('id').name,"Apple") - return s - - def test_error_when_no_session(self): - res = {} - res["key_name"] = "testKey" - u = reverse('passkeys:reg_complete') - r = self.client.post(u, data=json.dumps(res), headers={"USER_AGENT": ""}, HTTP_USER_AGENT="", - content_type="application/json") - try: - j = json.loads(r.content) - except Exception: - raise AssertionError("Failed to get the required JSON after reg_completed") - self.assertTrue("status" in j) - self.assertEqual(j["status"], "ERR") - self.assertEqual(j["message"], "FIDO Status can't be found, please try again") - - def test_passkey_login(self): - authenticator = self.test_key_reg() - self.client.get('/auth/logout') - r = self.client.get(reverse('passkeys:auth_begin')) - self.assertEqual(r.status_code, 200) - j = json.loads(r.content) - j['publicKey']['challenge'] = j['publicKey']['challenge'].encode("ascii") - - res = authenticator.get(j, "https://" + j["publicKey"]["rpId"]) - u = reverse('login') - self.client.post(u, {'passkeys': json.dumps(res), "username": "", "password": ""},headers={"USER_AGENT":""}, HTTP_USER_AGENT="") - self.assertTrue(self.client.session.get('_auth_user_id',False)) - self.assertTrue(self.client.session.get("passkey",{}).get("passkey",False)) - self.assertEqual(self.client.session.get("passkey",{}).get("name"),"testKey") - - def test_base_username(self): - authenticator = self.test_key_reg() - self.client.get('/auth/logout') - session = self.session - session["base_username"]= "test" - session.save(must_create=True) - self.client.cookies["sessionid"] = session.session_key - r = self.client.get(reverse('passkeys:auth_begin')) - self.assertEqual(r.status_code, 200) - j = json.loads(r.content) - self.assertEqual(j['publicKey']['allowCredentials'][0]['id'],urlsafe_b64encode(authenticator.credential_id).decode("utf8").strip('=')) - - def test_passkey_login_no_session(self): - pass - - - def test_server_id_callable(self): - from test_app.tests.test_fido import get_server_id - settings.FIDO_SERVER_ID = get_server_id - r = self.client.get(reverse('passkeys:auth_begin')) - self.assertEqual(r.status_code, 200) - j = json.loads(r.content) - self.assertEqual(j['publicKey']['rpId'],'testserver1') - - def test_server_name_callable(self): - from test_app.tests.test_fido import get_server_name - settings.FIDO_SERVER_NAME = get_server_name - r = self.client.get(reverse('passkeys:reg_begin')) - self.assertEqual(r.status_code, 200) - j = json.loads(r.content) - self.assertEqual(j['publicKey']['rp']["name"],'MySite') diff --git a/example/test_app/tests/test_passkeys.py b/example/test_app/tests/test_passkeys.py deleted file mode 100644 index 7ba3558..0000000 --- a/example/test_app/tests/test_passkeys.py +++ /dev/null @@ -1,40 +0,0 @@ -from django.test import RequestFactory,TransactionTestCase, Client - -class test_passkeys(TransactionTestCase): - def setUp(self) -> None: - from django.contrib.auth import get_user_model - - self.user_model = get_user_model() - self.user = self.user_model.objects.create_user(username="test",password="test") - self.client = Client() - self.factory = RequestFactory() - - def test_raiseException(self): - from django.contrib.auth import authenticate - try: - authenticate(request=None,username="test",password="test") - self.assertFalse(True) - except Exception as e: - self.assertEqual(str(e),"request is required for passkeys.backend.PasskeyModelBackend") - - def test_not_add_passkeys_field(self): - request = self.factory.post("/auth/login",{"username":"","password":""}) - from django.contrib.auth import authenticate - try: - user = authenticate(request=request,username="",password="") - self.assertFalse(True) - except Exception as e: - self.assertEqual(str(e),"Can't find 'passkeys' key in request.POST, did you add the hidden input?") - - def test_username_password_failed_login(self): - self.client.post("/auth/login",{"username":"test","password":"test123",'passkeys':''}) - self.assertFalse(self.client.session.get('_auth_user_id',False)) - - def test_username_password_login(self): - self.client.post("/auth/login",{"username":"test","password":"test",'passkeys':''}) - self.assertTrue(self.client.session.get('_auth_user_id',False)) - self.assertFalse(self.client.session.get('passkey', {}).get('passkey', False)) - - def test_no_data(self): - self.client.post("/auth/login",{"username":"","password":"",'passkeys':''}) - self.assertFalse(self.client.session.get('_auth_user_id',False)) diff --git a/example/test_app/tests/test_views.py b/example/test_app/tests/test_views.py deleted file mode 100644 index be4fab8..0000000 --- a/example/test_app/tests/test_views.py +++ /dev/null @@ -1,46 +0,0 @@ -from django.test import TransactionTestCase, Client -from django.urls import reverse - -from passkeys.models import UserPasskey -from .test_fido import test_fido - -class test_views(TransactionTestCase): - - def setUp(self) -> None: - from django.contrib.auth import get_user_model - self.user_model = get_user_model() - #self.user = self.user_model.objects.create_user(username="test", password="test") - self.client = Client() - #self.client.post("/auth/login", {"username": "test", "password": "test", 'passkeys': ''}) - test = test_fido() - test.setUp() - self.authenticator = test.test_key_reg() - self.client.post("/auth/login", {"username": "test", "password": "test", 'passkeys': ''}) - self.user = self.user_model.objects.get(username="test") - - def test_disabling_key(self): - key = UserPasskey.objects.filter(user=self.user).latest('id') - self.client.post(reverse('passkeys:toggle'),{"id":key.id}) - self.assertFalse(UserPasskey.objects.get(id=key.id).enabled) - - self.client.post(reverse('passkeys:toggle'),{"id":key.id}) - self.assertTrue(UserPasskey.objects.get(id=key.id).enabled) - - def test_deleting_key(self): - key = UserPasskey.objects.filter(user=self.user).latest('id') - self.client.post(reverse('passkeys:delKey'),{"id":key.id}) - self.assertEqual(UserPasskey.objects.filter(id=key.id).count(), 0) - - def test_wrong_ownership(self): - test = test_fido() - test.setUp() - authenticator = test.test_key_reg() - key = UserPasskey.objects.filter(user=self.user).latest('id') - self.user = self.user_model.objects.create_user(username="test2", password="test2") - self.client.post("/auth/login", {"username": "test2", "password": "test2", 'passkeys': ''}) - r = self.client.post(reverse('passkeys:delKey'),{"id":key.id}) - self.assertEqual(r.status_code, 403) - self.assertEqual(r.content,b"Error: You own this token so you can't delete it") - r = self.client.post(reverse('passkeys:toggle'),{"id":key.id}) - self.assertEqual(r.status_code, 403) - self.assertEqual(r.content, b"Error: You own this token so you can't toggle it") diff --git a/passkeys/FIDO2.py b/passkeys/FIDO2.py index 345796f..2ba3ba3 100644 --- a/passkeys/FIDO2.py +++ b/passkeys/FIDO2.py @@ -27,7 +27,7 @@ def enable_json_mapping(): def getUserCredentials(user): User = get_user_model() username_field = User.USERNAME_FIELD - filter_args = {"user__"+username_field : user} + filter_args = {"user__" + username_field: user} return [AttestedCredentialData(websafe_decode(uk.token)) for uk in UserPasskey.objects.filter(**filter_args)] @@ -57,22 +57,26 @@ def get_current_platform(request): return "Google" elif "Windows" in ua.os.family: return "Microsoft" - else: return "Key" + else: + return "Key" + @login_required def reg_begin(request): """Starts registering a new FIDO Device, called from API""" enable_json_mapping() server = getServer(request) - auth_attachment = getattr(settings,'KEY_ATTACHMENT', None) + auth_attachment = getattr(settings, 'KEY_ATTACHMENT', None) registration_data, state = server.register_begin({ - u'id': urlsafe_b64encode(request.user.username.encode("utf8")), + u'id': urlsafe_b64encode(request.user.username.encode("utf8")), u'name': request.user.get_username(), u'displayName': request.user.get_full_name() - }, getUserCredentials(request.user), authenticator_attachment = auth_attachment, resident_key_requirement=fido2.webauthn.ResidentKeyRequirement.PREFERRED) + }, getUserCredentials(request.user), authenticator_attachment=auth_attachment, + resident_key_requirement=fido2.webauthn.ResidentKeyRequirement.PREFERRED) request.session['fido2_state'] = state return JsonResponse(dict(registration_data)) - #return HttpResponse(cbor.encode(registration_data), content_type = 'application/octet-stream') + # return HttpResponse(cbor.encode(registration_data), content_type = 'application/octet-stream') + @login_required @csrf_exempt @@ -80,31 +84,33 @@ def reg_complete(request): """Completes the registeration, called by API""" try: if not "fido2_state" in request.session: - return JsonResponse({'status': 'ERR', "message": "FIDO Status can't be found, please try again"}, status=401) + return JsonResponse({'status': 'ERR', "message": "FIDO Status can't be found, please try again"}, + status=401) enable_json_mapping() data = json.loads(request.body) - name = data.pop("key_name",'') + name = data.pop("key_name", '') server = getServer(request) - auth_data = server.register_complete(request.session.pop("fido2_state"), response = data) + auth_data = server.register_complete(request.session.pop("fido2_state"), response=data) encoded = websafe_encode(auth_data.credential_data) platform = get_current_platform(request) if name == "": name = platform - uk = UserPasskey(user=request.user, token=encoded, name = name,platform=platform) + uk = UserPasskey(user=request.user, token=encoded, name=name, platform=platform) if data.get("id"): uk.credential_id = data.get('id') uk.save() return JsonResponse({'status': 'OK'}) - except Exception as exp: # pragma: no cover - print(traceback.format_exc()) # pragma: no cover - return JsonResponse({'status': 'ERR', "message": "Error on server, please try again later"}, status=500 ) # pragma: no cover + except Exception as exp: # pragma: no cover + print(traceback.format_exc()) # pragma: no cover + return JsonResponse({'status': 'ERR', "message": "Error on server, please try again later"}, + status=500) # pragma: no cover def auth_begin(request): enable_json_mapping() server = getServer(request) - credentials=[] + credentials = [] username = None if "base_username" in request.session: username = request.session["base_username"] @@ -124,7 +130,7 @@ def auth_complete(request): server = getServer(request) data = json.loads(request.POST["passkeys"]) key = None - #userHandle = data.get("response",{}).get('userHandle') + # userHandle = data.get("response",{}).get('userHandle') credential_id = data['id'] # # if userHandle: @@ -136,24 +142,27 @@ def auth_complete(request): # if keys.exists(): # credentials = [AttestedCredentialData(websafe_decode(keys[0].properties["device"]))] - keys = UserPasskey.objects.filter(credential_id = credential_id,enabled=1) + keys = UserPasskey.objects.filter(credential_id=credential_id, enabled=1) if keys.exists(): - credentials=[AttestedCredentialData(websafe_decode(keys[0].token))] + credentials = [AttestedCredentialData(websafe_decode(keys[0].token))] key = keys[0] try: cred = server.authenticate_complete( - request.session.pop('fido2_state'), credentials = credentials, response = data + request.session.pop('fido2_state'), credentials=credentials, response=data ) - except ValueError: # pragma: no cover - return None # pragma: no cover - except Exception as excep: # pragma: no cover - raise Exception(excep) # pragma: no cover + except ValueError: # pragma: no cover + return None # pragma: no cover + except Exception as excep: # pragma: no cover + raise Exception(excep) # pragma: no cover + if key: key.last_used = timezone.now() - request.session["passkey"] = {'passkey': True, 'name': key.name, "id":key.id, "platform": key.platform, - 'cross_platform': get_current_platform(request) != key.platform} + request.session["passkey"] = {'passkey': True, 'name': key.name, "id": key.id, "platform": key.platform, + 'cross_platform': get_current_platform(request) != key.platform} + request.session.save() key.save() return key.user - return None # pragma: no cover + + return None # pragma: no cover diff --git a/passkeys/backend.py b/passkeys/backend.py index c1eb6af..e0c7b8b 100644 --- a/passkeys/backend.py +++ b/passkeys/backend.py @@ -1,19 +1,26 @@ from django.contrib.auth.backends import ModelBackend from .FIDO2 import auth_complete +import logging + +LOGGER = logging.getLogger(__name__) + + class PasskeyModelBackend(ModelBackend): - def authenticate(self, request, username='',password='', **kwargs): + def authenticate(self, request, username='', password='', **kwargs): + if username != '' and password != '': + if request is not None: + request.session["passkey"] = {'passkey': False} - if request is None: - raise Exception('request is required for passkeys.backend.PasskeyModelBackend') + return super().authenticate(request, username=username, password=password, **kwargs) - if username!='' and password != '': - request.session["passkey"]={'passkey':False} - return super().authenticate(request,username=username,password=password, **kwargs) + if request is None: + LOGGER.error( + "Please pass the request parameter to the authenticate method for this authentication backend to work.") + return None passkeys = request.POST.get('passkeys') - if passkeys is None: - raise Exception("Can't find 'passkeys' key in request.POST, did you add the hidden input?") - if passkeys != '': - return auth_complete(request) - return None + if passkeys in (None, ''): + return None + + return auth_complete(request) diff --git a/passkeys/models.py b/passkeys/models.py index 34721d0..88a4f72 100644 --- a/passkeys/models.py +++ b/passkeys/models.py @@ -4,11 +4,11 @@ class UserPasskey(models.Model): user_model = get_user_model() - user = models.ForeignKey(user_model,on_delete=models.CASCADE) + user = models.ForeignKey(user_model, on_delete=models.CASCADE) name = models.CharField(max_length=255) - enabled= models.BooleanField(default=True) - platform = models.CharField(max_length=255,default='') + enabled = models.BooleanField(default=True) + platform = models.CharField(max_length=255, default='') added_on = models.DateTimeField(auto_now_add=True) - last_used = models.DateTimeField(null=True,default=None) + last_used = models.DateTimeField(null=True, default=None) credential_id = models.CharField(max_length=255, unique=True) - token = models.CharField(max_length=255, null=False) \ No newline at end of file + token = models.CharField(max_length=255, null=False) diff --git a/passkeys/tests/__init__.py b/passkeys/tests/__init__.py new file mode 100644 index 0000000..7bc027f --- /dev/null +++ b/passkeys/tests/__init__.py @@ -0,0 +1,4 @@ +from .platform import CurrentPlatformTestCase +from .fido import FidoTestCase +from .backend import PasskeyModelBackendTestCase +from .views import ViewsTestCase diff --git a/passkeys/tests/backend.py b/passkeys/tests/backend.py new file mode 100644 index 0000000..6c5b1f2 --- /dev/null +++ b/passkeys/tests/backend.py @@ -0,0 +1,21 @@ +from django.contrib.auth import get_user_model +from django.test import TransactionTestCase + +from passkeys.backend import PasskeyModelBackend + + +class PasskeyModelBackendTestCase(TransactionTestCase): + + def setUp(self) -> None: + self.user_model = get_user_model() + self.user = self.user_model.objects.create_user(username="test", password="test") + + def test_username_password_failed(self): + backend = PasskeyModelBackend() + user = backend.authenticate(None, 'test', 'test123') + self.assertEqual(user, None) + + def test_username_password_success(self): + backend = PasskeyModelBackend() + user = backend.authenticate(None, 'test', 'test') + self.assertNotEqual(user, None) diff --git a/passkeys/tests/fido.py b/passkeys/tests/fido.py new file mode 100644 index 0000000..a8ca781 --- /dev/null +++ b/passkeys/tests/fido.py @@ -0,0 +1,163 @@ +import json +from base64 import urlsafe_b64encode + +from django.conf import settings +from django.contrib.auth import get_user_model +from django.test import RequestFactory, TransactionTestCase +from django.urls import reverse + +from passkeys.backend import PasskeyModelBackend +from passkeys.models import UserPasskey +from passkeys.tests.soft_webauthn import SoftWebauthnDevice + +USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15" + +PLATFORM = "Apple" + + +def get_server_id(request): + return "fido-server-id" + + +def get_server_name(request): + return "fido-server-name" + + +class FidoTestCase(TransactionTestCase): + def setUp(self) -> None: + settings.FIDO_SERVER_ID = get_server_id + settings.FIDO_SERVER_NAME = get_server_name + + self.user_model = get_user_model() + self.user_a = self.user_model.objects.create_user(username="a", password="a") + self.user_b = self.user_model.objects.create_user(username="b", password="b") + + def test_server_id_callable(self): + self.client.logout() + settings.FIDO_SERVER_ID = get_server_id + response = self.client.get(reverse('passkeys:auth_begin')) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()['publicKey']['rpId'], 'fido-server-id') + + def test_server_name_callable(self): + self.client.login(username="b", password="b") + response = self.client.get(reverse('passkeys:reg_begin')) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()['publicKey']['rp']["name"], 'fido-server-name') + self.client.logout() + + def __test_registration(self, key_name=None): + # make sure no keys exist + UserPasskey.objects.all().delete() + + # check anonymous access + response = self.client.get(reverse('passkeys:reg_begin')) + self.assertEqual(response.status_code, 302) + + response = self.client.get(reverse('passkeys:reg_complete')) + self.assertEqual(response.status_code, 302) + + # login + self.client.login(username="b", password="b") + + # start registration + response = self.client.get(reverse('passkeys:reg_begin')) + self.assertEqual(response.status_code, 200) + + data = json.loads(response.content) + data['publicKey']['challenge'] = data['publicKey']['challenge'].encode("ascii") + + # creqte authentication data + authenticator = SoftWebauthnDevice() + + request = authenticator.create(data, "https://" + data["publicKey"]["rp"]["id"]) + if key_name is not None: + request["key_name"] = key_name + + # complete registration + response = self.client.post( + reverse('passkeys:reg_complete'), + data=json.dumps(request), + headers={"USER_AGENT": USER_AGENT}, + HTTP_USER_AGENT=USER_AGENT, + content_type="application/json" + ) + + self.assertEqual(response.status_code, 200) + + key = UserPasskey.objects.latest('id') + self.assertNotEqual(key, None) + + if key_name is None: + self.assertEqual(key.name, key.platform) + else: + self.assertEqual(key.name, key_name) + + self.assertEqual(key.platform, PLATFORM) + self.assertEqual(key.user, self.user_b) + + return authenticator + + def test_registration(self): + self.__test_registration('test-key') + + def test_registration_auto_key_name(self): + self.__test_registration() + + def test_base_username(self): + authenticator = self.__test_registration() + + self.client.logout() + + session = self.client.session + session.update({"base_username": "b"}) + session.save() + + response = self.client.get(reverse('passkeys:auth_begin')) + self.assertEqual(response.status_code, 200) + + self.assertEqual(response.json()['publicKey']['allowCredentials'][0]['id'], + urlsafe_b64encode(authenticator.credential_id).decode("utf8").strip('=')) + + def test_error_when_no_session(self): + self.client.login(username='a', password='a') + res = {"key_name": "testKey"} + url = reverse('passkeys:reg_complete') + r = self.client.post(url, data=json.dumps(res), headers={"USER_AGENT": ""}, HTTP_USER_AGENT="", + content_type="application/json") + try: + j = json.loads(r.content) + except Exception: + raise AssertionError("Failed to get the required JSON after reg_completed") + + self.assertTrue("status" in j) + self.assertEqual(j["status"], "ERR") + self.assertEqual(j["message"], "FIDO Status can't be found, please try again") + + def test_passkey_login(self): + authenticator = self.__test_registration("TestKey") + self.client.logout() + + response = self.client.get(reverse('passkeys:auth_begin')) + self.assertEqual(response.status_code, 200) + result = response.json() + result['publicKey']['challenge'] = result['publicKey']['challenge'].encode("ascii") + + res = authenticator.get(result, "https://" + result["publicKey"]["rpId"]) + + factory = RequestFactory() + request = factory.post('/login/', + { + "username": "", + "password": "", + 'passkeys': json.dumps(res) + }, + headers={"USER_AGENT": USER_AGENT}) + request.session = self.client.session + + backend = PasskeyModelBackend() + user = backend.authenticate(request, "", "") + + self.assertEqual(user, self.user_b) + self.assertTrue(self.client.session.get("passkey", {}).get("passkey", True)) + self.assertEqual(self.client.session.get("passkey", {}).get("name"), "TestKey") diff --git a/passkeys/tests/platform.py b/passkeys/tests/platform.py new file mode 100644 index 0000000..5e02e26 --- /dev/null +++ b/passkeys/tests/platform.py @@ -0,0 +1,49 @@ +from django.test import TestCase, RequestFactory + +from passkeys.FIDO2 import get_current_platform + + +class CurrentPlatformTestCase(TestCase): + + def setUp(self) -> None: + self.request_factory = RequestFactory() + + def check_platform(self, user_agent, platform): + request = self.request_factory.get('/', HTTP_USER_AGENT=user_agent) + self.assertEqual(get_current_platform(request), platform) + + def test_mac(self): + self.check_platform( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15", + "Apple") + + def test_ios(self): + self.check_platform( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15", + "Apple") + + def test_ipad(self): + self.check_platform( + "Mozilla/5.0 (iPad; CPU OS 16_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Mobile/15E148 Safari/604.1", + "Apple") + + def test_chrome_mac(self): + self.check_platform( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", + "Chrome on Apple") + + def test_chrome_windows(self): + self.check_platform( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", + "Microsoft") + + def test_android(self): + self.check_platform( + "Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.130 Mobile Safari/537.36", + "Google") + self.check_platform( + "Mozilla/5.0 (Linux; Android 10; SM-A205U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.130 Mobile Safari/537.36", + "Google") + self.check_platform( + "Mozilla/5.0 (Linux; Android 10; LM-Q720) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.130 Mobile Safari/537.36", + "Google") diff --git a/example/test_app/soft_webauthn.py b/passkeys/tests/soft_webauthn.py similarity index 100% rename from example/test_app/soft_webauthn.py rename to passkeys/tests/soft_webauthn.py diff --git a/passkeys/tests/views.py b/passkeys/tests/views.py new file mode 100644 index 0000000..2e9649c --- /dev/null +++ b/passkeys/tests/views.py @@ -0,0 +1,95 @@ +from django.test import TransactionTestCase, Client +from django.urls import reverse +from django.contrib.auth import get_user_model + +from passkeys.models import UserPasskey + + +class ViewsTestCase(TransactionTestCase): + + def setUp(self) -> None: + self.user_model = get_user_model() + + self.user_a = self.user_model.objects.create_user(username="a", password="a") + self.user_b = self.user_model.objects.create_user(username="b", password="b") + + UserPasskey.objects.create(user=self.user_a, name='UserPasskey A', credential_id='userpasskey-a', enabled=True) + UserPasskey.objects.create(user=self.user_b, name='UserPasskey B', credential_id='userpasskey-b', enabled=True) + + def test_index(self): + self.client.logout() + + # anonymous access + response = self.client.post(reverse('passkeys:home')) + self.assertEqual(response.status_code, 302) + + # login + self.client.login(username="a", password="a") + + # retrieve key list, check if correct keys are listed + response = self.client.post(reverse('passkeys:home')) + self.assertEqual(response.status_code, 200) + + self.assertContains(response, 'UserPasskey A') + self.assertNotContains(response, 'UserPasskey B') + + def test_key_toggle(self): + self.client.logout() + + key = UserPasskey.objects.filter(user=self.user_a).latest('pk') + + # no anonymous access + response = self.client.post(reverse('passkeys:toggle'), {"id": key.id}) + self.assertEqual(response.status_code, 302) + + # login + self.client.login(username="a", password="a") + + # enable key + response = self.client.post(reverse('passkeys:toggle'), {"id": key.id}) + self.assertEqual(response.status_code, 200) + key.refresh_from_db() + self.assertFalse(key.enabled) + + # disable key + response = self.client.post(reverse('passkeys:toggle'), {"id": key.id}) + self.assertEqual(response.status_code, 200) + key.refresh_from_db() + self.assertTrue(UserPasskey.objects.get(id=key.id).enabled) + + # ownership error + key = UserPasskey.objects.filter(user=self.user_b).latest('pk') + response = self.client.post(reverse('passkeys:toggle'), {"id": key.id}) + self.assertEqual(response.status_code, 403) + key.refresh_from_db() + self.assertTrue(UserPasskey.objects.get(id=key.id).enabled) + + # invalid key id + response = self.client.post(reverse('passkeys:toggle'), {"id": 9999999}) + self.assertEqual(response.status_code, 403) + key.refresh_from_db() + self.assertTrue(UserPasskey.objects.get(id=key.id).enabled) + + def test_key_delete(self): + self.client.logout() + key = UserPasskey.objects.filter(user=self.user_a).latest('pk') + + # no anonymous access + response = self.client.post(reverse('passkeys:delKey'), {"id": key.id}) + self.assertEqual(response.status_code, 302) + + # login + self.client.login(username="a", password="a") + + # successful delete + self.client.post(reverse('passkeys:delKey'), {"id": key.id}) + self.assertEqual(UserPasskey.objects.filter(id=key.id).count(), 0) + + # ownership error + key = UserPasskey.objects.filter(user=self.user_b).latest('pk') + response = self.client.post(reverse('passkeys:delKey'), {"id": key.id}) + self.assertEqual(response.status_code, 403) + + # invalid key id + response = self.client.post(reverse('passkeys:delKey'), {"id": 9999999}) + self.assertEqual(response.status_code, 403) diff --git a/passkeys/views.py b/passkeys/views.py index fe63888..b0a229f 100644 --- a/passkeys/views.py +++ b/passkeys/views.py @@ -5,33 +5,41 @@ from .models import UserPasskey + @login_required -def index(request,enroll=False): # noqa - keys = UserPasskey.objects.filter(user=request.user) # pragma: no cover - return render(request,'passkeys/passkeys.html',{"keys":keys,"enroll":enroll}) # pragma: no cover +def index(request, enroll=False): # noqa + keys = UserPasskey.objects.filter(user=request.user) # pragma: no cover + return render(request, 'passkeys/passkeys.html', {"keys": keys, "enroll": enroll}) # pragma: no cover + @require_http_methods(["POST"]) @login_required def delKey(request): - id=request.POST.get("id") - if not id: + pk = request.POST.get("id") + if not pk: return HttpResponse("Error: You are missing a key", status=403) - key=UserPasskey.objects.get(id=id) - if key.user.pk == request.user.pk: - key.delete() - return HttpResponse("Deleted Successfully") - return HttpResponse("Error: You own this token so you can't delete it", status=403) + + key = UserPasskey.objects.filter(user=request.user, pk=pk).first() + + if key is None: + return HttpResponse("Error: You own this token so you can't delete it", status=403) + + key.delete() + return HttpResponse("Deleted Successfully") + @require_http_methods(["POST"]) @login_required def toggleKey(request): - id=request.POST.get("id") - if not id: + pk = request.POST.get("id") + if not pk: return HttpResponse("Error: You are missing a key", status=403) - q=UserPasskey.objects.filter(user=request.user, id=id) - if q.count()==1: - key=q[0] - key.enabled=not key.enabled - key.save() - return HttpResponse("OK") - return HttpResponse("Error: You own this token so you can't toggle it", status=403) + + key = UserPasskey.objects.filter(user=request.user, pk=pk).first() + + if key is None: + return HttpResponse("Error: You own this token so you can't toggle it", status=403) + + key.enabled = not key.enabled + key.save() + return HttpResponse("OK") diff --git a/tox.ini b/tox.ini index c9d2394..b12129d 100644 --- a/tox.ini +++ b/tox.ini @@ -26,9 +26,9 @@ deps = -rrequirements_test.txt setenv = - DJANGO_SETTINGS_MODULE = test_app.test_settings + DJANGO_SETTINGS_MODULE = test_app.settings allowlist_externals = coverage commands = - coverage run manage.py test + coverage run manage.py test passkeys From ac489f6fd08321905e72ee2c2a9aac808ceee4a8 Mon Sep 17 00:00:00 2001 From: Christian Thieme Date: Tue, 5 Nov 2024 23:47:55 +0100 Subject: [PATCH 2/9] Created runtests.py to run tests without example project --- runtests.py | 15 ++++++++ tests/__init__.py | 0 tests/settings.py | 78 +++++++++++++++++++++++++++++++++++++++ tests/templates/base.html | 3 ++ tests/urls.py | 5 +++ tox.ini | 2 +- 6 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 runtests.py create mode 100644 tests/__init__.py create mode 100644 tests/settings.py create mode 100644 tests/templates/base.html create mode 100644 tests/urls.py diff --git a/runtests.py b/runtests.py new file mode 100644 index 0000000..1f2b81a --- /dev/null +++ b/runtests.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +import os +import sys + +import django +from django.conf import settings +from django.test.utils import get_runner + +if __name__ == "__main__": + os.environ["DJANGO_SETTINGS_MODULE"] = "tests.settings" + django.setup() + TestRunner = get_runner(settings) + test_runner = TestRunner() + failures = test_runner.run_tests(["passkeys"]) + sys.exit(bool(failures)) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/settings.py b/tests/settings.py new file mode 100644 index 0000000..c6d4624 --- /dev/null +++ b/tests/settings.py @@ -0,0 +1,78 @@ +from pathlib import Path + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = Path(__file__).resolve().parent + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'secret' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'passkeys', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'tests.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + } +} + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + +STATIC_URL = '/static/' + +AUTHENTICATION_BACKENDS = ['passkeys.backend.PasskeyModelBackend'] + +FIDO_SERVER_ID = "localhost" # Server rp id for FIDO2, it the full domain of your project + +FIDO_SERVER_NAME = "TestApp" + +KEY_ATTACHMENT = None # Set None to allow all authenticator attachment diff --git a/tests/templates/base.html b/tests/templates/base.html new file mode 100644 index 0000000..b8e230b --- /dev/null +++ b/tests/templates/base.html @@ -0,0 +1,3 @@ +{% block head %}{% endblock %} + +{% block content %}{% endblock %} \ No newline at end of file diff --git a/tests/urls.py b/tests/urls.py new file mode 100644 index 0000000..d7a6f8f --- /dev/null +++ b/tests/urls.py @@ -0,0 +1,5 @@ +from django.urls import path, include + +urlpatterns = [ + path("passkeys/", include("passkeys.urls")), +] diff --git a/tox.ini b/tox.ini index b12129d..39dfec8 100644 --- a/tox.ini +++ b/tox.ini @@ -30,5 +30,5 @@ setenv = allowlist_externals = coverage commands = - coverage run manage.py test passkeys + coverage run runtests.py From 928b738bd06b247ff80d5f5c74909d639f70a89a Mon Sep 17 00:00:00 2001 From: Christian Thieme Date: Wed, 6 Nov 2024 00:15:08 +0100 Subject: [PATCH 3/9] tox.ini adapted to runtests.py --- tox.ini | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tox.ini b/tox.ini index 39dfec8..dce2e0d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,5 @@ [tox] envlist= - #docs, py37-django{20,21,22,32}, py38-django{22,32,40,41,42}, py39-django{22,32,40,41,42}, @@ -10,8 +9,6 @@ envlist= [testenv] -changedir= - example/ deps = django22: django>=2.2,<2.3 django32: django>=3.2,<3.3 @@ -22,13 +19,11 @@ deps = django51: django>=5.1,<5.2 ua-parser user-agents - django-sslserver -rrequirements_test.txt -setenv = - DJANGO_SETTINGS_MODULE = test_app.settings allowlist_externals = coverage + commands = coverage run runtests.py From 5447fb6ca9bbf0ad8c54c44f458ed0832af8dea0 Mon Sep 17 00:00:00 2001 From: Christian Thieme Date: Wed, 6 Nov 2024 00:20:36 +0100 Subject: [PATCH 4/9] fido test fixed which failed only when using tox --- passkeys/tests/fido.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/passkeys/tests/fido.py b/passkeys/tests/fido.py index a8ca781..db7371b 100644 --- a/passkeys/tests/fido.py +++ b/passkeys/tests/fido.py @@ -145,6 +145,7 @@ def test_passkey_login(self): res = authenticator.get(result, "https://" + result["publicKey"]["rpId"]) + # build a fake request for the authentication backend factory = RequestFactory() request = factory.post('/login/', { @@ -153,6 +154,11 @@ def test_passkey_login(self): 'passkeys': json.dumps(res) }, headers={"USER_AGENT": USER_AGENT}) + + # set user agent for running tests in tox + request.META['HTTP_USER_AGENT'] = USER_AGENT + + # keeping client session is required request.session = self.client.session backend = PasskeyModelBackend() From 7f0406b72db7a518d4af6a966198152045ca1381 Mon Sep 17 00:00:00 2001 From: Mohamed ElKalioby Date: Wed, 13 Nov 2024 20:37:24 +0300 Subject: [PATCH 5/9] Update basic checks --- .github/workflows/basic_checks.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/basic_checks.yml b/.github/workflows/basic_checks.yml index 59d62eb..18c91f9 100644 --- a/.github/workflows/basic_checks.yml +++ b/.github/workflows/basic_checks.yml @@ -22,14 +22,9 @@ jobs: python setup.py install pip install -r requirements.txt pip install -r requirements_test.txt - - name: Run Migrations - run: | - cd example - python manage.py migrate - name: Run Tests run: | - cd example - coverage run manage.py test + coverage run runtests.py coverage report - name: Coverage Badge uses: tj-actions/coverage-badge-py@v2 From ee52c920a6d99089869e048edb9e02c795905e98 Mon Sep 17 00:00:00 2001 From: Mohamed ElKalioby Date: Wed, 13 Nov 2024 20:41:14 +0300 Subject: [PATCH 6/9] Update basic checks --- .github/workflows/basic_checks.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/basic_checks.yml b/.github/workflows/basic_checks.yml index 18c91f9..5be491a 100644 --- a/.github/workflows/basic_checks.yml +++ b/.github/workflows/basic_checks.yml @@ -29,8 +29,7 @@ jobs: - name: Coverage Badge uses: tj-actions/coverage-badge-py@v2 with: - working-directory: example - output: ../coverage.svg + output: coverage.svg - name: Verify Changed files uses: tj-actions/verify-changed-files@v14 @@ -41,7 +40,6 @@ jobs: - name: Commit files if: steps.verify-changed-files.outputs.files_changed == 'true' run: | - #mv example/coverage.svg coverage.svg git config --local user.email "github-actions[bot]@users.noreply.github.com" git config --local user.name "github-actions[bot]" git add coverage.svg From 63d653ed6f2579e7f04cc066a285442f74909299 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 13 Nov 2024 17:41:56 +0000 Subject: [PATCH 7/9] Updated coverage.svg --- coverage.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coverage.svg b/coverage.svg index 6bfc8fa..ee07d4c 100644 --- a/coverage.svg +++ b/coverage.svg @@ -15,7 +15,7 @@ coverage coverage - 99% - 99% + 96% + 96% From 70c5edc5ff8b9f4c3ec02b1c5db5d71fcf204c48 Mon Sep 17 00:00:00 2001 From: Mohamed ElKalioby Date: Wed, 13 Nov 2024 21:12:29 +0300 Subject: [PATCH 8/9] More fixes --- README.md | 2 +- passkeys/FIDO2.py | 8 ++++---- passkeys/tests/backend.py | 5 +++++ passkeys/tests/soft_webauthn.py | 2 +- passkeys/tests/views.py | 6 ++++++ runtests.py | 2 +- 6 files changed, 18 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e841201..7abbed2 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ Tidelift will coordinate the fix and disclosure. * [pulse-mind](https://github.com/pulse-mind) * [smark-1](https://github.com/smark-1) * [rafaelurben](https://github.com/rafaelurben) - +* [christian-thieme](https://github.com/christian-thieme) diff --git a/passkeys/FIDO2.py b/passkeys/FIDO2.py index 2ba3ba3..187abd2 100644 --- a/passkeys/FIDO2.py +++ b/passkeys/FIDO2.py @@ -36,12 +36,12 @@ def getServer(request=None): if callable(settings.FIDO_SERVER_ID): fido_server_id = settings.FIDO_SERVER_ID(request) else: - fido_server_id = settings.FIDO_SERVER_ID + fido_server_id = settings.FIDO_SERVER_ID # pragma: no cover if callable(settings.FIDO_SERVER_NAME): fido_server_name = settings.FIDO_SERVER_NAME(request) else: - fido_server_name = settings.FIDO_SERVER_NAME + fido_server_name = settings.FIDO_SERVER_NAME # pragma: no cover rp = PublicKeyCredentialRpEntity(id=fido_server_id, name=fido_server_name) return Fido2Server(rp) @@ -58,7 +58,7 @@ def get_current_platform(request): elif "Windows" in ua.os.family: return "Microsoft" else: - return "Key" + return "Key" # pragma: no cover @login_required @@ -115,7 +115,7 @@ def auth_begin(request): if "base_username" in request.session: username = request.session["base_username"] if request.user.is_authenticated: - username = request.user.username + username = request.user.username # pragma: no cover if username: credentials = getUserCredentials(username) auth_data, state = server.authenticate_begin(credentials) diff --git a/passkeys/tests/backend.py b/passkeys/tests/backend.py index 6c5b1f2..5baa2d8 100644 --- a/passkeys/tests/backend.py +++ b/passkeys/tests/backend.py @@ -19,3 +19,8 @@ def test_username_password_success(self): backend = PasskeyModelBackend() user = backend.authenticate(None, 'test', 'test') self.assertNotEqual(user, None) + + def test_username_password_success(self): + backend = PasskeyModelBackend() + user = backend.authenticate(None, 'test', 'test') + self.assertNotEqual(user, None) diff --git a/passkeys/tests/soft_webauthn.py b/passkeys/tests/soft_webauthn.py index 3e36738..40cfc5b 100644 --- a/passkeys/tests/soft_webauthn.py +++ b/passkeys/tests/soft_webauthn.py @@ -43,7 +43,7 @@ def cred_init(self, rp_id, user_handle): def cred_as_attested(self): """return current credential as AttestedCredentialData""" - return AttestedCredentialData.create( + return AttestedCredentialData.create( #noqa self.aaguid, self.credential_id, ES256.from_cryptography_key(self.private_key.public_key())) diff --git a/passkeys/tests/views.py b/passkeys/tests/views.py index 2e9649c..4f8c633 100644 --- a/passkeys/tests/views.py +++ b/passkeys/tests/views.py @@ -70,6 +70,8 @@ def test_key_toggle(self): key.refresh_from_db() self.assertTrue(UserPasskey.objects.get(id=key.id).enabled) + response = self.client.post(reverse('passkeys:toggle'), {}) + self.assertEqual(response.status_code, 403) def test_key_delete(self): self.client.logout() key = UserPasskey.objects.filter(user=self.user_a).latest('pk') @@ -93,3 +95,7 @@ def test_key_delete(self): # invalid key id response = self.client.post(reverse('passkeys:delKey'), {"id": 9999999}) self.assertEqual(response.status_code, 403) + + #Missing Parameter + response = self.client.post(reverse('passkeys:delKey'), {}) + self.assertEqual(response.status_code, 403) diff --git a/runtests.py b/runtests.py index 1f2b81a..965e347 100644 --- a/runtests.py +++ b/runtests.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import os import sys From 04057a2ade0dcf52a22eb62bc2940e732e4b4138 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 13 Nov 2024 18:13:25 +0000 Subject: [PATCH 9/9] Updated coverage.svg --- coverage.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coverage.svg b/coverage.svg index ee07d4c..3438732 100644 --- a/coverage.svg +++ b/coverage.svg @@ -15,7 +15,7 @@ coverage coverage - 96% - 96% + 97% + 97%