Skip to content

[passkey] nag people into passkeys - #321

Open
deployor wants to merge 1 commit into
hackclub:mainfrom
deployor:passkey-nudge
Open

[passkey] nag people into passkeys#321
deployor wants to merge 1 commit into
hackclub:mainfrom
deployor:passkey-nudge

Conversation

@deployor

@deployor deployor commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Adds a proper passkey push:

  • setup step after email-code login/signup/oauth
  • dedicated "Sign in with a passkey" button on /login, /welcome, /oauth_welcome. some password managers like 1Password can offer passkey sign-in on their own when they detect one, but a dedicated button is best practice, it also picks up Apple Passwords and other platform passkeys
  • only shown in WebAuthn-capable browsers
  • no second email code when adding a passkey right after login (a session younger than 15 min counts as re-auth for add_passkey)

code stuff:

  • one new migration, adds identities.passkey_prompt_dismissed_at
  • schema.rb also shows can_ban, v2_login_codes.purpose and an identity_sessions index — those come from older commits that never made it into schema.rb, not part of this PR, but they have to be there or schema.rb doesn't match the migrations
  • new passkey_logins controller: passkey-first login with no email, finds your account from the passkey itself
  • new passkey_setups controller: the setup step page
  • passkeys now register as discoverable (resident_key: required) so they work for passkey-first login
  • login completion routes eligible people to the setup step instead of straight home

Copilot AI lite review requested due to automatic review settings September 2, 2026 01:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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_to parameter. Use url_from so 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_to parameter. Use url_from so 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_to parameter. Use url_from so 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])
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.

2 participants