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.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/dependabot.yml +6 -0
  4. data/.github/workflows/release.yml +28 -0
  5. data/.github/workflows/ruby.yml +9 -2
  6. data/.rubocop.yml +1 -0
  7. data/Appraisals +4 -0
  8. data/CHANGELOG.md +13 -0
  9. data/Gemfile.lock +1 -1
  10. data/README.md +24 -5
  11. data/app/controllers/devise/passkeys_controller.rb +6 -1
  12. data/app/controllers/devise/second_factor_webauthn_credentials_controller.rb +7 -17
  13. data/app/controllers/devise/two_factor_authentications_controller.rb +1 -7
  14. data/app/views/devise/second_factor_webauthn_credentials/new.html.erb +1 -11
  15. data/app/views/devise/two_factor_authentications/new.html.erb +1 -12
  16. data/config/locales/en.yml +4 -1
  17. data/gemfiles/rails_8_1.gemfile +21 -0
  18. data/lib/devise/strategies/database_authenticatable.rb +5 -1
  19. data/lib/devise/webauthn/helpers/credentials_helper.rb +82 -4
  20. data/lib/devise/webauthn/test/authenticator_helpers.rb +42 -0
  21. data/lib/devise/webauthn/version.rb +1 -1
  22. data/lib/generators/devise/webauthn/controllers_generator.rb +5 -1
  23. data/lib/generators/devise/webauthn/templates/controllers/second_factor_webauthn_credentials_controller.rb.tt +31 -0
  24. data/lib/generators/devise/webauthn/templates/controllers/two_factor_authentications_controller.rb.tt +13 -0
  25. data/lib/generators/devise/webauthn/views_generator.rb +2 -0
  26. data/lib/generators/devise/webauthn/webauthn_credential_model/templates/webauthn_credential_migration.rb.erb +17 -0
  27. data/lib/generators/devise/webauthn/webauthn_credential_model/webauthn_credential_model_generator.rb +15 -9
  28. metadata +10 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5b2e30cdcc561dc53baf71ada282183019321b12b1820afc6107b67c000c1297
4
- data.tar.gz: f01204525d77a874a131b4a4f13928faa9d6819160792bc12efdb0ad9b9b6d5b
3
+ metadata.gz: bf586d5c54c83dcd4dd18c38e2dfe2c8d16cd86dc90e3c5c09b516d3c60dff04
4
+ data.tar.gz: 5a56dc38f1bd836b69dae7057f44ba95868b1edbfe5820eab77466ec2db59ffd
5
5
  SHA512:
6
- metadata.gz: 3c4c14744bbaeb2ed55957baa3d868dd5eb4ebb48300a13a17cd110c329a373f18f97706da1d08f2cfe48d09d5e7a96d0d409c8af485658a55c357f121dbadde
7
- data.tar.gz: 3456027c51e2aab1434a47afe3943e4e75910d38bba9e0dc551662cc0a0f4708d52d108e30229fe390f93f89e8d411d7be669cc9c2ffdb31dea2aa41aa280669
6
+ metadata.gz: c62d4d71fd255dbf5867c76a524ed2b8e22df167e537e52a928f5a88351291ccb4cc74aef5e0466b27c42242d6b0ab105e67acc2ba9d7cb673625ff0b31100b4
7
+ data.tar.gz: fefeab5ad1470d5a2eb3f451c823763f237c8fe3f302f46d02917bbf337a0ab3011fc518b209d6105aed7ca23c21cb8ef9621f360a6a980204d3fe28508cf552
@@ -0,0 +1 @@
1
+ github: [cedarcode]
@@ -0,0 +1,6 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "github-actions"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
@@ -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
@@ -8,7 +8,7 @@ jobs:
8
8
 
9
9
  steps:
10
10
  - name: Check out repository code
11
- uses: actions/checkout@v5
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@v5
72
+ uses: actions/checkout@v6
66
73
 
67
74
  - name: Set up Ruby
68
75
  uses: ruby/setup-ruby@v1
data/.rubocop.yml CHANGED
@@ -7,6 +7,7 @@ AllCops:
7
7
  NewCops: enable
8
8
  Exclude:
9
9
  - "spec/internal/**/*"
10
+ - "spec/tmp/**/*"
10
11
  - "vendor/**/*"
11
12
  - "gemfiles/**/*"
12
13
  - "lib/devise/strategies/database_authenticatable.rb"
data/Appraisals CHANGED
@@ -4,6 +4,10 @@ appraise "rails-edge" do
4
4
  gem "rails", github: "rails/rails", branch: "main"
5
5
  end
6
6
 
7
+ appraise "rails-8_1" do
8
+ gem "rails", "~> 8.1"
9
+ end
10
+
7
11
  appraise "rails-8_0" do
8
12
  gem "rails", "~> 8.0"
9
13
  end
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- devise-webauthn (0.2.0)
4
+ devise-webauthn (0.2.2)
5
5
  devise (~> 4.9)
6
6
  webauthn (~> 3.0)
7
7
 
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Devise::Webauthn
2
2
  [![Gem Version](https://badge.fury.io/rb/devise-webauthn.svg)](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, :passkey_authenticatable
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
- ### Adding Passkeys
81
+ ### Passkey authentication
82
+
83
+ #### Adding Passkeys
81
84
  Signed-in users can add passkeys by visiting `/users/passkeys/new`.
82
85
 
83
- ### Sign In with Passkeys
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
- <%= form_with(
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
- <%= form_with(
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) %>
@@ -2,12 +2,15 @@ en:
2
2
  devise:
3
3
  passkeys:
4
4
  passkey_created: "Passkey created successfully."
5
- passkey_creation_failed: "Passkey creation failed."
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 resource.second_factor_enabled?
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: webauthn_authentication_options
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: resource.email
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 webauthn_authentication_options
64
- @webauthn_authentication_options ||= begin
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Devise
4
4
  module Webauthn
5
- VERSION = "0.2.0"
5
+ VERSION = "0.2.2"
6
6
  end
7
7
  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[passkeys].freeze
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
@@ -22,7 +22,9 @@ module Devise
22
22
  end
23
23
  else
24
24
  view_directory :passkeys
25
+ view_directory :second_factor_webauthn_credentials
25
26
  view_directory :sessions
27
+ view_directory :two_factor_authentications
26
28
  end
27
29
  end
28
30
 
@@ -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
@@ -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
- "webauthn_credential",
18
- "external_id:string:uniq",
19
- "name:string",
20
- "public_key:text",
21
- "sign_count:integer{8}",
22
- "#{user_model_name}:references",
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.0
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: 2025-12-03 00:00:00.000000000 Z
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.2.1
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: []