devise-webauthn 0.2.2 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf586d5c54c83dcd4dd18c38e2dfe2c8d16cd86dc90e3c5c09b516d3c60dff04
4
- data.tar.gz: 5a56dc38f1bd836b69dae7057f44ba95868b1edbfe5820eab77466ec2db59ffd
3
+ metadata.gz: aaa4817ea2af9200fa03ddf48357462fee35e64624b14d0de881b7805af7a34d
4
+ data.tar.gz: 88b7affbb03be29d61834f20924db27ecd8623c0f7c5463ce59554ebd727dcd7
5
5
  SHA512:
6
- metadata.gz: c62d4d71fd255dbf5867c76a524ed2b8e22df167e537e52a928f5a88351291ccb4cc74aef5e0466b27c42242d6b0ab105e67acc2ba9d7cb673625ff0b31100b4
7
- data.tar.gz: fefeab5ad1470d5a2eb3f451c823763f237c8fe3f302f46d02917bbf337a0ab3011fc518b209d6105aed7ca23c21cb8ef9621f360a6a980204d3fe28508cf552
6
+ metadata.gz: 963eaa9ff4ce30759da2bb0b90fd2e8af1118c3c51caad4b574b7e28873033cdf16cdc9a91c29d21a4be2676e78255665df5db50d524c29a9321dfc864490a78
7
+ data.tar.gz: 758cb30bc99014c144ad7b8467cb9a24da62c7c0a1c7db1b7565436973ab145041ad3ac8eabf1951b648e56caa5682459ec0ebaf5f0fd21f0b8b07341c95b30a
@@ -36,8 +36,12 @@ jobs:
36
36
  - rails_8_0
37
37
  - rails_7_2
38
38
  - rails_7_1
39
+ - devise_5_0
39
40
 
40
41
  exclude:
42
+ - ruby: 3.2
43
+ gemfile: rails_edge
44
+
41
45
  - ruby: 3.1
42
46
  gemfile: rails_edge
43
47
  - ruby: 3.1
data/Appraisals CHANGED
@@ -5,19 +5,19 @@ appraise "rails-edge" do
5
5
  end
6
6
 
7
7
  appraise "rails-8_1" do
8
- gem "rails", "~> 8.1"
8
+ gem "rails", "~> 8.1.x"
9
9
  end
10
10
 
11
11
  appraise "rails-8_0" do
12
- gem "rails", "~> 8.0"
12
+ gem "rails", "~> 8.0.x"
13
13
  end
14
14
 
15
15
  appraise "rails-7_2" do
16
- gem "rails", "~> 7.2"
16
+ gem "rails", "~> 7.2.x"
17
17
  end
18
18
 
19
19
  appraise "rails-7_1" do
20
- gem "rails", "~> 7.1"
20
+ gem "rails", "~> 7.1.x"
21
21
 
22
22
  gem "capybara", "~> 3.39"
23
23
  gem "importmap-rails", "~> 2.0"
@@ -27,3 +27,17 @@ appraise "rails-7_1" do
27
27
  gem "rspec-rails", "~> 7.1"
28
28
  gem "sqlite3", "~> 1.7"
29
29
  end
