Skip to content

Commit

Permalink
Merge pull request #101 from SmoFlaDru/dev-benno
Browse files Browse the repository at this point in the history
Dev benno - passkey deletion, improved profile page, debug logging
  • Loading branch information
Bensge authored Jul 5, 2024
2 parents 0446594 + 04c0a50 commit c9fe6b3
Show file tree
Hide file tree
Showing 9 changed files with 80 additions and 66 deletions.
37 changes: 16 additions & 21 deletions spybot/auth/passkeys.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import logging
import traceback
from base64 import urlsafe_b64encode

Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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()
),
Expand All @@ -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"})
Expand Down Expand Up @@ -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
5 changes: 5 additions & 0 deletions spybot/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
14 changes: 0 additions & 14 deletions spybot/templates/base.html

This file was deleted.

2 changes: 1 addition & 1 deletion spybot/templates/spybot/base/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
{% block header %}
{% endblock %}
</head>
<body>
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
<script src="{% static 'theme.js' %}"></script>
<div class="page">
{% include 'spybot/base/navbar.html' %}
Expand Down
2 changes: 1 addition & 1 deletion spybot/templates/spybot/home/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<div class="col-md-6 height-xs-150vw">
<div class="row row-cards row-height-100">
<div class="col-md-12 col-height-50">
{% include 'spybot/home/activity_chart.html'%}
{% include 'spybot/home/activity_fragment.html'%}
</div>
<div class="col-md-12 col-height-50">
{% include 'spybot/home/tod_histogram.html' %}
Expand Down
67 changes: 40 additions & 27 deletions spybot/templates/spybot/profile.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,56 @@

{% block content %}
<div class="row row-cards justify-content-md-center">
<div class="col-md-8">
<div class="card">
<div class="card-header row g-0">
<h3 class="card-title">Logged in as {{logged_in_user.name}}</h3>
</div>
<div class="col-md-8 card">
<div class="card-header row g-0">
<h3 class="card-title">Logged in as {{ logged_in_user.name }}</h3>
</div>
<div class="card">
<div class="card-header">Passkeys</div>
<div class="card-body">
<button id="addPasskeyButton">Add a new passkey</button>
</div>
<ul>
</div>
<div class="col-md-8 card">
<div class="card-header">Passkeys</div>
<div class="table-responsive">
<table class="table table-vcenter card-table">
<thead>
<tr>
<th>Name</th>
<th>Platform</th>
<th>Last used</th>
<th>Created</th>
<th class="w-1"></th>
</tr>
</thead>
<tbody hx-confirm="Are you sure you want to delete the passkey?" hx-target="closest tr" hx-swap="outerHTML swap:1s">
{% for key in passkeys %}
<li>
Name: {{ key.name }}<br>
Platform: {{ key.platform }}<br>
Last used: {{ key.last_used }}<br>
Added on: {{ key.added_on }}<br>
</li>
<tr>
<td>{{ key.name }}</td>
<td class="text-secondary">{{ key.platform }}</td>
<td class="text-secondary">{{ key.last_used }}</td>
<td class="text-secondary">{{ key.added_on }}</td>
<td><a href="#" hx-delete="/profile/passkey/{{ key.id }}">Delete</a></td>
</tr>
</tbody>
{% endfor %}

</ul>
</table>
</div>
<div class="card-body">
<button id="addPasskeyButton" class="btn btn-primary">Add a new passkey</button>
</div>
</ul>
</div>
</div>
</div>

<style>
</style>
{% endblock content %}

{% block header %}
<script>
window.addEventListener("load", () => {
document.querySelector('#addPasskeyButton').addEventListener("click", () => {
console.log("creating...");
passkeys.create();
});
});
</script>
<script>
window.addEventListener("load", () => {
document.querySelector('#addPasskeyButton').addEventListener("click", () => {
console.log("creating...");
passkeys.create();
});
});
</script>
{% endblock header %}
1 change: 1 addition & 0 deletions spybot/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/<str:id>', 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'),
Expand Down
18 changes: 16 additions & 2 deletions spybot/views/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

0 comments on commit c9fe6b3

Please sign in to comment.