passkeys-rails 0.1.5 → 0.1.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +155 -14
- data/app/controllers/concerns/passkeys_rails/authentication.rb +3 -8
- data/app/controllers/passkeys_rails/passkeys_controller.rb +3 -3
- data/app/interactors/passkeys_rails/finish_registration.rb +41 -14
- data/app/models/concerns/passkeys_rails/authenticatable.rb +2 -2
- data/lib/generators/passkeys_rails/USAGE +1 -1
- data/lib/generators/passkeys_rails/install_generator.rb +9 -1
- data/lib/generators/passkeys_rails/templates/passkeys_rails_config.rb +24 -0
- data/lib/passkeys-rails.rb +48 -0
- data/lib/passkeys_rails/test/integration_helpers.rb +46 -0
- data/lib/passkeys_rails/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 631e7354b6296e5fcc14144ba3bd305ca9047c31f9e5b0173f5eb7123f14fa71
|
4
|
+
data.tar.gz: 65e1f2d4cbd179ddee7449318b49a838d166db64db9221e1f079ac7e57735bf3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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,15 +1,38 @@
|
|
1
|
-
[![Gem Version](https://badge.fury.io/rb/passkeys-rails.svg?cachebust=
|
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
|
-
|
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
|
+
|
7
13
|
|
8
14
|
## Usage
|
9
|
-
|
10
|
-
PasskeysRails maintains an Agent model and related
|
15
|
+
|
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.
|
17
|
+
|
18
|
+
### Optionally providing a **"user"** model during registration
|
19
|
+
|
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.
|
21
|
+
|
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.
|
23
|
+
|
24
|
+
There are two **PasskeysRails** configuration options related to this: `default_class` and `class_whitelist` - see below.
|
25
|
+
|
26
|
+
#### `default_class`
|
27
|
+
|
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.
|
29
|
+
|
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.
|
11
33
|
|
12
34
|
## Installation
|
35
|
+
|
13
36
|
Add this line to your application's Gemfile:
|
14
37
|
|
15
38
|
```ruby
|
@@ -17,8 +40,9 @@ gem "passkeys_rails"
|
|
17
40
|
```
|
18
41
|
|
19
42
|
And then execute:
|
43
|
+
|
20
44
|
```bash
|
21
|
-
$ bundle
|
45
|
+
$ bundle install
|
22
46
|
```
|
23
47
|
|
24
48
|
Or install it yourself as:
|
@@ -26,22 +50,139 @@ Or install it yourself as:
|
|
26
50
|
$ gem install passkeys_rails
|
27
51
|
```
|
28
52
|
|
29
|
-
|
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.
|
30
70
|
|
31
|
-
|
71
|
+
1. Add `include PasskeysRails::Authenticatable` to model class(es)
|
32
72
|
|
33
|
-
|
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.
|
34
74
|
|
35
|
-
|
75
|
+
### Adding to a Grape API rails project
|
36
76
|
|
37
|
-
|
38
|
-
your user model(s). For example, the User model.
|
77
|
+
1. Call `PasskeysRails.authenticate(request)` to authenticate the request.
|
39
78
|
|
40
|
-
|
41
|
-
|
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
|
+
```
|
158
|
+
|
159
|
+
RSpec can include the `IntegrationHelpers` module in their `:feature` and `:request` specs.
|
160
|
+
|
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
|
+
```
|
167
|
+
|
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
|
+
```
|
177
|
+
|
178
|
+
### Mobile Application Integration
|
179
|
+
|
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.
|
42
181
|
|
43
182
|
## Contributing
|
44
|
-
|
183
|
+
|
184
|
+
Contact me if you'd like to contribute time, energy, etc. to this project.
|
45
185
|
|
46
186
|
## License
|
187
|
+
|
47
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 ||= (
|
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
|
-
|
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
|
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
|
-
|
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
|
47
|
-
params[:
|
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, :
|
6
|
+
delegate :credential, :username, :challenge, :authenticatable_info, to: :context
|
7
7
|
|
8
8
|
def call
|
9
9
|
verify_credential!
|
@@ -40,25 +40,52 @@ module PasskeysRails
|
|
40
40
|
context.fail! code: :passkey_error, message: e.message
|
41
41
|
end
|
42
42
|
|
43
|
-
create_authenticatable! if
|
43
|
+
create_authenticatable! if aux_class_name.present?
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
47
|
-
def
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
+
|
55
|
+
def aux_class_name
|
56
|
+
@aux_class_name ||= authenticatable_class || PasskeysRails.default_class
|
57
|
+
end
|
58
|
+
|
59
|
+
def aux_class
|
60
|
+
whitelist = PasskeysRails.class_whitelist
|
53
61
|
|
54
|
-
begin
|
55
|
-
|
56
|
-
|
62
|
+
@aux_class ||= begin
|
63
|
+
if whitelist.is_a?(Array)
|
64
|
+
unless whitelist.include?(aux_class_name)
|
65
|
+
context.fail!(code: :invalid_authenticatable_class, message: "authenticatable_class (#{aux_class_name}) is not in the whitelist")
|
66
|
+
end
|
67
|
+
elsif whitelist.present?
|
68
|
+
context.fail!(code: :invalid_class_whitelist,
|
69
|
+
message: "class_whitelist is invalid. It should be nil or an array of zero or more class names.")
|
57
70
|
end
|
58
|
-
|
59
|
-
|
60
|
-
|
71
|
+
|
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
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def create_authenticatable!
|
81
|
+
authenticatable = aux_class.create! do |obj|
|
82
|
+
obj.agent = agent if obj.respond_to?(:agent=)
|
83
|
+
obj.registering_with(authenticatable_params) if obj.respond_to?(:registering_with)
|
61
84
|
end
|
85
|
+
|
86
|
+
agent.update!(authenticatable:)
|
87
|
+
rescue ActiveRecord::RecordInvalid => e
|
88
|
+
context.fail!(code: :record_invalid, message: e.message)
|
62
89
|
end
|
63
90
|
|
64
91
|
def webauthn_credential
|
@@ -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(
|
12
|
+
def registering_with(_params)
|
13
13
|
# initialize required attributes
|
14
14
|
end
|
15
15
|
end
|
@@ -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 => "/
|
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
|
@@ -21,4 +21,28 @@ PasskeysRails.config do |c|
|
|
21
21
|
# Default is 30 days
|
22
22
|
#
|
23
23
|
# c.auth_token_expires_in = 30.days
|
24
|
+
|
25
|
+
# Model to use when creating or authenticating a passkey.
|
26
|
+
# This can be overridden when calling the API, but if no
|
27
|
+
# value is supplied when calling the API, this value is used.
|
28
|
+
#
|
29
|
+
# If nil, there is no default, and if none is supplied when
|
30
|
+
# calling the API, no resource is created other than
|
31
|
+
# a PaskeysRails::Agent that is used to track the passkey.
|
32
|
+
#
|
33
|
+
# This library doesn't assume that there will only be one
|
34
|
+
# model, but it is a common use case, so setting the
|
35
|
+
# default_class simplifies the use of the API in that case.
|
36
|
+
#
|
37
|
+
# c.default_class = "User"
|
38
|
+
|
39
|
+
# By providing a class_whitelist, the API will require that
|
40
|
+
# any supplied class is in the whitelist. If it is not, the
|
41
|
+
# auth API will return an error. This prevents a caller from
|
42
|
+
# attempting to create an unintended records on registration.
|
43
|
+
# If nil, any model will be allowed.
|
44
|
+
# This should be an array of symbols or strings,
|
45
|
+
# for example: %w[User AdminUser]
|
46
|
+
#
|
47
|
+
# c.class_whitelist = nil
|
24
48
|
end
|
data/lib/passkeys-rails.rb
CHANGED
@@ -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
|
@@ -19,6 +23,50 @@ module PasskeysRails
|
|
19
23
|
# Set it to 0 for no expiration (not recommended in production).
|
20
24
|
mattr_accessor :auth_token_expires_in, default: 30.days
|
21
25
|
|
26
|
+
# Model to use when creating or authenticating a passkey.
|
27
|
+
# This can be overridden when calling the API, but if no
|
28
|
+
# value is supplied when calling the API, this value is used.
|
29
|
+
# If nil, there is no default, and if none is supplied when
|
30
|
+
# calling the API, no resource is created other than
|
31
|
+
# a PaskeysRails::Agent that is used to track the passkey.
|
32
|
+
#
|
33
|
+
# This library doesn't assume that there will only be one
|
34
|
+
# model, but it is a common use case, so setting the
|
35
|
+
# default_class simplifies the use of the API in that case.
|
36
|
+
mattr_accessor :default_class, default: "User"
|
37
|
+
|
38
|
+
# By providing a class_whitelist, the API will require that
|
39
|
+
# any supplied class is in the whitelist. If it is not, the
|
40
|
+
# auth API will return an error. This prevents a caller from
|
41
|
+
# attempting to create an unintended record on registration.
|
42
|
+
# If nil, any model will be allowed.
|
43
|
+
# If [], no model will be allowed.
|
44
|
+
# This should be an array of symbols or strings,
|
45
|
+
# for example: %w[User AdminUser]
|
46
|
+
mattr_accessor :class_whitelist, default: nil
|
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
|
+
|
22
70
|
class << self
|
23
71
|
def config
|
24
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
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: passkeys-rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Troy Anderson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-07-
|
11
|
+
date: 2023-07-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -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
|