Skip to content

Commit ac13b19

Browse files
authored
Merge pull request #157 from duo-labs/fix/156-profile-page
fix/156-profile-page
2 parents 657c2bc + 30d6cfc commit ac13b19

File tree

11 files changed

+175
-89
lines changed

11 files changed

+175
-89
lines changed

_app/homepage/helpers.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from typing import List
22

3+
from webauthn.helpers.structs import AuthenticatorTransport
34

4-
def transports_to_ui_string(transports: List[str]) -> str:
5+
6+
def transports_to_ui_string(transports: List[AuthenticatorTransport]) -> str:
57
"""
68
Generate a human-readable string of transports
79

_app/homepage/services/__init__.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1-
from .redis import RedisService # noqa: F401
2-
from .registration import RegistrationService # noqa: F401
3-
from .credential import CredentialService # noqa: F401
4-
from .authentication import AuthenticationService # noqa: F401
5-
from .session import SessionService # noqa: F401
6-
from .metadata import MetadataService # noqa: F401
1+
from .redis import RedisService
2+
from .registration import RegistrationService
3+
from .credential import CredentialService
4+
from .authentication import AuthenticationService
5+
from .session import SessionService
6+
from .metadata import MetadataService
7+
8+
__all__ = [
9+
"RedisService",
10+
"RegistrationService",
11+
"CredentialService",
12+
"AuthenticationService",
13+
"SessionService",
14+
"MetadataService",
15+
]

_app/homepage/services/credential.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def store_credential(
5555

5656
self._temporarily_store_in_redis(new_credential)
5757

58-
transports_str = transports_to_ui_string(transports or [])
58+
transports_str = transports_to_ui_string(mapped_transports or [])
5959
cred_type = "discoverable credential" if is_discoverable_credential else "credential"
6060

6161
logger.info(f'User "{username}" registered a {cred_type} with transports {transports_str}')

_app/homepage/services/registration.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from typing import Union, List, Optional
2-
import secrets
32

43
from django.conf import settings
54
from webauthn import (
@@ -96,8 +95,7 @@ def generate_registration_options(
9695
rp_id=settings.RP_ID,
9796
rp_name=settings.RP_NAME,
9897
user_name=username,
99-
# TODO: Remove when https://github.com/MasterKale/SimpleWebAuthn/issues/530 gets fixed
100-
user_id=secrets.token_bytes(32),
98+
user_id=f"webauthnio-{username}".encode(),
10199
attestation=_attestation,
102100
authenticator_selection=authenticator_selection,
103101
supported_pub_key_algs=supported_pub_key_algs,

_app/homepage/templates/homepage/profile.html

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,40 @@ <h4>{{ cred.id }}</h4>
8585
<strong>AAGUID:</strong> {{ cred.aaguid }}
8686
</li>
8787
<li class="credential-delete">
88-
<form action="{% url 'credential-delete' cred.raw_id %}" method="post">
89-
{% csrf_token %}
90-
<button type="submit" class="btn btn-outline-danger">Delete</button>
91-
</form>
88+
<button type="button" class="btn btn-outline-danger" onclick="deleteCredential('{{cred.raw_id}}')">Delete</button>
9289
</li>
9390
</ul>
9491
</div>
9592
{% endfor %}
93+
{% csrf_token %}
94+
<script>
95+
/**
96+
* Delete the credential then reload the page so we don't pollute the browser history stack
97+
*
98+
* @param {string} id
99+
*/
100+
async function deleteCredential(id) {
101+
// Normalize the ID a bit
102+
const _id = id.trim();
103+
104+
// Get CSRF token
105+
const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
106+
107+
try {
108+
await fetch(
109+
`credential/${_id}/delete`,
110+
{
111+
method: 'post',
112+
headers: {'X-CSRFToken': csrftoken},
113+
mode: 'same-origin',
114+
},
115+
);
116+
window.location.reload();
117+
} catch (err) {
118+
console.error('Error deleting credential', err);
119+
}
120+
}
121+
</script>
96122
</div>
97123
</div>
98124
</section>

_app/homepage/templates/homepage/sections/webauthn_form.html

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,8 @@ <h2>WebAuthn isn't supported. Please consider switching to a modern browser.</h2
551551
},
552552
// Internal Methods
553553
async _startRegistration() {
554+
this._syncUsernameInputState();
555+
554556
// Submit options
555557
const {
556558
regUserVerification,
@@ -631,6 +633,8 @@ <h2>WebAuthn isn't supported. Please consider switching to a modern browser.</h2
631633
}
632634
},
633635
async _startAuthentication(startConditionalUI = false) {
636+
this._syncUsernameInputState();
637+
634638
const {
635639
authUserVerification,
636640
} = this.options;
@@ -682,7 +686,7 @@ <h2>WebAuthn isn't supported. Please consider switching to a modern browser.</h2
682686

683687
if (verificationJSON.verified === true) {
684688
// Reload page to display profile
685-
window.location.href = '{% url "index" %}';
689+
window.location.href = '{% url "profile" %}';
686690
} else {
687691
this.showErrorAlert(`Authentication failed: ${verificationJSON.error}`);
688692
}
@@ -732,7 +736,17 @@ <h2>WebAuthn isn't supported. Please consider switching to a modern browser.</h2
732736
formattedHints = formattedHints.replace(/,/g, ", ");
733737

734738
this._regHints.formatted = formattedHints;
735-
}
739+
},
740+
/**
741+
* If the user hits the browser back button from the profile page then the input can still
742+
* be populated, but Alpine won't know this. This helps sync form state a bit.
743+
*/
744+
_syncUsernameInputState() {
745+
const elemUsername = document.getElementById('input-email');
746+
if (elemUsername.value && !this.username) {
747+
this.username = elemUsername.value;
748+
}
749+
},
736750
}));
737751
});
738752
</script>

_app/homepage/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
urlpatterns = [
66
path("", views.index, name="index"),
7+
path("profile", views.profile, name="profile"),
78
path("logout", views.logout, name="logout"),
89
path("registration/options", views.registration_options, name="registration-options"),
910
path(

_app/homepage/views/__init__.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,21 @@
1-
from .index import index # noqa: F401
2-
from .registration_options import registration_options # noqa: F401
3-
from .registration_verification import registration_verification # noqa: F401
4-
from .authentication_options import authentication_options # noqa: F401
5-
from .authentication_verification import authentication_verification # noqa: F401
6-
from .logout import logout # noqa: F401
7-
from .credential_delete import credential_delete # noqa: F401
8-
from .well_known import apple_app_site_association # noqa: F401
1+
from .index import index
2+
from .registration_options import registration_options
3+
from .registration_verification import registration_verification
4+
from .authentication_options import authentication_options
5+
from .authentication_verification import authentication_verification
6+
from .logout import logout
7+
from .credential_delete import credential_delete
8+
from .well_known import apple_app_site_association
9+
from .profile import profile
10+
11+
__all__ = [
12+
"index",
13+
"registration_options",
14+
"registration_verification",
15+
"authentication_options",
16+
"authentication_verification",
17+
"logout",
18+
"credential_delete",
19+
"apple_app_site_association",
20+
"profile",
21+
]
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from django.shortcuts import redirect
1+
from django.http import JsonResponse
22
from django.views.decorators.http import require_http_methods
33

44
from homepage.services import CredentialService
@@ -8,4 +8,4 @@
88
def credential_delete(request, credential_id):
99
credential_service = CredentialService()
1010
credential_service.delete_credential_by_id(credential_id=credential_id)
11-
return redirect("index")
11+
return JsonResponse({"deleted": True})

_app/homepage/views/index.py

Lines changed: 9 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,24 @@
1+
from django.http import HttpRequest
12
from django.shortcuts import render
2-
from webauthn.helpers.structs import CredentialDeviceType
3+
from django.views.decorators.cache import never_cache
34

45
from homepage.const import libraries, demos
5-
from homepage.services import SessionService, CredentialService, MetadataService
6-
from homepage.helpers import (
7-
transports_to_ui_string,
8-
truncate_credential_id_to_ui_string,
9-
)
6+
from homepage.services import SessionService
107

118

12-
def index(request):
9+
@never_cache
10+
def index(request: HttpRequest):
1311
"""
1412
Render the homepage
1513
"""
16-
context = {
17-
"libraries": libraries,
18-
"demos": demos,
19-
}
2014

2115
session_service = SessionService()
2216
session_service.start_session(request=request)
2317

2418
template = "homepage/index.html"
25-
if session_service.user_is_logged_in(request=request):
26-
template = "homepage/profile.html"
27-
28-
username = request.session["username"]
29-
credential_service = CredentialService()
30-
metadata_service = MetadataService()
31-
32-
user_credentials = credential_service.retrieve_credentials_by_username(username=username)
33-
34-
parsed_credentials = []
35-
36-
for cred in user_credentials:
37-
description = ""
38-
39-
if cred.device_type == CredentialDeviceType.SINGLE_DEVICE:
40-
description += "device-bound "
41-
else:
42-
description += "synced "
43-
44-
if cred.is_discoverable_credential is None:
45-
# We can't really describe it if we didn't get a signal back
46-
description += "credential of unknown discoverability"
47-
elif cred.is_discoverable_credential:
48-
description += "passkey"
49-
else:
50-
description += "non-discoverable credential"
51-
52-
aaguid = str(cred.aaguid)
53-
provider_name = metadata_service.get_provider_name(
54-
aaguid=aaguid,
55-
device_type=cred.device_type,
56-
)
57-
58-
if not provider_name:
59-
provider_name = "(Unavailable)"
60-
61-
if not aaguid:
62-
aaguid = "(Unavailable)"
63-
64-
parsed_credentials.append(
65-
{
66-
"id": truncate_credential_id_to_ui_string(cred.id),
67-
"raw_id": cred.id,
68-
"transports": transports_to_ui_string(cred.transports or []),
69-
"description": description,
70-
"provider_name": provider_name,
71-
"aaguid": aaguid,
72-
}
73-
)
74-
75-
context["credentials"] = parsed_credentials
19+
context = {
20+
"libraries": libraries,
21+
"demos": demos,
22+
}
7623

7724
return render(request, template, context)

0 commit comments

Comments
 (0)