Skip to content

Add WebAuthn/Passkey authentication support#5267

Open
florida117 wants to merge 12 commits intoNginxProxyManager:developfrom
florida117:feature/passkey-webauthn-support
Open

Add WebAuthn/Passkey authentication support#5267
florida117 wants to merge 12 commits intoNginxProxyManager:developfrom
florida117:feature/passkey-webauthn-support

Conversation

@florida117
Copy link

@florida117 florida117 commented Feb 3, 2026

Summary

Implements passwordless passkey (WebAuthn) authentication for the admin interface, as requested in #3363.

  • Passwordless login: "Sign in with Passkey" button on the login page — the browser presents registered passkeys directly, no email or password needed
  • Passkey management: Users can register, rename, and delete passkeys from the user dropdown menu
  • Bypasses 2FA: Passkeys are inherently multi-factor (device possession + biometric/PIN), so TOTP is skipped when authenticating with a passkey
  • Auto-detected RP settings: WebAuthn relying party ID and origin are automatically derived from the incoming request headers, so passkeys work behind reverse proxies without configuration

Configuration

WebAuthn relying party settings are auto-detected from the request. When behind a reverse proxy, the standard X-Forwarded-Host and X-Forwarded-Proto headers are used (Express trust proxy is already enabled).

Environment variables are available as optional overrides if needed:

Variable Default Description
WEBAUTHN_RP_ID req.hostname (auto-detected) Relying party domain
WEBAUTHN_RP_NAME Nginx Proxy Manager Display name shown during registration
WEBAUTHN_ORIGIN req.protocol + req.get("host") (auto-detected) Full origin URL

Passkeys registered under one origin will not work from a different origin — this is by design in the WebAuthn spec.

Backend changes

  • New webauthn_credential database table via Knex migration
  • New Objection.js model (backend/models/webauthn_credential.js)
  • Business logic module (backend/internal/webauthn.js) handling registration, authentication, listing, renaming, and deletion
  • getTokenFromPasskey() added to backend/internal/token.js
  • New unauthenticated routes: POST /tokens/passkey/options, POST /tokens/passkey/verify
  • New authenticated routes: GET/POST/PUT/DELETE /users/:id/passkeys/...
  • OpenAPI schema definitions for all new endpoints

Frontend changes

  • @simplewebauthn/browser integration in AuthContext (loginWithPasskey)
  • "Sign in with Passkey" button on login page (conditionally hidden if browser lacks WebAuthn support)
  • PasskeyModal for managing passkeys (register, list, rename, delete)
  • Passkeys menu item added to user dropdown in SiteHeader
  • Full i18n support via react-intl locale strings

Dependencies added

  • Backend: @simplewebauthn/server@^11.0.0
  • Frontend: @simplewebauthn/browser@^11.0.0

Test plan

  • Run npx knex migrate:latest — verify webauthn_credential table is created
  • Verify existing email/password login still works
  • Verify existing TOTP 2FA flow still works
  • Register a passkey via Settings > Passkeys
  • Log out and sign in using the "Sign in with Passkey" button (passwordless)
  • Rename a registered passkey
  • Delete a registered passkey
  • Verify passkey button is hidden in browsers without WebAuthn support
  • Test with different databases (SQLite, MySQL, PostgreSQL)
  • Test access via reverse proxy — RP settings should auto-detect from forwarded headers

Closes #3363

🤖 Generated with Claude Code

@florida117 florida117 force-pushed the feature/passkey-webauthn-support branch 2 times, most recently from 9ea04e4 to a3fe8c6 Compare February 3, 2026 12:59
florida117 and others added 8 commits February 4, 2026 12:07
Implements passwordless passkey login alongside the existing
email/password flow. Passkeys provide phishing-resistant,
multi-factor authentication (device + biometric) and bypass
TOTP 2FA when used.

Closes NginxProxyManager#3363

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
BearerAuth -> bearerAuth to match the scheme defined in
components/security-schemes.json.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add missing parameter descriptions and examples for userID/passkeyID
path parameters, add response/request examples to all passkey endpoint
schemas, and suppress false-positive no-http-verbs-in-path warnings
for WebAuthn "options" endpoints via x-lint-ignore.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Setup page now uses a three-step flow: enter name/email, choose
between password or passkey, then complete the chosen method. The
backend returns a JWT token in setup mode so the frontend can
authenticate immediately without a separate login call.

Also fix WebAuthn origin mismatch by preferring the Origin request
header over constructing it from protocol/host, which dropped the
port in proxied environments.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Backend now returns has_password field when fetching user data,
allowing the frontend to distinguish between password and
passkey-only accounts. The change password modal hides the current
password field and shows "Set Password" for accounts without a
password. The backend setPassword handler skips the current password
check when no password auth exists.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Users with at least one registered passkey can now remove their password
to go fully passwordless. Backend validates passkey existence before
allowing removal, and the frontend shows a "Remove Password" button in
the Change Password modal when both a password and passkeys are present.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The custom omissions early return in internalUser.get() was placed
before the has_password assignment, preventing it from ever being
set on the response. This caused the frontend to always default to
showing the "current password" field when changing passwords.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@florida117 florida117 force-pushed the feature/passkey-webauthn-support branch from 754664b to 01b4da8 Compare February 4, 2026 12:07
florida117 and others added 4 commits February 4, 2026 12:28
…ing example

The POST /users endpoint returns token and expires properties during
initial setup, but the User schema had additionalProperties: false
without defining them, causing 7 Cypress test failures. Also adds
missing example to has_password to fix SwaggerSchema lint warning.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@nginxproxymanagerci
Copy link

Docker Image for build 11 is available on DockerHub:

nginxproxymanager/nginx-proxy-manager-dev:pr-5267

Note

Ensure you backup your NPM instance before testing this image! Especially if there are database changes.
This is a different docker image namespace than the official image.

Warning

Changes and additions to DNS Providers require verification by at least 2 members of the community!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support Passkey authentication

1 participant