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.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +1 -1
- data/.github/workflows/ruby.yml +5 -4
- data/.rubocop.yml +2 -0
- data/.rubocop_todo.yml +20 -0
- data/Appraisals +0 -1
- data/CHANGELOG.md +56 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +4 -1
- data/README.md +27 -10
- data/app/assets/javascript/devise/webauthn.js +28 -5
- data/app/controllers/devise/passkey_authentication_options_controller.rb +19 -0
- data/app/controllers/devise/passkey_registration_options_controller.rb +48 -0
- data/app/controllers/devise/security_key_authentication_options_controller.rb +28 -0
- data/app/controllers/devise/security_key_registration_options_controller.rb +48 -0
- data/app/views/devise/passkeys/new.html.erb +1 -1
- data/app/views/devise/second_factor_webauthn_credentials/new.html.erb +1 -1
- data/app/views/devise/sessions/new.html.erb +3 -1
- data/app/views/devise/two_factor_authentications/new.html.erb +3 -1
- data/gemfiles/devise_5_0.gemfile +1 -1
- data/gemfiles/rails_7_1.gemfile +1 -2
- data/gemfiles/rails_7_2.gemfile +1 -1
- data/gemfiles/rails_8_0.gemfile +1 -1
- data/gemfiles/rails_8_1.gemfile +1 -1
- data/gemfiles/rails_edge.gemfile +1 -1
- data/lib/devise/models/webauthn_credential_authenticatable.rb +8 -3
- data/lib/devise/strategies/passkey_authenticatable.rb +21 -5
- data/lib/devise/strategies/webauthn_two_factor_authenticatable.rb +8 -0
- data/lib/devise/webauthn/helpers/credentials_helper.rb +21 -104
- data/lib/devise/webauthn/routes.rb +9 -0
- data/lib/devise/webauthn/test/authenticator_helpers.rb +1 -1
- data/lib/devise/webauthn/url_helpers.rb +5 -1
- data/lib/devise/webauthn/version.rb +1 -1
- data/lib/generators/devise/webauthn/controllers_generator.rb +8 -1
- data/lib/generators/devise/webauthn/templates/controllers/passkey_authentication_options_controller.rb.tt +8 -0
- data/lib/generators/devise/webauthn/templates/controllers/passkey_registration_options_controller.rb.tt +8 -0
- data/lib/generators/devise/webauthn/templates/controllers/security_key_authentication_options_controller.rb.tt +8 -0
- data/lib/generators/devise/webauthn/templates/controllers/security_key_registration_options_controller.rb.tt +8 -0
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2c1fc0044f5c2bf947d79fd3db30ea05907980dac2abeacd4d43af55c7fc8fc3
|
|
4
|
+
data.tar.gz: d9513c05eaad396f2d6f8764a8f8964086678fa03e55b98375406e1de7303b67
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 53c013359a84a25616c4a4778a4e0c766916665d7c31192b74feab1f471f36ffe4867ef1ed74f82f80119c1bdde16c996df2e2782df1b4c3de3ac67e49ce9f60
|
|
7
|
+
data.tar.gz: afdd185b69e7208bcc489c6f707c2d3c77a3a44447f27650db5f985958dd2856b7ce117a684ba2273639d469bf4c39d02a0abe523d7d943b22b1c438d3e076c6
|
data/.github/workflows/ruby.yml
CHANGED
|
@@ -8,7 +8,7 @@ jobs:
|
|
|
8
8
|
|
|
9
9
|
steps:
|
|
10
10
|
- name: Check out repository code
|
|
11
|
-
uses: actions/checkout@
|
|
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@
|
|
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
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
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
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
devise-webauthn (0.
|
|
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.
|
|
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
|
-
|
|
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
|
-
<%=
|
|
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(
|
|
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-
|
|
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-
|
|
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
|
|
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-
|
|
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-
|
|
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
|
|
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 =
|
|
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.
|
|
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 =
|
|
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.
|
|
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
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
</div>
|
|
24
24
|
<% end %>
|
|
25
25
|
|
|
26
|
-
<%=
|
|
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" %>
|
data/gemfiles/devise_5_0.gemfile
CHANGED
|
@@ -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"
|
data/gemfiles/rails_7_1.gemfile
CHANGED
|
@@ -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: "../"
|
data/gemfiles/rails_7_2.gemfile
CHANGED
|
@@ -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: "../"
|
data/gemfiles/rails_8_0.gemfile
CHANGED
|
@@ -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: "../"
|
data/gemfiles/rails_8_1.gemfile
CHANGED
|
@@ -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: "../"
|
data/gemfiles/rails_edge.gemfile
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
44
|
-
|
|
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(
|
|
6
|
+
def passkey_creation_form_for(resource_or_resource_name, **options, &block)
|
|
8
7
|
form_with(
|
|
9
|
-
url: passkeys_path(
|
|
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: {
|
|
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
|
|
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: {
|
|
27
|
-
concat
|
|
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(
|
|
30
|
+
def security_key_creation_form_for(resource_or_resource_name, **options, &block)
|
|
35
31
|
form_with(
|
|
36
|
-
url: second_factor_webauthn_credentials_path(
|
|
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(
|
|
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
|
|
43
|
+
def login_with_security_key_form_for(resource_or_resource_name, **options, &block)
|
|
48
44
|
form_with(
|
|
49
|
-
url: two_factor_authentication_path(
|
|
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: {
|
|
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
|
|
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.
|
|
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|
|
|
@@ -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
|
-
|
|
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
|
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.
|
|
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
|