devise-webauthn 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +1 -1
  3. data/.github/workflows/ruby.yml +5 -4
  4. data/.rubocop.yml +2 -0
  5. data/.rubocop_todo.yml +20 -0
  6. data/Appraisals +0 -1
  7. data/CHANGELOG.md +56 -0
  8. data/Gemfile +1 -0
  9. data/Gemfile.lock +4 -1
  10. data/README.md +27 -10
  11. data/app/assets/javascript/devise/webauthn.js +28 -5
  12. data/app/controllers/devise/passkey_authentication_options_controller.rb +19 -0
  13. data/app/controllers/devise/passkey_registration_options_controller.rb +48 -0
  14. data/app/controllers/devise/security_key_authentication_options_controller.rb +28 -0
  15. data/app/controllers/devise/security_key_registration_options_controller.rb +48 -0
  16. data/app/views/devise/passkeys/new.html.erb +1 -1
  17. data/app/views/devise/second_factor_webauthn_credentials/new.html.erb +1 -1
  18. data/app/views/devise/sessions/new.html.erb +3 -1
  19. data/app/views/devise/two_factor_authentications/new.html.erb +3 -1
  20. data/gemfiles/devise_5_0.gemfile +1 -1
  21. data/gemfiles/rails_7_1.gemfile +1 -2
  22. data/gemfiles/rails_7_2.gemfile +1 -1
  23. data/gemfiles/rails_8_0.gemfile +1 -1
  24. data/gemfiles/rails_8_1.gemfile +1 -1
  25. data/gemfiles/rails_edge.gemfile +1 -1
  26. data/lib/devise/models/webauthn_credential_authenticatable.rb +8 -3
  27. data/lib/devise/strategies/passkey_authenticatable.rb +21 -5
  28. data/lib/devise/strategies/webauthn_two_factor_authenticatable.rb +8 -0
  29. data/lib/devise/webauthn/helpers/credentials_helper.rb +21 -104
  30. data/lib/devise/webauthn/routes.rb +9 -0
  31. data/lib/devise/webauthn/test/authenticator_helpers.rb +1 -1
  32. data/lib/devise/webauthn/url_helpers.rb +5 -1
  33. data/lib/devise/webauthn/version.rb +1 -1
  34. data/lib/generators/devise/webauthn/controllers_generator.rb +8 -1
  35. data/lib/generators/devise/webauthn/templates/controllers/passkey_authentication_options_controller.rb.tt +8 -0
  36. data/lib/generators/devise/webauthn/templates/controllers/passkey_registration_options_controller.rb.tt +8 -0
  37. data/lib/generators/devise/webauthn/templates/controllers/security_key_authentication_options_controller.rb.tt +8 -0
  38. data/lib/generators/devise/webauthn/templates/controllers/security_key_registration_options_controller.rb.tt +8 -0
  39. metadata +10 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bedbd7cbfbce63ffd28dd38a828a34081880b037355c0fedb7ed0ae37921f105
4
- data.tar.gz: 5bfc78ad1e764096d35fbf31b56b08d6370b731e757bd64e2c6af2b54c90c1f7
3
+ metadata.gz: 2c1fc0044f5c2bf947d79fd3db30ea05907980dac2abeacd4d43af55c7fc8fc3
4
+ data.tar.gz: d9513c05eaad396f2d6f8764a8f8964086678fa03e55b98375406e1de7303b67
5
5
  SHA512:
6
- metadata.gz: 58786281bafd7c1032e331a09b48716aba0b49fd725856b33beb59708c2afc77310c174d0300a32c1294e0741fc6ff64fe0405f54d397e77be2853ec4ab0e925
7
- data.tar.gz: e6a5d6fa654fb35bc854f96aa7783861da4370d65dd1dd72c43e10baa6cbea0291524fdbf2a175225778d80e584c734b6f88707a7731e61056657ce4b19bbd3b
6
+ metadata.gz: 53c013359a84a25616c4a4778a4e0c766916665d7c31192b74feab1f471f36ffe4867ef1ed74f82f80119c1bdde16c996df2e2782df1b4c3de3ac67e49ce9f60
7
+ data.tar.gz: afdd185b69e7208bcc489c6f707c2d3c77a3a44447f27650db5f985958dd2856b7ce117a684ba2273639d469bf4c39d02a0abe523d7d943b22b1c438d3e076c6
@@ -17,7 +17,7 @@ jobs:
17
17
  id-token: write
18
18
 
19
19
  steps:
20
- - uses: actions/checkout@v6
20
+ - uses: actions/checkout@v7
21
21
 
22
22
  - name: Setup Ruby
23
23
  uses: ruby/setup-ruby@v1
@@ -8,7 +8,7 @@ jobs:
8
8
 
9
9
  steps:
10
10
  - name: Check out repository code
11
- uses: actions/checkout@v6
11
+ uses: actions/checkout@v7
12
12
 
13
13
  - name: Set up Ruby
14
14
  uses: ruby/setup-ruby@v1
@@ -24,6 +24,7 @@ jobs:
24
24
  fail-fast: false
25
25
  matrix:
26
26
  ruby:
27
+ - '4.0'
27
28
  - 3.4
28
29
  - 3.3
29
30
  - 3.2
@@ -73,14 +74,14 @@ jobs:
73
74
 
74
75
  steps:
75
76
  - name: Check out repository code
76
- uses: actions/checkout@v6
77
+ uses: actions/checkout@v7
77
78
 
78
79
  - name: Set up Ruby
79
80
  uses: ruby/setup-ruby@v1
80
81
  with:
81
82
  ruby-version: ${{ matrix.ruby }}
83
+ rubygems: latest
82
84
  bundler-cache: true
83
85
 
84
86
  - name: Run tests
85
- run: |
86
- bundle exec rspec
87
+ run: bundle exec rspec
data/.rubocop.yml CHANGED
@@ -1,3 +1,5 @@
1
+ inherit_from: .rubocop_todo.yml
2
+
1
3
  plugins:
2
4
  - rubocop-rspec
