Skip to main content
django-passkeys-admin

django-passkeys-admin

PyPI Python Django CI License

A passkey-only Django admin: staff sign in with a passkey and nothing else, while every other account keeps its password. Built on django-passkeys.

Requires Python 3.12 or newer and Django 5.2 LTS or newer. Every supported combination — Python 3.12/3.13/3.14 against Django 5.2/6.0/6.1 — is exercised by the test suite on every push.

The gap this fills

django-passkeys provides the WebAuthn ceremonies and an authentication backend that accepts a password or a passkey. Pointing LOGIN_URL at a passkey page changes which form staff see; it does not change which credential the backend accepts, so a staff password keeps working everywhere it worked before. django-otp-webauthn adds a passkey as a second factor, and django-allauth is a whole account system. None of them takes passwords away from the admin.

This package does, and it covers what going passwordless then requires:

Guarantee How it holds
Passkey only, for staff only The backend refuses a staff account its own correct password, and refuses a passkey to anyone who is not staff.
The session lasts as long as the passkey Every admin request checks that the passkey which opened the session still exists and is enabled. Deleting a passkey shuts out every session it opened.
The first passkey A passwordless admin cannot be entered to enrol the first passkey. manage.py enrol_passkey prints a signed, single-use link that grants a short session able to do exactly that and nothing else.
No way back in through a reset A password reset form mixin withholds resets from staff accounts.
Never the last passkey The management page refuses to delete the last passkey an account holds.
Rate limited Starting an authentication ceremony is limited per client address.

Installation

pip install django-passkeys-admin

Add both apps, the backend and the settings django-passkeys needs:

INSTALLED_APPS = [
    ...
    "passkeys",
    "passkeys_admin",
]

AUTHENTICATION_BACKENDS = ["passkeys_admin.backends.PasskeyOnlyStaffBackend"]

FIDO_SERVER_ID = "example.com"
FIDO_SERVER_NAME = "Example"
PASSKEYS_ALLOW_NO_PASSKEY_FIELD = True

FIDO_SERVER_ID is the WebAuthn relying party: the domain the admin is served from, with no scheme or port. PASSKEYS_ALLOW_NO_PASSKEY_FIELD makes the django-passkeys backend return nothing, rather than raise, when it is handed a request carrying neither a password nor a passkey — which any other login form in the project will eventually do.

Run the migrations, which create the django-passkeys table:

python manage.py migrate

Then make the admin site passkey-only. Either replace the default site:

# yourproject/apps.py
from django.contrib.admin.apps import AdminConfig


class PasskeyAdminConfig(AdminConfig):
    default_site = "passkeys_admin.sites.PasskeyAdminSite"
INSTALLED_APPS = [
    "yourproject.apps.PasskeyAdminConfig",  # in place of "django.contrib.admin"
    ...,
]

or mix it into the site you already have:

from django.contrib import admin

from passkeys_admin.sites import PasskeyAdminMixin


class AdminSite(PasskeyAdminMixin, admin.AdminSite): ...

The passkey pages and the ceremony endpoints are the site's own URLs, under passkeys/, so there is nothing to add to urlpatterns. Do not include passkeys.urls: its views let any signed-in account register a passkey and delete or disable its last one, which is what this package exists to prevent.

The first passkey

Nobody can sign in to a passkey-only admin before they have a passkey, so the first one is enrolled from a link:

python manage.py enrol_passkey alice --base-url https://example.com

The argument is whatever the user model's USERNAME_FIELD holds. The link is valid for fifteen minutes and void as soon as it is followed, or as soon as the account's passkeys change. It signs the user in to the passkey page alone: the rest of the admin stays closed until they sign in with the passkey they have just enrolled.

Usage

Linking the passkey page

Each staff member manages their passkeys at admin/passkeys/. To put it beside the other user links, override userlinks in your admin/base_site.html:

