From c5898bd9a4b8fed5cccbd5d82e0e3e3d11c8bfc9 Mon Sep 17 00:00:00 2001 From: Dan Milne Date: Thu, 5 Mar 2026 23:09:01 +1100 Subject: [PATCH] Add passkey option on TOTP page and auto-trigger passkey for TOTP users MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a user has both passkeys and TOTP configured, auto-trigger the passkey flow on login to save them from the password→TOTP path. Also add a "Use Passkey Instead" button on the TOTP verification page as an escape hatch for users who end up there. Co-Authored-By: Claude Opus 4.6 --- app/controllers/sessions_controller.rb | 4 +++ app/controllers/webauthn_controller.rb | 3 ++- .../controllers/webauthn_controller.js | 9 +++++-- app/views/sessions/verify_totp.html.erb | 25 +++++++++++++++++++ 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 76f2ea7..40b9d38 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -147,6 +147,10 @@ class SessionsController < ApplicationController nil end + # Pass data to the view for passkey option + @user_has_webauthn = user&.can_authenticate_with_webauthn? + @pending_email = user&.email_address + # Just render the form end diff --git a/app/controllers/webauthn_controller.rb b/app/controllers/webauthn_controller.rb index d058148..535615f 100644 --- a/app/controllers/webauthn_controller.rb +++ b/app/controllers/webauthn_controller.rb @@ -148,7 +148,8 @@ class WebauthnController < ApplicationController # Only return minimal necessary info - no user_id or preferred_method render json: { has_webauthn: user.can_authenticate_with_webauthn?, - requires_webauthn: user.require_webauthn? + requires_webauthn: user.require_webauthn?, + has_totp: user.totp_enabled? } end diff --git a/app/javascript/controllers/webauthn_controller.js b/app/javascript/controllers/webauthn_controller.js index 0e26509..41b29bd 100644 --- a/app/javascript/controllers/webauthn_controller.js +++ b/app/javascript/controllers/webauthn_controller.js @@ -49,8 +49,9 @@ export default class extends Controller { } }); - // Auto-trigger passkey authentication if required - if (data.requires_webauthn) { + // Auto-trigger passkey authentication if required, or if user has both + // webauthn and TOTP (to save them from the password→TOTP flow) + if (data.requires_webauthn || (data.has_webauthn && data.has_totp)) { setTimeout(() => this.authenticate(), 100); } } else { @@ -289,6 +290,10 @@ export default class extends Controller { if (!emailInput) { emailInput = document.querySelector('input[name="user[email_address]"]'); } + // Fallback to hidden webauthn_email field (e.g., on TOTP verification page) + if (!emailInput) { + emailInput = document.querySelector('input[name="webauthn_email"]'); + } return emailInput ? emailInput.value.trim() : ""; } diff --git a/app/views/sessions/verify_totp.html.erb b/app/views/sessions/verify_totp.html.erb index 34bdec8..d735771 100644 --- a/app/views/sessions/verify_totp.html.erb +++ b/app/views/sessions/verify_totp.html.erb @@ -34,6 +34,31 @@ <% end %> + <% if @user_has_webauthn %> +
+ +
+
+
+
+
+
+ Or +
+
+ + +
+
+ <% end %> +