Settings → Account: passkeys are offered at bare IP addresses, where browsers refuse them, and an IPv6 host is shown as \"[\ #429
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#429
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-09-29 in a read-only review of postulo at
95348908d, readingaccounts/passkeys.pyand running its helpers against request hosts.Checked again on 2026-10-01 against
mainat0a5cad587: still present.The Account page decides whether a passkey can be made here with
passkeys.usable_here, and names the host it would be tied to withpasskeys.bound_to. Both rest on_host, which cuts theHostheader at the first colon, and neither knows that WebAuthn accepts only a domain as its relying-party id: an IP address is refused by the browser whether the page is served over HTTP or HTTPS. So the page offers "Add one" at hosts where the browser will refuse, and garbles IPv6 hosts in what it prints.accounts/passkeys.py:26,LOCAL_HOSTS;accounts/passkeys.py:29-40,_host;accounts/passkeys.py:43-56,usable_hereandbound_to;templates/settings/account.html:105-130._hostreturnsrequest.get_host().partition(":")[0], which for[::1]:8000is[and for[fd7a:115c::1]:8443is[fd7a.LOCAL_HOSTSlists"[::1]"and"::1", neither of which_hostcan ever produce; and over HTTPS at an IPv6 address the page says "A passkey is tied to the address you made it at, which here is [fd7a."LOCAL_HOSTSalso lists127.0.0.1, andusable_herereturns true for any request thatis_secure(). An instance opened athttp://127.0.0.1:8000, or over HTTPS at a bare IPv4 or IPv6 address, therefore shows "Add one"; the browser then refuses the ceremony, because the WebAuthn specification requires the caller's effective domain to be a valid domain, not an IP address. The browser suite checks passkeys atlocalhost(tests/e2e/test_passkeys.py:138) for the same reason.passkeys.py:17-18); at an address host it does the opposite, offering a button that fails and, over plain HTTP at[::1], blaming HTTP for a refusal that has a different cause.django.http.request.split_domain_port, which handles bracketed IPv6, and strip the brackets. Treat any host thatipaddress.ip_addressaccepts as not usable for passkeys, with its own sentence ("Passkeys need the instance to be reached by a name, not a bare address"), before the secure-context test. ReduceLOCAL_HOSTStolocalhost(subdomains oflocalhostare secure contexts too).bound_tothen only ever prints a domain.Done when
tests/test_passkeys.pyrequest the Account page withHTTP_HOSTset to127.0.0.1:8000,[::1]:8000and, marked secure,[fd7a:115c::1]:8443, and find no "Add one" and the address message each time; the existinglocalhostandtestservercases keep passing. (SetMFA_WEBAUTHN_ALLOW_INSECURE_ORIGIN = Falsein those tests, sinceconfig/settings/test.py:35turns it on.)