{% block userlinks %}
  <a href='{% url "admin:passkeys" %}'>{% translate "Passkeys" %}</a> /
  {{ block.super }}
{% endblock userlinks %}

Password resets

Mix PasskeyOnlyPasswordResetMixin into every password reset form the project offers. Without it, a staff account could still be handed a password it has no use for in the admin, but that every other password login would accept.

from django.contrib.auth.forms import PasswordResetForm

from passkeys_admin.forms import PasskeyOnlyPasswordResetMixin


class ResetForm(PasskeyOnlyPasswordResetMixin, PasswordResetForm):
    pass

Tests

A test that reaches the admin needs a session a passkey opened. force_login() no longer is one, so the package ships a mixin that is:

from django.test import TestCase

from passkeys_admin.testing import PasskeyLoginMixin


class DashboardTests(PasskeyLoginMixin, TestCase):
    def test_the_index_loads(self):
        self.passkey_login(self.staff_user)
        ...

Settings

Setting Default
PASSKEYS_ADMIN_REQUIRED True False restores password logins for staff, for a development machine whose origin is not the deployed one.
PASSKEYS_ADMIN_ENROLMENT_MAX_AGE 900 Seconds an enrolment link, and the session it grants, stays valid.
PASSKEYS_ADMIN_CEREMONY_RATE 30 Authentication ceremonies one client address may start per window.
PASSKEYS_ADMIN_CEREMONY_WINDOW 60 Length of that window, in seconds.

Notes

  • Browsers. The ceremonies parse their options with PublicKeyCredential.parseCreationOptionsFromJSON() and parseRequestOptionsFromJSON(), which every browser has shipped since Safari 18.4 (Chrome and Edge 129, Firefox 119). The browser's answer is serialised field by field rather than with toJSON(), which some password manager extensions replace with an implementation that drops fields or throws.
  • Behind a proxy, the rate limit counts REMOTE_ADDR, and the address there is the proxy's unless the server is told to trust its forwarding header — --forwarded-allow-ips for gunicorn and uvicorn. Otherwise every client shares one counter. The rate also needs a cache shared by the workers: with the default per-process cache, the effective rate is the configured one times the number of workers.
  • Manifest static storage. django-passkeys ships bootstrap-toggle.min.js with a sourceMappingURL to a map it does not ship, which ManifestStaticFilesStorage refuses at collectstatic. This package uses none of django-passkeys' static files, but collectstatic collects them all the same.
  • One admin site. enrol_passkey reverses the link through the admin namespace, which resolves to the site named admin, or to the only one there is.

License

MIT — see LICENSE.

Metadata

Release files for django-passkeys-admin 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-passkeys-admin 1.0.0
File Size Uploaded Provenance
django_passkeys_admin-1.0.0.tar.gz 342.4 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for django-passkeys-admin 1.0.0
File Interpreter ABI Platform
django_passkeys_admin-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 361.9 kB

Release files / django_passkeys_admin-1.0.0.tar.gz

Download URL django_passkeys_admin-1.0.0.tar.gz
Size 342.4 kB
Tags Source
SHA-256 checksum
How to use checksums
db31a0179e711a5cdbe22d31bb9b40dd8b120a1d3065e5a92ae2d37b5e34593b
BLAKE2b-256 checksum
How to use checksums
35860964d9ac0c40217b01261b0626f9f949cdd8660f46efe75b57b79730e117
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.4 {"installer":{"name":"uv","version":"0.13.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / django_passkeys_admin-1.0.0-py3-none-any.whl

Download URL django_passkeys_admin-1.0.0-py3-none-any.whl
Size 19.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
790a5e56a3e11bf669819d877ab5ccd759c126690873639feff338a4d8132fdd
BLAKE2b-256 checksum
How to use checksums
e4e81fb31e973e9a8b779fb67ec68ba49df0d6ddd46c6902cda49bce65cc3fbe
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.4 {"installer":{"name":"uv","version":"0.13.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for django-passkeys-admin 1.0.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page