devise-webauthn 0.2.0 → 0.2.2
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/FUNDING.yml +1 -0
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/release.yml +28 -0
- data/.github/workflows/ruby.yml +9 -2
- data/.rubocop.yml +1 -0
- data/Appraisals +4 -0
- data/CHANGELOG.md +13 -0
- data/Gemfile.lock +1 -1
- data/README.md +24 -5
- data/app/controllers/devise/passkeys_controller.rb +6 -1
- data/app/controllers/devise/second_factor_webauthn_credentials_controller.rb +7 -17
- data/app/controllers/devise/two_factor_authentications_controller.rb +1 -7
- data/app/views/devise/second_factor_webauthn_credentials/new.html.erb +1 -11
- data/app/views/devise/two_factor_authentications/new.html.erb +1 -12
- data/config/locales/en.yml +4 -1
- data/gemfiles/rails_8_1.gemfile +21 -0
- data/lib/devise/strategies/database_authenticatable.rb +5 -1
- data/lib/devise/webauthn/helpers/credentials_helper.rb +82 -4
- data/lib/devise/webauthn/test/authenticator_helpers.rb +42 -0
- data/lib/devise/webauthn/version.rb +1 -1
- data/lib/generators/devise/webauthn/controllers_generator.rb +5 -1
- data/lib/generators/devise/webauthn/templates/controllers/second_factor_webauthn_credentials_controller.rb.tt +31 -0
- data/lib/generators/devise/webauthn/templates/controllers/two_factor_authentications_controller.rb.tt +13 -0
- data/lib/generators/devise/webauthn/views_generator.rb +2 -0
- data/lib/generators/devise/webauthn/webauthn_credential_model/templates/webauthn_credential_migration.rb.erb +17 -0
- data/lib/generators/devise/webauthn/webauthn_credential_model/webauthn_credential_model_generator.rb +15 -9
- metadata +10 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bf586d5c54c83dcd4dd18c38e2dfe2c8d16cd86dc90e3c5c09b516d3c60dff04
|
|
4
|
+
data.tar.gz: 5a56dc38f1bd836b69dae7057f44ba95868b1edbfe5820eab77466ec2db59ffd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c62d4d71fd255dbf5867c76a524ed2b8e22df167e537e52a928f5a88351291ccb4cc74aef5e0466b27c42242d6b0ab105e67acc2ba9d7cb673625ff0b31100b4
|
|
7
|
+
data.tar.gz: fefeab5ad1470d5a2eb3f451c823763f237c8fe3f302f46d02917bbf337a0ab3011fc518b209d6105aed7ca23c21cb8ef9621f360a6a980204d3fe28508cf552
|
data/.github/FUNDING.yml
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
github: [cedarcode]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
release:
|
|
9
|
+
name: Publish to Rubygems
|
|
10
|
+
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
environment: release
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: write
|
|
17
|
+
id-token: write
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v6
|
|
21
|
+
|
|
22
|
+
- name: Setup Ruby
|
|
23
|
+
uses: ruby/setup-ruby@v1
|
|
24
|
+
with:
|
|
25
|
+
bundler-cache: true
|
|
26
|
+
|
|
27
|
+
- name: Publish to RubyGems
|
|
28
|
+
uses: rubygems/release-gem@v1
|
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@v6
|
|
12
12
|
|
|
13
13
|
- name: Set up Ruby
|
|
14
14
|
uses: ruby/setup-ruby@v1
|
|
@@ -32,6 +32,7 @@ jobs:
|
|
|
32
32
|
- 2.7
|
|
33
33
|
gemfile:
|
|
34
34
|
- rails_edge
|
|
35
|
+
- rails_8_1
|
|
35
36
|
- rails_8_0
|
|
36
37
|
- rails_7_2
|
|
37
38
|
- rails_7_1
|
|
@@ -39,11 +40,15 @@ jobs:
|
|
|
39
40
|
exclude:
|
|
40
41
|
- ruby: 3.1
|
|
41
42
|
gemfile: rails_edge
|
|
43
|
+
- ruby: 3.1
|
|
44
|
+
gemfile: rails_8_1
|
|
42
45
|
- ruby: 3.1
|
|
43
46
|
gemfile: rails_8_0
|
|
44
47
|
|
|
45
48
|
- ruby: 3.0
|
|
46
49
|
gemfile: rails_edge
|
|
50
|
+
- ruby: 3.0
|
|
51
|
+
gemfile: rails_8_1
|
|
47
52
|
- ruby: 3.0
|
|
48
53
|
gemfile: rails_8_0
|
|
49
54
|
- ruby: 3.0
|
|
@@ -51,6 +56,8 @@ jobs:
|
|
|
51
56
|
|
|
52
57
|
- ruby: 2.7
|
|
53
58
|
gemfile: rails_edge
|
|
59
|
+
- ruby: 2.7
|
|
60
|
+
gemfile: rails_8_1
|
|
54
61
|
- ruby: 2.7
|
|
55
62
|
gemfile: rails_8_0
|
|
56
63
|
- ruby: 2.7
|
|
@@ -62,7 +69,7 @@ jobs:
|
|
|
62
69
|
|
|
63
70
|
steps:
|
|
64
71
|
- name: Check out repository code
|
|
65
|
-
uses: actions/checkout@
|
|
72
|
+
uses: actions/checkout@v6
|
|
66
73
|
|
|
67
74
|
- name: Set up Ruby
|
|
68
75
|
uses: ruby/setup-ruby@v1
|
data/.rubocop.yml
CHANGED
data/Appraisals
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## [v0.2.2](https://github.com/cedarcode/devise-webauthn/compare/v0.2.1...v0.2.2/) - 2025-12-11
|
|
6
|
+
|
|
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.
|
|
10
|
+
|
|
11
|
+
## [v0.2.1](https://github.com/cedarcode/devise-webauthn/compare/v0.2.0...v0.2.1/) - 2025-12-10
|
|
12
|
+
|
|
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.
|
|
17
|
+
|
|
5
18
|
## [v0.2.0](https://github.com/cedarcode/devise-webauthn/compare/v0.1.2...v0.2.0/) - 2025-12-03
|
|
6
19
|
|
|
7
20
|
- Add new `webauthn_two_factor_authenticatable` module for enabling 2FA using WebAuthn credentials.
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Devise::Webauthn
|
|
2
2
|
[](https://badge.fury.io/rb/devise-webauthn)
|
|
3
3
|
|
|
4
|
-
Devise::Webauthn is a [Devise](https://github.com/heartcombo/devise) extension that adds [WebAuthn](https://www.w3.org/TR/2025/WD-webauthn-3-20250127/) support to your Rails application, allowing users to authenticate with [passkeys](https://www.w3.org/TR/2025/WD-webauthn-3-20250127/#passkey).
|
|
4
|
+
Devise::Webauthn is a [Devise](https://github.com/heartcombo/devise) extension that adds [WebAuthn](https://www.w3.org/TR/2025/WD-webauthn-3-20250127/) support to your Rails application, allowing users to authenticate with [passkeys](https://www.w3.org/TR/2025/WD-webauthn-3-20250127/#passkey) and use [security keys](https://www.w3.org/TR/webauthn-3/#server-side-credential) for two factor authentication.
|
|
5
5
|
|
|
6
6
|
## Requirements
|
|
7
7
|
|
|
@@ -54,11 +54,12 @@ Then, follow these steps to integrate Devise::Webauthn:
|
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
3. **Update Your Devise Model:**
|
|
57
|
-
Add `:passkey_authenticatable` to your Devise model (e.g., `User`):
|
|
57
|
+
Add `:passkey_authenticatable` to your Devise model (e.g., `User`) for passkeys authentication and `:webauthn_two_factor_authenticatable` for WebAuthn-based 2FA if desired. For example:
|
|
58
58
|
```ruby
|
|
59
59
|
class User < ApplicationRecord
|
|
60
60
|
devise :database_authenticatable, :registerable,
|
|
61
|
-
:recoverable, :rememberable, :validatable,
|
|
61
|
+
:recoverable, :rememberable, :validatable,
|
|
62
|
+
:passkey_authenticatable, :webauthn_two_factor_authenticatable
|
|
62
63
|
end
|
|
63
64
|
```
|
|
64
65
|
|
|
@@ -77,10 +78,12 @@ Then, follow these steps to integrate Devise::Webauthn:
|
|
|
77
78
|
|
|
78
79
|
## How It Works
|
|
79
80
|
|
|
80
|
-
###
|
|
81
|
+
### Passkey authentication
|
|
82
|
+
|
|
83
|
+
#### Adding Passkeys
|
|
81
84
|
Signed-in users can add passkeys by visiting `/users/passkeys/new`.
|
|
82
85
|
|
|
83
|
-
|
|
86
|
+
#### Sign In with Passkeys
|
|
84
87
|
When a user visits `/users/sign_in` they can choose to authenticate using a passkey. The authentication flow is handled by `PasskeysAuthenticatable` strategy.
|
|
85
88
|
|
|
86
89
|
The WebAuthn passkey sign-in flow works as follows:
|
|
@@ -89,6 +92,22 @@ The WebAuthn passkey sign-in flow works as follows:
|
|
|
89
92
|
3. User selects a passkey and verifies with their [authenticator](https://www.w3.org/TR/webauthn-3/#webauthn-authenticator).
|
|
90
93
|
4. The server verifies the response and signs in the user.
|
|
91
94
|
|
|
95
|
+
### Two-Factor Authentication (2FA) with WebAuthn
|
|
96
|
+
|
|
97
|
+
#### Adding Security Keys for 2FA
|
|
98
|
+
Signed-in users can add security keys by visiting `/users/second_factor_webauthn_credentials/new`.
|
|
99
|
+
|
|
100
|
+
#### 2FA Sign In with Security Keys
|
|
101
|
+
When a user that has 2FA enabled (i.e., has registered passkeys or security keys) visits `/users/sign_in`, after entering their primary credentials (e.g., email and password), they will be prompted to complete the second factor authentication using WebAuthn. The authentication flow is handled by `WebauthnTwoFactorAuthenticatable` strategy.
|
|
102
|
+
|
|
103
|
+
The two factor authentication flow with WebAuthn works as follows:
|
|
104
|
+
1. User enters their primary credentials (e.g., email and password) and submits the form.
|
|
105
|
+
2. If the user has 2FA enabled, they are redirected to a second factor authentication page.
|
|
106
|
+
3. User clicks "Use security key", starting a WebAuthn authentication ceremony.
|
|
107
|
+
4. Browser shows available credentials (which can be both passkeys and security keys).
|
|
108
|
+
5. User selects a credential and verifies with their [authenticator](https://www.w3.org/TR/webauthn-3/#webauthn-authenticator).
|
|
109
|
+
6. The server verifies the response and signs in the user.
|
|
110
|
+
|
|
92
111
|
## Customization
|
|
93
112
|
|
|
94
113
|
### Customizing Views
|
|
@@ -23,7 +23,12 @@ module Devise
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def destroy
|
|
26
|
-
resource.passkeys.destroy(params[:id])
|
|
26
|
+
if resource.passkeys.destroy(params[:id])
|
|
27
|
+
set_flash_message! :notice, :passkey_deleted
|
|
28
|
+
else
|
|
29
|
+
set_flash_message! :alert, :passkey_deletion_failed, scope: :"devise.failure"
|
|
30
|
+
end
|
|
31
|
+
|
|
27
32
|
redirect_to after_update_path
|
|
28
33
|
end
|
|
29
34
|
|
|
@@ -4,22 +4,7 @@ module Devise
|
|
|
4
4
|
class SecondFactorWebauthnCredentialsController < DeviseController
|
|
5
5
|
before_action :authenticate_scope!
|
|
6
6
|
|
|
7
|
-
def new
|
|
8
|
-
@options = WebAuthn::Credential.options_for_create(
|
|
9
|
-
user: {
|
|
10
|
-
id: resource.webauthn_id,
|
|
11
|
-
name: resource.email
|
|
12
|
-
},
|
|
13
|
-
exclude: resource.webauthn_credentials.pluck(:external_id),
|
|
14
|
-
authenticator_selection: {
|
|
15
|
-
resident_key: "discouraged",
|
|
16
|
-
user_verification: "discouraged"
|
|
17
|
-
}
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
# Store challenge in session for later verification
|
|
21
|
-
session[:webauthn_challenge] = @options.challenge
|
|
22
|
-
end
|
|
7
|
+
def new; end
|
|
23
8
|
|
|
24
9
|
def create
|
|
25
10
|
security_key_from_params = WebAuthn::Credential.from_create(JSON.parse(params[:public_key_credential]))
|
|
@@ -38,7 +23,12 @@ module Devise
|
|
|
38
23
|
end
|
|
39
24
|
|
|
40
25
|
def destroy
|
|
41
|
-
resource.second_factor_webauthn_credentials.destroy(params[:id])
|
|
26
|
+
if resource.second_factor_webauthn_credentials.destroy(params[:id])
|
|
27
|
+
set_flash_message! :notice, :security_key_deleted
|
|
28
|
+
else
|
|
29
|
+
set_flash_message! :alert, :security_key_deletion_failed, scope: :"devise.failure"
|
|
30
|
+
end
|
|
31
|
+
|
|
42
32
|
redirect_to after_update_path
|
|
43
33
|
end
|
|
44
34
|
|
|
@@ -6,13 +6,7 @@ module Devise
|
|
|
6
6
|
prepend_before_action :ensure_sign_in_initiated
|
|
7
7
|
prepend_before_action :require_no_authentication
|
|
8
8
|
|
|
9
|
-
def new
|
|
10
|
-
@options = WebAuthn::Credential.options_for_get(
|
|
11
|
-
allow: @resource.webauthn_credentials.pluck(:external_id),
|
|
12
|
-
user_verification: "discouraged"
|
|
13
|
-
)
|
|
14
|
-
session[:two_factor_authentication_challenge] = @options.challenge
|
|
15
|
-
end
|
|
9
|
+
def new; end
|
|
16
10
|
|
|
17
11
|
def create
|
|
18
12
|
self.resource = warden.authenticate!(auth_options)
|
|
@@ -1,14 +1,4 @@
|
|
|
1
|
-
<%=
|
|
2
|
-
url: second_factor_webauthn_credentials_path(resource),
|
|
3
|
-
method: :post,
|
|
4
|
-
data: {
|
|
5
|
-
action: "webauthn-credentials#create:prevent",
|
|
6
|
-
controller: "webauthn-credentials",
|
|
7
|
-
webauthn_credentials_options_param: @options,
|
|
8
|
-
}
|
|
9
|
-
) do |form| %>
|
|
10
|
-
<%= form.hidden_field(:public_key_credential,
|
|
11
|
-
data: { "webauthn-credentials-target": "credentialHiddenInput" }) %>
|
|
1
|
+
<%= security_key_creation_form_for(resource) do |form| %>
|
|
12
2
|
<%= form.label :name, 'Security Key name' %>
|
|
13
3
|
<%= form.text_field :name, required: true %>
|
|
14
4
|
<%= form.submit 'Create Security Key' %>
|
|
@@ -1,12 +1 @@
|
|
|
1
|
-
<%=
|
|
2
|
-
url: two_factor_authentication_path(resource_name),
|
|
3
|
-
method: :post,
|
|
4
|
-
data: {
|
|
5
|
-
action: "webauthn-credentials#get:prevent",
|
|
6
|
-
controller: "webauthn-credentials",
|
|
7
|
-
webauthn_credentials_options_param: @options
|
|
8
|
-
},
|
|
9
|
-
) do |f| %>
|
|
10
|
-
<%= f.hidden_field(:public_key_credential, data: { "webauthn-credentials-target": "credentialHiddenInput" }) %>
|
|
11
|
-
<%= f.button('Use security key', type: "submit") %>
|
|
12
|
-
<% end %>
|
|
1
|
+
<%= login_with_security_key_button('Use security key', resource: @resource) %>
|
data/config/locales/en.yml
CHANGED
|
@@ -2,12 +2,15 @@ en:
|
|
|
2
2
|
devise:
|
|
3
3
|
passkeys:
|
|
4
4
|
passkey_created: "Passkey created successfully."
|
|
5
|
-
|
|
5
|
+
passkey_deleted: "Passkey deleted successfully."
|
|
6
6
|
second_factor_webauthn_credentials:
|
|
7
7
|
security_key_created: "Security Key created successfully."
|
|
8
|
+
security_key_deleted: "Security Key deleted successfully."
|
|
8
9
|
failure:
|
|
9
10
|
passkey_not_found: "Your passkey doesn't exist or is not valid."
|
|
10
11
|
passkey_verification_failed: "Passkey verification failed."
|
|
12
|
+
passkey_deletion_failed: "Passkey deletion failed."
|
|
13
|
+
security_key_deletion_failed: "Security Key deletion failed."
|
|
11
14
|
sign_in_not_initiated: "Sign in was not initiated."
|
|
12
15
|
two_factor_required: "Two-factor authentication is required to sign in."
|
|
13
16
|
webauthn_credential_not_found: "Your WebAuthn credential doesn't exist or is not valid."
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "appraisal", "~> 2.5"
|
|
6
|
+
gem "capybara", "~> 3.40"
|
|
7
|
+
gem "combustion", "~> 1.3"
|
|
8
|
+
gem "importmap-rails", "~> 2.2"
|
|
9
|
+
gem "propshaft", "~> 1.2"
|
|
10
|
+
gem "pry-byebug", "~> 3.11"
|
|
11
|
+
gem "puma", "~> 6.6"
|
|
12
|
+
gem "rails", "~> 8.1"
|
|
13
|
+
gem "rspec-rails", "~> 8.0"
|
|
14
|
+
gem "rubocop", "~> 1.79"
|
|
15
|
+
gem "rubocop-rails", "~> 2.32"
|
|
16
|
+
gem "rubocop-rspec", "~> 3.6"
|
|
17
|
+
gem "selenium-webdriver"
|
|
18
|
+
gem "sqlite3", "~> 2.7"
|
|
19
|
+
gem "stimulus-rails", "~> 1.3"
|
|
20
|
+
|
|
21
|
+
gemspec path: "../"
|
|
@@ -11,7 +11,7 @@ module Devise
|
|
|
11
11
|
hashed = false
|
|
12
12
|
|
|
13
13
|
if validate(resource){ hashed = true; resource.valid_password?(password) }
|
|
14
|
-
if
|
|
14
|
+
if second_factor_enabled?(resource)
|
|
15
15
|
session[:current_authentication_resource_id] = resource.id
|
|
16
16
|
request.flash[:notice] = two_factor_required_message
|
|
17
17
|
request.commit_flash
|
|
@@ -41,6 +41,10 @@ module Devise
|
|
|
41
41
|
def two_factor_required_message
|
|
42
42
|
I18n.t(:"#{scope}.two_factor_required", resource_name: scope, scope: "devise.failure", default: :two_factor_required)
|
|
43
43
|
end
|
|
44
|
+
|
|
45
|
+
def second_factor_enabled?(resource)
|
|
46
|
+
resource.respond_to?(:second_factor_enabled?) && resource.second_factor_enabled?
|
|
47
|
+
end
|
|
44
48
|
end
|
|
45
49
|
end
|
|
46
50
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rubocop:disable Metrics/ModuleLength
|
|
3
4
|
module Devise
|
|
4
5
|
module Webauthn
|
|
5
6
|
module CredentialsHelper
|
|
@@ -27,7 +28,41 @@ module Devise
|
|
|
27
28
|
data: {
|
|
28
29
|
action: "webauthn-credentials#get:prevent",
|
|
29
30
|
controller: "webauthn-credentials",
|
|
30
|
-
webauthn_credentials_options_param:
|
|
31
|
+
webauthn_credentials_options_param: passkey_authentication_options
|
|
32
|
+
},
|
|
33
|
+
class: form_classes
|
|
34
|
+
) 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)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def security_key_creation_form_for(resource, form_classes: nil, &block)
|
|
42
|
+
form_with(
|
|
43
|
+
url: second_factor_webauthn_credentials_path(resource),
|
|
44
|
+
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
|
+
}
|
|
51
|
+
) do |f|
|
|
52
|
+
concat f.hidden_field(:public_key_credential,
|
|
53
|
+
data: { "webauthn-credentials-target": "credentialHiddenInput" })
|
|
54
|
+
concat capture(f, &block)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def login_with_security_key_button(text = nil, resource:, button_classes: nil, form_classes: nil, &block)
|
|
59
|
+
form_with(
|
|
60
|
+
url: two_factor_authentication_path(resource),
|
|
61
|
+
method: :post,
|
|
62
|
+
data: {
|
|
63
|
+
action: "webauthn-credentials#get:prevent",
|
|
64
|
+
controller: "webauthn-credentials",
|
|
65
|
+
webauthn_credentials_options_param: security_key_authentication_options(resource)
|
|
31
66
|
},
|
|
32
67
|
class: form_classes
|
|
33
68
|
) do |f|
|
|
@@ -44,7 +79,7 @@ module Devise
|
|
|
44
79
|
options = WebAuthn::Credential.options_for_create(
|
|
45
80
|
user: {
|
|
46
81
|
id: resource.webauthn_id,
|
|
47
|
-
name:
|
|
82
|
+
name: resource_human_palatable_identifier
|
|
48
83
|
},
|
|
49
84
|
exclude: resource.passkeys.pluck(:external_id),
|
|
50
85
|
authenticator_selection: {
|
|
@@ -60,8 +95,8 @@ module Devise
|
|
|
60
95
|
end
|
|
61
96
|
end
|
|
62
97
|
|
|
63
|
-
def
|
|
64
|
-
@
|
|
98
|
+
def passkey_authentication_options
|
|
99
|
+
@passkey_authentication_options ||= begin
|
|
65
100
|
options = WebAuthn::Credential.options_for_get(
|
|
66
101
|
user_verification: "required"
|
|
67
102
|
)
|
|
@@ -72,6 +107,49 @@ module Devise
|
|
|
72
107
|
options
|
|
73
108
|
end
|
|
74
109
|
end
|
|
110
|
+
|
|
111
|
+
def create_security_key_options(resource)
|
|
112
|
+
@create_security_key_options ||= begin
|
|
113
|
+
options = WebAuthn::Credential.options_for_create(
|
|
114
|
+
user: {
|
|
115
|
+
id: resource.webauthn_id,
|
|
116
|
+
name: resource_human_palatable_identifier
|
|
117
|
+
},
|
|
118
|
+
exclude: resource.webauthn_credentials.pluck(:external_id),
|
|
119
|
+
authenticator_selection: {
|
|
120
|
+
resident_key: "discouraged",
|
|
121
|
+
user_verification: "discouraged"
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Store challenge in session for later verification
|
|
126
|
+
session[:webauthn_challenge] = options.challenge
|
|
127
|
+
|
|
128
|
+
options
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def security_key_authentication_options(resource)
|
|
133
|
+
@security_key_authentication_options ||= begin
|
|
134
|
+
options = WebAuthn::Credential.options_for_get(
|
|
135
|
+
allow: resource.webauthn_credentials.pluck(:external_id),
|
|
136
|
+
user_verification: "discouraged"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Store challenge in session for later verification
|
|
140
|
+
session[:two_factor_authentication_challenge] = options.challenge
|
|
141
|
+
|
|
142
|
+
options
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def resource_human_palatable_identifier
|
|
147
|
+
authentication_keys = resource.class.authentication_keys
|
|
148
|
+
authentication_keys = authentication_keys.keys if authentication_keys.is_a?(Hash)
|
|
149
|
+
|
|
150
|
+
authentication_keys.filter_map { |authentication_key| resource.public_send(authentication_key) }.first
|
|
151
|
+
end
|
|
75
152
|
end
|
|
76
153
|
end
|
|
77
154
|
end
|
|
155
|
+
# rubocop:enable Metrics/ModuleLength
|
|
@@ -11,6 +11,48 @@ module Devise
|
|
|
11
11
|
options.resident_key = true
|
|
12
12
|
page.driver.browser.add_virtual_authenticator(options)
|
|
13
13
|
end
|
|
14
|
+
|
|
15
|
+
def add_passkey_to_authenticator(authenticator, resource)
|
|
16
|
+
add_credential_to_authenticator(authenticator, resource, passkey: true)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def add_security_key_to_authenticator(authenticator, resource)
|
|
20
|
+
add_credential_to_authenticator(authenticator, resource, passkey: false)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# rubocop:disable Metrics/AbcSize
|
|
24
|
+
# rubocop:disable Metrics/MethodLength
|
|
25
|
+
def add_credential_to_authenticator(authenticator, resource, passkey:)
|
|
26
|
+
credential_id = SecureRandom.random_bytes(16)
|
|
27
|
+
encoded_credential_id = Base64.urlsafe_encode64(credential_id)
|
|
28
|
+
key = OpenSSL::PKey.generate_key("ED25519")
|
|
29
|
+
encoded_private_key = Base64.urlsafe_encode64(key.private_to_der)
|
|
30
|
+
|
|
31
|
+
cose_public_key = COSE::Key::OKP.from_pkey(OpenSSL::PKey.read(key.public_to_der))
|
|
32
|
+
cose_public_key.alg = -8
|
|
33
|
+
encoded_cose_public_key = Base64.urlsafe_encode64(cose_public_key.serialize)
|
|
34
|
+
|
|
35
|
+
credential_json = {
|
|
36
|
+
"credentialId" => encoded_credential_id,
|
|
37
|
+
"isResidentCredential" => passkey,
|
|
38
|
+
"rpId" => "localhost",
|
|
39
|
+
"privateKey" => encoded_private_key,
|
|
40
|
+
"signCount" => 0
|
|
41
|
+
}
|
|
42
|
+
credential_json["userHandle"] = resource.webauthn_id if passkey
|
|
43
|
+
|
|
44
|
+
authenticator.add_credential(credential_json)
|
|
45
|
+
|
|
46
|
+
resource.webauthn_credentials.create!(
|
|
47
|
+
name: "My Credential",
|
|
48
|
+
external_id: Base64.urlsafe_encode64(credential_id, padding: false),
|
|
49
|
+
public_key: encoded_cose_public_key,
|
|
50
|
+
sign_count: 0,
|
|
51
|
+
authentication_factor: passkey ? :first_factor : :second_factor
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
# rubocop:enable Metrics/MethodLength
|
|
55
|
+
# rubocop:enable Metrics/AbcSize
|
|
14
56
|
end
|
|
15
57
|
end
|
|
16
58
|
end
|
|
@@ -5,7 +5,11 @@ require "rails/generators"
|
|
|
5
5
|
module Devise
|
|
6
6
|
module Webauthn
|
|
7
7
|
class ControllersGenerator < Rails::Generators::Base
|
|
8
|
-
CONTROLLERS = %w[
|
|
8
|
+
CONTROLLERS = %w[
|
|
9
|
+
passkeys
|
|
10
|
+
second_factor_webauthn_credentials
|
|
11
|
+
two_factor_authentications
|
|
12
|
+
].freeze
|
|
9
13
|
|
|
10
14
|
desc "Create inherited Devise::Webauthn controllers in your app/controllers folder."
|
|
11
15
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class <%= @scope_prefix %>SecondFactorWebauthnCredentialsController < Devise::SecondFactorWebauthnCredentialsController
|
|
4
|
+
# GET /resource/second_factor_webauthn_credentials/new
|
|
5
|
+
# def new
|
|
6
|
+
# super
|
|
7
|
+
# end
|
|
8
|
+
|
|
9
|
+
# POST /second_factor_webauthn_credentials/passkeys
|
|
10
|
+
# def create
|
|
11
|
+
# super
|
|
12
|
+
# end
|
|
13
|
+
|
|
14
|
+
# DELETE /resource/second_factor_webauthn_credentials/:id
|
|
15
|
+
# def destroy
|
|
16
|
+
# super
|
|
17
|
+
# end
|
|
18
|
+
|
|
19
|
+
# private
|
|
20
|
+
|
|
21
|
+
# Verifies the security key coming in the params and saves it to the database.
|
|
22
|
+
# def verify_and_save_security_key(security_key_from_params)
|
|
23
|
+
# super
|
|
24
|
+
# end
|
|
25
|
+
|
|
26
|
+
# The default url to be used after creating a second factor key. You can overwrite
|
|
27
|
+
# this method in your own SecondFactorWebauthnCredentialsController.
|
|
28
|
+
# def after_update_path
|
|
29
|
+
# super
|
|
30
|
+
# end
|
|
31
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class <%= @scope_prefix %>TwoFactorAuthenticationsController < Devise::TwoFactorAuthenticationsController
|
|
4
|
+
# GET /resource/two_factor_authentication/new
|
|
5
|
+
# def new
|
|
6
|
+
# super
|
|
7
|
+
# end
|
|
8
|
+
|
|
9
|
+
# POST /resource/two_factor_authentication
|
|
10
|
+
# def create
|
|
11
|
+
# super
|
|
12
|
+
# end
|
|
13
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateWebauthnCredentials < ActiveRecord::Migration[<%= Rails.version.to_f %>]
|
|
4
|
+
def change
|
|
5
|
+
create_table :webauthn_credentials do |t|
|
|
6
|
+
t.string :external_id, null: false
|
|
7
|
+
t.string :name, null: false
|
|
8
|
+
t.text :public_key, null: false
|
|
9
|
+
t.integer :sign_count, limit: 8, null: false
|
|
10
|
+
t.references :<%= user_model_name %>, null: false, foreign_key: true
|
|
11
|
+
t.integer :authentication_factor, limit: 1, null: false
|
|
12
|
+
|
|
13
|
+
t.timestamps
|
|
14
|
+
end
|
|
15
|
+
add_index :webauthn_credentials, :external_id, unique: true
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/generators/devise/webauthn/webauthn_credential_model/webauthn_credential_model_generator.rb
CHANGED
|
@@ -6,22 +6,28 @@ require "rails/generators/active_record"
|
|
|
6
6
|
module Devise
|
|
7
7
|
module Webauthn
|
|
8
8
|
class WebauthnCredentialModelGenerator < Rails::Generators::Base
|
|
9
|
+
include Rails::Generators::Migration
|
|
10
|
+
|
|
9
11
|
hide!
|
|
10
12
|
namespace "devise:webauthn:webauthn_credential_model"
|
|
11
13
|
|
|
14
|
+
source_root File.expand_path("templates", __dir__)
|
|
15
|
+
|
|
12
16
|
desc "Generate a WebauthnCredential model with the required fields for WebAuthn"
|
|
13
17
|
class_option :resource_name, type: :string, default: "user", desc: "The resource name for Devise (default: user)"
|
|
14
18
|
|
|
19
|
+
def self.next_migration_number(dirname)
|
|
20
|
+
ActiveRecord::Generators::Base.next_migration_number(dirname)
|
|
21
|
+
end
|
|
22
|
+
|
|
15
23
|
def generate_model
|
|
16
|
-
invoke "active_record:model", [
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"authentication_factor:integer{1}"
|
|
24
|
-
]
|
|
24
|
+
invoke "active_record:model", ["webauthn_credential"], migration: false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# TODO: Remove this in favor of strandard model generator with
|
|
28
|
+
# not null modifier (`!`) once we drop support for Rails < 8.
|
|
29
|
+
def generate_migration
|
|
30
|
+
migration_template "webauthn_credential_migration.rb.erb", "db/migrate/create_webauthn_credentials.rb"
|
|
25
31
|
end
|
|
26
32
|
|
|
27
33
|
def inject_webauthn_credential_content
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: devise-webauthn
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Cedarcode
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: devise
|
|
@@ -38,13 +37,15 @@ dependencies:
|
|
|
38
37
|
- - "~>"
|
|
39
38
|
- !ruby/object:Gem::Version
|
|
40
39
|
version: '3.0'
|
|
41
|
-
description:
|
|
42
40
|
email:
|
|
43
41
|
- webauthn@cedarcode.com
|
|
44
42
|
executables: []
|
|
45
43
|
extensions: []
|
|
46
44
|
extra_rdoc_files: []
|
|
47
45
|
files:
|
|
46
|
+
- ".github/FUNDING.yml"
|
|
47
|
+
- ".github/dependabot.yml"
|
|
48
|
+
- ".github/workflows/release.yml"
|
|
48
49
|
- ".github/workflows/ruby.yml"
|
|
49
50
|
- ".gitignore"
|
|
50
51
|
- ".rspec"
|
|
@@ -73,6 +74,7 @@ files:
|
|
|
73
74
|
- gemfiles/rails_7_1.gemfile
|
|
74
75
|
- gemfiles/rails_7_2.gemfile
|
|
75
76
|
- gemfiles/rails_8_0.gemfile
|
|
77
|
+
- gemfiles/rails_8_1.gemfile
|
|
76
78
|
- gemfiles/rails_edge.gemfile
|
|
77
79
|
- lib/devise/models/passkey_authenticatable.rb
|
|
78
80
|
- lib/devise/models/webauthn_credential_authenticatable.rb
|
|
@@ -94,7 +96,10 @@ files:
|
|
|
94
96
|
- lib/generators/devise/webauthn/stimulus/templates/webauthn_credentials_controller.js
|
|
95
97
|
- lib/generators/devise/webauthn/templates/controllers/README
|
|
96
98
|
- lib/generators/devise/webauthn/templates/controllers/passkeys_controller.rb.tt
|
|
99
|
+
- lib/generators/devise/webauthn/templates/controllers/second_factor_webauthn_credentials_controller.rb.tt
|
|
100
|
+
- lib/generators/devise/webauthn/templates/controllers/two_factor_authentications_controller.rb.tt
|
|
97
101
|
- lib/generators/devise/webauthn/views_generator.rb
|
|
102
|
+
- lib/generators/devise/webauthn/webauthn_credential_model/templates/webauthn_credential_migration.rb.erb
|
|
98
103
|
- lib/generators/devise/webauthn/webauthn_credential_model/webauthn_credential_model_generator.rb
|
|
99
104
|
- lib/generators/devise/webauthn/webauthn_id/webauthn_id_generator.rb
|
|
100
105
|
homepage: https://github.com/cedarcode/devise-webauthn
|
|
@@ -105,7 +110,6 @@ metadata:
|
|
|
105
110
|
source_code_uri: https://github.com/cedarcode/devise-webauthn
|
|
106
111
|
changelog_uri: https://github.com/cedarcode/devise-webauthn/blob/master/CHANGELOG.md
|
|
107
112
|
rubygems_mfa_required: 'true'
|
|
108
|
-
post_install_message:
|
|
109
113
|
rdoc_options: []
|
|
110
114
|
require_paths:
|
|
111
115
|
- lib
|
|
@@ -120,8 +124,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
120
124
|
- !ruby/object:Gem::Version
|
|
121
125
|
version: '0'
|
|
122
126
|
requirements: []
|
|
123
|
-
rubygems_version: 3.
|
|
124
|
-
signing_key:
|
|
127
|
+
rubygems_version: 3.6.7
|
|
125
128
|
specification_version: 4
|
|
126
129
|
summary: Devise extension to support WebAuthn.
|
|
127
130
|
test_files: []
|