webauthn-rails 0.0.1 → 0.1.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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +167 -13
  4. data/Rakefile +12 -4
  5. data/lib/generators/erb/webauthn_authentication/templates/app/views/passkeys/new.html.erb.tt +18 -0
  6. data/lib/generators/erb/webauthn_authentication/templates/app/views/second_factor_authentications/new.html.erb.tt +18 -0
  7. data/lib/generators/erb/webauthn_authentication/templates/app/views/second_factor_webauthn_credentials/new.html.erb.tt +18 -0
  8. data/lib/generators/erb/webauthn_authentication/webauthn_authentication_generator.rb +35 -0
  9. data/lib/generators/test_unit/webauthn_authentication/templates/test/controllers/passkeys_controller_test.rb +103 -0
  10. data/lib/generators/test_unit/webauthn_authentication/templates/test/controllers/second_factor_authentications_controller_test.rb +131 -0
  11. data/lib/generators/test_unit/webauthn_authentication/templates/test/controllers/second_factor_webauthn_credentials_controller_test.rb +103 -0
  12. data/lib/generators/test_unit/webauthn_authentication/templates/test/controllers/webauthn_sessions_controller_test.rb +123 -0
  13. data/lib/generators/test_unit/webauthn_authentication/templates/test/system/manage_webauthn_credentials_test.rb +76 -0
  14. data/lib/generators/test_unit/webauthn_authentication/templates/test/test_helpers/virtual_authenticator_test_helper.rb +9 -0
  15. data/lib/generators/test_unit/webauthn_authentication/webauthn_authentication_generator.rb +25 -0
  16. data/lib/generators/webauthn_authentication/bundle_helper.rb +30 -0
  17. data/lib/generators/webauthn_authentication/templates/app/controllers/passkeys_controller.rb +61 -0
  18. data/lib/generators/webauthn_authentication/templates/app/controllers/second_factor_authentications_controller.rb +62 -0
  19. data/lib/generators/webauthn_authentication/templates/app/controllers/second_factor_webauthn_credentials_controller.rb +59 -0
  20. data/lib/generators/webauthn_authentication/templates/app/controllers/webauthn_sessions_controller.rb +50 -0
  21. data/lib/generators/webauthn_authentication/templates/app/javascript/controllers/webauthn_credentials_controller.js +64 -0
  22. data/lib/generators/webauthn_authentication/templates/app/models/webauthn_credential.rb +12 -0
  23. data/lib/generators/webauthn_authentication/templates/config/initializers/webauthn.rb +8 -0
  24. data/lib/generators/webauthn_authentication/webauthn_authentication_generator.rb +184 -0
  25. data/lib/tasks/webauthn/rails_tasks.rake +4 -0
  26. data/lib/webauthn/rails/version.rb +1 -3
  27. data/lib/webauthn/rails.rb +1 -5
  28. metadata +55 -18
  29. data/.rspec +0 -3
  30. data/CHANGELOG.md +0 -5
  31. data/LICENSE.txt +0 -21
  32. data/sig/webauthn/rails.rbs +0 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c23bb425dcafa1c050ddda0f7533a264ea071451215000d54da251c712a5189c
4
- data.tar.gz: fa31284ff35f0804a31ef43eeb5e0905407581b197e4ba331ad52a26f33a775e
3
+ metadata.gz: 20ded36acb2b7a3f4d67cdd4e860dc41e6f8490bb9143be6a0575117e1e7a84e
4
+ data.tar.gz: 8b2d46393c0d24e340b82f7f5d908e34fdd7cedb30501b8e8ba9663f217c23b9
5
5
  SHA512:
