Add passkey option on TOTP page and auto-trigger passkey for TOTP users
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 <noreply@anthropic.com>
This commit is contained in:
@@ -147,6 +147,10 @@ class SessionsController < ApplicationController
|
|||||||
nil
|
nil
|
||||||
end
|
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
|
# Just render the form
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -148,7 +148,8 @@ class WebauthnController < ApplicationController
|
|||||||
# Only return minimal necessary info - no user_id or preferred_method
|
# Only return minimal necessary info - no user_id or preferred_method
|
||||||
render json: {
|
render json: {
|
||||||
has_webauthn: user.can_authenticate_with_webauthn?,
|
has_webauthn: user.can_authenticate_with_webauthn?,
|
||||||
requires_webauthn: user.require_webauthn?
|
requires_webauthn: user.require_webauthn?,
|
||||||
|
has_totp: user.totp_enabled?
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -49,8 +49,9 @@ export default class extends Controller {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto-trigger passkey authentication if required
|
// Auto-trigger passkey authentication if required, or if user has both
|
||||||
if (data.requires_webauthn) {
|
// 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);
|
setTimeout(() => this.authenticate(), 100);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -289,6 +290,10 @@ export default class extends Controller {
|
|||||||
if (!emailInput) {
|
if (!emailInput) {
|
||||||
emailInput = document.querySelector('input[name="user[email_address]"]');
|
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() : "";
|
return emailInput ? emailInput.value.trim() : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,31 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<% if @user_has_webauthn %>
|
||||||
|
<div data-controller="webauthn" data-webauthn-check-url-value="/webauthn/check">
|
||||||
|
<input type="hidden" name="webauthn_email" value="<%= @pending_email %>">
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="relative my-4">
|
||||||
|
<div class="absolute inset-0 flex items-center">
|
||||||
|
<div class="w-full border-t border-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
<div class="relative flex justify-center text-sm">
|
||||||
|
<span class="px-2 bg-white text-gray-500">Or</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button"
|
||||||
|
data-action="click->webauthn#authenticate"
|
||||||
|
class="w-full rounded-md px-3.5 py-2.5 bg-green-600 hover:bg-green-500 text-white font-medium cursor-pointer flex items-center justify-center">
|
||||||
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||||
|
</svg>
|
||||||
|
Use Passkey Instead
|
||||||
|
</button>
|
||||||
|
<div data-webauthn-target="error" class="mt-2 text-sm text-red-600" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-0 flex items-center">
|
<div class="absolute inset-0 flex items-center">
|
||||||
|
|||||||
Reference in New Issue
Block a user