Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,23 @@
- Options for getting or creating passkeys and security keys are now served by dedicated Rails controllers and retrieved via JavaScript fetch requests. [#73](https://github.com/cedarcode/devise-webauthn/pull/73) [@nicolastemciuc]
- BREAKING!: Remove helpers for generating WebAuthn options. [#106](https://github.com/cedarcode/devise-webauthn/pull/115) [@nicolastemciuc]

- BREAKING: `login_with_passkey_button` and `login_with_security_key_button` helpers have been renamed to `login_with_passkey_form_for` and `login_with_security_key_form_for`. They now take a block and no longer generate the submit button automatically. You need to explicitly add the button inside the block:
```erb
<%# Before %>
<%%= login_with_passkey_button(:user, "Log in with passkeys") %>

<%# After %>
<%%= login_with_passkey_form_for(:user) do |form| %>
<%%= form.submit "Log in with passkeys" %>
<%% end %>
```

### Fixed

- Fix form helpers (`passkey_creation_form_for`, `login_with_passkey_button`, `security_key_creation_form_for`, `login_with_security_key_button`) to accept a `resource_name` instead of requiring the `resource` object from the view context. [#114](https://github.com/cedarcode/devise-webauthn/pull/114) [@RenzoMinelli]



## [v0.3.0](https://github.com/cedarcode/devise-webauthn/compare/v0.2.2...v0.3.0/) - 2026-01-16

### Added
Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,18 @@ Devise::Webauthn provides helpers that can be used in your views. These helpers

For example, for a resource named `user`, you can use the following helpers:

To add a button for logging in with passkeys:
To add a form for logging in with passkeys:
```erb
<%= login_with_passkey_button_for(:user, "Log in with passkeys") %>
<%= login_with_passkey_form_for(:user) do |form| %>
<%= form.submit "Log in with passkeys" %>
<% end %>
```

To add a form for logging in with security keys (2FA):
```erb
<%= login_with_security_key_form_for(@resource) do |form| %>
<%= form.submit "Use security key" %>
<% end %>
```

To add a passkeys creation form:
Expand Down
4 changes: 3 additions & 1 deletion app/views/devise/sessions/new.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
</div>
<% end %>

<%= login_with_passkey_button_for(resource_name, "Log in with passkeys") %>
<%= login_with_passkey_form_for(resource_name) do |form| %>
<%= form.submit "Log in with passkeys" %>
<% end %>

<%= render "devise/shared/links" %>
4 changes: 3 additions & 1 deletion app/views/devise/two_factor_authentications/new.html.erb
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
<%= login_with_security_key_button_for(resource_name, 'Use security key') %>
<%= login_with_security_key_form_for(resource_name) do |form| %>
<%= form.submit 'Use security key' %>
<% end %>
20 changes: 4 additions & 16 deletions lib/devise/webauthn/helpers/credentials_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,15 @@ def passkey_creation_form_for(resource_or_resource_name, form_classes: nil, &blo
end
end

def login_with_passkey_button_for(resource_or_resource_name, text = nil, button_classes: nil,
form_classes: nil, &block)
def login_with_passkey_form_for(resource_or_resource_name, form_classes: nil, &block)
form_with(
url: session_path(resource_or_resource_name),
method: :post,
class: form_classes
) do |f|
tag.webauthn_get(data: { options_url: passkey_authentication_options_path(resource_or_resource_name) }) do
concat f.hidden_field(:public_key_credential, data: { webauthn_target: "response" })

concat f.button(text, type: "submit", class: button_classes, &block)
concat capture(f, &block)
end
end
end
Expand All @@ -46,8 +44,7 @@ def security_key_creation_form_for(resource_or_resource_name, form_classes: nil,
end
end

def login_with_security_key_button_for(resource_or_resource_name, text = nil, button_classes: nil,
form_classes: nil, &block)
def login_with_security_key_form_for(resource_or_resource_name, form_classes: nil, &block)
form_with(
url: two_factor_authentication_path(resource_or_resource_name),
method: :post,
Expand All @@ -57,19 +54,10 @@ def login_with_security_key_button_for(resource_or_resource_name, text = nil, bu
options_url: security_key_authentication_options_path(resource_or_resource_name)
}) do
concat f.hidden_field(:public_key_credential, data: { webauthn_target: "response" })
concat f.button(text, type: "submit", class: button_classes, &block)
concat capture(f, &block)
end
end
end

private

def resource_human_palatable_identifier
authentication_keys = resource.class.authentication_keys
authentication_keys = authentication_keys.keys if authentication_keys.is_a?(Hash)

authentication_keys.filter_map { |authentication_key| resource.public_send(authentication_key) }.first
end
end
end
end
165 changes: 165 additions & 0 deletions spec/helpers/devise/webauthn/credentials_helper_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# frozen_string_literal: true

# rubocop:disable RSpec/MultipleExpectations
RSpec.describe Devise::Webauthn::CredentialsHelper, type: :helper do
let(:user) do
Account.create!(
email: "testuser@example.com",
password: "$3cretp@ssword123"
)
end

before do
require "devise/version"
Rails.application.reload_routes_unless_loaded if Rails::VERSION::MAJOR >= 8 && Devise::VERSION < "5"
end

def parse(html)
Capybara.string(html)
end

def have_hidden_credential_field
have_css(
"input[type='hidden'][name='public_key_credential'][data-webauthn-target='response']",
visible: :hidden
)
end

describe "#passkey_creation_form_for" do
it "renders a form with webauthn_create element and hidden credential field" do
html = helper.passkey_creation_form_for(user) do |form|
form.submit "Create Passkey"
end

page = parse(html)
expect(page).to have_css("form")
expect(page).to have_css("webauthn-create[data-options-url='/accounts/passkey_registration_options']")
expect(page).to have_hidden_credential_field
expect(page).to have_button("Create Passkey")
end

it "accepts form_classes option" do
html = helper.passkey_creation_form_for(user, form_classes: "custom-form") do |form|
form.submit "Create"
end

expect(parse(html)).to have_css("form.custom-form")
end

it "allows custom content in the block" do
html = helper.passkey_creation_form_for(user) do |form|
helper.content_tag(:div, class: "button-wrapper") do
form.submit "Create", class: "btn-primary"
end
end

page = parse(html)
expect(page).to have_css("div.button-wrapper")
expect(page).to have_css("input.btn-primary[type='submit']")
end
end

describe "#login_with_passkey_form_for" do
it "renders a form with webauthn_get element and hidden credential field" do
html = helper.login_with_passkey_form_for(:account) do |form|
form.submit "Log in with passkeys"
end

page = parse(html)
expect(page).to have_css("form[action='/accounts/sign_in']")
expect(page).to have_css("webauthn-get[data-options-url='/accounts/passkey_authentication_options']")
expect(page).to have_hidden_credential_field
expect(page).to have_button("Log in with passkeys")
end

it "accepts form_classes option" do
html = helper.login_with_passkey_form_for(:account, form_classes: "passkey-form") do |form|
form.submit "Login"
end

expect(parse(html)).to have_css("form.passkey-form")
end

it "allows custom content in the block" do
html = helper.login_with_passkey_form_for(:account) do |form|
# artifact of the test, concat simulates ERB's <%= %>
helper.concat form.submit("Sign in", class: "btn-primary")
helper.concat form.check_box(:remember_me)
helper.concat form.label(:remember_me, "Remember me")
end

page = parse(html)

expect(page).to have_css("input.btn-primary[type='submit']")
expect(page).to have_css("input[type='checkbox'][name='remember_me']")
expect(page).to have_css("label[for='remember_me']", text: "Remember me")
end
Comment thread
santiagorodriguez96 marked this conversation as resolved.
Comment thread
santiagorodriguez96 marked this conversation as resolved.
end

describe "#security_key_creation_form_for" do
it "renders a form with webauthn_create element" do
html = helper.security_key_creation_form_for(user) do |form|
form.submit "Add Security Key"
end

page = parse(html)
expect(page).to have_css("form")
expect(page).to have_css("webauthn-create[data-options-url='/accounts/security_key_registration_options']")
expect(page).to have_hidden_credential_field
expect(page).to have_button("Add Security Key")
end

it "accepts form_classes option" do
html = helper.security_key_creation_form_for(user, form_classes: "security-key-form") do |form|
form.submit "Add"
end

expect(parse(html)).to have_css("form.security-key-form")
end

it "allows custom content in the block" do
html = helper.security_key_creation_form_for(user) do |form|
helper.content_tag(:div, class: "button-wrapper") do
form.submit "Add", class: "btn-primary"
end
end

page = parse(html)
expect(page).to have_css("div.button-wrapper")
expect(page).to have_css("input.btn-primary[type='submit']")
end
end

describe "#login_with_security_key_form_for" do
it "renders a form with webauthn_get element" do
html = helper.login_with_security_key_form_for(:account) do |form|
form.submit "Use security key"
end

page = parse(html)
expect(page).to have_css("form")
expect(page).to have_css("webauthn-get[data-options-url='/accounts/security_key_authentication_options']")
expect(page).to have_hidden_credential_field
expect(page).to have_button("Use security key")
end

it "accepts form_classes option" do
html = helper.login_with_security_key_form_for(:account, form_classes: "two-factor-form") do |form|
form.submit "Verify"
end

expect(parse(html)).to have_css("form.two-factor-form")
end

it "allows custom content in the block" do
html = helper.login_with_security_key_form_for(:account) do |form|
helper.safe_join([helper.content_tag(:p, "Authenticate with your security key"), form.submit("Authenticate")])
end

page = parse(html)
expect(page).to have_css("p", text: "Authenticate with your security key")
expect(page).to have_button("Authenticate")
end
end
end
# rubocop:enable RSpec/MultipleExpectations