6
- metadata.gz: 7e67df5d493f1d58d9567198be453b130a4ce7f6a0bdfb8ef98227bfec80255d79fbb42d8eb11656f30f1a7a0dc2f6693f027fa4160e9ceec02aabafd87a833a
7
- data.tar.gz: 4b57092b8e648edd83f61b74801d4c134e74c94c7caf63dd1bf0988c50bdc564a450f44e0ddb4a8fdb578162683997c4c2e42f54500faedfdf2c068d3854b544
6
+ metadata.gz: 89c27f2bd69320aef0d3a36207788478ce85e7eea4b1f722950ec12749fa3fdcaa5a3eed1f322b77425d5d8b8e1d848cf912d84cd55c7872fa2142750626b345
7
+ data.tar.gz: d125c6229d26da7d19794479091ec59b706e10166a70a5e3f228a4d3571906a5b3d3a3db23c81cfb2c735e56c91c9bdf1ee01f249689761641d46aad1319f8ff
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Cedarcode
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,32 +1,186 @@
1
- # Webauthn::Rails
1
+ # WebAuthn Rails
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/webauthn/rails`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ [![Gem Version](https://badge.fury.io/rb/webauthn-rails.svg)](https://badge.fury.io/rb/webauthn-rails)
4
4
 
5
- ## Installation
5
+ **webauthn-rails** adds passkeys to your Rails app with almost no setup. It provides a generator that installs everything you need for a secure passwordless and two-factor authentication flow, built on top of the [Rails Authentication system](https://guides.rubyonrails.org/security.html). Webauthn Rails combines [Stimulus](https://stimulus.hotwired.dev/) for the frontend experience with the [WebAuthn Ruby gem](https://github.com/cedarcode/webauthn-ruby) on the server side – giving you a ready-to-use authentication system.
6
6
 
7
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
7
+ ## Requirements
8
8
 
9
- Install the gem and add to the application's Gemfile by executing:
9
+ - **Ruby**: 3.2+
10
+ - **Rails**: 8.0+
11
+ - **Stimulus Rails**: This gem requires [stimulus-rails](https://github.com/hotwired/stimulus-rails) to be installed and configured in your application
10
12
 
11
- $ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
13
+ ### JavaScript Dependencies
12
14
 
13
- If bundler is not being used to manage dependencies, install the gem by executing:
15
+ The generator automatically handles JavaScript dependencies based on your setup:
14
16
 
15
- $ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
17
+ - **Importmap**: Pins `@github/webauthn-json/browser-ponyfill` to your importmap
18
+ - **Node.js/Yarn/Bun**: Adds the package to your package manager
16
19
 
17
20
  ## Usage
18
21
 
19
- TODO: Write usage instructions here
22
+ Install the gem by running:
20
23
 
21
- ## Development
24
+ ```bash
25
+ $ bundle add webauthn-rails --group development
26
+ ```
22
27
 
23
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
28
+ Next, you need to run the generator:
24
29
 
25
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
30
+ ```bash
31
+ $ bin/rails generate webauthn_authentication
32
+ ```
33
+
34
+ If you haven't generated Rails authentication yet, you can pass the `--with-rails-authentication` flag in order to generate it alongside the webauthn authentication:
35
+ ```bash
36
+ $ bin/rails generate webauthn_authentication --with-rails-authentication
37
+ ```
38
+
39
+ This generator will:
40
+
41
+ - **Optionally** invoke the [Rails Authentication generator](https://github.com/rails/rails/blob/main/railties/lib/rails/generators/rails/authentication/authentication_generator.rb) if the `--with-rails-authentication` flag is passed.
42
+ - Modifies the `SessionsController` to support WebAuthn two-factor authentication.
43
+ - Create controllers for handling passwordless and two-factor authentication, as well as credential management.
44
+ - Update new session views to support passkey authentication.
45
+ - Add views for credential management and two-factor authentication.
46
+ - Update the `User` model to include association with credentials and webauthn-related logic.
47
+ - Generate database migrations for WebAuthn credentials.
48
+ - Add passkey authentication, two-factor authentication and credential management routes.
49
+ - Generate a Stimulus controller for WebAuthn interactions.
50
+ - Create the WebAuthn initializer.
51
+
52
+ ### Post-Installation Configuration
53
+
54
+ After running the generator, you **must** configure the WebAuthn settings:
55
+
56
+ 1. Edit `config/initializers/webauthn.rb` and set your allowed origins and Relying Party name:
57
+
58
+ ```ruby
59
+ WebAuthn.configure do |config|
60
+ # This value needs to match `window.location.origin` evaluated by
61
+ # the User Agent during registration and authentication ceremonies.
62
+ config.allowed_origins = ["https://yourapp.com"]
63
+
64
+ # Relying Party name for display purposes
65
+ config.rp_name = "Your App Name"
66
+ end
67
+ ```
68
+
69
+ 2. Run the migrations:
70
+
71
+ ```bash
72
+ $ bin/rails db:migrate
73
+ ```
74
+
75
+ ## How it Works
76
+
77
+ ### User Sign-In
78
+
79
+ Users can sign in by visiting `/session/new`. The generated setup supports two ways to log in:
80
+
81
+ - Email and password – via the standard Rails Authentication flow. On top of that, if the user has enabled two-factor authentication, they will be prompted to verify with a WebAuthn credential.
82
+ - Passkey (WebAuthn) – by selecting a [passkey](https://www.w3.org/TR/webauthn-3/#discoverable-credential) linked to the user’s account.
83
+
84
+ The WebAuthn passkey sign-in flow works as follows:
85
+ 1. User clicks "Sign in with Passkey", starting a WebAuthn authentication ceremony.
86
+ 2. Browser shows available passkeys.
87
+ 3. User selects a passkey and verifies with their [authenticator](https://www.w3.org/TR/webauthn-3/#webauthn-authenticator).
88
+ 4. The server verifies the response and signs in the user.
89
+
90
+ The WebAuthn two-factor authentication flow works as follows:
91
+ 1. User signs in with email and password.
92
+ 2. If the user has 2FA enabled, they are asked to use a webauthn credential to complete sign-in.
93
+ 3. User selects a credential and verifies with their authenticator.
94
+ 4. The server verifies the response and completes sign-in.
95
+
96
+ ### Adding Credentials
97
+
98
+ Signed-in users can add passkeys by visiting `/passkeys/new`, and second factor credentials by visiting `/second_factor_webauthn_credentials/new`.
99
+
100
+
101
+ ### Models
102
+
103
+ #### User Model
104
+
105
+ The generator adds WebAuthn functionality to your User model:
106
+
107
+ ```ruby
108
+ class User < ApplicationRecord
109
+ has_many :webauthn_credentials, dependent: :destroy
110
+ with_options class_name: "WebauthnCredential" do
111
+ has_many :second_factor_webauthn_credentials, -> { second_factor }
112
+ has_many :passkeys, -> { passkey }
113
+ end
114
+
115
+ after_initialize do
116
+ self.webauthn_id ||= WebAuthn.generate_user_id
117
+ end
118
+
119
+ def second_factor_enabled?
120
+ webauthn_credentials.any?
121
+ end
122
+ end
123
+ ```
124
+
125
+ #### WebauthnCredential Model
126
+
127
+ Stores the public keys and metadata for each registered authenticator:
128
+
129
+ ```ruby
130
+ class WebauthnCredential < ApplicationRecord
131
+ belongs_to :user
132
+
133
+ validates :external_id, :public_key, :nickname, :sign_count, presence: true
134
+ validates :external_id, uniqueness: true
135
+ validates :sign_count,
136
+ numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 2**32 - 1 }
137
+
138
+ enum :authentication_factor, { first_factor: 0, second_factor: 1 }
139
+
140
+ scope :passkey, -> { first_factor }
141
+ end
142
+ ```
143
+
144
+ ## Customization
145
+
146
+ ### Views
147
+
148
+ The generator creates view templates that you can customize:
149
+
150
+ - `app/views/passkeys/new.html.erb` - Add new passkey form.
151
+ - `app/views/second_factor_webauthn_credentials/new.html.erb` - Add new second factor credential form.
152
+ - `app/views/second_factor_authentications/new.html.erb` - Two-factor authentication form.
153
+
154
+ ### Stimulus Controller
155
+
156
+ The generated Stimulus controller (`webauthn_credentials_controller.js`) handles the WebAuthn JavaScript API interactions. You can extend or customize it for your specific needs.
26
157
 
27
158
  ## Contributing
28
159
 
29
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/webauthn-rails.
160
+ Issues and pull requests are welcome on GitHub at https://github.com/cedarcode/webauthn-rails.
161
+
162
+ ### Development
163
+
164
+ After checking out the repo, run:
165
+
166
+ ```bash
167
+ $ bundle install
168
+ ```
169
+
170
+ To run the tests:
171
+
172
+ ```bash
173
+ $ bundle exec rake test
174
+ $ bundle exec rake test_dummy
175
+ ```
176
+
177
+ To run the linter:
178
+
179
+ ```bash
180
+ $ bundle exec rubocop
181
+ ```
182
+
183
+ Before submitting a PR, make sure both tests pass and there are no linting errors.
30
184
 
31
185
  ## License
32
186
 
data/Rakefile CHANGED
@@ -1,8 +1,16 @@
1
- # frozen_string_literal: true
1
+ require "bundler/setup"
2
+ require 'rake/testtask'
2
3
 
3
4
  require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
5
5
 
6
- RSpec::Core::RakeTask.new(:spec)
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << 'lib'
8
+ t.libs << 'test'
9
+ t.test_files = FileList['test/**/*_test.rb'].exclude('test/tmp/**/*_test.rb').exclude('test/dummy/**/*_test.rb')
10
+ end
7
11
 
8
- task default: :spec
12
+ Rake::TestTask.new(:test_dummy) do |t|
13
+ t.libs << 'test/dummy/lib'
14
+ t.libs << 'test/dummy/test'
15
+ t.test_files = FileList['test/dummy/**/*_test.rb']
16
+ end
@@ -0,0 +1,18 @@
1
+ <%%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
2
+ <%%= tag.div(flash[:notice], style: "color:green") if flash[:notice] %>
3
+
4
+ <%%= form_with(
5
+ scope: :credential,
6
+ url: passkeys_path,
7
+ data: {
8
+ controller: "webauthn-credentials",
9
+ action: "webauthn-credentials#create:prevent",
10
+ "webauthn-credentials-options-url-value": create_options_passkeys_path,
11
+ }) do |form| %>
12
+ <div class="field">
13
+ <%%= form.label :nickname, 'Security Key nickname' %>
14
+ <%%= form.text_field :nickname, required: true %>
15
+ </div>
16
+ <%%= form.hidden_field :public_key_credential, data: { "webauthn-credentials-target": "credentialHiddenInput" } %>
17
+ <%%= form.submit "Add Security Key", disabled: true, data: { "webauthn-credentials-target": "submitButton" } %>
18
+ <%% end %>
@@ -0,0 +1,18 @@
1
+ <%%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
2
+ <%%= tag.div(flash[:notice], style: "color:green") if flash[:notice] %>
3
+
4
+ <h3>Two-factor authentication</h3>
5
+ <p>Use a security key to sign in.</p>
6
+
7
+ <%%= form_with(
8
+ scope: :session,
9
+ url: second_factor_authentication_path,
10
+ method: :post,
11
+ data: {
12
+ controller: "webauthn-credentials",
13
+ action: "webauthn-credentials#get:prevent",
14
+ "webauthn-credentials-options-url-value": get_options_second_factor_authentication_path,
15
+ }) do |f| %>
16
+ <%%= f.hidden_field :public_key_credential, data: { "webauthn-credentials-target": "credentialHiddenInput" } %>
17
+ <%%= f.submit "Use Security Key", disabled: true, data: { "webauthn-credentials-target": "submitButton" } %>
18
+ <%% end %>
@@ -0,0 +1,18 @@
1
+ <%%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
2
+ <%%= tag.div(flash[:notice], style: "color:green") if flash[:notice] %>
3
+
4
+ <%%= form_with(
5
+ scope: :credential,
6
+ url: second_factor_webauthn_credentials_path,
7
+ data: {
8
+ controller: "webauthn-credentials",
9
+ action: "webauthn-credentials#create:prevent",
10
+ "webauthn-credentials-options-url-value": create_options_second_factor_webauthn_credentials_path,
11
+ }) do |form| %>
12
+ <div class="field">
13
+ <%%= form.label :nickname, 'Security Key nickname' %>
14
+ <%%= form.text_field :nickname, required: true %>
15
+ </div>
16
+ <%%= form.hidden_field :public_key_credential, data: { "webauthn-credentials-target": "credentialHiddenInput" } %>
17
+ <%%= form.submit "Add Security Key", disabled: true, data: { "webauthn-credentials-target": "submitButton" } %>
18
+ <%% end %>
@@ -0,0 +1,35 @@
1
+ require "rails/generators/erb"
2
+
3
+ module Erb
4
+ module Generators
5
+ class WebauthnAuthenticationGenerator < Rails::Generators::Base
6
+ hide!
7
+ source_root File.expand_path("../templates", __FILE__)
8
+
9
+ def create_files
10
+ template "app/views/passkeys/new.html.erb.tt"
11
+ template "app/views/second_factor_authentications/new.html.erb.tt"
12
+ template "app/views/second_factor_webauthn_credentials/new.html.erb.tt"
13
+ end
14
+
15
+ def inject_into_rails_session_view
16
+ append_to_file "app/views/sessions/new.html.erb" do
17
+ <<-ERB.strip_heredoc
18
+ <%= form_with(
19
+ scope: :session,
20
+ url: webauthn_session_path,
21
+ method: :post,
22
+ data: {
23
+ controller: "webauthn-credentials",
24
+ action: "webauthn-credentials#get:prevent",
25
+ "webauthn-credentials-options-url-value": get_options_webauthn_session_path,
26
+ }) do |f| %>
27
+ <%= f.hidden_field :public_key_credential, data: { "webauthn-credentials-target": "credentialHiddenInput" } %>
28
+ <%= f.submit "Sign In with Passkey", disabled: true, data: { "webauthn-credentials-target": "submitButton" } %>
29
+ <% end %>
30
+ ERB
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,103 @@
1
+ require "test_helper"
2
+ require "webauthn/fake_client"
3
+
4
+ class PasskeysControllerTest < ActionDispatch::IntegrationTest
5
+ setup do
6
+ @user = users(:one)
7
+ @client = WebAuthn::FakeClient.new(WebAuthn.configuration.allowed_origins.first)
8
+ end
9
+
10
+ test "create_options" do
11
+ sign_in_as @user
12
+ post create_options_passkeys_url
13
+
14
+ assert_response :success
15
+ body = JSON.parse(response.body)
16
+ assert body["challenge"].present?
17
+ assert body["authenticatorSelection"]["residentKey"] == "required"
18
+ assert body["authenticatorSelection"]["userVerification"] == "required"
19
+
20
+ assert_equal session[:current_registration][:challenge], body["challenge"]
21
+ end
22
+
23
+ test "create_options unauthenticated" do
24
+ post create_options_passkeys_url
25
+
26
+ assert_response :redirect
27
+ assert_redirected_to new_session_url
28
+ end
29
+
30
+ test "create" do
31
+ sign_in_as @user
32
+
33
+ post create_options_passkeys_url
34
+ challenge = session[:current_registration][:challenge]
35
+
36
+ public_key_credential = @client.create(
37
+ challenge: challenge,
38
+ user_verified: true,
39
+ )
40
+
41
+ assert_difference("WebauthnCredential.count", 1) do
42
+ post passkeys_url, params: {
43
+ credential: {
44
+ nickname: "My Passkey",
45
+ public_key_credential: public_key_credential.to_json
46
+ }
47
+ }
48
+ end
49
+
50
+ assert_redirected_to root_path
51
+ assert_match (/Security Key registered successfully/), flash[:notice]
52
+ assert_nil session[:current_registration]
53
+ end
54
+
55
+ test "create with WebAuthn error" do
56
+ sign_in_as @user
57
+
58
+ post create_options_passkeys_url
59
+ challenge = session[:current_registration][:challenge]
60
+
61
+ public_key_credential = @client.create(
62
+ challenge: challenge,
63
+ user_verified: false,
64
+ )
65
+
66
+ assert_no_difference("WebauthnCredential.count") do
67
+ post passkeys_url, params: {
68
+ credential: {
69
+ nickname: "My Passkey",
70
+ public_key_credential: public_key_credential.to_json
71
+ }
72
+ }
73
+ end
74
+
75
+ assert_redirected_to new_passkey_path
76
+ assert_match (/Verification failed/), flash[:alert]
77
+ assert_nil session[:current_registration]
78
+ end
79
+
80
+ test "create unauthenticated" do
81
+ post passkeys_url
82
+
83
+ assert_response :redirect
84
+ assert_redirected_to new_session_url
85
+ end
86
+
87
+ test "destroy" do
88
+ credential = WebauthnCredential.passkey.create!(
89
+ nickname: "My Passkey",
90
+ user: @user,
91
+ external_id: "external-id",
92
+ public_key: "public-key",
93
+ sign_count: 0,
94
+ )
95
+
96
+ sign_in_as @user
97
+
98
+ assert_difference("WebauthnCredential.count", -1) do
99
+ delete passkey_url(credential)
100
+ end
101
+ assert_redirected_to root_path
102
+ end
103
+ end
@@ -0,0 +1,131 @@
1
+ require "test_helper"
2
+ require "webauthn/fake_client"
3
+
4
+ class SecondFactorAuthenticationsControllerTest < ActionDispatch::IntegrationTest
5
+ setup do
6
+ @user = users(:one)
7
+ @client = WebAuthn::FakeClient.new(WebAuthn.configuration.allowed_origins.first)
8
+
9
+ creation_options = WebAuthn::Credential.options_for_create(
10
+ user: { id: @user.webauthn_id, name: @user.email_address },
11
+ authenticator_selection: { resident_key: "discouraged", user_verification: "discouraged" }
12
+ )
13
+ create_options = @client.create(challenge: creation_options.challenge)
14
+ credential = WebAuthn::Credential.from_create(create_options)
15
+
16
+ WebauthnCredential.second_factor.create!(
17
+ nickname: "My Security Key",
18
+ user: @user,
19
+ external_id: credential.id,
20
+ public_key: credential.public_key,
21
+ sign_count: 0
22
+ )
23
+ end
24
+
25
+ test "get_options" do
26
+ post session_path, params: { email_address: @user.email_address, password: "password" }
27
+
28
+ post get_options_second_factor_authentication_url
29
+
30
+ assert_response :success
31
+ body = JSON.parse(response.body)
32
+ assert body["challenge"].present?
33
+ assert body["userVerification"] == "discouraged"
34
+
35
+ assert_equal session[:current_authentication][:challenge], body["challenge"]
36
+ end
37
+
38
+ test "create" do
39
+ post session_path, params: { email_address: @user.email_address, password: "password" }
40
+
41
+ post get_options_second_factor_authentication_url
42
+ challenge = session[:current_authentication][:challenge]
43
+
44
+ public_key_credential = @client.get(challenge: challenge, user_verified: false)
45
+
46
+ post second_factor_authentication_url, params: {
47
+ session: {
48
+ public_key_credential: public_key_credential.to_json
49
+ }
50
+ }
51
+
52
+ assert_redirected_to root_path
53
+ assert_nil session[:current_authentication]
54
+ end
55
+
56
+ test "create with a passkey" do
57
+ client = WebAuthn::FakeClient.new(WebAuthn.configuration.allowed_origins.first)
58
+
59
+ creation_options = WebAuthn::Credential.options_for_create(
60
+ user: { id: @user.webauthn_id, name: @user.email_address },
61
+ authenticator_selection: { resident_key: "discouraged", user_verification: "discouraged" }
62
+ )
63
+ create_options = client.create(challenge: creation_options.challenge)
64
+ credential = WebAuthn::Credential.from_create(create_options)
65
+
66
+
67
+ WebauthnCredential.passkey.create!(
68
+ nickname: "My Security Key",
69
+ user: @user,
70
+ external_id: credential.id,
71
+ public_key: credential.public_key,
72
+ sign_count: 0
73
+ )
74
+
75
+ post session_path, params: { email_address: @user.email_address, password: "password" }
76
+
77
+ post get_options_second_factor_authentication_url
78
+ challenge = session[:current_authentication][:challenge]
79
+
80
+ public_key_credential = client.get(challenge: challenge, user_verified: false)
81
+
82
+ post second_factor_authentication_url, params: {
83
+ session: {
84
+ public_key_credential: public_key_credential.to_json
85
+ }
86
+ }
87
+
88
+ assert_redirected_to root_path
89
+ assert_nil session[:current_authentication]
90
+ end
91
+
92
+ test "create with WebAuthn error" do
93
+ post session_path, params: { email_address: @user.email_address, password: "password" }
94
+
95
+ post get_options_second_factor_authentication_url
96
+
97
+ public_key_credential = @client.get(
98
+ user_verified: false
99
+ )
100
+
101
+ post second_factor_authentication_url, params: {
102
+ session: {
103
+ public_key_credential: public_key_credential.to_json
104
+ }
105
+ }
106
+
107
+ assert_redirected_to new_second_factor_authentication_path
108
+ assert_match (/Verification failed/), flash[:alert]
109
+ assert_nil session[:current_authentication]
110
+ end
111
+
112
+ test "create with unrecognized credential" do
113
+ post session_path, params: { email_address: @user.email_address, password: "password" }
114
+
115
+ post get_options_second_factor_authentication_url
116
+ challenge = session[:current_authentication][:challenge]
117
+
118
+ public_key_credential = @client.get(challenge: challenge, user_verified: false)
119
+ public_key_credential["id"]= "invalid-id"
120
+
121
+ post second_factor_authentication_url, params: {
122
+ session: {
123
+ public_key_credential: public_key_credential.to_json
124
+ }
125
+ }
126
+
127
+ assert_redirected_to new_second_factor_authentication_path
128
+ assert_match (/Credential not recognized/), flash[:alert]
129
+ assert_nil session[:current_authentication]
130
+ end
131
+ end