[passkey] nag people into passkeys - #321
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
There are confirmed correctness and security-hardening issues in the new passkey-first login flow and welcome UI initialization that should be fixed before merge.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR introduces a first-class passkey “push” across the authentication flow: eligible users are routed to a dedicated passkey setup prompt after completing login, and unauthenticated users can sign in directly via a new passkey-first flow (discoverable credentials).
Changes:
- Adds passkey-first sign-in endpoints (
/passkey/login/options,/passkey/login/verify) plus UI entry points on login/welcome surfaces. - Adds a post-login passkey setup prompt with skip + “don’t show again” dismissal, backed by
identities.passkey_prompt_dismissed_at. - Updates WebAuthn registration to require discoverable credentials and relaxes step-up for passkey registration when the session is very recent.
File summaries
| File | Description |
|---|---|
| spec/requests/passkey_setups_spec.rb | Request coverage for setup prompt show/skip and step-up bypass for options |
| spec/requests/passkey_prompt_flow_spec.rb | Request coverage for post-login routing through setup prompt |
| spec/requests/passkey_logins_spec.rb | Request coverage for passkey-first login options/verify and UI presence |
| spec/models/identity_spec.rb | Unit coverage for passkey promotion eligibility and dismissal |
| spec/models/identity_session_spec.rb | Unit coverage for recently_authenticated? step-up window logic |
| db/schema.rb | Schema update to include dismissal timestamp (plus noted prior drift) |
| db/migrate/20260831000001_add_passkey_prompt_dismissed_at_to_identities.rb | Adds passkey_prompt_dismissed_at column |
| config/routes.rb | Adds routes for passkey login + setup prompt |
| config/locales/en.yml | Adds i18n strings for passkey login/setup UI |
| app/views/passkey_setups/show.html.erb | New passkey setup prompt view + skip/don’t-show-again UI |
| app/views/logins/new.html.erb | Adds passkey sign-in button and form on login page |
| app/models/identity.rb | Adds passkey promotion gating + dismissal helper methods |
| app/models/identity_session.rb | Adds recently_authenticated? used for step-up bypass |
| app/frontend/js/passkey-setup.js | Alpine component for setup prompt + WebAuthn registration |
| app/frontend/js/passkey-login.js | Alpine component for passkey-first sign-in |
| app/frontend/js/alpine.js | Registers new Alpine components |
| app/controllers/passkey_setups_controller.rb | Setup prompt controller (show/skip + safe return_to) |
| app/controllers/passkey_logins_controller.rb | Passkey-first options/verify controller |
| app/controllers/logins_controller.rb | Routes successful login through passkey prompt when eligible |
| app/controllers/identity_webauthn_credentials_controller.rb | Makes registration discoverable + return_to-aware redirects |
| app/controllers/concerns/webauthn_authenticatable.rb | Adds discoverable credential lookup + verification helper |
| app/controllers/concerns/step_up_authenticatable.rb | Allows recent-login bypass for add-passkey step-up |
| app/controllers/application_controller.rb | Adds passkey-promotion eligibility helper + redirect helper |
| app/components/auth_welcome.rb | Adds passkey sign-in UI to the welcome component |
Review details
Suppressed comments (3)
app/controllers/passkey_logins_controller.rb:49
- This redirect preserves an unsanitized
return_toparameter. Useurl_fromso only safe relative paths are carried forward.
redirect_to login_path(return_to: params[:return_to])
app/controllers/passkey_logins_controller.rb:70
- These error-path redirects preserve an unsanitized
return_toparameter. Useurl_fromso external/invalid URLs are dropped before being echoed back into the login flow.
redirect_to login_path(return_to: params[:return_to])
app/controllers/passkey_logins_controller.rb:73
- These error-path redirects preserve an unsanitized
return_toparameter. Useurl_fromso external/invalid URLs are dropped before being echoed back into the login flow.
redirect_to login_path(return_to: params[:return_to])
- Files reviewed: 24/24 changed files
- Comments generated: 4
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| end | ||
|
|
||
| def render_passkey_login | ||
| div(x_data: "passkeyLogin") do |
| end | ||
|
|
||
| def verify | ||
| credential_data = JSON.parse(params[:credential_data]) |
|
|
||
| unless credential | ||
| flash[:error] = "Passkey not found. Try using your email instead." | ||
| redirect_to login_path(return_to: params[:return_to]) |
| redirect_to url_from(params[:return_to]) || root_path | ||
| rescue WebauthnCredentialCompromisedError | ||
| flash[:error] = "Security issue detected with your passkey. It has been disabled for your protection. Please use another login method or register a new passkey." | ||
| redirect_to login_path(return_to: params[:return_to]) |
Adds a proper passkey push:
code stuff: