passkeys-rails 0.1.6 → 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c1c218e160d157a8ddcf931fae3a053fb6e130130b61a8e03f5a9a2bcade07b1
4
- data.tar.gz: 42fb39898dd79867bd8d18fb95b39ac335a47ed5a06d3ccb13b5f3fc2b64b6af
3
+ metadata.gz: 631e7354b6296e5fcc14144ba3bd305ca9047c31f9e5b0173f5eb7123f14fa71
4
+ data.tar.gz: 65e1f2d4cbd179ddee7449318b49a838d166db64db9221e1f079ac7e57735bf3
5
5
  SHA512:
6
- metadata.gz: 85c226aabf90e17b56744947b37eab49f98c4cbacc42a4963316ea60a68fa6c49b40ea81297d2786a16151361fa6e0f637a5af18d147db12894f5288ef260558
7
- data.tar.gz: 9a26aa0b7c5e907b954f2564244ab222205775da8087c0c9b17cde3e00706aace1b21b4c3c57d728b91472ed035eeb3f4c87d2ecc4ccecb1e41c5f767d548ccc
6
+ metadata.gz: f8799f782713ae01ca312506c39ef2bed3a56448fb0b4e78d4c75a441b4a3fbac731a5bcf3f23421f9b240e84346f80028bd4545c8edbc14b4ed67769e92e3ef
7
+ data.tar.gz: 23ce3eab9360b27dd23732589dc3794bb6c6c8cfe6ca9bf289830265ad8697ce0a2779836c0b4ff37c2a5c3211dffaaf74e817e63d0bc96417ad62af8048e444
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ### 0.1.7
2
+
3
+ * Added IntegrationHelpers to support client testing.
4
+ * Updated methods for interfacing with Rails client app.
5
+ * Changed route path added by the generator.
6
+
7
+ ### 0.1.6
8
+
9
+ * Added default_class and class_whitelist config parameters.
10
+
11
+ ### 0.1.5
12
+
13
+ * Updated validation to ensure the agent has completed registration to be considered valid.
14
+
1
15
  ### 0.1.4
2
16
 
3
17
  * Changed namespace from Passkeys::Rails to PasskeysRails
