✨ FEATURE: Add passkey support#52
Open
tobiasgruber wants to merge 14 commits into
Open
Conversation
Add usernameless, passwordless passkey login to the Neos backend login screen, alongside the existing WebAuthn 2nd factor. A user-verified discoverable passkey authenticates the Neos.Neos:Backend account directly (no password) and satisfies the 2FA gate in one tap. Off by default: gated behind webAuthn.passwordlessLoginEnabled, enforced server-side in the controller (not just by hiding the button). Architecture: a parallel Flow auth provider + token (WebAuthnPasswordlessProvider / WebAuthnPasswordlessToken) authenticate the same backend account in-request; with Neos' oneToken strategy and party-based user resolution this grants full backend access. The token is persisted via refreshTokens() and survives the redirect to /neos. - WebAuthnService: createPasswordlessAuthenticationOptions (empty allowCredentials, UV required), verifyPasswordlessAssertion, and the unit-tested resolveBackendAccountByUserHandle guard (only Neos.Neos:Backend accounts may use this path). Registration requests discoverable (resident) credentials only when passwordless is enabled, so U2F-only 2nd-factor behaviour is unchanged when off. - PasswordlessLoginController (options/verify XHR, SkipCsrfProtection), Routes.yaml, Settings.yaml (provider), Policy.yaml (public grant for the endpoints), Settings.2FA.yaml (passwordlessLoginEnabled: false), new session key, and session start for the logged-out flow. - Login screen: Views.yaml extends the core Neos login FusionView to load a server-gated PasswordlessLoginButton that also loads webauthn.js; JS reuses the assertion ceremony (click-only, no auto-trigger). - Tests: unit test for the account-resolution guard; E2E "passwordless" variant (happy path) and "disabled by default" scenarios (no button + 403). - E2E infra: reuseExistingServer + KEEP_SUT for a fast local loop, mount Tests/ into the SUT so PHPUnit runs there, and make removeAllUsers tolerant of zero users. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a `discoverable` flag to SecondFactor so a passwordless-capable credential ("Passkey") is distinguished from a non-discoverable one ("Passkey as 2nd factor"). Credentials registered while passwordless login is enabled use residentKey:required and are persisted as discoverable; legacy rows default to non-discoverable (migration).
Surface the distinction as a per-credential badge in the management module, and nudge users without a Passkey to register one via a CTA banner (gated by the passwordless setting). Rename all user-facing "security key" wording to "passkey" across labels and translations.
Add E2E coverage for the Passkey badge, the registration banner, the post-login redirect to the originally requested page, and cancelling the passkey sign-in.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… passkeys are enabled
… top) and style the twofactor backend module more consistently
…ogin An abandoned WebAuthn passwordless login left the shared session container (SESSION_OBJECT_ID) populated with only `webAuthnPasswordlessOptions` and no authentication status. A later regular login then crashed the SecondFactorMiddleware: SecondFactorSessionStorageService::getAuthenticationStatus(): Return value must be of type string, null returned Root cause: initializeTwoFactorSessionObject() keyed its "already initialised?" check on the presence of the whole container, but the passwordless flow (PasswordlessLoginController::optionsAction) writes its options into that container before any status exists — so the status was never set, and the string-typed getter returned null. Fixes: - initializeTwoFactorSessionObject() now gates on the auth status key, not the container's presence (root cause). - getAuthenticationStatus() defaults a missing status to AUTHENTICATION_NEEDED (defence in depth against the TypeError). Regression tests: - Tests/Unit/Service/SecondFactorSessionStorageServiceTest.php (new) - Tests/E2E/features/passwordless/login.feature: "Abandoning a passwordless ceremony does not break a later password login"
…bove the verification code for a better UX
… to setup a passkey
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add config-opt-in option for passkey login support for Neos (username-less and password-less).
Needs a database migration:
./flow doctrine:migrateNeeds to be tested in production with different passkey providers: