devise-webauthn 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5b2e30cdcc561dc53baf71ada282183019321b12b1820afc6107b67c000c1297
4
- data.tar.gz: f01204525d77a874a131b4a4f13928faa9d6819160792bc12efdb0ad9b9b6d5b
3
+ metadata.gz: 8083a823cce7edf858475a062ea009193c3a86cf9b4a821db1dd2715430580d4
4
+ data.tar.gz: df00de569113a642c13f87cc4f6c93836acb5c5df1ef1cecfe492b01247a17aa
5
5
  SHA512:
6
- metadata.gz: 3c4c14744bbaeb2ed55957baa3d868dd5eb4ebb48300a13a17cd110c329a373f18f97706da1d08f2cfe48d09d5e7a96d0d409c8af485658a55c357f121dbadde
7
- data.tar.gz: 3456027c51e2aab1434a47afe3943e4e75910d38bba9e0dc551662cc0a0f4708d52d108e30229fe390f93f89e8d411d7be669cc9c2ffdb31dea2aa41aa280669
6
+ metadata.gz: ce764390d121fc68bd1732d655700ccae382ced384a24f350256f2bb633910d0038972637a8e9f4f5e512d0e13030c53851f7288733fb233e8d86fe00ab5c8b8
7
+ data.tar.gz: d2072d245253437be40005c81b9879f22f0a7c0fa3e0e3cd39b9b03f812aa05cdcd7404cbd4123afac473785364472f3e025dbdf025ae3f74af117854dcbca38
@@ -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
@@ -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
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,13 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## [v0.2.1](https://github.com/cedarcode/devise-webauthn/compare/v0.2.0...v0.2.1/) - 2025-12-10
6
+
7
+ - Add form helpers for security key registration and 2FA authentication.
8
+ - Fix incorrect call to `resource_name` instead of using passed `resource` param in `login_with_security_key_button` helper.
9
+ - Fix `NoMethodError` when calling `second_factor_enabled?` on resources without 2FA.
10
+ - Avoid assuming `email` as the authentication key of the resource in form helpers.
11
+
5
12
  ## [v0.2.0](https://github.com/cedarcode/devise-webauthn/compare/v0.1.2...v0.2.0/) - 2025-12-03
6
13
 
7
14
  - 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.1)
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
@@ -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]))
@@ -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,7 +2,6 @@ en:
2
2
  devise:
3
3
  passkeys:
4
4
  passkey_created: "Passkey created successfully."
5
- passkey_creation_failed: "Passkey creation failed."
6
5
  second_factor_webauthn_credentials:
7
6
  security_key_created: "Security Key created successfully."
8
7
  failure:
@@ -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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Devise
4
4
  module Webauthn
5
- VERSION = "0.2.0"
5
+ VERSION = "0.2.1"
6
6
  end
7
7
  end
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.1
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,14 @@ 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/workflows/release.yml"
48
48
  - ".github/workflows/ruby.yml"
49
49
  - ".gitignore"
50
50
  - ".rspec"
@@ -73,6 +73,7 @@ files:
73
73
  - gemfiles/rails_7_1.gemfile
74
74
  - gemfiles/rails_7_2.gemfile
75
75
  - gemfiles/rails_8_0.gemfile
76
+ - gemfiles/rails_8_1.gemfile
76
77
  - gemfiles/rails_edge.gemfile
77
78
  - lib/devise/models/passkey_authenticatable.rb
78
79
  - lib/devise/models/webauthn_credential_authenticatable.rb
@@ -105,7 +106,6 @@ metadata:
105
106
  source_code_uri: https://github.com/cedarcode/devise-webauthn
106
107
  changelog_uri: https://github.com/cedarcode/devise-webauthn/blob/master/CHANGELOG.md
107
108
  rubygems_mfa_required: 'true'
108
- post_install_message:
109
109
  rdoc_options: []
110
110
  require_paths:
111
111
  - lib
@@ -120,8 +120,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
120
120
  - !ruby/object:Gem::Version
121
121
  version: '0'
122
122
  requirements: []
123
- rubygems_version: 3.2.1
124
- signing_key:
123
+ rubygems_version: 3.6.7
125
124
  specification_version: 4
126
125
  summary: Devise extension to support WebAuthn.
127
126
  test_files: []