Passkeys (WebAuthn)¶
dj-rest-auth includes optional passkey support for passwordless authentication using the FIDO2/WebAuthn standard. Users can register hardware security keys or platform authenticators (Touch ID, Windows Hello, Android biometrics) and use them to log in without a password.
Overview¶
- FIDO2/WebAuthn: Industry-standard passwordless authentication
- Platform authenticators: Touch ID, Windows Hello, Android biometrics
- Roaming authenticators: Hardware security keys (YubiKey, etc.)
- Discoverable credentials: Supports resident keys for usernameless login
- Credential management: List, rename, and delete registered passkeys
Setup¶
1) Install passkey extras¶
with-passkeys installs the webauthn library.
2) Enable app¶
INSTALLED_APPS = [
# ...
'rest_framework',
'rest_framework.authtoken',
'dj_rest_auth',
'dj_rest_auth.passkeys',
]
3) Configure relying party settings¶
REST_AUTH = {
'PASSKEY_RP_ID': 'example.com',
'PASSKEY_RP_NAME': 'My Application',
'PASSKEY_RP_ORIGINS': ['https://example.com'],
}
Required Settings
PASSKEY_RP_ID, PASSKEY_RP_NAME, and PASSKEY_RP_ORIGINS must all be configured. A ValidationError is raised if any are missing.
Local Development
For local development, use:
4) Run migrations¶
5) Include URLs¶
from django.urls import include, path
urlpatterns = [
path('dj-rest-auth/', include('dj_rest_auth.urls')),
path('dj-rest-auth/passkeys/', include('dj_rest_auth.passkeys.urls')),
]
Registration flow¶
Passkey registration is a two-step challenge-response process. The user must be authenticated.
- Client sends POST to
/passkeys/register/begin/(optionally with a credential name). - API returns WebAuthn
PublicKeyCredentialCreationOptions(challenge, relying party info, user info). - Client passes options to the browser's
navigator.credentials.create()API. - Client sends the resulting credential to
/passkeys/register/complete/. - API verifies the response and stores the credential.
sequenceDiagram
participant B as Browser
participant S as Server
B->>S: POST /passkeys/register/begin/
S-->>B: PublicKeyCredentialCreationOptions
B->>B: navigator.credentials.create()
B->>S: POST /passkeys/register/complete/
S-->>B: 201 Created (credential details)
Login flow¶
Passkey login is also a two-step challenge-response process. No prior authentication is required.
- Client sends POST to
/passkeys/login/begin/(optionally withusernameoremailto scope credentials). - API returns WebAuthn
PublicKeyCredentialRequestOptionsand asession_id. - Client passes options to the browser's
navigator.credentials.get()API. - Client sends the resulting assertion and
session_idto/passkeys/login/complete/. - API verifies the assertion and returns a full auth response (token, JWT, or session).
sequenceDiagram
participant B as Browser
participant S as Server
B->>S: POST /passkeys/login/begin/
S-->>B: PublicKeyCredentialRequestOptions + session_id
B->>B: navigator.credentials.get()
B->>S: POST /passkeys/login/complete/
S-->>B: Auth response (token/JWT)
Endpoints¶
Register Begin¶
POST /dj-rest-auth/passkeys/register/begin/
Authentication required.
Request fields:
name(optional) — friendly name for the credential
Response: WebAuthn PublicKeyCredentialCreationOptions JSON.
Register Complete¶
POST /dj-rest-auth/passkeys/register/complete/
Authentication required.
Request fields:
credential— the credential response fromnavigator.credentials.create()name(optional) — friendly name (defaults to "Passkey")
Response (201): registered credential details.
Login Begin¶
POST /dj-rest-auth/passkeys/login/begin/
No authentication required.
Request fields:
username(optional) — scope credentials to a specific useremail(optional) — scope credentials to a specific user
Response: WebAuthn PublicKeyCredentialRequestOptions JSON + session_id.
Login Complete¶
POST /dj-rest-auth/passkeys/login/complete/
No authentication required.
Request fields:
credential— the assertion response fromnavigator.credentials.get()session_id— the UUID returned by login begin
Response: standard auth response (same as login — token key, JWT, or session).
List Passkeys¶
GET /dj-rest-auth/passkeys/
Authentication required. Returns all passkeys for the authenticated user, ordered by creation date (newest first).
Response fields per credential:
id,name,credential_id(base64url),created_at,last_used_at,transports,discoverable
Passkey Detail¶
GET /dj-rest-auth/passkeys/{id}/
PATCH /dj-rest-auth/passkeys/{id}/
DELETE /dj-rest-auth/passkeys/{id}/
Authentication required. Users can only access their own credentials.
- GET — retrieve credential details
- PATCH — rename a credential (
{"name": "new name"}) - DELETE — remove a credential (204 No Content)
Security behavior¶
- Challenges are cached server-side and expire after
PASSKEY_CHALLENGE_TIMEOUT(default 300s) - Challenges are single-use — deleted from cache after verification
sign_countis checked on each authentication to detect cloned authenticators- Session IDs are random UUIDs, unique per login attempt
- Inactive users cannot authenticate via passkey
- Credential IDs are unique across all users
Settings¶
Configure via REST_AUTH:
REST_AUTH = {
# Required
'PASSKEY_RP_ID': 'example.com',
'PASSKEY_RP_NAME': 'My Application',
'PASSKEY_RP_ORIGINS': ['https://example.com'],
# Optional
'PASSKEY_CHALLENGE_TIMEOUT': 300, # seconds (default: 300)
}
All serializers are overridable:
REST_AUTH = {
'PASSKEY_REGISTER_BEGIN_SERIALIZER': 'myapp.serializers.CustomRegisterBeginSerializer',
'PASSKEY_REGISTER_COMPLETE_SERIALIZER': 'myapp.serializers.CustomRegisterCompleteSerializer',
'PASSKEY_LOGIN_BEGIN_SERIALIZER': 'myapp.serializers.CustomLoginBeginSerializer',
'PASSKEY_LOGIN_COMPLETE_SERIALIZER': 'myapp.serializers.CustomLoginCompleteSerializer',
'PASSKEY_LIST_SERIALIZER': 'myapp.serializers.CustomListSerializer',
'PASSKEY_UPDATE_SERIALIZER': 'myapp.serializers.CustomUpdateSerializer',
}