diff --git a/spybot/auth/passkeys.py b/spybot/auth/passkeys.py index 5eda635..0864daa 100644 --- a/spybot/auth/passkeys.py +++ b/spybot/auth/passkeys.py @@ -1,4 +1,5 @@ import json +import logging import traceback from base64 import urlsafe_b64encode @@ -15,12 +16,14 @@ from Spybot2 import settings from spybot.models import UserPasskey, MergedUser +log = logging.getLogger(__name__) + fido2.features.webauthn_json_mapping.enabled = True def get_server(request=None): """Get Server Info from settings and returns a Fido2Server""" - fido_server_id = settings.SERVER_IP + fido_server_id = settings.SERVER_IP.split(':')[0] fido_server_name = settings.FIDO_SERVER_NAME rp = PublicKeyCredentialRpEntity(id=fido_server_id, name=fido_server_name) @@ -34,19 +37,9 @@ def get_user_credentials(user: MergedUser): return [AttestedCredentialData(websafe_decode(uk.token)) for uk in UserPasskey.objects.filter(**filter_args)] -def get_current_platform(request): +def user_agent_info(request): ua = user_agent_parse(request.META["HTTP_USER_AGENT"]) - if 'Safari' in ua.browser.family: - return "Apple" - elif 'Chrome' in ua.browser.family and ua.os.family == "Mac OS X": - return "Chrome on Apple" - elif 'Android' in ua.os.family: - return "Google" - elif "Windows" in ua.os.family: - return "Microsoft" - else: - return "Key" - + return ua.get_device(), ua.get_os(), ua.get_browser() def generate_authentication_options(request): server = get_server(request) @@ -69,7 +62,7 @@ def generate_registration_options(request): auth_attachment = getattr(settings, 'KEY_ATTACHMENT', None) registration_data, state = server.register_begin( PublicKeyCredentialUserEntity( - name=request.user.get_username(), + name=request.user.get_full_name(), id=urlsafe_b64encode(request.user.username.encode("utf8")), display_name=request.user.get_full_name() ), @@ -88,19 +81,18 @@ def verify_registration(request): if not "fido2_state" in request.session: return JsonResponse({'status': 'ERR', "message": "FIDO Status can't be found, please try again"}) data = json.loads(request.body) - name = data.pop("key_name", '') + user_agent_fields = user_agent_info(request) + name = user_agent_fields[0] server = get_server(request) 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 + platform = f"{user_agent_fields[2]} on {user_agent_fields[1]}" 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'}) + return JsonResponse({'status': 'OK', 'verified': True}) except Exception as exp: print(traceback.format_exc()) return JsonResponse({'status': 'ERR', "message": "Error on server, please try again later"}) @@ -133,17 +125,20 @@ def verify_authentication(request): cred = server.authenticate_complete( request.session.pop('fido2_state'), credentials=credentials, response=data ) - except ValueError: # pragma: no cover + except ValueError as exception: # pragma: no cover + log.error("valueerror in verify_authentication: %s", exception) return None # pragma: no cover except Exception as exception: # pragma: no cover + log.error("exception in verify_authentication: %s", exception) raise Exception(exception) # 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} + 'cross_platform': user_agent_info(request)[1] != key.platform} key.save() login(request, key.user, 'django.contrib.auth.backends.ModelBackend') return JsonResponse({'verified': True, 'user': key.user.id}) + log.error("no credentials found") return None # pragma: no cover diff --git a/spybot/static/styles.css b/spybot/static/styles.css index b45453e..bf19073 100644 --- a/spybot/static/styles.css +++ b/spybot/static/styles.css @@ -30,3 +30,8 @@ body[data-spybot-theme]:not([data-spybot-theme="dark"]) .spybot-dark-theme-only body[data-spybot-theme]:not([data-spybot-theme="auto"]) .spybot-auto-theme-only { display: none; } + +tr.htmx-swapping td { + opacity: 0; + transition: opacity 1s ease-out; +} \ No newline at end of file diff --git a/spybot/templates/base.html b/spybot/templates/base.html deleted file mode 100644 index 4889052..0000000 --- a/spybot/templates/base.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - {% block head %} - {% endblock %} - - - {% block content %} - {% endblock %} - - \ No newline at end of file diff --git a/spybot/templates/spybot/base/base.html b/spybot/templates/spybot/base/base.html index 82f077b..082026a 100644 --- a/spybot/templates/spybot/base/base.html +++ b/spybot/templates/spybot/base/base.html @@ -16,7 +16,7 @@ {% block header %} {% endblock %} - +
{% include 'spybot/base/navbar.html' %} diff --git a/spybot/templates/spybot/home/activity_chart.html b/spybot/templates/spybot/home/activity_fragment.html similarity index 100% rename from spybot/templates/spybot/home/activity_chart.html rename to spybot/templates/spybot/home/activity_fragment.html diff --git a/spybot/templates/spybot/home/home.html b/spybot/templates/spybot/home/home.html index 8be1c5b..b1d6aa5 100644 --- a/spybot/templates/spybot/home/home.html +++ b/spybot/templates/spybot/home/home.html @@ -7,7 +7,7 @@
- {% include 'spybot/home/activity_chart.html'%} + {% include 'spybot/home/activity_fragment.html'%}
{% include 'spybot/home/tod_histogram.html' %} diff --git a/spybot/templates/spybot/profile.html b/spybot/templates/spybot/profile.html index 7049978..3913db3 100644 --- a/spybot/templates/spybot/profile.html +++ b/spybot/templates/spybot/profile.html @@ -5,43 +5,56 @@ {% block content %}
-
-
-
-

Logged in as {{logged_in_user.name}}

-
+
+
+

Logged in as {{ logged_in_user.name }}

-
-
Passkeys
-
- -
-
    +
+
+
Passkeys
+
+ + + + + + + + + + + {% for key in passkeys %} -
  • - Name: {{ key.name }}
    - Platform: {{ key.platform }}
    - Last used: {{ key.last_used }}
    - Added on: {{ key.added_on }}
    -
  • + + + + + + + + {% endfor %} - - +
    NamePlatformLast usedCreated
    {{ key.name }}{{ key.platform }}{{ key.last_used }}{{ key.added_on }}Delete
    +
    + +
    +
    +
    {% endblock content %} {% block header %} - + {% endblock header %} \ No newline at end of file diff --git a/spybot/urls.py b/spybot/urls.py index b26eebd..9737d58 100644 --- a/spybot/urls.py +++ b/spybot/urls.py @@ -16,6 +16,7 @@ path('activity_fragment', activity_chart.fragment, name='activity_fragment'), path('recent_events_fragment', views.recent_events_fragment, name='recent_events_fragment'), path('profile', views.profile, name='profile'), + path('profile/passkey/', views.profile_passkey, name='profile_passkey'), path('login', views.login, name='login'), path('login_teamspeak', views.login_teamspeak, name='login_teamspeak'), path('link_auth', auth.link_login, name='link_login'), diff --git a/spybot/views/views.py b/spybot/views/views.py index 1d1ab36..cf626d9 100644 --- a/spybot/views/views.py +++ b/spybot/views/views.py @@ -3,9 +3,10 @@ from datetime import timedelta from typing import List +from django.contrib.auth.decorators import login_required from django.db.models import Q -from django.http import JsonResponse -from django.shortcuts import render +from django.http import JsonResponse, HttpResponse, Http404, HttpResponseForbidden +from django.shortcuts import render, get_object_or_404 from django.utils import timezone from spybot import visualization @@ -305,3 +306,16 @@ def login(request): def login_teamspeak(request): user = get_user(request) return render(request, 'spybot/login_teamspeak.html', {**get_context(request), 'user': user}) + + +@login_required +def profile_passkey(request, id: str): + if request.method == "DELETE": + print(f"trying to delete passkey with id {id}") + passkey = get_object_or_404(UserPasskey, id=id) + if passkey.user != request.user: + return HttpResponseForbidden() + + passkey.delete() + return HttpResponse('') + return None