3
5
  - rubocop-rails
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,20 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2026-01-02 20:36:46 UTC using RuboCop version 1.79.1.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 1
10
+ # Configuration parameters: Max, CountAsOne.
11
+ RSpec/ExampleLength:
12
+ Exclude:
13
+ - 'spec/requests/devise/two_factor_authentication_spec.rb'
14
+
15
+ # Offense count: 9
16
+ # Configuration parameters: Max.
17
+ RSpec/MultipleExpectations:
18
+ Exclude:
19
+ - 'spec/requests/devise/passkey_authentication_spec.rb'
20
+ - 'spec/requests/devise/two_factor_authentication_spec.rb'
data/Appraisals CHANGED
@@ -22,7 +22,6 @@ appraise "rails-7_1" do
22
22
  gem "capybara", "~> 3.39"
23
23
  gem "importmap-rails", "~> 2.0"
24
24
  gem "pry-byebug", "~> 3.10"
25
- gem "psych", "~> 4.0"
26
25
  gem "rack", "~> 2.2"
27
26
  gem "rspec-rails", "~> 7.1"
28
27
  gem "sqlite3", "~> 1.7"
data/CHANGELOG.md CHANGED
@@ -2,6 +2,59 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## [v0.5.0](https://github.com/cedarcode/devise-webauthn/compare/v0.4.0...v0.5.0/) - 2026-07-13
6
+
7
+ ### Added
8
+
9
+ - Submit WebAuthn forms with `requestSubmit` so Turbo can intercept. [#140](https://github.com/cedarcode/devise-webauthn/pull/140) [@santiagorodriguez96]
10
+
11
+ ### Changed
12
+
13
+ - BREAKING!: The WebAuthn options endpoints (`passkey_registration_options`, `passkey_authentication_options`, `security_key_registration_options` and `security_key_authentication_options`) now respond to `POST` instead of `GET`, since they mutate server state (they store the WebAuthn challenge in the session). If you overrode any of these controllers, rename the overridden `index` action to `create`; if you copied the bundled JavaScript into your app, update your copy. [#147](https://github.com/cedarcode/devise-webauthn/pull/147)[@santiagorodriguez96]
14
+ - BREAKING!: `webauthn_id` is now generated and persisted when WebAuthn registration options are requested, instead of eagerly in a `before_validation` callback, so backfilling existing records is no longer needed and the migration generated by `devise:webauthn:webauthn_id` no longer includes one. If your app relies on `webauthn_id` being set on create (e.g. you added a `NOT NULL` constraint to the column), add your own callback or database default, or drop the constraint. [#148](https://github.com/cedarcode/devise-webauthn/pull/148) [@santiagorodriguez96]
15
+
16
+ ## [v0.4.0](https://github.com/cedarcode/devise-webauthn/compare/v0.3.1...v0.4.0/) - 2026-04-06
17
+
18
+ ### Added
19
+
20
+ - Dispatch `webauthn:unsupported` for browsers missing `parseOptionsFromJSON`. [#127](https://github.com/cedarcode/devise-webauthn/pull/127) [@santiagorodriguez96]
21
+ - Scope `login_with_passkey_form_for` to the Devise resource so that form builder fields (e.g. `f.check_box :remember_me`) are properly namespaced (e.g. `account[remember_me]`). [#134](https://github.com/cedarcode/devise-webauthn/pull/134) [@RenzoMinelli]
22
+ - Allow passing a -c flag to the controller generator to specify which controller to override. [#110](https://github.com/cedarcode/devise-webauthn/pull/110) [@nicolastemciuc]
23
+
24
+ ### Changed
25
+
26
+ - Change `webauthn_id` generation from `after_initialize` to `before_validation` and add a data backfill to the `webauthn_id` migration generator for existing records. [#125](https://github.com/cedarcode/devise-webauthn/pull/125) [@santiagorodriguez96]
27
+ - 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]
28
+ - BREAKING!: Remove helpers for generating WebAuthn options. [#106](https://github.com/cedarcode/devise-webauthn/pull/115) [@nicolastemciuc]
29
+ - 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. [#112](https://github.com/cedarcode/devise-webauthn/pull/112) [@RenzoMinelli]
30
+ ```erb
31
+ <%# Before %>
32
+ <%= login_with_passkey_button(:user, "Log in with passkeys") %>
33
+
34
+ <%# After %>
35
+ <%= login_with_passkey_form_for(:user) do |form| %>
36
+ <%= form.submit "Log in with passkeys" %>
37
+ <% end %>
38
+ ```
39
+ - BREAKING!: Replace `form_classes:` keyword argument with direct keyword arguments in all form helper methods (`passkey_creation_form_for`, `login_with_passkey_form_for`, `security_key_creation_form_for`, `login_with_security_key_form_for`). All options are delegated to `form_with`, allowing you to pass any HTML attributes or form options directly. [#111](https://github.com/cedarcode/devise-webauthn/pull/111) [@RenzoMinelli]
40
+ ```erb
41
+ <%# Before %>
42
+ <%= passkey_creation_form_for(:user, form_classes: "my-class") do |form| %>
43
+ ...
44
+ <% end %>
45
+
46
+ <%# After %>
47
+ <%= passkey_creation_form_for(:user, class: "my-class", id: "my-form", data: { turbo: false }) do |form| %>
48
+ ...
49
+ <% end %>
50
+ ```
51
+
52
+ ### Fixed
53
+
54
+ - Validate `userHandle` from authenticator response against `webauthn_id`. [#131](https://github.com/cedarcode/devise-webauthn/pull/131) [@santiagorodriguez96]
55
+ - Fix `Remember me` checkbox not honored when logging in with passkeys. [#133](https://github.com/cedarcode/devise-webauthn/pull/133) [@santiagorodriguez96]
56
+ - 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]
57
+
5
58
  ## [v0.3.1](https://github.com/cedarcode/devise-webauthn/compare/v0.3.0...v0.3.1/) - 2026-02-10
6
59
 
7
60
  ### Fixed
@@ -22,6 +75,9 @@
22
75
  - Previously generated Stimulus controller for handling WebAuthn client logic are no longer generated.
23
76
  - Stimulus is no longer needed for this engine to work.
24
77
  - Make helpers for generating WebAuthn options public methods. [#106](https://github.com/cedarcode/devise-webauthn/pull/106) [@santiagorodriguez96]
78
+ - BREAKING!: Our controller for managing second factor credentials now uses a separate method for each endpoint for setting the URL to redirect to. [#80](https://github.com/cedarcode/devise-webauthn/pull/80) [@nicolastemciuc]
79
+ - What used to be just an `after_update_path` for all endpoints, now it's an `after_(create|update|destroy)_path` for each endpoint.
80
+ - If you had overriden the controller to change the `after_update_path`, be mindful that now `create` and `destroy` endpoints will call its own method.
25
81
 
26
82
  ### Fixed
27
83
 
data/Gemfile CHANGED
@@ -17,6 +17,7 @@ gem "pry-byebug", "~> 3.11"
17
17
  gem "puma", "~> 6.6"
18
18
  gem "rails", "~> 8.0"
19
19
  gem "rspec-rails", "~> 8.0"
20
+ gem "rspec-retry"
20
21
  gem "rubocop", "~> 1.79"
21
22
  gem "rubocop-rails", "~> 2.32"
22
23
  gem "rubocop-rspec", "~> 3.6"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- devise-webauthn (0.3.1)
4
+ devise-webauthn (0.5.0)
5
5
  devise (>= 4.9)
6
6
  webauthn (~> 3.0)
7
7
 
@@ -266,6 +266,8 @@ GEM
266
266
  rspec-expectations (~> 3.13)
267
267
  rspec-mocks (~> 3.13)
268
268
  rspec-support (~> 3.13)
269
+ rspec-retry (0.6.2)
270
+ rspec-core (> 3.3)
269
271
  rspec-support (3.13.4)
270
272
  rubocop (1.79.1)
271
273
  json (~> 2.3)
@@ -351,6 +353,7 @@ DEPENDENCIES
351
353
  puma (~> 6.6)
352
354
  rails (~> 8.0)
353
355
  rspec-rails (~> 8.0)
356
+ rspec-retry
354
357
  rubocop (~> 1.79)
355
358
  rubocop-rails (~> 2.32)
356
359
  rubocop-rspec (~> 3.6)
data/README.md CHANGED
@@ -73,6 +73,8 @@ Then, follow these steps to integrate Devise::Webauthn:
73
73
  config.rp_name = "Your App Name"
74
74
  end
75
75
  ```
76
+ > [!TIP]
77
+ > You can find a working example on how to use this gem for passwordless and two factor authentication in [`devise-webauthn-rails-demo`](https://github.com/cedarcode/devise-webauthn-demo-app).
76
78
 
77
79
  5. **Include bundled WebAuthn JavaScript in your application:**
78
80
  The install generator automatically configures JavaScript loading based on your setup:
@@ -143,16 +145,27 @@ $ bin/rails generate devise:webauthn:views -v passkeys
143
145
  ```
144
146
 
145
147
  ### Helper methods
146
- Devise::Webauthn provides helpers that can be used in your views. For example, for a resource named `user`, you can use the following helpers:
148
+ Devise::Webauthn provides helpers that can be used in your views. These helpers accept either a resource name (e.g., `:user`) or a resource object (e.g., `@user`) as the first argument.
147
149
 
148
- To add a button for logging in with passkeys:
150
+ For example, for a resource named `user`, you can use the following helpers:
151
+
152
+ To add a form for logging in with passkeys:
153
+ ```erb
154
+ <%= login_with_passkey_form_for(:user) do |form| %>
155
+ <%= form.submit "Log in with passkeys" %>
156
+ <% end %>
157
+ ```
158
+
159
+ To add a form for logging in with security keys (2FA):
149
160
  ```erb
150
- <%= login_with_passkey_button("Log in with passkeys", session_path: user_session_path) %>
161
+ <%= login_with_security_key_form_for(@resource) do |form| %>
162
+ <%= form.submit "Use security key" %>
163
+ <% end %>
151
164
  ```
152
165
 
153
166
  To add a passkeys creation form:
154
167
  ```erb
155
- <%= passkey_creation_form_for(current_user) do |form| %>
168
+ <%= passkey_creation_form_for(:user) do |form| %>
156
169
  <%= form.label :name, 'Passkey name' %>
157
170
  <%= form.text_field :name, required: true %>
158
171
  <%= form.submit 'Create Passkey' %>
@@ -270,7 +283,7 @@ Used for registering new credentials (passkeys or security keys).
270
283
 
271
284
  ```html
272
285
  <form action="/passkeys" method="post">
273
- <webauthn-create data-options-json="<%= create_passkey_options(@user).to_json %>">
286
+ <webauthn-create data-options-url="<%= passkey_registration_options_path(:user) %>">
274
287
  <input type="hidden" name="public_key_credential" data-webauthn-target="response">
275
288
  <input type="text" name="name" placeholder="Passkey name">
276
289
  <button type="submit">Create Passkey</button>
@@ -283,9 +296,11 @@ Used for registering new credentials (passkeys or security keys).
283
296
  - The form's action should point to the appropriate endpoint – you can use the provided url helpers:
284
297
  - For creating passkeys: `passkeys_path(resource_name)`
285
298
  - For creating 2FA security keys: `second_factor_webauthn_credentials_path(resource_name)`
286
- - Requires a `data-options-json` attribute containing JSON-serialized WebAuthn creation options
299
+ - Requires a `data-options-url` attribute pointing to the endpoint serving the WebAuthn creation options – you can use the provided url helpers:
300
+ - For passkey creation options: `passkey_registration_options_path(resource_name)`
301
+ - For 2FA security key creation options: `security_key_registration_options_path(resource_name)`
287
302
  - Must contain a hidden input with `data-webauthn-target="response"` to store the credential response
288
- - Must contain the submit button the element intercepts form submission, calls the WebAuthn API, stores the credential in the hidden input, and then re-submits the form
303
+ - Must contain the submit button the element intercepts form submission, fetches the options, calls the WebAuthn API, stores the credential in the hidden input, and then re-submits the form
289
304
 
290
305
  #### `<webauthn-get>`
291
306
 
@@ -293,7 +308,7 @@ Used for authenticating with existing credentials.
293
308
 
294
309
  ```html
295
310
  <form action="/users/sign_in" method="post">
296
- <webauthn-get data-options-json="<%= passkey_authentication_options.to_json %>">
311
+ <webauthn-get data-options-url="<%= passkey_authentication_options_path(:user) %>">
297
312
  <input type="hidden" name="public_key_credential" data-webauthn-target="response">
298
313
  <button type="submit">Sign in with Passkey</button>
299
314
  </webauthn-get>
@@ -305,9 +320,11 @@ Used for authenticating with existing credentials.
305
320
  - The form's action should point to the appropriate endpoint – you can use the provided url helpers:
306
321
  - For passkey sign-in: `session_path(resource_name)`
307
322
  - For 2FA with WebAuthn: `two_factor_authentication_path(resource_name)`
308
- - Requires a `data-options-json` attribute containing JSON-serialized WebAuthn request options
323
+ - Requires a `data-options-url` attribute pointing to the endpoint serving the WebAuthn request options – you can use the provided url helpers:
324
+ - For passkey authentication options: `passkey_authentication_options_path(resource_name)`
325
+ - For 2FA security key authentication options: `security_key_authentication_options_path(resource_name)`
309
326
  - Must contain a hidden input with `data-webauthn-target="response"` to store the credential response
310
- - Must contain the submit button the element intercepts form submission, calls the WebAuthn API, stores the credential in the hidden input, and then re-submits the form
327
+ - Must contain the submit button the element intercepts form submission, fetches the options, calls the WebAuthn API, stores the credential in the hidden input, and then re-submits the form
311
328
 
312
329
  ## Development
313
330
 
@@ -3,7 +3,9 @@ function isWebAuthnSupported() {
3
3
  navigator.credentials &&
4
4
  navigator.credentials.create &&
5
5
  navigator.credentials.get &&
6
- window.PublicKeyCredential
6
+ window.PublicKeyCredential &&
7
+ PublicKeyCredential.parseCreationOptionsFromJSON &&
8
+ PublicKeyCredential.parseRequestOptionsFromJSON
7
9
  );
8
10
  }
9
11
 
@@ -17,17 +19,21 @@ export class WebauthnCreateElement extends HTMLElement {
17
19
  }
18
20
 
19
21
  this.closest('form').addEventListener('submit', async (event) => {
22
+ if (this.dataset.ceremonyCompleted) return;
23
+
20
24
  event.preventDefault();
21
25
 
22
26
  try {
23
- const options = JSON.parse(this.getAttribute('data-options-json'));
27
+ const options = await fetchOptions(this.getAttribute('data-options-url'));
24
28
  const publicKey = PublicKeyCredential.parseCreationOptionsFromJSON(options);
25
29
  const credential = await navigator.credentials.create({ publicKey });
26
30
 
27
31
  this.querySelector('[data-webauthn-target="response"]').value = await this.stringifyRegistrationCredentialWithGracefullyHandlingAuthenticatorIssues(credential);
28
32
 
29
- this.closest('form').submit();
33
+ this.dataset.ceremonyCompleted = "true";
34
+ this.closest('form').requestSubmit();
30
35
  } catch (error) {
36
+ delete this.dataset.ceremonyCompleted;
31
37
  this.handleError(error);
32
38
  }
33
39
  });
@@ -98,17 +104,21 @@ export class WebauthnGetElement extends HTMLElement {
98
104
  }
99
105
 
100
106
  this.closest('form').addEventListener('submit', async (event) => {
107
+ if (this.dataset.ceremonyCompleted) return;
108
+
101
109
  event.preventDefault();
102
110
 
103
111
  try {
104
- const options = JSON.parse(this.getAttribute('data-options-json'));
112
+ const options = await fetchOptions(this.getAttribute('data-options-url'));
105
113
  const publicKey = PublicKeyCredential.parseRequestOptionsFromJSON(options);
106
114
  const credential = await navigator.credentials.get({ publicKey });
107
115
 
108
116
  this.querySelector('[data-webauthn-target="response"]').value = await this.stringifyAuthenticationCredentialWithGracefullyHandlingAuthenticatorIssues(credential);
109
117
 
110
- this.closest('form').submit();
118
+ this.dataset.ceremonyCompleted = "true";
119
+ this.closest('form').requestSubmit();
111
120
  } catch (error) {
121
+ delete this.dataset.ceremonyCompleted;
112
122
  this.handleError(error);
113
123
  }
114
124
  });
@@ -166,6 +176,19 @@ export class WebauthnGetElement extends HTMLElement {
166
176
  }
167
177
  }
168
178
 
179
+ async function fetchOptions(url) {
180
+ const response = await fetch(url, {
181
+ method: 'POST',
182
+ headers: { 'Accept': 'application/json' }
183
+ });
184
+
185
+ if (!response.ok) {
186
+ throw new Error(`Fetching WebAuthn options failed with status ${response.status}`);
187
+ }
188
+
189
+ return response.json();
190
+ }
191
+
169
192
  function toBase64Url(buffer) {
170
193
  if (!buffer) return null;
171
194
 
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Devise
4
+ class PasskeyAuthenticationOptionsController < DeviseController
5
+ skip_forgery_protection
6
+
7
+ def create
8
+ passkey_options =
9
+ WebAuthn::Credential.options_for_get(
10
+ user_verification: "required"
11
+ )
12
+
13
+ # Store challenge in session for later verification
14
+ session[:authentication_challenge] = passkey_options.challenge
15
+
16
+ render json: passkey_options
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Devise
4
+ class PasskeyRegistrationOptionsController < DeviseController
5
+ skip_forgery_protection
6
+
7
+ before_action :authenticate_scope!
8
+ before_action :ensure_webauthn_id!
9
+
10
+ def create
11
+ passkey_options =
12
+ WebAuthn::Credential.options_for_create(
13
+ user: {
14
+ id: resource.webauthn_id,
15
+ name: resource_human_palatable_identifier
16
+ },
17
+ exclude: resource.passkeys.pluck(:external_id),
18
+ authenticator_selection: {
19
+ resident_key: "required",
20
+ user_verification: "required"
21
+ }
22
+ )
23
+
24
+ # Store challenge in session for later verification
25
+ session[:webauthn_challenge] = passkey_options.challenge
26
+
27
+ render json: passkey_options
28
+ end
29
+
30
+ private
31
+
32
+ def authenticate_scope!
33
+ send(:"authenticate_#{resource_name}!", force: true)
34
+ self.resource = send(:"current_#{resource_name}")
35
+ end
36
+
37
+ def ensure_webauthn_id!
38
+ resource.ensure_webauthn_id!
39
+ end
40
+
41
+ def resource_human_palatable_identifier
42
+ authentication_keys = resource.class.authentication_keys
43
+ authentication_keys = authentication_keys.keys if authentication_keys.is_a?(Hash)
44
+
45
+ authentication_keys.filter_map { |authentication_key| resource.public_send(authentication_key) }.first
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Devise
4
+ class SecurityKeyAuthenticationOptionsController < DeviseController
5
+ skip_forgery_protection
6
+
7
+ before_action :set_resource
8
+
9
+ def create
10
+ security_key_authentication_options =
11
+ WebAuthn::Credential.options_for_get(
12
+ allow: @resource.webauthn_credentials.pluck(:external_id),
13
+ user_verification: "discouraged"
14
+ )
15
+
16
+ # Store challenge in session for later verification
17
+ session[:two_factor_authentication_challenge] = security_key_authentication_options.challenge
18
+
19
+ render json: security_key_authentication_options
20
+ end
21
+
22
+ private
23
+
24
+ def set_resource
25
+ @resource = resource_class.find(session[:current_authentication_resource_id])
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Devise
4
+ class SecurityKeyRegistrationOptionsController < DeviseController
5
+ skip_forgery_protection
6
+
7
+ before_action :authenticate_scope!
8
+ before_action :ensure_webauthn_id!
9
+
10
+ def create
11
+ create_security_key_options =
12
+ WebAuthn::Credential.options_for_create(
13
+ user: {
14
+ id: resource.webauthn_id,
15
+ name: resource_human_palatable_identifier
16
+ },
17
+ exclude: resource.webauthn_credentials.pluck(:external_id),
18
+ authenticator_selection: {
19
+ resident_key: "discouraged",
20
+ user_verification: "discouraged"
21
+ }
22
+ )
23
+
24
+ # Store challenge in session for later verification
25
+ session[:webauthn_challenge] = create_security_key_options.challenge
26
+
27
+ render json: create_security_key_options
28
+ end
29
+
30
+ private
31
+
32
+ def authenticate_scope!
33
+ send(:"authenticate_#{resource_name}!", force: true)
34
+ self.resource = send(:"current_#{resource_name}")
35
+ end
36
+
37
+ def ensure_webauthn_id!
38
+ resource.ensure_webauthn_id!
39
+ end
40
+
41
+ def resource_human_palatable_identifier
42
+ authentication_keys = resource.class.authentication_keys
43
+ authentication_keys = authentication_keys.keys if authentication_keys.is_a?(Hash)
44
+
45
+ authentication_keys.filter_map { |authentication_key| resource.public_send(authentication_key) }.first
46
+ end
47
+ end
48
+ end
@@ -1,4 +1,4 @@
1
- <%= passkey_creation_form_for(resource) do |form| %>
1
+ <%= passkey_creation_form_for(resource_name) do |form| %>
2
2
  <%= form.label :name, 'Passkey name' %>
3
3
  <%= form.text_field :name, required: true %>
4
4
  <%= form.submit 'Create Passkey' %>
@@ -1,4 +1,4 @@
1
- <%= security_key_creation_form_for(resource) do |form| %>
1
+ <%= security_key_creation_form_for(resource_name) do |form| %>
2
2
  <%= form.label :name, 'Security Key name' %>
3
3
  <%= form.text_field :name, required: true %>
4
4
  <%= form.submit 'Create Security Key' %>
@@ -23,6 +23,8 @@
23
23
  </div>
24
24
  <% end %>
25
25
 
26
- <%= login_with_passkey_button("Log in with passkeys", session_path: session_path(resource_name)) %>
26
+ <%= login_with_passkey_form_for(resource_name) do |form| %>
27
+ <%= form.submit "Log in with passkeys" %>
28
+ <% end %>
27
29
 
28
30
  <%= render "devise/shared/links" %>
@@ -1 +1,3 @@
1
- <%= login_with_security_key_button('Use security key', resource: @resource) %>
1
+ <%= login_with_security_key_form_for(resource_name) do |form| %>
2
+ <%= form.submit 'Use security key' %>
3
+ <% end %>
@@ -12,12 +12,12 @@ gem "pry-byebug", "~> 3.10"
12
12
  gem "puma", "~> 6.6"
13
13
  gem "rails", ">= 7.1"
14
14
  gem "rspec-rails", ">= 7.1"
15
+ gem "rspec-retry"
15
16
  gem "rubocop", "~> 1.79"
16
17
  gem "rubocop-rails", "~> 2.32"
17
18
  gem "rubocop-rspec", "~> 3.6"
18
19
  gem "selenium-webdriver"
19
20
  gem "sqlite3", ">= 1.6", "!= 1.7.0", "!= 1.7.1", "!= 1.7.2", "!= 1.7.3"
20
- gem "stimulus-rails", "~> 1.3"
21
21
 
22
22
  install_if -> { RUBY_VERSION < "3.0" } do
23
23
  gem "rack", "~> 2.2"
@@ -12,13 +12,12 @@ gem "pry-byebug", "~> 3.10"
12
12
  gem "puma", "~> 6.6"
13
13
  gem "rails", "~> 7.1.x"
14
14
  gem "rspec-rails", "~> 7.1"
15
+ gem "rspec-retry"
15
16
  gem "rubocop", "~> 1.79"
16
17
  gem "rubocop-rails", "~> 2.32"
17
18
  gem "rubocop-rspec", "~> 3.6"
18
19
  gem "selenium-webdriver"
19
20
  gem "sqlite3", "~> 1.7"
20
- gem "stimulus-rails", "~> 1.3"
21
- gem "psych", "~> 4.0"
22
21
  gem "rack", "~> 2.2"
23
22
 
24
23
  gemspec path: "../"
@@ -12,11 +12,11 @@ gem "pry-byebug", "~> 3.11"
12
12
  gem "puma", "~> 6.6"
13
13
  gem "rails", "~> 7.2.x"
14
14
  gem "rspec-rails", "~> 8.0"
15
+ gem "rspec-retry"
15
16
  gem "rubocop", "~> 1.79"
16
17
  gem "rubocop-rails", "~> 2.32"
17
18
  gem "rubocop-rspec", "~> 3.6"
18
19
  gem "selenium-webdriver"
19
20
  gem "sqlite3", "~> 2.7"
20
- gem "stimulus-rails", "~> 1.3"
21
21
 
22
22
  gemspec path: "../"
@@ -12,11 +12,11 @@ gem "pry-byebug", "~> 3.11"
12
12
  gem "puma", "~> 6.6"
13
13
  gem "rails", "~> 8.0.x"
14
14
  gem "rspec-rails", "~> 8.0"
15
+ gem "rspec-retry"
15
16
  gem "rubocop", "~> 1.79"
16
17
  gem "rubocop-rails", "~> 2.32"
17
18
  gem "rubocop-rspec", "~> 3.6"
18
19
  gem "selenium-webdriver"
19
20
  gem "sqlite3", "~> 2.7"
20
- gem "stimulus-rails", "~> 1.3"
21
21
 
22
22
  gemspec path: "../"
@@ -12,11 +12,11 @@ gem "pry-byebug", "~> 3.11"
12
12
  gem "puma", "~> 6.6"
13
13
  gem "rails", "~> 8.1.x"
14
14
  gem "rspec-rails", "~> 8.0"
15
+ gem "rspec-retry"
15
16
  gem "rubocop", "~> 1.79"
16
17
  gem "rubocop-rails", "~> 2.32"
17
18
  gem "rubocop-rspec", "~> 3.6"
18
19
  gem "selenium-webdriver"
19
20
  gem "sqlite3", "~> 2.7"
20
- gem "stimulus-rails", "~> 1.3"
21
21
 
22
22
  gemspec path: "../"
@@ -12,11 +12,11 @@ gem "pry-byebug", "~> 3.11"
12
12
  gem "puma", "~> 6.6"
13
13
  gem "rails", branch: "main", git: "https://github.com/rails/rails"
14
14
  gem "rspec-rails", "~> 8.0"
15
+ gem "rspec-retry"
15
16
  gem "rubocop", "~> 1.79"
16
17
  gem "rubocop-rails", "~> 2.32"
17
18
  gem "rubocop-rspec", "~> 3.6"
18
19
  gem "selenium-webdriver"
19
20
  gem "sqlite3", "~> 2.7"
20
- gem "stimulus-rails", "~> 1.3"
21
21
 
22
22
  gemspec path: "../"
@@ -11,10 +11,15 @@ module Devise
11
11
  has_many :webauthn_credentials, dependent: :destroy
12
12
 
13
13
  validates :webauthn_id, uniqueness: true, allow_blank: true
14
+ end
15
+
16
+ def ensure_webauthn_id!
17
+ return webauthn_id if webauthn_id.present?
18
+
19
+ self.webauthn_id = WebAuthn.generate_user_id
20
+ save!(validate: false)
14
21
 
15
- after_initialize do
16
- self.webauthn_id ||= WebAuthn.generate_user_id
17
- end
22
+ webauthn_id
18
23
  end
19
24
  end
20
25
  end
@@ -7,15 +7,19 @@ module Devise
7
7
  passkey_param.present? && session[:authentication_challenge].present?
8
8
  end
9
9
 
10
- def authenticate!
10
+ def authenticate! # rubocop:disable Metrics/AbcSize
11
11
  passkey_from_params = WebAuthn::Credential.from_get(JSON.parse(passkey_param))
12
- stored_passkey = WebauthnCredential.passkey.find_by(external_id: passkey_from_params.id)
12
+
13
+ return fail!(:passkey_not_found) if passkey_from_params.user_handle.nil?
14
+
15
+ resource = resource_class.find_by(webauthn_id: passkey_from_params.user_handle)
16
+ stored_passkey = resource&.passkeys&.find_by(external_id: passkey_from_params.id)
13
17
 
14
18
  return fail!(:passkey_not_found) if stored_passkey.blank?
15
19
 
16
20
  verify_passkeys(passkey_from_params, stored_passkey)
17
21
 
18
- resource = stored_passkey.public_send(resource_name)
22
+ remember_me(resource)
19
23
  success!(resource)
20
24
  rescue WebAuthn::Error
21
25
  fail!(:passkey_verification_failed)
@@ -40,8 +44,20 @@ module Devise
40
44
  stored_passkey.update!(sign_count: passkey_from_params.sign_count)
41
45
  end
42
46
 
43
- def resource_name
44
- mapping.to.name.underscore
47
+ def remember_me(resource)
48
+ resource.remember_me = remember_me? if resource.respond_to?(:remember_me=)
49
+ end
50
+
51
+ def remember_me?
52
+ params_auth_hash.is_a?(Hash) && Devise::TRUE_VALUES.include?(params_auth_hash[:remember_me])
53
+ end
54
+
55
+ def params_auth_hash
56
+ params[scope]
57
+ end
58
+
59
+ def resource_class
60
+ mapping.to
45
61
  end
46
62
  end
47
63
  end
@@ -16,6 +16,9 @@ module Devise
16
16
  stored_credential = resource&.webauthn_credentials&.find_by(external_id: credential_from_params.id)
17
17
 
18
18
  return fail!(:webauthn_credential_not_found) if stored_credential.blank?
19
+ if user_handle_mismatch?(credential_from_params, resource)
20
+ return fail!(:webauthn_credential_verification_failed)
21
+ end
19
22
 
20
23
  verify_credential(credential_from_params, stored_credential)
21
24
 
@@ -47,6 +50,11 @@ module Devise
47
50
  stored_credential.update!(sign_count: credential_from_params.sign_count)
48
51
  end
49
52
 
53
+ def user_handle_mismatch?(credential_from_params, resource)
54
+ credential_from_params.user_handle.present? &&
55
+ credential_from_params.user_handle != resource.webauthn_id
56
+ end
57
+
50
58
  def resource_class
51
59
  mapping.to
52
60
  end
@@ -1,140 +1,57 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Metrics/ModuleLength
4
3
  module Devise
5
4
  module Webauthn
6
5
  module CredentialsHelper
7
- def passkey_creation_form_for(resource, form_classes: nil, &block)
6
+ def passkey_creation_form_for(resource_or_resource_name, **options, &block)
8
7
  form_with(
9
- url: passkeys_path(resource),
10
- method: :post,
11
- class: form_classes
8
+ **options, url: passkeys_path(resource_or_resource_name), method: :post
12
9
  ) do |f|
13
- tag.webauthn_create(data: { options_json: create_passkey_options(resource) }) do
10
+ tag.webauthn_create(data: { options_url: passkey_registration_options_path(resource_or_resource_name) }) do
14
11
  concat f.hidden_field(:public_key_credential, data: { webauthn_target: "response" })
15
12
  concat capture(f, &block)
16
13
  end
17
14
  end
18
15
  end
19
16
 
20
- def login_with_passkey_button(text = nil, session_path:, button_classes: nil, form_classes: nil, &block)
17
+ def login_with_passkey_form_for(resource_or_resource_name, **options, &block)
18
+ scope = Devise::Mapping.find_scope!(resource_or_resource_name)
19
+
21
20
  form_with(
22
- url: session_path,
23
- method: :post,
24
- class: form_classes
21
+ **options, scope: scope, url: session_path(resource_or_resource_name), method: :post
25
22
  ) do |f|
26
- tag.webauthn_get(data: { options_json: passkey_authentication_options }) do
27
- concat f.hidden_field(:public_key_credential, data: { webauthn_target: "response" })
28
-
29
- concat f.button(text, type: "submit", class: button_classes, &block)
23
+ tag.webauthn_get(data: { options_url: passkey_authentication_options_path(resource_or_resource_name) }) do
24
+ concat hidden_field_tag(:public_key_credential, nil, data: { webauthn_target: "response" })
25
+ concat capture(f, &block)
30
26
  end
31
27
  end
32
28
  end
33
29
 
34
- def security_key_creation_form_for(resource, form_classes: nil, &block)
30
+ def security_key_creation_form_for(resource_or_resource_name, **options, &block)
35
31
  form_with(
36
- url: second_factor_webauthn_credentials_path(resource),
37
- method: :post,
38
- class: form_classes
32
+ **options, url: second_factor_webauthn_credentials_path(resource_or_resource_name), method: :post
39
33
  ) do |f|
40
- tag.webauthn_create(data: { options_json: create_security_key_options(resource) }) do
34
+ tag.webauthn_create(
35
+ data: { options_url: security_key_registration_options_path(resource_or_resource_name) }
36
+ ) do
41
37
  concat f.hidden_field(:public_key_credential, data: { webauthn_target: "response" })
42
38
  concat capture(f, &block)
43
39
  end
44
40
  end
45
41
  end
46
42
 
47
- def login_with_security_key_button(text = nil, resource:, button_classes: nil, form_classes: nil, &block)
43
+ def login_with_security_key_form_for(resource_or_resource_name, **options, &block)
48
44
  form_with(
49
- url: two_factor_authentication_path(resource),
50
- method: :post,
51
- class: form_classes
45
+ **options, url: two_factor_authentication_path(resource_or_resource_name), method: :post
52
46
  ) do |f|
53
- tag.webauthn_get(data: { options_json: security_key_authentication_options(resource) }) do
47
+ tag.webauthn_get(data: {
48
+ options_url: security_key_authentication_options_path(resource_or_resource_name)
49
+ }) do
54
50
  concat f.hidden_field(:public_key_credential, data: { webauthn_target: "response" })
55
- concat f.button(text, type: "submit", class: button_classes, &block)
51
+ concat capture(f, &block)
56
52
  end
57
53
  end
58
54
  end
59
-
60
- def create_passkey_options(resource)
61
- @create_passkey_options ||= begin
62
- options = WebAuthn::Credential.options_for_create(
63
- user: {
64
- id: resource.webauthn_id,
65
- name: resource_human_palatable_identifier
66
- },
67
- exclude: resource.passkeys.pluck(:external_id),
68
- authenticator_selection: {
69
- resident_key: "required",
70
- user_verification: "required"
71
- }
72
- )
73
-
74
- # Store challenge in session for later verification
75
- session[:webauthn_challenge] = options.challenge
76
-
77
- options
78
- end
79
- end
80
-
81
- def passkey_authentication_options
82
- @passkey_authentication_options ||= begin
83
- options = WebAuthn::Credential.options_for_get(
84
- user_verification: "required"
85
- )
86
-
87
- # Store challenge in session for later verification
88
- session[:authentication_challenge] = options.challenge
89
-
90
- options
91
- end
92
- end
93
-
94
- def create_security_key_options(resource)
95
- @create_security_key_options ||= begin
96
- options = WebAuthn::Credential.options_for_create(
97
- user: {
98
- id: resource.webauthn_id,
99
- name: resource_human_palatable_identifier
100
- },
101
- exclude: resource.webauthn_credentials.pluck(:external_id),
102
- authenticator_selection: {
103
- resident_key: "discouraged",
104
- user_verification: "discouraged"
105
- }
106
- )
107
-
108
- # Store challenge in session for later verification
109
- session[:webauthn_challenge] = options.challenge
110
-
111
- options
112
- end
113
- end
114
-
115
- def security_key_authentication_options(resource)
116
- @security_key_authentication_options ||= begin
117
- options = WebAuthn::Credential.options_for_get(
118
- allow: resource.webauthn_credentials.pluck(:external_id),
119
- user_verification: "discouraged"
120
- )
121
-
122
- # Store challenge in session for later verification
123
- session[:two_factor_authentication_challenge] = options.challenge
124
-
125
- options
126
- end
127
- end
128
-
129
- private
130
-
131
- def resource_human_palatable_identifier
132
- authentication_keys = resource.class.authentication_keys
133
- authentication_keys = authentication_keys.keys if authentication_keys.is_a?(Hash)
134
-
135
- authentication_keys.filter_map { |authentication_key| resource.public_send(authentication_key) }.first
136
- end
137
55
  end
138
56
  end
139
57
  end
140
- # rubocop:enable Metrics/ModuleLength
@@ -7,6 +7,10 @@ module ActionDispatch
7
7
 
8
8
  def devise_passkey_authentication(_mapping, controllers)
9
9
  resources :passkeys, only: %i[new create destroy], controller: controllers[:passkeys]
10
+
11
+ resources :passkey_authentication_options, only: :create,
12
+ controller: controllers[:passkey_authentication_options]
13
+ resources :passkey_registration_options, only: :create, controller: controllers[:passkey_registration_options]
10
14
  end
11
15
 
12
16
  def devise_two_factor_authentication(_mapping, controllers)
@@ -17,6 +21,11 @@ module ActionDispatch
17
21
  resources :second_factor_webauthn_credentials,
18
22
  only: %i[new create update destroy],
19
23
  controller: controllers[:second_factor_webauthn_credentials]
24
+
25
+ resources :security_key_authentication_options, only: :create,
26
+ controller: controllers[:security_key_authentication_options]
27
+ resources :security_key_registration_options, only: :create,
28
+ controller: controllers[:security_key_registration_options]
20
29
  end
21
30
  end
22
31
  end
@@ -39,7 +39,7 @@ module Devise
39
39
  "privateKey" => encoded_private_key,
40
40
  "signCount" => 0
41
41
  }
42
- credential_json["userHandle"] = resource.webauthn_id if passkey
42
+ credential_json["userHandle"] = resource.ensure_webauthn_id! if passkey
43
43
 
44
44
  authenticator.add_credential(credential_json)
45
45
 
@@ -24,9 +24,13 @@ module Devise
24
24
  {
25
25
  passkeys: [nil],
26
26
  passkey: [nil, :new],
27
+ passkey_authentication_options: [nil],
28
+ passkey_registration_options: [nil],
27
29
  two_factor_authentication: [nil, :new],
28
30
  second_factor_webauthn_credentials: [nil],
29
- second_factor_webauthn_credential: [nil, :new]
31
+ second_factor_webauthn_credential: [nil, :new],
32
+ security_key_authentication_options: [nil],
33
+ security_key_registration_options: [nil]
30
34
  }.each do |route, actions|
31
35
  %i[path url].each do |path_or_url|
32
36
  actions.each do |action|
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Devise
4
4
  module Webauthn
5
- VERSION = "0.3.1"
5
+ VERSION = "0.5.0"
6
6
  end
7
7
  end
@@ -9,6 +9,10 @@ module Devise
9
9
  passkeys
10
10
  second_factor_webauthn_credentials
11
11
  two_factor_authentications
12
+ passkey_authentication_options
13
+ passkey_registration_options
14
+ security_key_authentication_options
15
+ security_key_registration_options
12
16
  ].freeze
13
17
 
14
18
  desc "Create inherited Devise::Webauthn controllers in your app/controllers folder."
@@ -16,10 +20,13 @@ module Devise
16
20
  source_root File.expand_path("templates/controllers", __dir__)
17
21
  argument :scope, required: true,
18
22
  desc: "The scope to create controllers in, e.g. users, admins"
23
+ class_option :controllers, aliases: "-c", type: :array,
24
+ desc: "Select specific controllers to generate (#{CONTROLLERS.join(', ')})"
19
25
 
20
26
  def create_controllers
21
27
  @scope_prefix = scope.blank? ? "" : "#{scope.camelize}::"
22
- CONTROLLERS.each do |name|
28
+ controllers = options[:controllers] || CONTROLLERS
29
+ controllers.each do |name|
23
30
  template "#{name}_controller.rb",
24
31
  "app/controllers/#{scope}/#{name}_controller.rb"
25
32
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= @scope_prefix %>PasskeyAuthenticationOptionsController < Devise::PasskeyAuthenticationOptionsController
4
+ # GET /resource/passkey_authentication_options
5
+ # def index
6
+ # super
7
+ # end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= @scope_prefix %>PasskeyRegistrationOptionsController < Devise::PasskeyRegistrationOptionsController
4
+ # GET /resource/passkey_registration_options
5
+ # def index
6
+ # super
7
+ # end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= @scope_prefix %>SecurityKeyAuthenticationOptionsController < Devise::SecurityKeyAuthenticationOptionsController
4
+ # GET /resource/security_key_authentication_options
5
+ # def index
6
+ # super
7
+ # end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= @scope_prefix %>SecurityKeyRegistrationOptionsController < Devise::SecurityKeyRegistrationOptionsController
4
+ # GET /resource/securiy_key_registration_options
5
+ # def index
6
+ # super
7
+ # end
8
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: devise-webauthn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cedarcode
@@ -50,6 +50,7 @@ files:
50
50
  - ".gitignore"
51
51
  - ".rspec"
52
52
  - ".rubocop.yml"
53
+ - ".rubocop_todo.yml"
53
54
  - ".ruby-version"
54
55
  - Appraisals
55
56
  - CHANGELOG.md
@@ -59,8 +60,12 @@ files:
59
60
  - README.md
60
61
  - Rakefile
61
62
  - app/assets/javascript/devise/webauthn.js
63
+ - app/controllers/devise/passkey_authentication_options_controller.rb
64
+ - app/controllers/devise/passkey_registration_options_controller.rb
62
65
  - app/controllers/devise/passkeys_controller.rb
63
66
  - app/controllers/devise/second_factor_webauthn_credentials_controller.rb
67
+ - app/controllers/devise/security_key_authentication_options_controller.rb
68
+ - app/controllers/devise/security_key_registration_options_controller.rb
64
69
  - app/controllers/devise/two_factor_authentications_controller.rb
65
70
  - app/views/devise/passkeys/new.html.erb
66
71
  - app/views/devise/second_factor_webauthn_credentials/new.html.erb
@@ -96,8 +101,12 @@ files:
96
101
  - lib/generators/devise/webauthn/install/templates/webauthn.rb
97
102
  - lib/generators/devise/webauthn/javascript_configuration/javascript_configuration_generator.rb
98
103
  - lib/generators/devise/webauthn/templates/controllers/README
104
+ - lib/generators/devise/webauthn/templates/controllers/passkey_authentication_options_controller.rb.tt
105
+ - lib/generators/devise/webauthn/templates/controllers/passkey_registration_options_controller.rb.tt
99
106
  - lib/generators/devise/webauthn/templates/controllers/passkeys_controller.rb.tt
100
107
  - lib/generators/devise/webauthn/templates/controllers/second_factor_webauthn_credentials_controller.rb.tt
108
+ - lib/generators/devise/webauthn/templates/controllers/security_key_authentication_options_controller.rb.tt
109
+ - lib/generators/devise/webauthn/templates/controllers/security_key_registration_options_controller.rb.tt
101
110
  - lib/generators/devise/webauthn/templates/controllers/two_factor_authentications_controller.rb.tt
102
111
  - lib/generators/devise/webauthn/views_generator.rb
103
112
  - lib/generators/devise/webauthn/webauthn_credential_model/templates/webauthn_credential_migration.rb.erb