30
+
31
+ appraise "devise-5_0" do
32
+ gem "devise", "~> 5.0.0.rc"
33
+
34
+ gem "rails", ">= 7.1"
35
+ gem "capybara", "~> 3.39"
36
+ gem "importmap-rails", "~> 2.0"
37
+ gem "pry-byebug", "~> 3.10"
38
+ install_if "-> { RUBY_VERSION < \"3.0\" }" do
39
+ gem "rack", "~> 2.2"
40
+ end
41
+ gem "rspec-rails", ">= 7.1"
42
+ gem "sqlite3", ">= 1.6", "!= 1.7.0", "!= 1.7.1", "!= 1.7.2", "!= 1.7.3"
43
+ end
data/CHANGELOG.md CHANGED
@@ -2,37 +2,73 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## [v0.3.0](https://github.com/cedarcode/devise-webauthn/compare/v0.2.2...v0.3.0/) - 2026-01-16
6
+
7
+ ### Added
8
+
9
+ - WebAuthn JavaScript is now bundled as engine assets using custom HTML elements (`<webauthn-create>`, `<webauthn-get>`) instead of generating a Stimulus controller into the host application. [#84](https://github.com/cedarcode/devise-webauthn/pull/84) [@santiagorodriguez96]
10
+ - Add endpoint to `SecondFactorWebauthnCredentialsController` for "upgrading" second factor webauthn credentials (i. e., security keys) to passkeys. [#80](https://github.com/cedarcode/devise-webauthn/pull/80) [@nicolastemciuc]
11
+
12
+ ### Changed
13
+
14
+ - Loosen `devise` upper constraint to allow for v5. [#94](https://github.com/cedarcode/devise-webauthn/pull/94) [@santiagorodriguez96]
15
+ - BREAKING!: Our [Form helpers](https://github.com/cedarcode/devise-webauthn/blob/355a6836315439f71265bb368bff4e8067033072/lib/devise/webauthn/helpers/credentials_helper.rb#L7-L58) now use the bundled WebAuthn JS asset now instead of the Stimulus controllers, so they expect it to be included in your application. [#84](https://github.com/cedarcode/devise-webauthn/pull/84) [@santiagorodriguez96]
16
+ - Previously generated Stimulus controller for handling WebAuthn client logic are no longer generated.
17
+ - Stimulus is no longer needed for this engine to work.
18
+ - Make helpers for generating WebAuthn options public methods. [#106](https://github.com/cedarcode/devise-webauthn/pull/106) [@santiagorodriguez96]
19
+
20
+ ### Fixed
21
+
22
+ - Fix `Remember me` checkbox not honored when going through the 2FA challenge flow. [#87](https://github.com/cedarcode/devise-webauthn/pull/87) [@santiagorodriguez96]
23
+
5
24
  ## [v0.2.2](https://github.com/cedarcode/devise-webauthn/compare/v0.2.1...v0.2.2/) - 2025-12-11
6
25
 
7
- - Generate webauthn credentials table with not null constraints in attributes that must be present.
8
- - Update controllers and views generators to generate 2FA-related controllers and views.
9
- - Add flash messages when removing credentials.
26
+ ### Added
27
+
28
+ - Update controllers and views generators to generate 2FA-related controllers and views. [#75](https://github.com/cedarcode/devise-webauthn/pull/75) [@santiagorodriguez96]
29
+ - Add flash messages when removing credentials. [#78](https://github.com/cedarcode/devise-webauthn/pull/78) [@nicolastemciuc]
30
+
31
+ ### Changed
32
+
33
+ - Generate webauthn credentials table with not null constraints in attributes that must be present. [#70](https://github.com/cedarcode/devise-webauthn/pull/70) [@santiagorodriguez96]
10
34
 
11
35
  ## [v0.2.1](https://github.com/cedarcode/devise-webauthn/compare/v0.2.0...v0.2.1/) - 2025-12-10
12
36
 
13
- - Add form helpers for security key registration and 2FA authentication.
14
- - Fix incorrect call to `resource_name` instead of using passed `resource` param in `login_with_security_key_button` helper.
15
- - Fix `NoMethodError` when calling `second_factor_enabled?` on resources without 2FA.
16
- - Avoid assuming `email` as the authentication key of the resource in form helpers.
37
+ ### Added
38
+
39
+ - Add form helpers for security key registration and 2FA authentication. [#52](https://github.com/cedarcode/devise-webauthn/pull/52) [@santiagorodriguez96]
40
+
41
+ ### Fixed
42
+
43
+ - Fix incorrect call to `resource_name` instead of using passed `resource` param in `login_with_security_key_button` helper. [#65](https://github.com/cedarcode/devise-webauthn/pull/65) [@santiagorodriguez96]
44
+ - Fix `NoMethodError` when calling `second_factor_enabled?` on resources without 2FA. [#62](https://github.com/cedarcode/devise-webauthn/pull/62) [@nicolastemciuc]
45
+ - Avoid assuming `email` as the authentication key of the resource in form helpers. [#66](https://github.com/cedarcode/devise-webauthn/pull/66) [@santiagorodriguez96]
17
46
 
18
47
  ## [v0.2.0](https://github.com/cedarcode/devise-webauthn/compare/v0.1.2...v0.2.0/) - 2025-12-03
19
48
 
20
- - Add new `webauthn_two_factor_authenticatable` module for enabling 2FA using WebAuthn credentials.
49
+ ### Added
50
+
51
+ - Add new `webauthn_two_factor_authenticatable` module for enabling 2FA using WebAuthn credentials. [#49](https://github.com/cedarcode/devise-webauthn/pull/49) [@santiagorodriguez96]
21
52
 
22
53
  ## [v0.1.2](https://github.com/cedarcode/devise-webauthn/compare/v0.1.1...v0.1.2/) - 2025-12-03
23
54
 
24
55
  ### Fixed
25
56
 
26
- - Fixed sign in with passkey for resources with name different from "User"
57
+ - Fixed sign in with passkey for resources with name different from "User". [#47](https://github.com/cedarcode/devise-webauthn/pull/47) [@joaquintomas2003], [@santiagorodriguez96]
27
58
 
28
59
  ## [v0.1.1](https://github.com/cedarcode/devise-webauthn/compare/v0.1.0...v0.1.1/) - 2025-11-13
29
60
 
30
61
  ### Changed
31
62
 
32
- - Updated gemspec metadata.
63
+ - Updated gemspec metadata. [#43](https://github.com/cedarcode/devise-webauthn/pull/43) [@joaquintomas2003]
33
64
 
34
65
  ## [v0.1.0](https://github.com/cedarcode/devise-webauthn/compare/v0.0.0...v0.1.0/) - 2025-11-12
35
66
 
36
67
  ### Initial release
37
68
 
38
- - Provides passkey authentication for apps using Devise.
69
+ - Provides passkey authentication for apps using Devise. [@joaquintomas2003], [@nicolastemciuc], [@RenzoMinelli], [@santiagorodriguez96]
70
+
71
+ [@RenzoMinelli]: https://github.com/RenzoMinelli
72
+ [@joaquintomas2003]: https://github.com/joaquintomas2003
73
+ [@nicolastemciuc]: https://github.com/nicolastemciuc
74
+ [@santiagorodriguez96]: https://github.com/santiagorodriguez96
data/Gemfile CHANGED
@@ -7,9 +7,10 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
7
7
  # Specify your gem's dependencies in devise-webauthn.gemspec
8
8
  gemspec
9
9
 
10
- gem "appraisal", "~> 2.5"
10
+ gem "appraisal", "~> 2.5", require: false
11
11
  gem "capybara", "~> 3.40"
12
12
  gem "combustion", "~> 1.3"
13
+ gem "devise", "~> 4.9"
13
14
  gem "importmap-rails", "~> 2.2"
14
15
  gem "propshaft", "~> 1.2"
15
16
  gem "pry-byebug", "~> 3.11"
@@ -21,4 +22,3 @@ gem "rubocop-rails", "~> 2.32"
21
22
  gem "rubocop-rspec", "~> 3.6"
22
23
  gem "selenium-webdriver"
23
24
  gem "sqlite3", "~> 2.7"
24
- gem "stimulus-rails", "~> 1.3"
data/Gemfile.lock CHANGED
@@ -1,8 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- devise-webauthn (0.2.2)
5
- devise (~> 4.9)
4
+ devise-webauthn (0.3.0)
5
+ devise (>= 4.9)
6
6
  webauthn (~> 3.0)
7
7
 
8
8
  GEM
@@ -303,8 +303,6 @@ GEM
303
303
  websocket (~> 1.0)
304
304
  sqlite3 (2.7.3)
305
305
  mini_portile2 (~> 2.8.0)
306
- stimulus-rails (1.3.4)
307
- railties (>= 6.0.0)
308
306
  stringio (3.1.7)
309
307
  thor (1.4.0)
310
308
  timeout (0.4.3)
@@ -345,6 +343,7 @@ DEPENDENCIES
345
343
  appraisal (~> 2.5)
346
344
  capybara (~> 3.40)
347
345
  combustion (~> 1.3)
346
+ devise (~> 4.9)
348
347
  devise-webauthn!
349
348
  importmap-rails (~> 2.2)
350
349
  propshaft (~> 1.2)
@@ -357,7 +356,6 @@ DEPENDENCIES
357
356
  rubocop-rspec (~> 3.6)
358
357
  selenium-webdriver
359
358
  sqlite3 (~> 2.7)
360
- stimulus-rails (~> 1.3)
361
359
 
362
360
  BUNDLED WITH
363
361
  2.7.1
data/README.md CHANGED
@@ -6,9 +6,7 @@ Devise::Webauthn is a [Devise](https://github.com/heartcombo/devise) extension t
6
6
  ## Requirements
7
7
 
8
8
  - **Ruby**: 2.7+
9
- - **Stimulus Rails**: This gem requires [stimulus-rails](https://github.com/hotwired/stimulus-rails) to be installed and configured in your application.
10
- > **Note:** Stimulus Rails is needed for the generated code to work out of the box.
11
- > If you prefer not to have this dependency, you’ll need to manually implement the JavaScript logic for WebAuthn interactions.
9
+ - **JavaScript**: This gem includes WebAuthn JavaScript as custom HTML elements. You'll need to import the JavaScript file in your application.
12
10
 
13
11
  ## Installation
14
12
 
@@ -31,7 +29,7 @@ Or install it yourself as:
31
29
  First, ensure you have Devise set up in your Rails application. For a full guide on setting up Devise, refer to the [Devise documentation](https://github.com/heartcombo/devise?tab=readme-ov-file#getting-started).
32
30
  Then, follow these steps to integrate Devise::Webauthn:
33
31
  1. **Run Devise::Webauthn Generator:**
34
- Run the generator to set up necessary configurations, migrations, and Stimulus controller:
32
+ Run the generator to set up necessary configurations and migrations:
35
33
  ```bash
36
34
  $ bin/rails generate devise:webauthn:install
37
35
  ```
@@ -45,7 +43,7 @@ Then, follow these steps to integrate Devise::Webauthn:
45
43
  - Create the WebAuthn initializer (`config/initializers/webauthn.rb`)
46
44
  - Generate the `WebauthnCredential` model and migration
47
45
  - Add `webauthn_id` field to your devise model (e.g., `User`)
48
- - Install the Stimulus controller
46
+ - Configure JavaScript loading for your application (see [JavaScript Setup](#javascript-setup))
49
47
 
50
48
  2. **Run Migrations:**
51
49
  After running the generator, execute the migrations to update your database schema:
@@ -76,6 +74,29 @@ Then, follow these steps to integrate Devise::Webauthn:
76
74
  end
77
75
  ```
78
76
 
77
+ 5. **Include bundled WebAuthn JavaScript in your application:**
78
+ The install generator automatically configures JavaScript loading based on your setup:
79
+
80
+ **For importmap-rails:**
81
+ - Adds `pin "devise/webauthn", to: "devise/webauthn.js"` to `config/importmap.rb`
82
+ - Adds `import "devise/webauthn"` to `app/javascript/application.js`
83
+
84
+ **For node setups (esbuild, Bun, etc.):**
85
+ - Adds `<%= javascript_include_tag "devise/webauthn" %>` to your application layout
86
+
87
+ If the automatic setup doesn't work for your configuration, you can manually include the JavaScript:
88
+ ```erb
89
+ <%= javascript_include_tag "devise/webauthn" %>
90
+ ```
91
+
92
+ #### Behavior
93
+
94
+ When the form is submitted:
95
+ 1. The default form submission is prevented
96
+ 2. The browser's WebAuthn prompt is triggered with the provided options
97
+ 3. Upon successful authentication, the credential response is stored in the hidden input
98
+ 4. The form is submitted with the credential data
99
+
79
100
  ## How It Works
80
101
 
81
102
  ### Passkey authentication
@@ -138,6 +159,90 @@ To add a passkeys creation form:
138
159
  <% end %>
139
160
  ```
140
161
 
162
+ ### Handling unsupported WebAuthn
163
+
164
+ The custom elements check for WebAuthn API support when they connect to the DOM. If the browser doesn't support WebAuthn, a `webauthn:unsupported` event is dispatched and the form submission handler is not attached.
165
+
166
+ ```javascript
167
+ document.addEventListener('webauthn:unsupported', (event) => {
168
+ const { action } = event.detail; // 'create' or 'get'
169
+
170
+ // Hide the WebAuthn form and show a message
171
+ hideWebauthnFormWithMessage('Your browser does not support WebAuthn');
172
+ });
173
+ ```
174
+
175
+ ### Customizing Javascript Error Handling
176
+
177
+ By default, WebAuthn errors during registration or authentication are displayed using the browser's `alert()` dialog. You can customize this behavior by listening to the `webauthn:prompt:error` event.
178
+
179
+ #### Listening for WebAuthn Errors
180
+
181
+ The custom elements dispatch a `webauthn:prompt:error` event whenever an error occurs during the WebAuthn prompt interaction (registration or authentication). You can listen for this event and provide custom error handling:
182
+
183
+ ```javascript
184
+ document.addEventListener('webauthn:prompt:error', (event) => {
185
+ event.preventDefault(); // Prevent the default alert
186
+
187
+ const { error, action } = event.detail;
188
+
189
+ // Your custom error handling
190
+ console.error(`WebAuthn ${action} failed:`, error);
191
+ showFlashMessage(error.message, 'error');
192
+ });
193
+ ```
194
+
195
+ #### Event Details
196
+
197
+ The event includes the following information in `event.detail`:
198
+ - `error`: The error object thrown during the WebAuthn operation
199
+ - `action`: Either `"create"` (for registration) or `"get"` (for authentication)
200
+
201
+ #### Handling Specific Error Types
202
+
203
+ WebAuthn operations can fail for various reasons. Here are some common error types you might want to handle:
204
+
205
+ ```javascript
206
+ document.addEventListener('webauthn:prompt:error', (event) => {
207
+ event.preventDefault();
208
+
209
+ const { error, action } = event.detail;
210
+
211
+ switch (error.name) {
212
+ case 'NotAllowedError':
213
+ // User cancelled the operation or timeout
214
+ showFlashMessage('Operation cancelled or timed out', 'warning');
215
+ break;
216
+
217
+ default:
218
+ // Generic error message
219
+ showFlashMessage(`Authentication error: ${error.message}`, 'error');
220
+ }
221
+ });
222
+ ```
223
+
224
+ #### Different Handling for Registration vs Authentication
225
+
226
+ You can provide different error handling based on whether the error occurred during registration or authentication:
227
+
228
+ ```javascript
229
+ document.addEventListener('webauthn:prompt:error', (event) => {
230
+ event.preventDefault();
231
+
232
+ const { error, action } = event.detail;
233
+
234
+ if (action === 'create') {
235
+ // Handle registration errors
236
+ handleRegistrationError(error);
237
+ } else if (action === 'get') {
238
+ // Handle authentication errors
239
+ handleAuthenticationError(error);
240
+ }
241
+ });
242
+ ```
243
+
244
+ **Note:** If you don't call `event.preventDefault()`, the default `alert()` will still be shown.
245
+
141
246
  ### Customizing Controllers
142
247
  Similar to [controllers customization on Devise](https://github.com/heartcombo/devise?tab=readme-ov-file#configuring-controllers), you can customize the Devise::Webauthn controllers.
143
248
 
@@ -155,6 +260,54 @@ devise_for :users, controllers: {
155
260
 
156
261
  3. Change or extend the generated controllers as needed.
157
262
 
263
+ ### Manually implementing WebAuthn forms
264
+
265
+ The gem provides two custom HTML elements for WebAuthn operations. While the [form helpers](#helper-methods) handle this automatically, you can use these elements directly for custom implementations.
266
+
267
+ #### `<webauthn-create>`
268
+
269
+ Used for registering new credentials (passkeys or security keys).
270
+
271
+ ```html
272
+ <form action="/passkeys" method="post">
273
+ <webauthn-create data-options-json="<%= create_passkey_options(@user).to_json %>">
274
+ <input type="hidden" name="public_key_credential" data-webauthn-target="response">
275
+ <input type="text" name="name" placeholder="Passkey name">
276
+ <button type="submit">Create Passkey</button>
277
+ </webauthn-create>
278
+ </form>
279
+ ```
280
+
281
+ **Requirements:**
282
+ - Must be wrapped in a `<form>` element
283
+ - The form's action should point to the appropriate endpoint – you can use the provided url helpers:
284
+ - For creating passkeys: `passkeys_path(resource_name)`
285
+ - 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
287
+ - 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
289
+
290
+ #### `<webauthn-get>`
291
+
292
+ Used for authenticating with existing credentials.
293
+
294
+ ```html
295
+ <form action="/users/sign_in" method="post">
296
+ <webauthn-get data-options-json="<%= passkey_authentication_options.to_json %>">
297
+ <input type="hidden" name="public_key_credential" data-webauthn-target="response">
298
+ <button type="submit">Sign in with Passkey</button>
299
+ </webauthn-get>
300
+ </form>
301
+ ```
302
+
303
+ **Requirements:**
304
+ - Must be wrapped in a `<form>` element
305
+ - The form's action should point to the appropriate endpoint – you can use the provided url helpers:
306
+ - For passkey sign-in: `session_path(resource_name)`
307
+ - For 2FA with WebAuthn: `two_factor_authentication_path(resource_name)`
308
+ - Requires a `data-options-json` attribute containing JSON-serialized WebAuthn request options
309
+ - 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
158
311
 
159
312
  ## Development
160
313
 
@@ -0,0 +1,179 @@
1
+ function isWebAuthnSupported() {
2
+ return !!(
3
+ navigator.credentials &&
4
+ navigator.credentials.create &&
5
+ navigator.credentials.get &&
6
+ window.PublicKeyCredential
7
+ );
8
+ }
9
+
10
+ export class WebauthnCreateElement extends HTMLElement {
11
+ connectedCallback() {
12
+ this.style.display = 'contents';
13
+
14
+ if (!isWebAuthnSupported()) {
15
+ this.handleWebauthnUnsupported();
16
+ return;
17
+ }
18
+
19
+ this.closest('form').addEventListener('submit', async (event) => {
20
+ event.preventDefault();
21
+
22
+ try {
23
+ const options = JSON.parse(this.getAttribute('data-options-json'));
24
+ const publicKey = PublicKeyCredential.parseCreationOptionsFromJSON(options);
25
+ const credential = await navigator.credentials.create({ publicKey });
26
+
27
+ this.querySelector('[data-webauthn-target="response"]').value = await this.stringifyRegistrationCredentialWithGracefullyHandlingAuthenticatorIssues(credential);
28
+
29
+ this.closest('form').submit();
30
+ } catch (error) {
31
+ this.handleError(error);
32
+ }
33
+ });
34
+ }
35
+
36
+ handleError(error) {
37
+ const event = new CustomEvent('webauthn:prompt:error', {
38
+ detail: { error, action: 'create' },
39
+ bubbles: true,
40
+ cancelable: true
41
+ });
42
+
43
+ // If no listener prevents default, show alert
44
+ if (this.dispatchEvent(event)) {
45
+ alert(error.message || error);
46
+ }
47
+ }
48
+
49
+ handleWebauthnUnsupported() {
50
+ this.dispatchEvent(new CustomEvent('webauthn:unsupported', {
51
+ detail: { action: 'create' },
52
+ bubbles: true
53
+ }));
54
+ }
55
+
56
+ // Stringifies registration credentials gracefully handling malformed ones (e.g., due to issues with
57
+ // certain authenticators like 1Password).
58
+ // It first tries to stringify them normally, and if the credential cannot be stringified (because its
59
+ // malformed), it attempts a workaround to convert the malformed credential into a valid format. This
60
+ // workaround was introduced for 1Password and might fail for other authenticators.
61
+ //
62
+ // Authenticators that return a proper credential should not affected by this workaround!
63
+ async stringifyRegistrationCredentialWithGracefullyHandlingAuthenticatorIssues(credential) {
64
+ try {
65
+ return JSON.stringify(credential);
66
+ } catch (e) {
67
+ console.warn("Authenticator returned a malformed credential, attempting to fix it. Error was:", e);
68
+ }
69
+
70
+ const response = credential.response;
71
+ const publicKey = response.getPublicKey ? await response.getPublicKey() : null;
72
+
73
+ return JSON.stringify({
74
+ type: credential.type,
75
+ id: credential.id,
76
+ rawId: credential.id,
77
+ authenticatorAttachment: credential.authenticatorAttachment,
78
+ clientExtensionResults: await credential.getClientExtensionResults(),
79
+ response: {
80
+ attestationObject: toBase64Url(response.attestationObject),
81
+ authenticatorData: toBase64Url(response.authenticatorData),
82
+ clientDataJSON: toBase64Url(response.clientDataJSON),
83
+ publicKey: toBase64Url(publicKey),
84
+ publicKeyAlgorithm: response.getPublicKeyAlgorithm(),
85
+ transports: response.getTransports(),
86
+ },
87
+ });
88
+ }
89
+ }
90
+
91
+ export class WebauthnGetElement extends HTMLElement {
92
+ connectedCallback() {
93
+ this.style.display = 'contents';
94
+
95
+ if (!isWebAuthnSupported()) {
96
+ this.handleWebauthnUnsupported();
97
+ return;
98
+ }
99
+
100
+ this.closest('form').addEventListener('submit', async (event) => {
101
+ event.preventDefault();
102
+
103
+ try {
104
+ const options = JSON.parse(this.getAttribute('data-options-json'));
105
+ const publicKey = PublicKeyCredential.parseRequestOptionsFromJSON(options);
106
+ const credential = await navigator.credentials.get({ publicKey });
107
+
108
+ this.querySelector('[data-webauthn-target="response"]').value = await this.stringifyAuthenticationCredentialWithGracefullyHandlingAuthenticatorIssues(credential);
109
+
110
+ this.closest('form').submit();
111
+ } catch (error) {
112
+ this.handleError(error);
113
+ }
114
+ });
115
+ }
116
+
117
+ handleError(error) {
118
+ const event = new CustomEvent('webauthn:prompt:error', {
119
+ detail: { error, action: 'get' },
120
+ bubbles: true,
121
+ cancelable: true
122
+ });
123
+
124
+ // If no listener prevents default, show alert
125
+ if (this.dispatchEvent(event)) {
126
+ alert(error.message || error);
127
+ }
128
+ }
129
+
130
+ handleWebauthnUnsupported() {
131
+ this.dispatchEvent(new CustomEvent('webauthn:unsupported', {
132
+ detail: { action: 'get' },
133
+ bubbles: true
134
+ }));
135
+ }
136
+
137
+ // Stringifies authentication credentials gracefully handling malformed ones (e.g., due to issues with
138
+ // certain authenticators like 1Password).
139
+ // It first tries to stringify them normally, and if the credential cannot be stringified (because its
140
+ // malformed), it attempts a workaround to convert the malformed credential into a valid format. This
141
+ // workaround was introduced for 1Password and might fail for other authenticators.
142
+ //
143
+ // Authenticators that return a proper credential should not affected by this workaround!
144
+ async stringifyAuthenticationCredentialWithGracefullyHandlingAuthenticatorIssues(credential) {
145
+ try {
146
+ return JSON.stringify(credential);
147
+ } catch (e) {
148
+ console.warn("Authenticator returned a malformed credential, attempting to fix it. Error was:", e);
149
+ }
150
+
151
+ const response = credential.response;
152
+
153
+ return JSON.stringify({
154
+ type: credential.type,
155
+ id: credential.id,
156
+ rawId: credential.id,
157
+ authenticatorAttachment: credential.authenticatorAttachment,
158
+ clientExtensionResults: await credential.getClientExtensionResults(),
159
+ response: {
160
+ authenticatorData: toBase64Url(response.authenticatorData),
161
+ clientDataJSON: toBase64Url(response.clientDataJSON),
162
+ signature: toBase64Url(response.signature),
163
+ userHandle: response.userHandle ? toBase64Url(response.userHandle) : null,
164
+ },
165
+ });
166
+ }
167
+ }
168
+
169
+ function toBase64Url(buffer) {
170
+ if (!buffer) return null;
171
+
172
+ const binary = String.fromCharCode(...new Uint8Array(buffer));
173
+ const base64 = btoa(binary);
174
+
175
+ return base64.replaceAll("+", "-").replaceAll("/", "_");
176
+ }
177
+
178
+ customElements.define('webauthn-create', WebauthnCreateElement);
179
+ customElements.define('webauthn-get', WebauthnGetElement);
@@ -14,14 +14,24 @@ module Devise
14
14
  else
15
15
  set_flash_message! :alert, :webauthn_credential_verification_failed, scope: :"devise.failure"
16
16
  end
17
- redirect_to after_update_path
17
+ redirect_to after_create_path
18
18
  rescue WebAuthn::Error
19
19
  set_flash_message! :alert, :webauthn_credential_verification_failed, scope: :"devise.failure"
20
- redirect_to after_update_path
20
+ redirect_to after_create_path
21
21
  ensure
22
22
  session.delete(:webauthn_challenge)
23
23
  end
24
24
 
25
+ def update
26
+ if resource.second_factor_webauthn_credentials.find(params[:id]).update(authentication_factor: 0)
27
+ set_flash_message! :notice, :security_key_promoted
28
+ else
29
+ set_flash_message! :alert, :security_key_promotion_failed, scope: :"devise.failure"
30
+ end
31
+
32
+ redirect_to after_update_path
33
+ end
34
+
25
35
  def destroy
26
36
  if resource.second_factor_webauthn_credentials.destroy(params[:id])
27
37
  set_flash_message! :notice, :security_key_deleted
@@ -29,7 +39,7 @@ module Devise
29
39
  set_flash_message! :alert, :security_key_deletion_failed, scope: :"devise.failure"
30
40
  end
31
41
 
32
- redirect_to after_update_path
42
+ redirect_to after_destroy_path
33
43
  end
34
44
 
35
45
  private
@@ -52,9 +62,21 @@ module Devise
52
62
  )
53
63
  end
54
64
 
65
+ # The default url to be used after creating a second factor key. You can overwrite
66
+ # this method in your own SecondFactorWebauthnCredentialsController.
67
+ def after_create_path
68
+ new_second_factor_webauthn_credential_path(resource_name)
69
+ end
70
+
55
71
  # The default url to be used after creating a second factor key. You can overwrite
56
72
  # this method in your own SecondFactorWebauthnCredentialsController.
57
73
  def after_update_path
74
+ request.referer || new_second_factor_webauthn_credential_path(resource_name)
75
+ end
76
+
77
+ # The default url to be used after deleting a second factor key. You can overwrite
78
+ # this method in your own SecondFactorWebauthnCredentialsController.
79
+ def after_destroy_path
58
80
  new_second_factor_webauthn_credential_path(resource_name)
59
81
  end
60
82
  end
@@ -6,11 +6,13 @@ en:
6
6
  second_factor_webauthn_credentials:
7
7
  security_key_created: "Security Key created successfully."
8
8
  security_key_deleted: "Security Key deleted successfully."
9
+ security_key_promoted: "Security Key promoted to passkey successfully."
9
10
  failure:
10
11
  passkey_not_found: "Your passkey doesn't exist or is not valid."
11
12
  passkey_verification_failed: "Passkey verification failed."
12
13
  passkey_deletion_failed: "Passkey deletion failed."
13
14
  security_key_deletion_failed: "Security Key deletion failed."
15
+ security_key_promotion_failed: "Security Key promotion to passkey failed."
14
16
  sign_in_not_initiated: "Sign in was not initiated."
15
17
  two_factor_required: "Two-factor authentication is required to sign in."
16
18
  webauthn_credential_not_found: "Your WebAuthn credential doesn't exist or is not valid."
@@ -29,6 +29,6 @@ Gem::Specification.new do |spec|
29
29
  spec.metadata["rubygems_mfa_required"] = "true"
30
30
  spec.required_ruby_version = ">= 2.7"
31
31
 
32
- spec.add_dependency "devise", "~> 4.9"
32
+ spec.add_dependency "devise", ">= 4.9"
33
33
  spec.add_dependency "webauthn", "~> 3.0"
34
34
  end
@@ -0,0 +1,26 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal", "~> 2.5", require: false
6
+ gem "capybara", "~> 3.39"
7
+ gem "combustion", "~> 1.3"
8
+ gem "devise", "~> 5.0.0.rc"
9
+ gem "importmap-rails", "~> 2.0"
10
+ gem "propshaft", "~> 1.2"
11
+ gem "pry-byebug", "~> 3.10"
12
+ gem "puma", "~> 6.6"
13
+ gem "rails", ">= 7.1"
14
+ gem "rspec-rails", ">= 7.1"
15
+ gem "rubocop", "~> 1.79"
16
+ gem "rubocop-rails", "~> 2.32"
17
+ gem "rubocop-rspec", "~> 3.6"
18
+ gem "selenium-webdriver"
19
+ gem "sqlite3", ">= 1.6", "!= 1.7.0", "!= 1.7.1", "!= 1.7.2", "!= 1.7.3"
20
+ gem "stimulus-rails", "~> 1.3"
21
+
22
+ install_if -> { RUBY_VERSION < "3.0" } do
23
+ gem "rack", "~> 2.2"
24
+ end
25
+
26
+ gemspec path: "../"
@@ -2,14 +2,15 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "appraisal", "~> 2.5"
5
+ gem "appraisal", "~> 2.5", require: false
6
6
  gem "capybara", "~> 3.39"
7
7
  gem "combustion", "~> 1.3"
8
+ gem "devise", "~> 4.9"
8
9
  gem "importmap-rails", "~> 2.0"
9
10
  gem "propshaft", "~> 1.2"
10
11
  gem "pry-byebug", "~> 3.10"
11
12
  gem "puma", "~> 6.6"
12
- gem "rails", "~> 7.1"
13
+ gem "rails", "~> 7.1.x"
13
14
  gem "rspec-rails", "~> 7.1"
14
15
  gem "rubocop", "~> 1.79"
15
16
  gem "rubocop-rails", "~> 2.32"
@@ -2,14 +2,15 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "appraisal", "~> 2.5"
5
+ gem "appraisal", "~> 2.5", require: false
6
6
  gem "capybara", "~> 3.40"
7
7
  gem "combustion", "~> 1.3"
8
+ gem "devise", "~> 4.9"
8
9
  gem "importmap-rails", "~> 2.2"
9
10
  gem "propshaft", "~> 1.2"
10
11
  gem "pry-byebug", "~> 3.11"
11
12
  gem "puma", "~> 6.6"
12
- gem "rails", "~> 7.2"
13
+ gem "rails", "~> 7.2.x"
13
14
  gem "rspec-rails", "~> 8.0"
14
15
  gem "rubocop", "~> 1.79"
15
16
  gem "rubocop-rails", "~> 2.32"
@@ -2,14 +2,15 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "appraisal", "~> 2.5"
5
+ gem "appraisal", "~> 2.5", require: false
6
6
  gem "capybara", "~> 3.40"
7
7
  gem "combustion", "~> 1.3"
8
+ gem "devise", "~> 4.9"
8
9
  gem "importmap-rails", "~> 2.2"
9
10
  gem "propshaft", "~> 1.2"
10
11
  gem "pry-byebug", "~> 3.11"
11
12
  gem "puma", "~> 6.6"
12
- gem "rails", "~> 8.0"
13
+ gem "rails", "~> 8.0.x"
13
14
  gem "rspec-rails", "~> 8.0"
14
15
  gem "rubocop", "~> 1.79"
15
16
  gem "rubocop-rails", "~> 2.32"
@@ -2,14 +2,15 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "appraisal", "~> 2.5"
5
+ gem "appraisal", "~> 2.5", require: false
6
6
  gem "capybara", "~> 3.40"
7
7
  gem "combustion", "~> 1.3"
8
+ gem "devise", "~> 4.9"
8
9
  gem "importmap-rails", "~> 2.2"
9
10
  gem "propshaft", "~> 1.2"
10
11
  gem "pry-byebug", "~> 3.11"
11
12
  gem "puma", "~> 6.6"
12
- gem "rails", "~> 8.1"
13
+ gem "rails", "~> 8.1.x"
13
14
  gem "rspec-rails", "~> 8.0"
14
15
  gem "rubocop", "~> 1.79"
15
16
  gem "rubocop-rails", "~> 2.32"
@@ -2,9 +2,10 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "appraisal", "~> 2.5"
5
+ gem "appraisal", "~> 2.5", require: false
6
6
  gem "capybara", "~> 3.40"
7
7
  gem "combustion", "~> 1.3"
8
+ gem "devise", "~> 4.9"
8
9
  gem "importmap-rails", "~> 2.2"
9
10
  gem "propshaft", "~> 1.2"
10
11
  gem "pry-byebug", "~> 3.11"
@@ -13,6 +13,7 @@ module Devise
13
13
  if validate(resource){ hashed = true; resource.valid_password?(password) }
14
14
  if second_factor_enabled?(resource)
15
15
  session[:current_authentication_resource_id] = resource.id
16
+ session[:current_authentication_remember_me] = remember_me?
16
17
  request.flash[:notice] = two_factor_required_message
17
18
  request.commit_flash
18
19
  redirect!(two_factor_authentication_path, {}, message: two_factor_required_message)
@@ -4,26 +4,32 @@ module Devise
4
4
  module Strategies
5
5
  class WebauthnTwoFactorAuthenticatable < Devise::Strategies::Base
6
6
  def valid?
7
- credential_param.present? && session[:two_factor_authentication_challenge].present?
7
+ credential_param.present? &&
8
+ session[:current_authentication_resource_id].present? &&
9
+ session[:two_factor_authentication_challenge].present?
8
10
  end
9
11
 
12
+ # rubocop:disable Metrics/AbcSize
10
13
  def authenticate!
11
14
  credential_from_params = WebAuthn::Credential.from_get(JSON.parse(credential_param))
12
- stored_credential = WebauthnCredential.find_by(external_id: credential_from_params.id)
15
+ resource = resource_class.find_by(id: session[:current_authentication_resource_id])
16
+ stored_credential = resource&.webauthn_credentials&.find_by(external_id: credential_from_params.id)
13
17
 
14
18
  return fail!(:webauthn_credential_not_found) if stored_credential.blank?
15
19
 
16
20
  verify_credential(credential_from_params, stored_credential)
17
21
 
18
- resource = stored_credential.public_send(resource_name)
22
+ resource.remember_me = session[:current_authentication_remember_me] if resource.respond_to?(:remember_me=)
19
23
  success!(resource)
20
24
 
21
25
  session.delete(:current_authentication_resource_id)
26
+ session.delete(:current_authentication_remember_me)
22
27
  rescue WebAuthn::Error
23
28
  fail!(:webauthn_credential_verification_failed)
24
29
  ensure
25
30
  session.delete(:two_factor_authentication_challenge)
26
31
  end
32
+ # rubocop:enable Metrics/AbcSize
27
33
 
28
34
  private
29
35
 
@@ -41,8 +47,8 @@ module Devise
41
47
  stored_credential.update!(sign_count: credential_from_params.sign_count)
42
48
  end
43
49
 
44
- def resource_name
45
- mapping.to.name.underscore
50
+ def resource_class
51
+ mapping.to
46
52
  end
47
53
  end
48
54
  end
@@ -34,6 +34,12 @@ module Devise
34
34
  initializer "devise.webauthn.url_helpers" do
35
35
  Devise.include_helpers(Devise::Webauthn)
36
36
  end
37
+
38
+ initializer "devise.webauthn.assets" do
39
+ if ::Rails.application.config.respond_to?(:assets)
40
+ ::Rails.application.config.assets.precompile += %w[devise/webauthn.js]
41
+ end
42
+ end
37
43
  end
38
44
  end
39
45
  end
@@ -8,16 +8,12 @@ module Devise
8
8
  form_with(
9
9
  url: passkeys_path(resource),
10
10
  method: :post,
11
- class: form_classes,
12
- data: {
13
- action: "webauthn-credentials#create:prevent",
14
- controller: "webauthn-credentials",
15
- webauthn_credentials_options_param: create_passkey_options(resource)
16
- }
11
+ class: form_classes
17
12
  ) do |f|
18
- concat f.hidden_field(:public_key_credential,
19
- data: { "webauthn-credentials-target": "credentialHiddenInput" })
20
- concat capture(f, &block)
13
+ tag.webauthn_create(data: { options_json: create_passkey_options(resource) }) do
14
+ concat f.hidden_field(:public_key_credential, data: { webauthn_target: "response" })
15
+ concat capture(f, &block)
16
+ end
21
17
  end
22
18
  end
23
19
 
@@ -25,16 +21,13 @@ module Devise
25
21
  form_with(
26
22
  url: session_path,
27
23
  method: :post,
28
- data: {
29
- action: "webauthn-credentials#get:prevent",
30
- controller: "webauthn-credentials",
31
- webauthn_credentials_options_param: passkey_authentication_options
32
- },
33
24
  class: form_classes
34
25
  ) do |f|
35
- concat f.hidden_field(:public_key_credential,
36
- data: { "webauthn-credentials-target": "credentialHiddenInput" })
37
- concat f.button(text, type: "submit", class: button_classes, &block)
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)
30
+ end
38
31
  end
39
32
  end
40
33
 
@@ -42,16 +35,12 @@ module Devise
42
35
  form_with(
43
36
  url: second_factor_webauthn_credentials_path(resource),
44
37
  method: :post,
45
- class: form_classes,
46
- data: {
47
- action: "webauthn-credentials#create:prevent",
48
- controller: "webauthn-credentials",
49
- webauthn_credentials_options_param: create_security_key_options(resource)
50
- }
38
+ class: form_classes
51
39
  ) do |f|
52
- concat f.hidden_field(:public_key_credential,
53
- data: { "webauthn-credentials-target": "credentialHiddenInput" })
54
- concat capture(f, &block)
40
+ tag.webauthn_create(data: { options_json: create_security_key_options(resource) }) do
41
+ concat f.hidden_field(:public_key_credential, data: { webauthn_target: "response" })
42
+ concat capture(f, &block)
43
+ end
55
44
  end
56
45
  end
57
46
 
@@ -59,21 +48,15 @@ module Devise
59
48
  form_with(
60
49
  url: two_factor_authentication_path(resource),
61
50
  method: :post,
62
- data: {
63
- action: "webauthn-credentials#get:prevent",
64
- controller: "webauthn-credentials",
65
- webauthn_credentials_options_param: security_key_authentication_options(resource)
66
- },
67
51
  class: form_classes
68
52
  ) do |f|
69
- concat f.hidden_field(:public_key_credential,
70
- data: { "webauthn-credentials-target": "credentialHiddenInput" })
71
- concat f.button(text, type: "submit", class: button_classes, &block)
53
+ tag.webauthn_get(data: { options_json: security_key_authentication_options(resource) }) do
54
+ concat f.hidden_field(:public_key_credential, data: { webauthn_target: "response" })
55
+ concat f.button(text, type: "submit", class: button_classes, &block)
56
+ end
72
57
  end
73
58
  end
74
59
 
75
- private
76
-
77
60
  def create_passkey_options(resource)
78
61
  @create_passkey_options ||= begin
79
62
  options = WebAuthn::Credential.options_for_create(
@@ -143,6 +126,8 @@ module Devise
143
126
  end
144
127
  end
145
128
 
129
+ private
130
+
146
131
  def resource_human_palatable_identifier
147
132
  authentication_keys = resource.class.authentication_keys
148
133
  authentication_keys = authentication_keys.keys if authentication_keys.is_a?(Hash)
@@ -15,7 +15,7 @@ module ActionDispatch
15
15
  controller: controllers[:two_factor_authentications]
16
16
 
17
17
  resources :second_factor_webauthn_credentials,
18
- only: %i[new create destroy],
18
+ only: %i[new create update destroy],
19
19
  controller: controllers[:second_factor_webauthn_credentials]
20
20
  end
21
21
  end
@@ -12,17 +12,17 @@ module Devise
12
12
  page.driver.browser.add_virtual_authenticator(options)
13
13
  end
14
14
 
15
- def add_passkey_to_authenticator(authenticator, resource)
16
- add_credential_to_authenticator(authenticator, resource, passkey: true)
15
+ def add_passkey_to_authenticator(authenticator, resource, name: "My Passkey")
16
+ add_credential_to_authenticator(authenticator, resource, passkey: true, name: name)
17
17
  end
18
18
 
19
- def add_security_key_to_authenticator(authenticator, resource)
20
- add_credential_to_authenticator(authenticator, resource, passkey: false)
19
+ def add_security_key_to_authenticator(authenticator, resource, name: "My Security Key")
20
+ add_credential_to_authenticator(authenticator, resource, passkey: false, name: name)
21
21
  end
22
22
 
23
23
  # rubocop:disable Metrics/AbcSize
24
24
  # rubocop:disable Metrics/MethodLength
25
- def add_credential_to_authenticator(authenticator, resource, passkey:)
25
+ def add_credential_to_authenticator(authenticator, resource, passkey:, name: "My Credential")
26
26
  credential_id = SecureRandom.random_bytes(16)
27
27
  encoded_credential_id = Base64.urlsafe_encode64(credential_id)
28
28
  key = OpenSSL::PKey.generate_key("ED25519")
@@ -44,7 +44,7 @@ module Devise
44
44
  authenticator.add_credential(credential_json)
45
45
 
46
46
  resource.webauthn_credentials.create!(
47
- name: "My Credential",
47
+ name: name,
48
48
  external_id: Base64.urlsafe_encode64(credential_id, padding: false),
49
49
  public_key: encoded_cose_public_key,
50
50
  sign_count: 0,
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Devise
4
4
  module Webauthn
5
- VERSION = "0.2.2"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
@@ -26,8 +26,8 @@ module Devise
26
26
  invoke "devise:webauthn:webauthn_id", [], resource_name: options[:resource_name]
27
27
  end
28
28
 
29
- def generate_stimulus_controller
30
- invoke "devise:webauthn:stimulus"
29
+ def generate_javascript_configuration
30
+ invoke "devise:webauthn:javascript"
31
31
  end
32
32
 
33
33
  def final_message
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module Devise
6
+ module Webauthn
7
+ class JavascriptConfigurationGenerator < Rails::Generators::Base
8
+ hide!
9
+ namespace "devise:webauthn:javascript"
10
+
11
+ desc "Configure JavaScript loading for devise-webauthn"
12
+
13
+ def configure_javascript
14
+ if importmap?
15
+ setup_importmap
16
+ elsif using_node?
17
+ setup_node
18
+ else
19
+ say "Could not detect JavaScript setup. Please manually configure `devise/webauthn.js` loading.", :red
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def importmap?
26
+ File.exist?(File.join(destination_root, "config/importmap.rb"))
27
+ end
28
+
29
+ def using_node?
30
+ File.exist?(File.join(destination_root, "package.json"))
31
+ end
32
+
33
+ def setup_importmap
34
+ say "Detected importmap-rails setup", :green
35
+
36
+ append_to_file "config/importmap.rb", %(pin "devise/webauthn", to: "devise/webauthn.js"\n)
37
+ say "Added pin to config/importmap.rb", :green
38
+
39
+ if File.exist?(File.join(destination_root, "app/javascript/application.js"))
40
+ append_to_file "app/javascript/application.js", %(import "devise/webauthn"\n)
41
+ say "Added import to app/javascript/application.js", :green
42
+ else
43
+ say "Could not find app/javascript/application.js!", :red
44
+ say " Please add `import \"devise/webauthn\"` to your application.js file manually."
45
+ end
46
+ end
47
+
48
+ def setup_node
49
+ say "Detected JavaScript bundler setup (Bun/Node)", :green
50
+
51
+ if File.exist?(File.join(destination_root, "app/views/layouts/application.html.erb"))
52
+ inject_into_file "app/views/layouts/application.html.erb",
53
+ %(\n <%= javascript_include_tag "devise/webauthn" %>),
54
+ before: "</head>"
55
+ say "Added javascript_include_tag to app/views/layouts/application.html.erb", :green
56
+ else
57
+ say "Could not find app/views/layouts/application.html.erb.", :red
58
+ say " Please add `<%= javascript_include_tag \"devise/webauthn\" %>` " \
59
+ "within the <head> tag in your custom layout."
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -11,6 +11,11 @@ class <%= @scope_prefix %>SecondFactorWebauthnCredentialsController < Devise::Se
11
11
  # super
12
12
  # end
13
13
 
14
+ # PUT /resource/second_factor_webauthn_credentials/:id
15
+ # def update
16
+ # super
17
+ # end
18
+
14
19
  # DELETE /resource/second_factor_webauthn_credentials/:id
15
20
  # def destroy
16
21
  # super
@@ -23,9 +28,21 @@ class <%= @scope_prefix %>SecondFactorWebauthnCredentialsController < Devise::Se
23
28
  # super
24
29
  # end
25
30
 
31
+ # The default url to be used after creating a second factor key. You can overwrite
32
+ # this method in your own SecondFactorWebauthnCredentialsController.
33
+ # def after_create_path
34
+ # super
35
+ # end
36
+
26
37
  # The default url to be used after creating a second factor key. You can overwrite
27
38
  # this method in your own SecondFactorWebauthnCredentialsController.
28
39
  # def after_update_path
29
40
  # super
30
41
  # end
42
+
43
+ # The default url to be used after deleting a second factor key. You can overwrite
44
+ # this method in your own SecondFactorWebauthnCredentialsController.
45
+ # def after_destroy_path
46
+ # super
47
+ # end
31
48
  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.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cedarcode
@@ -13,14 +13,14 @@ dependencies:
13
13
  name: devise
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - "~>"
16
+ - - ">="
17
17
  - !ruby/object:Gem::Version
18
18
  version: '4.9'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
- - - "~>"
23
+ - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '4.9'
26
26
  - !ruby/object:Gem::Dependency
@@ -58,6 +58,7 @@ files:
58
58
  - LICENSE.txt
59
59
  - README.md
60
60
  - Rakefile
61
+ - app/assets/javascript/devise/webauthn.js
61
62
  - app/controllers/devise/passkeys_controller.rb
62
63
  - app/controllers/devise/second_factor_webauthn_credentials_controller.rb
63
64
  - app/controllers/devise/two_factor_authentications_controller.rb
@@ -71,6 +72,7 @@ files:
71
72
  - config.ru
72
73
  - config/locales/en.yml
73
74
  - devise-webauthn.gemspec
75
+ - gemfiles/devise_5_0.gemfile
74
76
  - gemfiles/rails_7_1.gemfile
75
77
  - gemfiles/rails_7_2.gemfile
76
78
  - gemfiles/rails_8_0.gemfile
@@ -92,8 +94,7 @@ files:
92
94
  - lib/generators/devise/webauthn/controllers_generator.rb
93
95
  - lib/generators/devise/webauthn/install/install_generator.rb
94
96
  - lib/generators/devise/webauthn/install/templates/webauthn.rb
95
- - lib/generators/devise/webauthn/stimulus/stimulus_generator.rb
96
- - lib/generators/devise/webauthn/stimulus/templates/webauthn_credentials_controller.js
97
+ - lib/generators/devise/webauthn/javascript/javascript_configuration_generator.rb
97
98
  - lib/generators/devise/webauthn/templates/controllers/README
98
99
  - lib/generators/devise/webauthn/templates/controllers/passkeys_controller.rb.tt
99
100
  - lib/generators/devise/webauthn/templates/controllers/second_factor_webauthn_credentials_controller.rb.tt
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rails/generators"
4
-
5
- module Devise
6
- module Webauthn
7
- class StimulusGenerator < Rails::Generators::Base
8
- hide!
9
- source_root File.expand_path("templates", __dir__)
10
-
11
- desc "Copy DeviseWebauthn Stimulus controller to your application"
12
-
13
- def copy_stimulus_controller
14
- copy_file "webauthn_credentials_controller.js",
15
- "app/javascript/controllers/webauthn_credentials_controller.js"
16
- end
17
-
18
- def show_instructions
19
- say "✓ Stimulus controller setup complete!", :green
20
- say "The webauthn_credentials controller has been installed and will be automatically registered by Stimulus."
21
- end
22
- end
23
- end
24
- end
@@ -1,31 +0,0 @@
1
- import { Controller } from "@hotwired/stimulus"
2
-
3
- export default class extends Controller {
4
- static targets = ["credentialHiddenInput"]
5
-
6
- async create({ params: { options } }) {
7
- try {
8
- const credentialOptions = PublicKeyCredential.parseCreationOptionsFromJSON(options);
9
- const credential = await navigator.credentials.create({ publicKey: credentialOptions });
10
-
11
- this.credentialHiddenInputTarget.value = JSON.stringify(credential);
12
-
13
- this.element.submit();
14
- } catch (error) {
15
- alert(error.message || error);
16
- }
17
- }
18
-
19
- async get({ params: { options } }) {
20
- try {
21
- const credentialOptions = PublicKeyCredential.parseRequestOptionsFromJSON(options);
22
- const credential = await navigator.credentials.get({ publicKey: credentialOptions });
23
-
24
- this.credentialHiddenInputTarget.value = JSON.stringify(credential);
25
-
26
- this.element.submit();
27
- } catch (error) {
28
- alert(error.message || error);
29
- }
30
- }
31
- }