data/README.md CHANGED
@@ -1,43 +1,38 @@
1
- [![Gem Version](https://badge.fury.io/rb/passkeys-rails.svg?cachebust=5)](https://badge.fury.io/rb/passkeys-rails)
1
+ [![Gem Version](https://badge.fury.io/rb/passkeys-rails.svg?cachebust=7)](https://badge.fury.io/rb/passkeys-rails)
2
2
  [![Build Status](https://app.travis-ci.com/alliedcode/passkeys-rails.svg?branch=main)](https://travis-ci.org/alliedcode/passkeys-rails)
3
3
  [![codecov](https://codecov.io/gh/alliedcode/passkeys-rails/branch/main/graph/badge.svg?token=UHSNJDUL21)](https://codecov.io/gh/alliedcode/passkeys-rails)
4
4
 
5
5
  # PasskeysRails
6
- Devise is awesome, but we don't need all that UI/UX for PassKeys. This gem is to make
7
- it easy to provide a back end that authenticates a mobile front end with PassKeys.
6
+
7
+ Devise is awesome, but we don't need all that UI/UX for PassKeys.
8
+
9
+ The purpose of this gem is to make it easy to provide a rails back end API that supports PassKey authentication. It uses the [`webauthn`](https://github.com/w3c/webauthn) gem to do the cryptographic work and presents a simple API interface for passkey registration and authentication.
10
+
11
+ The target use case for this gem is a mobile application that uses a rails based API service to manage resources. The goal is to make it simple to register and authenticate users using passkeys from mobile applications in a rails API service.
12
+
8
13
 
9
14
  ## Usage
10
- rails passkeys-rails::install
11
- PasskeysRails maintains an Agent model and related Passeys. If you have a user model,
12
- add `include PasskeysRails::Authenticatable` to your model and include the name of that
13
- class (e.g. "User") in the authenticatable_class param when calling the register API.
14
15
 
15
- ### Optionally providing a "user" model during registration
16
- PasskeysRails does not require that you supply your own model, but it's often useful to do so. For example,
17
- if you have a User model that you would like to have created at registration, you can supply the model name
18
- in the finishRegistration API call.
16
+ **PasskeysRails** maintains an `Agent` model and related `Passkeys`. If you have a user model, add `include PasskeysRails::Authenticatable` to your model and include the name of that class (e.g. `"User"`) in the `authenticatable_class` param when calling the register API or set the `PasskeysRails.default_class` to the name of that class.
19
17
 
20
- PasskeysRails supports multiple "user" models. Whatever model name you supply in the finishRegiration call will
21
- be created and provided an opportunity to do any required initialization at that time.
18
+ ### Optionally providing a **"user"** model during registration
22
19
 
23
- There are two PasskeysRails configuration options related to this.
20
+ **PasskeysRails** does not require that you supply your own model, but it's often useful to do so. For example, if you have a User model that you would like to have created at registration, you can supply the model name in the `finishRegistration` API call.
24
21
 
25
- default_class and class_whitelist
22
+ **PasskeysRails** supports multiple `"user"` models. Whatever model name you supply will be created during a successful the `finishRegiration` API call. When created, it will be provided an opportunity to do any initialization at that time.
26
23
 
27
- #### default_class
24
+ There are two **PasskeysRails** configuration options related to this: `default_class` and `class_whitelist` - see below.
28
25
 
29
- Configure default_class in passkeys_rails.rb. Its value will be used during registration if none
30
- is provided in the API call. It is "User" by default. Since it's just a default, it can be overridden
31
- in the API call for any other model. If no model is to be used, change it to nil.
26
+ #### `default_class`
32
27
 
33
- #### class_whitelist
28
+ Configure `default_class` in `passkeys_rails.rb`. Its value will be used during registration if none is provided in the API call. The default value is `"User"`. Since the `default_class` is just a default, it can be overridden in the `finishRegiration` API call to use a different model. If no model is to be used by default, set it to nil.
34
29
 
35
- Configure class_whitelist in passkeys_rails.rb. It is nil by default. When nil, no whitelist will be applied.
36
- If it is non-nil, it should be an array of class names that are allowed during registration. Supply an empty
37
- array to prevent PasskeysRails from attempting to create anything other than its own PasskeysRails::Agent during
38
- registration.
30
+ #### `class_whitelist`
31
+
32
+ Configure `class_whitelist` in `passkeys_rails.rb`. The default value is `nil`. When `nil`, no whitelist will be applied. If it is non-nil, it should be an array of class names that are allowed during registration. Supply an empty array to prevent **PasskeysRails** from attempting to create anything other than its own `PasskeysRails::Agent` during registration.
39
33
 
40
34
  ## Installation
35
+
41
36
  Add this line to your application's Gemfile:
42
37
 
43
38
  ```ruby
@@ -45,8 +40,9 @@ gem "passkeys_rails"
45
40
  ```
46
41
 
47
42
  And then execute:
43
+
48
44
  ```bash
49
- $ bundle
45
+ $ bundle install
50
46
  ```
51
47
 
52
48
  Or install it yourself as:
@@ -54,22 +50,139 @@ Or install it yourself as:
54
50
  $ gem install passkeys_rails
55
51
  ```
56
52
 
57
- Depending on your application's configuration some manual setup may be required:
53
+ Finally, execute:
54
+
55
+ ```bash
56
+ $ rails generate passkeys_rails:install
57
+ ```
58
+
59
+ This will add the `passkeys_rails.rb` configuration file, passkeys routes, and a couple of database migrations to your project.
60
+
61
+ ### Adding to an standard rails project
62
+
63
+ 1. Add `before_action :authenticate_passkey!`
64
+
65
+ To prevent access to controller actions, add `before_action :authenticate_passkey!`. If an action is attempted without an authenticated entity, an error will be rendered in JSON with an :unauthorized result code.
66
+
67
+ 1. Use `current_agent` and `current_agent.authenticatable`
68
+
69
+ To access the currently authenticated entity, use `current_agent`. If you associated the registration of the agent with one of your own models, use `current_agent.authenticatable`. For example, if you associated the `User` class with the registration, `current_agent.authenticatable` will be a User object.
70
+
71
+ 1. Add `include PasskeysRails::Authenticatable` to model class(es)
72
+
73
+ If you have one or more classes that you want to use with authentication - e.g. a User class and an AdminUser class - add `include PasskeysRails::Authenticatable` to each of those classes. That adds a `registered?` method that you can call on your model to determine if they are registerd with your service, and a `registering_with(params)` method that you can override to initialize attributes of your model when it is created during registration. `params` is a hash with params passed to the API when registering. When called, your object has been built, but not yet saved. Upon return, **PasskeysRails** will attempt to save your object before finishing registration. If it is not valid, the registration will fail as well, returning the error error details to the caller.
74
+
75
+ ### Adding to a Grape API rails project
76
+
77
+ 1. Call `PasskeysRails.authenticate(request)` to authenticate the request.
78
+
79
+ Call `PasskeysRails.authenticate(request)` to get an object back that responds to `.success?` and `.failure?` as well as `.agent`, `.code`, and `.message`.
80
+
81
+ Alternatively, call `PasskeysRails.authenticate!(request)` from a helper in your base class. It will raise a `PasskeysRails.Error` exception if the caller isn't authenticated. You can catch the exception and render an appropriate error. The exception contains the error code and message.
82
+
83
+ 1. Consider adding the following helpers to your base API class:
84
+
85
+ ```ruby
86
+ helpers do
87
+ # Authenticate the request and cache the result
88
+ def passkey
89
+ @passkey ||= PasskeysRails.authenticate(request)
90
+ end
91
+
92
+ # Raise an exception if the request is not authentic
93
+ def authenticate_passkey!
94
+ error!({ code: passkey.code, message: passkey.message }, :unauthorized) if passkey.failure?
95
+ end
96
+
97
+ # Return the Passkeys::Agent if authentic, else return nil
98
+ def current_agent
99
+ passkey.agent
100
+ end
101
+
102
+ # If you have set authenticatable to be a User, you can use this to access the user from Grape endpoint methods
103
+ def current_user
104
+ user = current_agent&.authenticatable
105
+ user.is_a?(User) ? user : nil # sanity check to be sure authenticatable is a User
106
+ end
107
+ end
108
+ ```
109
+
110
+ To prevent access to various endpoints, add `before_action :authenticate_passkey!` or call `authenticate_passkey!` from any method that requires authentication. If an action is attempted without an authenticated entity, an error will be rendered in JSON with an :unauthorized result code.
111
+
112
+ 1. Use `current_agent` and `current_agent.authenticatable`
113
+
114
+ To access the currently authenticated entity, use `current_agent`. If you associated the registration of the agent with one of your own models, use `current_agent.authenticatable`. For example, if you associated the `User` class with the registration, `current_agent.authenticatable` will be a User object.
115
+
116
+ ### Authentication Failure
117
+
118
+ 1. In the event of authentication failure, PasskeysRails returns an error code and message.
119
+
120
+ 1. In a standard rails controller, the error code and message are rendered in JSON if `before_action :authenticate_passkey!` fails.
121
+
122
+ 1. In Grape, the error code and message are available in the result of the `PasskeysRails.authenticate(request)` method.
123
+
124
+ 1. From standard rails controllers, you can also access `passkey_authentication_result` to get the code and message.
125
+
126
+ 1. For `PasskeysRails.authenticate(request)` and `passkey_authentication_result`, the result is an object that respods to `.success?` and `.failure?`.
127
+ - When `.success?` is true (`.failure?` is false), the resources is authentic and it also responds to `.agent`, returning a PasskeysRails::Agent
128
+ - When `.success?` is false (`.failure?` is true), it responds to `.code` and `.message` to expose the error details.
129
+ - When `.code` is `:missing_token`, `.message` is **X-Auth header is required**, which means the caller didn't supply the auth header.
130
+ - When `.code` is `:invalid_token`, `.message` is **Invalid token - no agent exists with agent_id**, which means that the auth data is not valid.
131
+ - When `.code` is `:expired_token`, `.message` is **The token has expired**, which means that the token is valid, but expired, thuis it's not considered authentic.
132
+ - When `.code` is `:token_error`, `.message` is a description of the error. This is a catch-all in the event we are unable to decode the token.
133
+
134
+ In the future, the intention is to have the `.code` value stay consistent even if the `.message` changes. This also allows you to localize the messages as need using the code.
135
+
136
+ ### Test Helpers
137
+
138
+ PasskeysRails includes some test helpers for integration tests. In order to use them, you need to include the module in your test cases/specs.
139
+
140
+ ### Integration tests
141
+
142
+ Integration test helpers are available by including the `PasskeysRails::IntegrationHelpers` module.
143
+
144
+ ```ruby
145
+ class PostTests < ActionDispatch::IntegrationTest
146
+ include PasskeysRails::Test::IntegrationHelpers
147
+ end
148
+ ```
149
+ Now you can use the following `logged_in_headers` method in your integration tests.`
150
+
151
+ ```ruby
152
+ test 'authenticated users can see posts' do
153
+ user = User.create
154
+ get '/posts', headers: logged_in_headers('username-123', user)
155
+ assert_response :success
156
+ end
157
+ ```
58
158
 
59
- 1. Add a before_action to all controllers that require authentication to use.
159
+ RSpec can include the `IntegrationHelpers` module in their `:feature` and `:request` specs.
60
160
 
61
- For example:
161
+ ```ruby
162
+ RSpec.configure do |config|
163
+ config.include PasskeysRails::Test::IntegrationHelpers, type: :feature
164
+ config.include PasskeysRails::Test::IntegrationHelpers, type: :request
165
+ end
166
+ ```
62
167
 
63
- before_action :authenticate_passkey!, except: [:index]
168
+ ```ruby
169
+ RSpec.describe 'Posts', type: :request do
170
+ let(:user) { User.create }
171
+ it "allows authenticated users to see posts" do
172
+ get '/posts', headers: logged_in_headers('username-123', user)
173
+ expect(response).to be_success
174
+ end
175
+ end
176
+ ```
64
177
 
65
- 2. Optionally include PasskeysRails::Authenticatable to the model(s) you are using as
66
- your user model(s). For example, the User model.
178
+ ### Mobile Application Integration
67
179
 
68
- 3. See the reference mobile applications for how to use passkeys-rails for passkey
69
- authentication.
180
+ **TODO**: Describe the APIs and point to the soon-to-be-created reference mobile applications for how to use **passkeys-rails** for passkey authentication.
70
181
 
71
182
  ## Contributing
72
- Contribution directions go here.
183
+
184
+ Contact me if you'd like to contribute time, energy, etc. to this project.
73
185
 
74
186
  ## License
187
+
75
188
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -9,22 +9,17 @@ module PasskeysRails
9
9
  end
10
10
 
11
11
  def current_agent
12
- @current_agent ||= (request.headers['HTTP_X_AUTH'].present? &&
13
- passkey_authentication_result.success? &&
12
+ @current_agent ||= (passkey_authentication_result.success? &&
14
13
  passkey_authentication_result.agent.registered? &&
15
14
  passkey_authentication_result.agent) || nil
16
15
  end
17
16
 
18
17
  def authenticate_passkey!
19
- return if current_agent.present?
20
-
21
- raise PasskeysRails::Error.new(:authentication,
22
- code: :unauthorized,
23
- message: "You are not authorized to access this resource.")
18
+ @authenticate_passkey ||= PasskeysRails.authenticate!(request)
24
19
  end
25
20
 
26
21
  def passkey_authentication_result
27
- @passkey_authentication_result ||= PasskeysRails::ValidateAuthToken.call(auth_token: request.headers['HTTP_X_AUTH'])
22
+ @passkey_authentication_result ||= PasskeysRails.authenticate(request)
28
23
  end
29
24
  end
30
25
  end
@@ -11,7 +11,7 @@ module PasskeysRails
11
11
 
12
12
  def register
13
13
  result = PasskeysRails::FinishRegistration.call!(credential: attestation_credential_params.to_h,
14
- authenticatable_class:,
14
+ authenticatable_info: authenticatable_info&.to_h,
15
15
  username: session.dig(:passkeys_rails, :username),
16
16
  challenge: session.dig(:passkeys_rails, :challenge))
17
17
 
@@ -43,8 +43,8 @@ module PasskeysRails
43
43
  credential.permit(:id, :rawId, :type, { response: %i[attestationObject clientDataJSON] })
44
44
  end
45
45
 
46
- def authenticatable_class
47
- params[:authenticatable_class]
46
+ def authenticatable_info
47
+ params.require[:authenticatable].permit(:class, :params) if params[:authenticatable].present?
48
48
  end
49
49
 
50
50
  def authentication_params
@@ -3,7 +3,7 @@ module PasskeysRails
3
3
  class FinishRegistration
4
4
  include Interactor
5
5
 
6
- delegate :credential, :username, :challenge, :authenticatable_class, to: :context
6
+ delegate :credential, :username, :challenge, :authenticatable_info, to: :context
7
7
 
8
8
  def call
9
9
  verify_credential!
@@ -44,6 +44,14 @@ module PasskeysRails
44
44
  end
45
45
  end
46
46
 
47
+ def authenticatable_class
48
+ authenticatable_info && authenticatable_info[:class]
49
+ end
50
+
51
+ def authenticatable_params
52
+ authenticatable_info && authenticatable_info[:params]
53
+ end
54
+
47
55
  def aux_class_name
48
56
  @aux_class_name ||= authenticatable_class || PasskeysRails.default_class
49
57
  end
@@ -52,25 +60,27 @@ module PasskeysRails
52
60
  whitelist = PasskeysRails.class_whitelist
53
61
 
54
62
  @aux_class ||= begin
55
- case whitelist
56
- when Array
63
+ if whitelist.is_a?(Array)
57
64
  unless whitelist.include?(aux_class_name)
58
65
  context.fail!(code: :invalid_authenticatable_class, message: "authenticatable_class (#{aux_class_name}) is not in the whitelist")
59
66
  end
60
- when present?
67
+ elsif whitelist.present?
61
68
  context.fail!(code: :invalid_class_whitelist,
62
69
  message: "class_whitelist is invalid. It should be nil or an array of zero or more class names.")
63
70
  end
64
71
 
65
- aux_class_name.constantize
66
- rescue StandardError
67
- context.fail!(code: :invalid_authenticatable_class, message: "authenticatable_class (#{aux_class_name}) is not defined")
72
+ begin
73
+ aux_class_name.constantize
74
+ rescue StandardError
75
+ context.fail!(code: :invalid_authenticatable_class, message: "authenticatable_class (#{aux_class_name}) is not defined")
76
+ end
68
77
  end
69
78
  end
70
79
 
71
80
  def create_authenticatable!
72
81
  authenticatable = aux_class.create! do |obj|
73
- obj.registering_with(agent) if obj.respond_to?(:registering_with)
82
+ obj.agent = agent if obj.respond_to?(:agent=)
83
+ obj.registering_with(authenticatable_params) if obj.respond_to?(:registering_with)
74
84
  end
75
85
 
76
86
  agent.update!(authenticatable:)
@@ -5,11 +5,11 @@ module PasskeysRails
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  included do
8
- has_one :agent, as: :authenticatable
8
+ has_one :agent, as: :authenticatable, class_name: "PasskeysRails::Agent"
9
9
 
10
10
  delegate :registered?, to: :agent, allow_nil: true
11
11
 
12
- def registering_with(_agent)
12
+ def registering_with(_params)
13
13
  # initialize required attributes
14
14
  end
15
15
  end
@@ -2,7 +2,7 @@ Description:
2
2
  Creates a PasskeysRails config file, updates the routes and adds migrations.
3
3
 
4
4
  Example:
5
- bin/rails generate passkeys-rails:install
5
+ bin/rails generate passkeys_rails:install
6
6
 
7
7
  This will:
8
8
  create config/passkeys_rails.rb
@@ -5,14 +5,22 @@ module PasskeysRails
5
5
  class InstallGenerator < ::Rails::Generators::Base
6
6
  source_root File.expand_path("templates", __dir__)
7
7
 
8
+ desc "Adds passkeys config file to your application."
8
9
  def copy_config
9
10
  template 'passkeys_rails_config.rb', "config/initializers/passkeys_rails.rb"
10
11
  end
11
12
 
13
+ desc "Adds passkeys routes to your application."
12
14
  def add_routes
13
- route 'mount PasskeysRails::Engine => "/passkeys_rails"'
15
+ route 'mount PasskeysRails::Engine => "/passkeys"'
14
16
  end
15
17
 
18
+ desc "Copies migrations to your application."
19
+ def copy_migrations
20
+ rake("passkeys_rails:install:migrations")
21
+ end
22
+
23
+ desc "Displays readme during installation."
16
24
  def show_readme
17
25
  readme "README" if behavior == :invoke
18
26
  end
@@ -4,6 +4,10 @@ require 'passkeys_rails/version'
4
4
  require_relative "generators/passkeys_rails/install_generator"
5
5
 
6
6
  module PasskeysRails
7
+ module Test
8
+ autoload :IntegrationHelpers, 'passkeys_rails/test/integration_helpers'
9
+ end
10
+
7
11
  # Secret used to encode the auth token.
8
12
  # Rails.application.secret_key_base is used if none is defined here.
9
13
  # Changing this value will invalidate all tokens that have been fetched
@@ -41,6 +45,28 @@ module PasskeysRails
41
45
  # for example: %w[User AdminUser]
42
46
  mattr_accessor :class_whitelist, default: nil
43
47
 
48
+ # Returns an Interactor::Context that indicates if the request is authentic.
49
+ #
50
+ # .success? is true if authentic
51
+ # .agent is the Passkey::Agent on success
52
+ #
53
+ # .failure? is true if failed (just the opposite of .success?)
54
+ # .code is the error code on failure
55
+ # .message is the human readable error message on failure
56
+ def self.authenticate(request)
57
+ PasskeysRails::ValidateAuthToken.call(auth_token: request.headers['X-Auth'])
58
+ end
59
+
60
+ # Raises a PasskeysRails::Error exception if the request is not authentic.
61
+ def self.authenticate!(request)
62
+ auth = authenticate(request)
63
+ return if auth.success?
64
+
65
+ raise PasskeysRails::Error.new(:authentication,
66
+ code: auth.code,
67
+ message: auth.message)
68
+ end
69
+
44
70
  class << self
45
71
  def config
46
72
  yield self
@@ -0,0 +1,46 @@
1
+ module PasskeysRails
2
+ # PasskeysRails::Test::IntegrationHelpers is a helper module for facilitating
3
+ # authentication on Rails integration tests to bypass the required steps for
4
+ # signin in or signin out a record.
5
+ #
6
+ # Examples
7
+ #
8
+ # class PostsTest < ActionDispatch::IntegrationTest
9
+ # include PasskeysRails::Test::IntegrationHelpers
10
+ #
11
+ # test 'authenticated users can see posts' do
12
+ # get '/posts', headers: logged_in_headers('username-1')
13
+ # assert_response :success
14
+ # end
15
+ # end
16
+ module Test
17
+ module IntegrationHelpers
18
+ def self.included(base)
19
+ base.class_eval do
20
+ setup :setup_integration_for_passkeys_rails
21
+ teardown :teardown_integration_for_passkeys_rails
22
+ end
23
+ end
24
+
25
+ def logged_in_headers(username, authenticatable = nil, headers: {})
26
+ @agent = Agent.create(username:, registered_at: Time.current, authenticatable:)
27
+ result = PasskeysRails::GenerateAuthToken.call(agent:)
28
+ raise result.message if result.failure?
29
+
30
+ headers.merge("X-Auth" => result.auth_token)
31
+ end
32
+
33
+ protected
34
+
35
+ attr_reader :agent
36
+
37
+ def setup_integration_for_passkeys_rails
38
+ # Nothing to do here
39
+ end
40
+
41
+ def teardown_integration_for_passkeys_rails
42
+ @agent&.destroy
43
+ end
44
+ end
45
+ end
46
+ end
@@ -1,3 +1,3 @@
1
1
  module PasskeysRails
2
- VERSION = "0.1.6".freeze
2
+ VERSION = "0.1.7".freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: passkeys-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Troy Anderson
@@ -378,6 +378,7 @@ files:
378
378
  - lib/passkeys-rails.rb
379
379
  - lib/passkeys_rails/engine.rb
380
380
  - lib/passkeys_rails/railtie.rb
381
+ - lib/passkeys_rails/test/integration_helpers.rb
381
382
  - lib/passkeys_rails/version.rb
382
383
  - lib/tasks/passkeys_rails_tasks.rake
383
384
  homepage: https://github.com/alliedcode/passkeys-rails