Skip to content

Commit 925538c

Browse files
RenzoMinelliclaude
andcommitted
feat!: rename login helpers to form_for pattern and require explicit button
BREAKING CHANGE: `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. Before: <%= login_with_passkey_button("Log in", session_path: path) %> After: <%= login_with_passkey_form_for(session_path: path) do |form| %> <%= form.submit "Log in" %> <% end %> This gives developers full control over button styling and form content. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent e1d495f commit 925538c

6 files changed

Lines changed: 168 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@
55
### Changed
66

77
- 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]
8+
- 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:
9+
```erb
10+
<%# Before %>
11+
<%%= login_with_passkey_button("Log in with passkeys", session_path: user_session_path) %>
12+
13+
<%# After %>
14+
<%%= login_with_passkey_form_for(session_path: user_session_path) do |form| %>
15+
<%%= form.submit "Log in with passkeys" %>
16+
<%% end %>
17+
```
818

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

README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,18 @@ $ bin/rails generate devise:webauthn:views -v passkeys
147147
### Helper methods
148148
Devise::Webauthn provides helpers that can be used in your views. For example, for a resource named `user`, you can use the following helpers:
149149

150-
To add a button for logging in with passkeys:
150+
To add a form for logging in with passkeys:
151151
```erb
152-
<%= login_with_passkey_button("Log in with passkeys", session_path: user_session_path) %>
152+
<%= login_with_passkey_form_for(session_path: user_session_path) do |form| %>
153+
<%= form.submit "Log in with passkeys" %>
154+
<% end %>
155+
```
156+
157+
To add a form for logging in with security keys (2FA):
158+
```erb
159+
<%= login_with_security_key_form_for(resource: @resource) do |form| %>
160+
<%= form.submit "Use security key" %>
161+
<% end %>
153162
```
154163

155164
To add a passkeys creation form:

app/views/devise/sessions/new.html.erb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
</div>
2424
<% end %>
2525

26-
<%= login_with_passkey_button("Log in with passkeys", session_path: session_path(resource_name)) %>
26+
<%= login_with_passkey_form_for(session_path: session_path(resource_name)) do |form| %>
27+
<%= form.submit "Log in with passkeys" %>
28+
<% end %>
2729

2830
<%= render "devise/shared/links" %>
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
<%= login_with_security_key_button('Use security key', resource: @resource) %>
1+
<%= login_with_security_key_form_for(resource: @resource) do |form| %>
2+
<%= form.submit 'Use security key' %>
3+
<% end %>

lib/devise/webauthn/helpers/credentials_helper.rb

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,15 @@ def passkey_creation_form_for(resource, form_classes: nil, &block)
1717
end
1818
end
1919

20-
def login_with_passkey_button(text = nil, session_path:, button_classes: nil, form_classes: nil, &block)
20+
def login_with_passkey_form_for(session_path:, form_classes: nil, &block)
2121
form_with(
2222
url: session_path,
2323
method: :post,
2424
class: form_classes
2525
) do |f|
2626
tag.webauthn_get(data: { options_url: passkey_authentication_options_path(resource) }) do
2727
concat f.hidden_field(:public_key_credential, data: { webauthn_target: "response" })
28-
29-
concat f.button(text, type: "submit", class: button_classes, &block)
28+
concat capture(f, &block)
3029
end
3130
end
3231
end
@@ -46,15 +45,15 @@ def security_key_creation_form_for(resource, form_classes: nil, &block)
4645
end
4746
end
4847

49-
def login_with_security_key_button(text = nil, resource:, button_classes: nil, form_classes: nil, &block)
48+
def login_with_security_key_form_for(resource:, form_classes: nil, &block)
5049
form_with(
5150
url: two_factor_authentication_path(resource),
5251
method: :post,
5352
class: form_classes
5453
) do |f|
5554
tag.webauthn_get(data: { options_url: security_key_authentication_options_path(resource) }) do
5655
concat f.hidden_field(:public_key_credential, data: { webauthn_target: "response" })
57-
concat f.button(text, type: "submit", class: button_classes, &block)
56+
concat capture(f, &block)
5857
end
5958
end
6059
end
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# frozen_string_literal: true
2+
3+
# rubocop:disable RSpec/MultipleExpectations
4+
RSpec.describe Devise::Webauthn::CredentialsHelper, type: :helper do
5+
let(:user) do
6+
Account.create!(
7+
email: "testuser@example.com",
8+
password: "$3cretp@ssword123"
9+
)
10+
end
11+
12+
before do
13+
allow(helper).to receive_messages(resource: user, session: {})
14+
end
15+
16+
def parsed(html)
17+
Capybara.string(html)
18+
end
19+
20+
def have_hidden_credential_field
21+
have_css(
22+
"input[type='hidden'][name='public_key_credential'][data-webauthn-target='response']",
23+
visible: :hidden
24+
)
25+
end
26+
27+
describe "#passkey_creation_form_for" do
28+
it "renders a form with webauthn_create element" do
29+
html = helper.passkey_creation_form_for(user) do |form|
30+
form.submit "Create Passkey"
31+
end
32+
33+
page = parsed(html)
34+
expect(page).to have_css("form")
35+
expect(page).to have_css("webauthn-create[data-options-url='/accounts/passkey_registration_options']")
36+
expect(page).to have_hidden_credential_field
37+
expect(page).to have_button("Create Passkey")
38+
end
39+
40+
it "accepts form_classes option" do
41+
html = helper.passkey_creation_form_for(user, form_classes: "custom-form") do |form|
42+
form.submit "Create"
43+
end
44+
45+
expect(parsed(html)).to have_css("form.custom-form")
46+
end
47+
end
48+
49+
describe "#login_with_passkey_form_for" do
50+
it "renders a form with webauthn_get element" do
51+
html = helper.login_with_passkey_form_for(session_path: "/accounts/sign_in") do |form|
52+
form.submit "Log in with passkeys"
53+
end
54+
55+
page = parsed(html)
56+
expect(page).to have_css("form[action='/accounts/sign_in']")
57+
expect(page).to have_css("webauthn-get[data-options-url='/accounts/passkey_authentication_options']")
58+
expect(page).to have_hidden_credential_field
59+
expect(page).to have_button("Log in with passkeys")
60+
end
61+
62+
it "accepts form_classes option" do
63+
html = helper.login_with_passkey_form_for(session_path: "/sign_in", form_classes: "passkey-form") do |form|
64+
form.submit "Login"
65+
end
66+
67+
expect(parsed(html)).to have_css("form.passkey-form")
68+
end
69+
70+
it "allows custom content in the block" do
71+
html = helper.login_with_passkey_form_for(session_path: "/sign_in") do |form|
72+
helper.content_tag(:div, class: "button-wrapper") do
73+
form.submit "Sign in", class: "btn-primary"
74+
end
75+
end
76+
77+
page = parsed(html)
78+
expect(page).to have_css("div.button-wrapper")
79+
expect(page).to have_css("input.btn-primary[type='submit']")
80+
end
81+
end
82+
83+
describe "#security_key_creation_form_for" do
84+
it "renders a form with webauthn_create element" do
85+
html = helper.security_key_creation_form_for(user) do |form|
86+
form.submit "Add Security Key"
87+
end
88+
89+
page = parsed(html)
90+
expect(page).to have_css("form")
91+
expect(page).to have_css("webauthn-create[data-options-url='/accounts/security_key_registration_options']")
92+
expect(page).to have_hidden_credential_field
93+
expect(page).to have_button("Add Security Key")
94+
end
95+
96+
it "accepts form_classes option" do
97+
html = helper.security_key_creation_form_for(user, form_classes: "security-key-form") do |form|
98+
form.submit "Add"
99+
end
100+
101+
expect(parsed(html)).to have_css("form.security-key-form")
102+
end
103+
end
104+
105+
describe "#login_with_security_key_form_for" do
106+
it "renders a form with webauthn_get element" do
107+
html = helper.login_with_security_key_form_for(resource: user) do |form|
108+
form.submit "Use security key"
109+
end
110+
111+
page = parsed(html)
112+
expect(page).to have_css("form")
113+
expect(page).to have_css("webauthn-get[data-options-url='/accounts/security_key_authentication_options']")
114+
expect(page).to have_hidden_credential_field
115+
expect(page).to have_button("Use security key")
116+
end
117+
118+
it "accepts form_classes option" do
119+
html = helper.login_with_security_key_form_for(resource: user, form_classes: "two-factor-form") do |form|
120+
form.submit "Verify"
121+
end
122+
123+
expect(parsed(html)).to have_css("form.two-factor-form")
124+
end
125+
126+
it "allows custom content in the block" do
127+
html = helper.login_with_security_key_form_for(resource: user) do |form|
128+
helper.safe_join([helper.content_tag(:p, "Authenticate with your security key"), form.submit("Authenticate")])
129+
end
130+
131+
page = parsed(html)
132+
expect(page).to have_css("p", text: "Authenticate with your security key")
133+
expect(page).to have_button("Authenticate")
134+
end
135+
end
136+
end
137+
# rubocop:enable RSpec/MultipleExpectations

0 commit comments

Comments
 (0)