passkeys-rails 0.2.1 → 0.3.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 +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +160 -96
- data/app/controllers/passkeys_rails/application_controller.rb +5 -0
- data/app/controllers/passkeys_rails/passkeys_controller.rb +47 -11
- data/app/interactors/passkeys_rails/begin_challenge.rb +2 -2
- data/app/interactors/passkeys_rails/begin_registration.rb +2 -0
- data/app/interactors/passkeys_rails/debug_login.rb +2 -12
- data/app/interactors/passkeys_rails/debug_register.rb +44 -0
- data/app/interactors/passkeys_rails/finish_authentication.rb +1 -0
- data/app/interactors/passkeys_rails/finish_registration.rb +23 -63
- data/app/interactors/passkeys_rails/refresh_token.rb +1 -0
- data/app/models/concerns/passkeys_rails/authenticatable_creator.rb +53 -0
- data/app/models/concerns/passkeys_rails/debuggable.rb +19 -0
- data/config/routes.rb +7 -6
- data/lib/generators/passkeys_rails/templates/passkeys_rails_config.rb +56 -0
- data/lib/passkeys-rails.rb +45 -42
- data/lib/passkeys_rails/configuration.rb +62 -0
- data/lib/passkeys_rails/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a137b9b4f2ab7aac24fef0747ff112588215d496034f0542983df3790bc32de7
|
4
|
+
data.tar.gz: 412185131a984a883a11af7fae1bbbb69806819a978871a08e0929921c766fe4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bdf2974d31cb561bbeabaf599a29d99bb9cda2e4050cff913cbe5104a747c4eba2eb11a53936fcef013861ede39051208a2edbc3062a6b25a34476678388eba9
|
7
|
+
data.tar.gz: 149c795d0b56a7569170160426b466ff42abc75bcb2f46fc5adae3b35ad3eb9d6d465ce05f0834cbbfaf3bc30944d809b776c1112913c0e490175c316784e859
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,21 @@
|
|
1
|
+
### 0.3.1
|
2
|
+
|
3
|
+
* Fixed a bug in reading session/cookie variables
|
4
|
+
* Added webauthn configuration parameters to this gem's configuration
|
5
|
+
* Moved configuration to its own class
|
6
|
+
* Added more info to the README
|
7
|
+
|
8
|
+
### 0.3.0
|
9
|
+
|
10
|
+
* Added debug_register endpoint.
|
11
|
+
* Fixed authenticatable_params for register enpoint.
|
12
|
+
* Added notifications to certain controller actions.
|
13
|
+
* Improved spec error helper.
|
14
|
+
|
15
|
+
### 0.2.1
|
16
|
+
|
17
|
+
Added ability to pass either the auth token string or a request with one in the header to authenticate methods.
|
18
|
+
|
1
19
|
### 0.2.0
|
2
20
|
|
3
21
|
* Added passkeys/debug_login functionality.
|
data/README.md
CHANGED
@@ -1,42 +1,60 @@
|
|
1
|
+
# PasskeysRails - easy to integrate back end for implementing mobile passkeys
|
2
|
+
|
1
3
|
[](https://badge.fury.io/rb/passkeys-rails)
|
2
4
|
[](https://travis-ci.org/alliedcode/passkeys-rails)
|
3
5
|
[](https://codecov.io/gh/alliedcode/passkeys-rails)
|
4
6
|
|
5
|
-
|
7
|
+
<p align="center" >
|
8
|
+
Created by <b>Troy Anderson, Allied Code</b> - <a href="https://alliedcode.com">alliedcode.com</a>
|
9
|
+
</p>
|
6
10
|
|
7
|
-
|
11
|
+
PasskeysRails is a gem you can add to a Rails app to enable passskey registration and authorization from mobile front ends. PasskeysRails leverages webauthn for the cryptographic work, and presents a simple API interface for passkey registration, authentication, and testing.
|
8
12
|
|
9
13
|
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
14
|
|
11
15
|
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
16
|
|
17
|
+
What about [devise](https://github.com/heartcombo/devise)? Devise is awesome, but we don't need all that UI/UX for PassKeys, especially for an API back end.
|
18
|
+
|
19
|
+
## Documentation
|
20
|
+
* [Usage](#usage)
|
21
|
+
* [Installation](#installation)
|
22
|
+
* [Rails Integration - Standard](#rails-Integration-standard)
|
23
|
+
* [Rails Integration - Grape](#rails-Integration-grape)
|
24
|
+
* [Notifications](#notifications)
|
25
|
+
* [Failure Codes](#failure-codes)
|
26
|
+
* [Testing](#testing)
|
27
|
+
* [Mobile App Integration](#mobile-application-integration)
|
28
|
+
* [Reference/Example Mobile Applications](#referenceexample-mobile-applications)
|
13
29
|
|
14
30
|
## Usage
|
15
31
|
|
16
|
-
**PasskeysRails** maintains
|
32
|
+
**PasskeysRails** maintains a `PasskeysRails::Agent` model and related `PasskeysRails::Passkeys`. In rails apps that maintain their own "user" model, add `include PasskeysRails::Authenticatable` to that 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.
|
33
|
+
|
34
|
+
In mobile apps, leverage the platform specific Passkeys APIs for ***registration*** and ***authentication***, and call the **PasskeysRails** API endpoints to complete the ceremony. **PasskeysRails** provides endpoints to support ***registration***, ***authentication***, ***token refresh***, and ***debugging***.
|
17
35
|
|
18
36
|
### Optionally providing a **"user"** model during registration
|
19
37
|
|
20
|
-
**PasskeysRails** does not require
|
38
|
+
**PasskeysRails** does not require any application specific models, but it's often useful to have one. For example, a User model can be created at registration. **PasskeysRails** provides two mechanisms to support this. Either provide the name of the model in the `authenticatable_class` param when calling the `finishRegistration` endpoint, or set a `default_class` in `config/initializers/passkeys_rails.rb`.
|
21
39
|
|
22
|
-
**PasskeysRails** supports multiple
|
40
|
+
**PasskeysRails** supports multiple different application specific models. Whatever model name supplied when calling the `finishRegistration` endpoint will be created during a successful the `finishRegiration` process. When created, it will be provided an opportunity to do any initialization at that time.
|
23
41
|
|
24
|
-
There are two **PasskeysRails** configuration options related to this: `default_class` and `class_whitelist
|
42
|
+
There are two **PasskeysRails** configuration options related to this: `default_class` and `class_whitelist`:
|
25
43
|
|
26
44
|
#### `default_class`
|
27
45
|
|
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.
|
46
|
+
Configure `default_class` in `config/initializers/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
47
|
|
30
48
|
#### `class_whitelist`
|
31
49
|
|
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.
|
50
|
+
Configure `class_whitelist` in `config/initializers/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.
|
33
51
|
|
34
52
|
## Installation
|
35
53
|
|
36
54
|
Add this line to your application's Gemfile:
|
37
55
|
|
38
56
|
```ruby
|
39
|
-
gem "
|
57
|
+
gem "passkeys-rails"
|
40
58
|
```
|
41
59
|
|
42
60
|
And then execute:
|
@@ -56,31 +74,34 @@ Finally, execute:
|
|
56
74
|
$ rails generate passkeys_rails:install
|
57
75
|
```
|
58
76
|
|
59
|
-
This will add the `passkeys_rails.rb` configuration file, passkeys routes, and a couple of database migrations to your project.
|
77
|
+
This will add the `config/initializers/passkeys_rails.rb` configuration file, passkeys routes, and a couple of database migrations to your project.
|
60
78
|
|
61
|
-
### Adding to an standard rails project
|
62
79
|
|
63
|
-
|
80
|
+
<a id="rails-Integration-standard"></a>
|
81
|
+
## Rails Integration <p><small>Adding to a standard rails project</small></p>
|
64
82
|
|
65
|
-
|
83
|
+
- ### Add `before_action :authenticate_passkey!`
|
66
84
|
|
67
|
-
|
85
|
+
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.
|
68
86
|
|
69
|
-
|
87
|
+
- ### Use `current_agent` and `current_agent.authenticatable`
|
88
|
+
|
89
|
+
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
90
|
|
71
|
-
|
91
|
+
- ### Add `include PasskeysRails::Authenticatable` to model class(es)
|
72
92
|
|
73
93
|
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
94
|
|
75
|
-
|
95
|
+
<a id="rails-Integration-grape"></a>
|
96
|
+
## Rails Integration - <p><small>Adding to a Grape API rails project</small></p>
|
76
97
|
|
77
|
-
|
98
|
+
- ### Call `PasskeysRails.authenticate(request)` to authenticate the request.
|
78
99
|
|
79
100
|
Call `PasskeysRails.authenticate(request)` to get an object back that responds to `.success?` and `.failure?` as well as `.agent`, `.code`, and `.message`.
|
80
101
|
|
81
102
|
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
103
|
|
83
|
-
|
104
|
+
- ### Consider adding the following helpers to your base API class:
|
84
105
|
|
85
106
|
```ruby
|
86
107
|
helpers do
|
@@ -109,13 +130,49 @@ This will add the `passkeys_rails.rb` configuration file, passkeys routes, and a
|
|
109
130
|
|
110
131
|
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
132
|
|
112
|
-
|
133
|
+
- ### Use `current_agent` and `current_agent.authenticatable`
|
113
134
|
|
114
135
|
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
136
|
|
116
|
-
|
137
|
+
## Notifications
|
138
|
+
|
139
|
+
Certain actions trigger notifications that can be subscribed. See `subscribe` in `config/initializers/passkeys_rails.rb`.
|
140
|
+
|
141
|
+
These are completely optional. **PasskeysRails** will manage all the credentials and keys without these being implemented. They are useful for taking application specific actions like logging based on the authentication related events.
|
142
|
+
|
143
|
+
### Events
|
144
|
+
|
145
|
+
- `:did_register ` - a new agent has registered
|
146
|
+
|
147
|
+
- `:did_authenticate` - an agent has been authenticated
|
148
|
+
|
149
|
+
- `:did_refresh` - an agent's auth token has been refreshed
|
150
|
+
|
151
|
+
A convenient place to set these up in is in `config/initializers/passkeys_rails.rb`
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
PasskeysRails.config do |c|
|
155
|
+
c.subscribe(:did_register) do |event, agent, request|
|
156
|
+
# do something with the agent and/or request
|
157
|
+
end
|
158
|
+
|
159
|
+
c.subscribe(:did_authenticate) do |event, agent, request|
|
160
|
+
# do something with the agent and/or request
|
161
|
+
end
|
162
|
+
end
|
163
|
+
```
|
117
164
|
|
118
|
-
|
165
|
+
Subscriptions can also be done elsewhere as subscribe is a PasskeysRails class method.
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
PasskeysRails.subscribe(:did_register) do |event, agent, request|
|
169
|
+
# do something with the agent and/or request
|
170
|
+
end
|
171
|
+
```
|
172
|
+
|
173
|
+
## Failure Codes
|
174
|
+
|
175
|
+
1. In the event of authentication failure, **PasskeysRails** API endpoints render an error code and message.
|
119
176
|
|
120
177
|
1. In a standard rails controller, the error code and message are rendered in JSON if `before_action :authenticate_passkey!` fails.
|
121
178
|
|
@@ -133,12 +190,10 @@ This will add the `passkeys_rails.rb` configuration file, passkeys routes, and a
|
|
133
190
|
|
134
191
|
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
192
|
|
136
|
-
|
193
|
+
## Testing
|
137
194
|
|
138
195
|
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
196
|
|
140
|
-
### Integration tests
|
141
|
-
|
142
197
|
Integration test helpers are available by including the `PasskeysRails::IntegrationHelpers` module.
|
143
198
|
|
144
199
|
```ruby
|
@@ -175,20 +230,38 @@ RSpec.describe 'Posts', type: :request do
|
|
175
230
|
end
|
176
231
|
```
|
177
232
|
|
178
|
-
|
233
|
+
## Mobile Application Integration
|
234
|
+
|
235
|
+
### Prerequisites
|
236
|
+
|
237
|
+
For iOS, you need to associate your app with your server. This amounts to setting up a special file on your server that defines the association. See [setup your apple-app-site-association](#Ensure-`.well-known/apple-app-site-association`-is-in-place)
|
179
238
|
|
180
|
-
|
239
|
+
|
240
|
+
### Mobile API Endpoints
|
241
|
+
|
242
|
+
There are 3 groups of API endpoints that your mobile application may consume.
|
181
243
|
|
182
244
|
1. Unauthenticated (public) endpoints
|
183
245
|
1. Authenticated (private) endpoints
|
184
246
|
1. Passey endpoints (for supporting authentication)
|
185
247
|
|
186
|
-
**Unauthenticated endpoints** can be consumed without
|
248
|
+
**Unauthenticated endpoints** can be consumed without any authentication.
|
187
249
|
|
188
250
|
**Authenticated endpoints** are protected by `authenticate_passkey!` or `PasskeysRails.authenticate!(request)`. Those methods check for and validate the `X-Auth` header, which must be set to the auth token returned in the `AuthResponse`, described below.
|
189
251
|
|
190
252
|
**Passkey endpoints** are supplied by this gem and allow you to register a user, authenticate (login) a user, and refresh the token. This section describes these endpoints.
|
191
253
|
|
254
|
+
This gem supports the Passkey endpoints.
|
255
|
+
|
256
|
+
### Endpoints
|
257
|
+
|
258
|
+
* [POST /passkeys/challenge](post-passkeys-challenge)
|
259
|
+
* [POST /passkeys/register](post-passkeys-register)
|
260
|
+
* [POST /passkeys/authenticate](post-passkeys-authenticate)
|
261
|
+
* [POST /passkeys/refresh](post-passkeys-refresh)
|
262
|
+
* [POST /passkeys/debug_register](post-passkeys-debug-register)
|
263
|
+
* [POST /passkeys/debug_login](post-passkeys-debug-login)
|
264
|
+
|
192
265
|
All Passkey endpoints accept and respond with JSON.
|
193
266
|
|
194
267
|
On **success**, they will respond with a 200 or 201 response code and relevant JSON.
|
@@ -214,7 +287,7 @@ Some endpoints return an `AuthResponse`, which has this JSON structure:
|
|
214
287
|
}
|
215
288
|
```
|
216
289
|
|
217
|
-
|
290
|
+
### POST /passkeys/challenge
|
218
291
|
|
219
292
|
Submit this to begin registration or authentication.
|
220
293
|
|
@@ -225,7 +298,7 @@ If the username is already in use, or anything else goes wrong, an error with co
|
|
225
298
|
Omit the `username` when authenticating (logging in).
|
226
299
|
The JSON response will be the `options_for_get` from webauthn.
|
227
300
|
|
228
|
-
|
301
|
+
### POST /passkeys/register
|
229
302
|
|
230
303
|
After calling the `challenge` endpoint with a `username`, and handling its response, finish registering by calling this endpoint.
|
231
304
|
|
@@ -256,16 +329,16 @@ On **success**, the response is an `AuthResponse`.
|
|
256
329
|
|
257
330
|
Possible **failure codes** (using the `ErrorResponse` structure) are:
|
258
331
|
|
259
|
-
- webauthn_error - something is wrong with the credential
|
260
|
-
- error - something else went wrong during credentail validation - see the `message` in the `ErrorResponse`
|
261
|
-
- passkey_error - unable to
|
262
|
-
- invalid_authenticatable_class - the supplied authenticatable class can't be created/found (check spelling & capitalization)
|
263
|
-
- invalid_class_whitelist - the whitelist in the passkeys_rails.rb configuration is invalid - be sure it's nil or an array
|
264
|
-
- invalid_authenticatable_class - the supplied authenticatable class is not allowed - maybe it's not in the whitelist
|
265
|
-
- record_invalid - the object of the supplied authenticatable class cannot be saved due to validation errors
|
266
|
-
- agent_not_found - the agent referenced in the credential cannot be found in the database
|
332
|
+
- `webauthn_error` - something is wrong with the credential
|
333
|
+
- `error` - something else went wrong during credentail validation - see the `message` in the `ErrorResponse`
|
334
|
+
- `passkey_error` - unable to persist the passkey
|
335
|
+
- `invalid_authenticatable_class` - the supplied authenticatable class can't be created/found (check spelling & capitalization)
|
336
|
+
- `invalid_class_whitelist` - the whitelist in the passkeys_rails.rb configuration is invalid - be sure it's nil or an array
|
337
|
+
- `invalid_authenticatable_class` - the supplied authenticatable class is not allowed - maybe it's not in the whitelist
|
338
|
+
- `record_invalid` - the object of the supplied authenticatable class cannot be saved due to validation errors
|
339
|
+
- `agent_not_found` - the agent referenced in the credential cannot be found in the database
|
267
340
|
|
268
|
-
|
341
|
+
### POST /passkeys/authenticate
|
269
342
|
|
270
343
|
After calling the `challenge` endpoint without a `username`, and handling its response, finish authenticating by calling this endpoint.
|
271
344
|
|
@@ -290,12 +363,12 @@ On **success**, the response is an `AuthResponse`.
|
|
290
363
|
|
291
364
|
Possible **failure codes** (using the `ErrorResponse` structure) are:
|
292
365
|
|
293
|
-
- webauthn_error - something is wrong with the credential
|
294
|
-
- passkey_not_found - the passkey referenced in the credential cannot be found in the database
|
366
|
+
- `webauthn_error` - something is wrong with the credential
|
367
|
+
- `passkey_not_found` - the passkey referenced in the credential cannot be found in the database
|
295
368
|
|
296
|
-
|
369
|
+
### POST /passkeys/refresh
|
297
370
|
|
298
|
-
The token will expire after some time (configurable in passkeys_rails.rb). Before that happens, refresh it using this API. Once it
|
371
|
+
The token will expire after some time (configurable in `config/initializers/passkeys_rails.rb`). Before that happens, refresh it using this API. Once it expires, to get a new token, use the `/authentication` API.
|
299
372
|
|
300
373
|
Supply the following JSON structure:
|
301
374
|
|
@@ -310,28 +383,28 @@ On **success**, the response is an `AuthResponse` with a new, refreshed token.
|
|
310
383
|
|
311
384
|
Possible **failure codes** (using the `ErrorResponse` structure) are:
|
312
385
|
|
313
|
-
- invalid_token - the token data is invalid
|
314
|
-
- expired_token - the token is expired
|
315
|
-
- token_error - some other error ocurred when decoding the token
|
386
|
+
- `invalid_token` - the token data is invalid
|
387
|
+
- `expired_token` - the token is expired
|
388
|
+
- `token_error` - some other error ocurred when decoding the token
|
316
389
|
|
317
|
-
|
390
|
+
### POST /passkeys/debug_register
|
318
391
|
|
319
|
-
As it may not be possible to acess Passkey functionality in mobile simulators, this endpoint may be called to
|
392
|
+
As it may not be possible to acess Passkey functionality in mobile simulators, this endpoint may be called to register a username while bypassing the normal challenge/response sequence.
|
320
393
|
|
321
|
-
This endpoint only responds if DEBUG_LOGIN_REGEX is set in the server environment. It is very insecure to set this variable in a production environment as it bypasses all Passkey checks. It is only intended to be used during mobile application development.
|
394
|
+
This endpoint only responds if DEBUG_LOGIN_REGEX is set in the server environment. It is **very insecure to set this variable in a production environment** as it bypasses all Passkey checks. It is only intended to be used during mobile application development.
|
322
395
|
|
323
396
|
To use this endpoint:
|
324
397
|
|
325
|
-
1. Manually create one or more PasskeysRails::Agent records in the database. A unique username is required for each.
|
326
|
-
|
327
398
|
1. Set DEBUG_LOGIN_REGEX to a regex that matches any username you want to use during development - for example `^test(-\d+)?$` will match `test`, `test-1`, `test-123`, etc.
|
328
399
|
|
329
|
-
1. In the mobile application, call this endpoint in stead of the /passkeys/challenge and /passkeys/
|
400
|
+
1. In the mobile application, call this endpoint in stead of the /passkeys/challenge and /passkeys/register. The response is identicial to that of /passkeys/register.
|
330
401
|
|
331
|
-
1. Use the response as if it was from /passkeys/
|
402
|
+
1. Use the response as if it was from /passkeys/register.
|
332
403
|
|
333
404
|
If you supply a username that doesn't match the DEBUG_LOGIN_REGEX, the endpoint will respond with an error.
|
334
405
|
|
406
|
+
Supply the following JSON structure:
|
407
|
+
|
335
408
|
```JSON
|
336
409
|
# POST body
|
337
410
|
{
|
@@ -342,71 +415,62 @@ On **success**, the response is an `AuthResponse`.
|
|
342
415
|
|
343
416
|
Possible **failure codes** (using the `ErrorResponse` structure) are:
|
344
417
|
|
345
|
-
- not_allowed - Invalid username (the username doesn't match the regex)
|
346
|
-
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
**TODO**: Point to the soon-to-be-created reference mobile applications for how to use **passkeys-rails** for passkey authentication.
|
351
|
-
|
352
|
-
## Contributing
|
418
|
+
- `not_allowed` - Invalid username (the username doesn't match the regex)
|
419
|
+
- `invalid_authenticatable_class` - the supplied authenticatable class can't be created/found (check spelling & capitalization)
|
420
|
+
- `invalid_class_whitelist` - the whitelist in the passkeys_rails.rb configuration is invalid - be sure it's nil or an array
|
421
|
+
- `invalid_authenticatable_class` - the supplied authenticatable class is not allowed - maybe it's not in the whitelist
|
422
|
+
- `record_invalid` - the object of the supplied authenticatable class cannot be saved due to validation errors
|
353
423
|
|
354
|
-
###
|
355
|
-
|
356
|
-
Thank you for considering contributing to PasskeysRails! We welcome your help to improve and enhance this project. Whether it's a bug fix, documentation update, or a new feature, your contributions are valuable to the community.
|
424
|
+
### POST /passkeys/debug_login
|
357
425
|
|
358
|
-
|
359
|
-
|
360
|
-
#### Code of Conduct
|
361
|
-
|
362
|
-
Please note that this project follows the [Code of Conduct](https://github.com/alliedcode/passkeys-rails/blob/main/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. If you encounter any behavior that violates the code, please report it to the project maintainers.
|
363
|
-
|
364
|
-
#### How to Contribute
|
365
|
-
|
366
|
-
1. Fork the repository on GitHub.
|
367
|
-
|
368
|
-
2. Create a new branch for your contribution. Use a descriptive name that reflects the purpose of your changes.
|
369
|
-
|
370
|
-
3. Make your changes and commit them with clear and concise messages. Remember to follow the project's coding style and guidelines.
|
426
|
+
As it may not be possible to acess Passkey functionality in mobile simulators, this endpoint may be called to login (authenticate) a username while bypassing the normal challenge/response sequence.
|
371
427
|
|
372
|
-
|
428
|
+
This endpoint only responds if DEBUG_LOGIN_REGEX is set in the server environment. It is **very insecure to set this variable in a production environment** as it bypasses all Passkey checks. It is only intended to be used during mobile application development.
|
373
429
|
|
374
|
-
|
430
|
+
To use this endpoint:
|
375
431
|
|
376
|
-
|
432
|
+
1. Manually create one or more PasskeysRails::Agent records in the database. A unique username is required for each.
|
377
433
|
|
378
|
-
|
434
|
+
1. Set DEBUG_LOGIN_REGEX to a regex that matches any username you want to use during development - for example `^test(-\d+)?$` will match `test`, `test-1`, `test-123`, etc.
|
379
435
|
|
380
|
-
|
436
|
+
1. In the mobile application, call this endpoint in stead of the /passkeys/challenge and /passkeys/authenticate. The response is identicial to that of /passkeys/authenticate.
|
381
437
|
|
382
|
-
|
438
|
+
1. Use the response as if it was from /passkeys/authenticate.
|
383
439
|
|
384
|
-
|
440
|
+
If you supply a username that doesn't match the DEBUG_LOGIN_REGEX, the endpoint will respond with an error.
|
385
441
|
|
386
|
-
|
442
|
+
Supply the following JSON structure:
|
387
443
|
|
388
|
-
|
444
|
+
```JSON
|
445
|
+
# POST body
|
446
|
+
{
|
447
|
+
"username": String
|
448
|
+
}
|
449
|
+
```
|
450
|
+
On **success**, the response is an `AuthResponse`.
|
389
451
|
|
390
|
-
|
452
|
+
Possible **failure codes** (using the `ErrorResponse` structure) are:
|
391
453
|
|
392
|
-
-
|
454
|
+
- `not_allowed` - Invalid username (the username doesn't match the regex)
|
455
|
+
- `agent_not_found` - No agent found with that username
|
393
456
|
|
394
|
-
|
457
|
+
## Reference/Example Mobile Applications
|
395
458
|
|
396
|
-
|
459
|
+
There is a sample iOS app that integrates with **passkeys-rails** based server implementations. It's a great place to get a quick start on implementing passkyes in your iOS, iPadOS or MacOS apps.
|
397
460
|
|
398
|
-
|
461
|
+
Check out the [PasskeysRailsDemo](https://github.com/alliedcode/PasskeysRailsDemo) app.
|
399
462
|
|
400
|
-
|
463
|
+
## Contributing
|
401
464
|
|
402
|
-
|
465
|
+
### Contribution Guidelines
|
403
466
|
|
404
|
-
|
467
|
+
Thank you for considering contributing to PasskeysRails! We welcome your help to improve and enhance this project. Whether it's a bug fix, documentation update, or a new feature, your contributions are valuable to the community.
|
405
468
|
|
406
|
-
|
469
|
+
To ensure a smooth collaboration, please follow the [Contribution Guidelines](https://github.com/alliedcode/passkeys-rails/blob/main/CONTRIBUTION_GUIDELINES.md) when submitting your contributions.
|
407
470
|
|
408
|
-
|
471
|
+
### Code of Conduct
|
409
472
|
|
473
|
+
Please note that this project follows the [Code of Conduct](https://github.com/alliedcode/passkeys-rails/blob/main/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. If you encounter any behavior that violates the code, please report it to the project maintainers.
|
410
474
|
|
411
475
|
## License
|
412
476
|
|
@@ -1,10 +1,15 @@
|
|
1
1
|
module PasskeysRails
|
2
2
|
class ApplicationController < ActionController::Base
|
3
|
+
rescue_from StandardError, with: :handle_standard_error
|
3
4
|
rescue_from ::Interactor::Failure, with: :handle_interactor_failure
|
4
5
|
rescue_from ActionController::ParameterMissing, with: :handle_missing_parameter
|
5
6
|
|
6
7
|
protected
|
7
8
|
|
9
|
+
def handle_standard_error(error)
|
10
|
+
render_error(:authentication, 'error', error.message.truncate(512), status: 500)
|
11
|
+
end
|
12
|
+
|
8
13
|
def handle_missing_parameter(error)
|
9
14
|
render_error(:authentication, 'missing_parameter', error.message)
|
10
15
|
end
|
@@ -1,33 +1,45 @@
|
|
1
1
|
module PasskeysRails
|
2
2
|
class PasskeysController < ApplicationController
|
3
|
+
skip_before_action :verify_authenticity_token
|
4
|
+
wrap_parameters false
|
5
|
+
|
3
6
|
def challenge
|
4
7
|
result = PasskeysRails::BeginChallenge.call!(username: challenge_params[:username])
|
5
8
|
|
6
9
|
# Store the challenge so we can verify the future register or authentication request
|
7
|
-
|
10
|
+
cookies[:passkeys_rails] = result.cookie_data
|
8
11
|
|
9
12
|
render json: result.response.as_json
|
10
13
|
end
|
11
14
|
|
12
15
|
def register
|
16
|
+
cookie_data = cookies["passkeys_rails"] || {}
|
13
17
|
result = PasskeysRails::FinishRegistration.call!(credential: attestation_credential_params.to_h,
|
14
|
-
authenticatable_info:
|
15
|
-
username:
|
16
|
-
challenge:
|
18
|
+
authenticatable_info: authenticatable_params&.to_h,
|
19
|
+
username: cookie_data["username"],
|
20
|
+
challenge: cookie_data["challenge"])
|
21
|
+
|
22
|
+
broadcast(:did_register, agent: result.agent)
|
17
23
|
|
18
|
-
render json:
|
24
|
+
render json: auth_response(result)
|
19
25
|
end
|
20
26
|
|
21
27
|
def authenticate
|
28
|
+
cookie_data = cookies["passkeys_rails"] || {}
|
22
29
|
result = PasskeysRails::FinishAuthentication.call!(credential: authentication_params.to_h,
|
23
|
-
challenge:
|
30
|
+
challenge: cookie_data["challenge"])
|
31
|
+
|
32
|
+
broadcast(:did_authenticate, agent: result.agent)
|
24
33
|
|
25
|
-
render json:
|
34
|
+
render json: auth_response(result)
|
26
35
|
end
|
27
36
|
|
28
37
|
def refresh
|
29
38
|
result = PasskeysRails::RefreshToken.call!(token: refresh_params[:auth_token])
|
30
|
-
|
39
|
+
|
40
|
+
broadcast(:did_refresh, agent: result.agent)
|
41
|
+
|
42
|
+
render json: auth_response(result)
|
31
43
|
end
|
32
44
|
|
33
45
|
# This action exists to allow easier mobile app debugging as it may not
|
@@ -36,11 +48,31 @@ module PasskeysRails
|
|
36
48
|
# CAUTION: It is very insecure to set DEBUG_LOGIN_REGEX in a production environment.
|
37
49
|
def debug_login
|
38
50
|
result = PasskeysRails::DebugLogin.call!(username: debug_login_params[:username])
|
39
|
-
|
51
|
+
|
52
|
+
broadcast(:did_authenticate, agent: result.agent)
|
53
|
+
|
54
|
+
render json: auth_response(result)
|
55
|
+
end
|
56
|
+
|
57
|
+
# This action exists to allow easier mobile app debugging as it may not
|
58
|
+
# be possible to acess Passkey functionality in mobile simulators.
|
59
|
+
# It is only routable if DEBUG_LOGIN_REGEX is set in the server environment.
|
60
|
+
# CAUTION: It is very insecure to set DEBUG_LOGIN_REGEX in a production environment.
|
61
|
+
def debug_register
|
62
|
+
result = PasskeysRails::DebugRegister.call!(username: debug_login_params[:username],
|
63
|
+
authenticatable_info: authenticatable_params&.to_h)
|
64
|
+
|
65
|
+
broadcast(:did_register, agent: result.agent)
|
66
|
+
|
67
|
+
render json: auth_response(result)
|
40
68
|
end
|
41
69
|
|
42
70
|
protected
|
43
71
|
|
72
|
+
def auth_response(result)
|
73
|
+
{ username: result.username, auth_token: result.auth_token }
|
74
|
+
end
|
75
|
+
|
44
76
|
def challenge_params
|
45
77
|
params.permit(:username)
|
46
78
|
end
|
@@ -52,8 +84,8 @@ module PasskeysRails
|
|
52
84
|
credential.permit(:id, :rawId, :type, { response: %i[attestationObject clientDataJSON] })
|
53
85
|
end
|
54
86
|
|
55
|
-
def
|
56
|
-
params.require
|
87
|
+
def authenticatable_params
|
88
|
+
params.require(:authenticatable).permit(:class, params: {}) if params[:authenticatable].present?
|
57
89
|
end
|
58
90
|
|
59
91
|
def authentication_params
|
@@ -71,5 +103,9 @@ module PasskeysRails
|
|
71
103
|
params.require(:username)
|
72
104
|
params.permit(:username)
|
73
105
|
end
|
106
|
+
|
107
|
+
def broadcast(event_name, agent:)
|
108
|
+
ActiveSupport::Notifications.instrument("passkeys_rails.#{event_name}", { agent:, request: })
|
109
|
+
end
|
74
110
|
end
|
75
111
|
end
|
@@ -10,7 +10,7 @@ module PasskeysRails
|
|
10
10
|
options = result.options
|
11
11
|
|
12
12
|
context.response = options
|
13
|
-
context.
|
13
|
+
context.cookie_data = cookie_data(options)
|
14
14
|
rescue Interactor::Failure => e
|
15
15
|
context.fail! code: e.context.code, message: e.context.message
|
16
16
|
end
|
@@ -25,7 +25,7 @@ module PasskeysRails
|
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
|
-
def
|
28
|
+
def cookie_data(options)
|
29
29
|
{
|
30
30
|
username:,
|
31
31
|
challenge: WebAuthn.standard_encoder.encode(options.challenge)
|
@@ -13,6 +13,8 @@ module PasskeysRails
|
|
13
13
|
private
|
14
14
|
|
15
15
|
def create_or_replace_unregistered_agent
|
16
|
+
context.fail! code: :origin_error, message: "config.wa_origin must be set" if WebAuthn.configuration.origin.blank?
|
17
|
+
|
16
18
|
Agent.unregistered.where(username:).destroy_all
|
17
19
|
|
18
20
|
agent = Agent.create(username:, webauthn_identifier: WebAuthn.generate_user_id)
|
@@ -5,6 +5,7 @@
|
|
5
5
|
module PasskeysRails
|
6
6
|
class DebugLogin
|
7
7
|
include Interactor
|
8
|
+
include Debuggable
|
8
9
|
|
9
10
|
delegate :username, to: :context
|
10
11
|
|
@@ -12,6 +13,7 @@ module PasskeysRails
|
|
12
13
|
ensure_debug_mode
|
13
14
|
ensure_regex_match
|
14
15
|
|
16
|
+
context.agent = agent
|
15
17
|
context.username = agent.username
|
16
18
|
context.auth_token = GenerateAuthToken.call!(agent:).auth_token
|
17
19
|
rescue Interactor::Failure => e
|
@@ -20,18 +22,6 @@ module PasskeysRails
|
|
20
22
|
|
21
23
|
private
|
22
24
|
|
23
|
-
def ensure_debug_mode
|
24
|
-
context.fail!(code: :not_allowed, message: 'Action not allowed') if username_regex.blank?
|
25
|
-
end
|
26
|
-
|
27
|
-
def ensure_regex_match
|
28
|
-
context.fail!(code: :not_allowed, message: 'Invalid username') unless username&.match?(username_regex)
|
29
|
-
end
|
30
|
-
|
31
|
-
def username_regex
|
32
|
-
PasskeysRails.debug_login_regex
|
33
|
-
end
|
34
|
-
|
35
25
|
def agent
|
36
26
|
@agent ||= begin
|
37
27
|
agent = Agent.find_by(username:)
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# This functionality exists to allow easier mobile app debugging as it may not
|
2
|
+
# be possible to acess Passkey functionality in mobile simulators.
|
3
|
+
# It is only operational if DEBUG_LOGIN_REGEX is set in the server environment.
|
4
|
+
# CAUTION: It is very insecure to set DEBUG_LOGIN_REGEX in a production environment.
|
5
|
+
module PasskeysRails
|
6
|
+
class DebugRegister
|
7
|
+
include Interactor
|
8
|
+
include Debuggable
|
9
|
+
include AuthenticatableCreator
|
10
|
+
|
11
|
+
delegate :username, :authenticatable_info, to: :context
|
12
|
+
|
13
|
+
def call
|
14
|
+
ensure_debug_mode
|
15
|
+
ensure_regex_match
|
16
|
+
|
17
|
+
ActiveRecord::Base.transaction do
|
18
|
+
create_authenticatable! if aux_class_name.present?
|
19
|
+
end
|
20
|
+
|
21
|
+
context.agent = agent
|
22
|
+
context.username = agent.username
|
23
|
+
context.auth_token = GenerateAuthToken.call!(agent:).auth_token
|
24
|
+
rescue Interactor::Failure => e
|
25
|
+
context.fail! code: e.context.code, message: e.context.message
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def agent
|
31
|
+
@agent ||= begin
|
32
|
+
result = BeginRegistration.call(username:)
|
33
|
+
context.fail!(code: result.code, message: result.message) if result.failure?
|
34
|
+
|
35
|
+
agent = Agent.find_by(username:)
|
36
|
+
context.fail!(code: :agent_not_found, message: "No agent found with that username") if agent.blank?
|
37
|
+
|
38
|
+
agent.update! registered_at: Time.current
|
39
|
+
|
40
|
+
agent
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -2,13 +2,19 @@
|
|
2
2
|
module PasskeysRails
|
3
3
|
class FinishRegistration
|
4
4
|
include Interactor
|
5
|
+
include AuthenticatableCreator
|
5
6
|
|
6
7
|
delegate :credential, :username, :challenge, :authenticatable_info, to: :context
|
7
8
|
|
8
9
|
def call
|
9
10
|
verify_credential!
|
10
|
-
store_passkey_and_register_agent!
|
11
11
|
|
12
|
+
agent.transaction do
|
13
|
+
store_passkey_and_register_agent!
|
14
|
+
create_authenticatable! if aux_class_name.present?
|
15
|
+
end
|
16
|
+
|
17
|
+
context.agent = agent
|
12
18
|
context.username = agent.username
|
13
19
|
context.auth_token = GenerateAuthToken.call!(agent:).auth_token
|
14
20
|
rescue Interactor::Failure => e
|
@@ -22,70 +28,24 @@ module PasskeysRails
|
|
22
28
|
rescue WebAuthn::Error => e
|
23
29
|
context.fail!(code: :webauthn_error, message: e.message)
|
24
30
|
rescue StandardError => e
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
agent.transaction do
|
30
|
-
begin
|
31
|
-
# Store Credential ID, Credential Public Key and Sign Count for future authentications
|
32
|
-
agent.passkeys.create!(
|
33
|
-
identifier: webauthn_credential.id,
|
34
|
-
public_key: webauthn_credential.public_key,
|
35
|
-
sign_count: webauthn_credential.sign_count
|
36
|
-
)
|
37
|
-
|
38
|
-
agent.update! registered_at: Time.current
|
39
|
-
rescue StandardError => e
|
40
|
-
context.fail! code: :passkey_error, message: e.message
|
41
|
-
end
|
42
|
-
|
43
|
-
create_authenticatable! if aux_class_name.present?
|
44
|
-
end
|
45
|
-
end
|
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
|
-
|
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
|
61
|
-
|
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.")
|
70
|
-
end
|
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
|
31
|
+
if e.message == "undefined method `end_with?' for nil:NilClass"
|
32
|
+
context.fail!(code: :webauthn_error, message: "origin is not set")
|
33
|
+
else
|
34
|
+
context.fail!(code: :error, message: e.message)
|
77
35
|
end
|
78
36
|
end
|
79
37
|
|
80
|
-
def
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
38
|
+
def store_passkey_and_register_agent!
|
39
|
+
# Store Credential ID, Credential Public Key and Sign Count for future authentications
|
40
|
+
agent.passkeys.create!(
|
41
|
+
identifier: webauthn_credential.id,
|
42
|
+
public_key: webauthn_credential.public_key,
|
43
|
+
sign_count: webauthn_credential.sign_count
|
44
|
+
)
|
45
|
+
|
46
|
+
agent.update! registered_at: Time.current
|
47
|
+
rescue StandardError => e
|
48
|
+
context.fail! code: :passkey_error, message: e.message
|
89
49
|
end
|
90
50
|
|
91
51
|
def webauthn_credential
|
@@ -95,7 +55,7 @@ module PasskeysRails
|
|
95
55
|
def agent
|
96
56
|
@agent ||= begin
|
97
57
|
agent = Agent.find_by(username:)
|
98
|
-
context.fail!(code: :agent_not_found, message: "Agent not found for
|
58
|
+
context.fail!(code: :agent_not_found, message: "Agent not found for cookie value: \"#{username}\"") if agent.blank?
|
99
59
|
|
100
60
|
agent
|
101
61
|
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module PasskeysRails
|
2
|
+
module AuthenticatableCreator
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
def create_authenticatable!
|
8
|
+
authenticatable = aux_class.new
|
9
|
+
authenticatable.agent = agent if authenticatable.respond_to?(:agent=)
|
10
|
+
authenticatable.registering_with(authenticatable_params) if authenticatable.respond_to?(:registering_with)
|
11
|
+
authenticatable.save!
|
12
|
+
|
13
|
+
agent.update!(authenticatable:)
|
14
|
+
rescue ActiveRecord::RecordInvalid => e
|
15
|
+
context.fail!(code: :record_invalid, message: e.message)
|
16
|
+
end
|
17
|
+
|
18
|
+
def aux_class_name
|
19
|
+
@aux_class_name ||= authenticatable_class || PasskeysRails.default_class
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def authenticatable_class
|
25
|
+
authenticatable_info && authenticatable_info[:class]
|
26
|
+
end
|
27
|
+
|
28
|
+
def authenticatable_params
|
29
|
+
authenticatable_info && authenticatable_info[:params]
|
30
|
+
end
|
31
|
+
|
32
|
+
def aux_class
|
33
|
+
whitelist = PasskeysRails.class_whitelist
|
34
|
+
|
35
|
+
@aux_class ||= begin
|
36
|
+
if whitelist.is_a?(Array)
|
37
|
+
unless whitelist.include?(aux_class_name)
|
38
|
+
context.fail!(code: :invalid_authenticatable_class, message: "authenticatable_class (#{aux_class_name}) is not in the whitelist")
|
39
|
+
end
|
40
|
+
elsif whitelist.present?
|
41
|
+
context.fail!(code: :invalid_class_whitelist,
|
42
|
+
message: "class_whitelist is invalid. It should be nil or an array of zero or more class names.")
|
43
|
+
end
|
44
|
+
|
45
|
+
begin
|
46
|
+
aux_class_name.constantize
|
47
|
+
rescue StandardError
|
48
|
+
context.fail!(code: :invalid_authenticatable_class, message: "authenticatable_class (#{aux_class_name}) is not defined")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module PasskeysRails
|
2
|
+
module Debuggable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
def ensure_debug_mode
|
8
|
+
context.fail!(code: :not_allowed, message: 'Action not allowed') if username_regex.blank?
|
9
|
+
end
|
10
|
+
|
11
|
+
def ensure_regex_match
|
12
|
+
context.fail!(code: :not_allowed, message: 'Invalid username') unless username&.match?(username_regex)
|
13
|
+
end
|
14
|
+
|
15
|
+
def username_regex
|
16
|
+
PasskeysRails.debug_login_regex
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/config/routes.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
PasskeysRails::Engine.routes.draw do
|
2
|
-
post 'passkeys
|
3
|
-
post 'passkeys
|
4
|
-
post 'passkeys
|
5
|
-
post 'passkeys
|
2
|
+
post 'challenge', to: 'passkeys#challenge'
|
3
|
+
post 'register', to: 'passkeys#register'
|
4
|
+
post 'authenticate', to: 'passkeys#authenticate'
|
5
|
+
post 'refresh', to: 'passkeys#refresh'
|
6
6
|
|
7
|
-
#
|
7
|
+
# These routes exist to allow easier mobile app debugging as it may not
|
8
8
|
# be possible to acess Passkey functionality in mobile simulators.
|
9
9
|
# CAUTION: It is very insecure to set DEBUG_LOGIN_REGEX in a production environment.
|
10
10
|
constraints(->(_request) { PasskeysRails.debug_login_regex.present? }) do
|
11
|
-
post 'passkeys
|
11
|
+
post 'debug_login', to: 'passkeys#debug_login'
|
12
|
+
post 'debug_register', to: 'passkeys#debug_register'
|
12
13
|
end
|
13
14
|
end
|
@@ -45,4 +45,60 @@ PasskeysRails.config do |c|
|
|
45
45
|
# for example: %w[User AdminUser]
|
46
46
|
#
|
47
47
|
# c.class_whitelist = nil
|
48
|
+
|
49
|
+
# To subscribe to various events in PasskeysRails, use the subscribe method.
|
50
|
+
# It can be called multiple times to subscribe to more than one event.
|
51
|
+
#
|
52
|
+
# Valid events:
|
53
|
+
# :did_register
|
54
|
+
# :did_authenticate
|
55
|
+
# :did_refresh
|
56
|
+
#
|
57
|
+
# Each event will include the event name, current agent and http request.
|
58
|
+
#
|
59
|
+
# For example:
|
60
|
+
# c.subscribe(:did_register) do |event, agent, request|
|
61
|
+
# puts("#{event} | #{agent.id} | #{request.headers}")
|
62
|
+
# end
|
63
|
+
|
64
|
+
# PasskeysRails uses webauthn to help with the protocol.
|
65
|
+
# The following settings are passed throught webauthn.
|
66
|
+
# wa_origin is the only one requried
|
67
|
+
|
68
|
+
# This value needs to match `window.location.origin` evaluated by
|
69
|
+
# the User Agent during registration and authentication ceremonies.
|
70
|
+
# c.wa_origin = ENV['DEFAULT_HOST'] || https://myapp.mydomain.com
|
71
|
+
|
72
|
+
# Relying Party name for display purposes
|
73
|
+
# c.wa_relying_party_name = "My App Name"
|
74
|
+
|
75
|
+
# Optionally configure a client timeout hint, in milliseconds.
|
76
|
+
# This hint specifies how long the browser should wait for any
|
77
|
+
# interaction with the user.
|
78
|
+
# This hint may be overridden by the browser.
|
79
|
+
# https://www.w3.org/TR/webauthn/#dom-publickeycredentialcreationoptions-timeout
|
80
|
+
# c.wa_credential_options_timeout = 120_000
|
81
|
+
|
82
|
+
# You can optionally specify a different Relying Party ID
|
83
|
+
# (https://www.w3.org/TR/webauthn/#relying-party-identifier)
|
84
|
+
# if it differs from the default one.
|
85
|
+
#
|
86
|
+
# In this case the default would be "auth.example.com", but you can set it to
|
87
|
+
# the suffix "example.com"
|
88
|
+
#
|
89
|
+
# c.wa_rp_id = "example.com"
|
90
|
+
|
91
|
+
# Configure preferred binary-to-text encoding scheme. This should match the encoding scheme
|
92
|
+
# used in your client-side (user agent) code before sending the credential to the server.
|
93
|
+
# Supported values: `:base64url` (default), `:base64` or `false` to disable all encoding.
|
94
|
+
#
|
95
|
+
# c.wa_encoding = :base64url
|
96
|
+
|
97
|
+
# Possible values: "ES256", "ES384", "ES512", "PS256", "PS384", "PS512", "RS256", "RS384", "RS512", "RS1"
|
98
|
+
# Default: ["ES256", "PS256", "RS256"]
|
99
|
+
#
|
100
|
+
# c.wa_algorithms = ["ES256", "PS256", "RS256"]
|
101
|
+
|
102
|
+
# Append an algorithm to the existing set
|
103
|
+
# c.wa_algorithm = "PS512"
|
48
104
|
end
|
data/lib/passkeys-rails.rb
CHANGED
@@ -1,49 +1,62 @@
|
|
1
1
|
# rubocop:disable Naming/FileName
|
2
2
|
require 'passkeys_rails/engine'
|
3
|
+
require 'passkeys_rails/configuration'
|
3
4
|
require 'passkeys_rails/version'
|
4
5
|
require_relative "generators/passkeys_rails/install_generator"
|
6
|
+
require 'forwardable'
|
5
7
|
|
6
8
|
module PasskeysRails
|
7
9
|
module Test
|
8
10
|
autoload :IntegrationHelpers, 'passkeys_rails/test/integration_helpers'
|
9
11
|
end
|
10
12
|
|
11
|
-
|
12
|
-
|
13
|
-
# Changing this value will invalidate all tokens that have been fetched
|
14
|
-
# through the API.
|
15
|
-
mattr_accessor(:auth_token_secret)
|
13
|
+
class << self
|
14
|
+
extend Forwardable
|
16
15
|
|
17
|
-
|
18
|
-
# Changing this value will invalidate all tokens that have been fetched
|
19
|
-
# through the API.
|
20
|
-
mattr_accessor :auth_token_algorithm, default: "HS256"
|
16
|
+
def_delegators :config, :auth_token_secret, :auth_token_algorithm, :auth_token_expires_in, :default_class, :class_whitelist
|
21
17
|
|
22
|
-
|
23
|
-
|
24
|
-
|
18
|
+
def config
|
19
|
+
@config ||= begin
|
20
|
+
config = Configuration.new
|
21
|
+
yield(config) if block_given?
|
22
|
+
apply_webauthn_configuration(config)
|
25
23
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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"
|
24
|
+
config
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
37
28
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
29
|
+
def self.apply_webauthn_configuration(config)
|
30
|
+
WebAuthn.configure do |c|
|
31
|
+
c.origin = config.wa_origin
|
32
|
+
c.rp_name = config.wa_relying_party_name if config.wa_relying_party_name
|
33
|
+
c.credential_options_timeout = config.wa_credential_options_timeout if config.wa_credential_options_timeout
|
34
|
+
c.rp_id = config.wa_rp_id if config.wa_rp_id
|
35
|
+
c.encoding = config.wa_encoding if config.wa_encoding
|
36
|
+
c.algorithms = config.wa_algorithms if config.wa_algorithms
|
37
|
+
c.algorithms << config.wa_algorithm if config.wa_algorithm
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Convenience method to subscribe to various events in PasskeysRails.
|
42
|
+
#
|
43
|
+
# Valid events:
|
44
|
+
# :did_register
|
45
|
+
# :did_authenticate
|
46
|
+
# :did_refresh
|
47
|
+
#
|
48
|
+
# Each event will include the event name, current agent and http request.
|
49
|
+
# For example:
|
50
|
+
#
|
51
|
+
# subscribe(:did_register) do |event, agent, request|
|
52
|
+
# # do something with the agent and/or request
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
def self.subscribe(event_name)
|
56
|
+
ActiveSupport::Notifications.subscribe("passkeys_rails.#{event_name}") do |name, _start, _finish, _id, payload|
|
57
|
+
yield(name.gsub(/^passkeys_rails\./, ''), payload[:agent], payload[:request]) if block_given?
|
58
|
+
end
|
59
|
+
end
|
47
60
|
|
48
61
|
# This is only used by the debug_login endpoint.
|
49
62
|
# CAUTION: It is very insecure to set DEBUG_LOGIN_REGEX in a production environment.
|
@@ -84,16 +97,6 @@ module PasskeysRails
|
|
84
97
|
message: auth.message)
|
85
98
|
end
|
86
99
|
|
87
|
-
class << self
|
88
|
-
def config
|
89
|
-
yield self
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
100
|
require 'passkeys_rails/railtie' if defined?(Rails)
|
94
101
|
end
|
95
|
-
|
96
|
-
ActiveSupport.on_load(:before_initialize) do
|
97
|
-
PasskeysRails.auth_token_secret ||= Rails.application.secret_key_base
|
98
|
-
end
|
99
102
|
# rubocop:enable Naming/FileName
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module PasskeysRails
|
2
|
+
class Configuration
|
3
|
+
# Secret used to encode the auth token.
|
4
|
+
# Rails.application.secret_key_base is used if none is defined here.
|
5
|
+
# Changing this value will invalidate all tokens that have been fetched
|
6
|
+
# through the API.
|
7
|
+
attr_accessor :auth_token_secret
|
8
|
+
|
9
|
+
# Algorithm used to generate the auth token.
|
10
|
+
# Changing this value will invalidate all tokens that have been fetched
|
11
|
+
# through the API.
|
12
|
+
attr_accessor :auth_token_algorithm
|
13
|
+
|
14
|
+
# How long the auth token is valid before requiring a refresh or new login.
|
15
|
+
# Set it to 0 for no expiration (not recommended in production).
|
16
|
+
attr_accessor :auth_token_expires_in
|
17
|
+
|
18
|
+
# Model to use when creating or authenticating a passkey.
|
19
|
+
# This can be overridden when calling the API, but if no
|
20
|
+
# value is supplied when calling the API, this value is used.
|
21
|
+
# If nil, there is no default, and if none is supplied when
|
22
|
+
# calling the API, no resource is created other than
|
23
|
+
# a PaskeysRails::Agent that is used to track the passkey.
|
24
|
+
#
|
25
|
+
# This library doesn't assume that there will only be one
|
26
|
+
# model, but it is a common use case, so setting the
|
27
|
+
# default_class simplifies the use of the API in that case.
|
28
|
+
attr_accessor :default_class
|
29
|
+
|
30
|
+
# By providing a class_whitelist, the API will require that
|
31
|
+
# any supplied class is in the whitelist. If it is not, the
|
32
|
+
# auth API will return an error. This prevents a caller from
|
33
|
+
# attempting to create an unintended record on registration.
|
34
|
+
# If nil, any model will be allowed.
|
35
|
+
# If [], no model will be allowed.
|
36
|
+
# This should be an array of symbols or strings,
|
37
|
+
# for example: %w[User AdminUser]
|
38
|
+
attr_accessor :class_whitelist
|
39
|
+
|
40
|
+
# webauthn settings
|
41
|
+
attr_accessor :wa_origin,
|
42
|
+
:wa_relying_party_name,
|
43
|
+
:wa_credential_options_timeout,
|
44
|
+
:wa_rp_id,
|
45
|
+
:wa_encoding,
|
46
|
+
:wa_algorithms,
|
47
|
+
:wa_algorithm
|
48
|
+
|
49
|
+
def initialize
|
50
|
+
# defaults
|
51
|
+
@auth_token_secret = Rails.application.secret_key_base
|
52
|
+
@auth_token_algorithm = "HS256"
|
53
|
+
@auth_token_expires_in = 30.days
|
54
|
+
@default_class = "User"
|
55
|
+
@wa_origin = "https://example.com"
|
56
|
+
end
|
57
|
+
|
58
|
+
def subscribe(event_name)
|
59
|
+
PasskeysRails.subscribe(event_name)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
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.
|
4
|
+
version: 0.3.1
|
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-
|
11
|
+
date: 2023-11-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -358,12 +358,15 @@ files:
|
|
358
358
|
- app/interactors/passkeys_rails/begin_challenge.rb
|
359
359
|
- app/interactors/passkeys_rails/begin_registration.rb
|
360
360
|
- app/interactors/passkeys_rails/debug_login.rb
|
361
|
+
- app/interactors/passkeys_rails/debug_register.rb
|
361
362
|
- app/interactors/passkeys_rails/finish_authentication.rb
|
362
363
|
- app/interactors/passkeys_rails/finish_registration.rb
|
363
364
|
- app/interactors/passkeys_rails/generate_auth_token.rb
|
364
365
|
- app/interactors/passkeys_rails/refresh_token.rb
|
365
366
|
- app/interactors/passkeys_rails/validate_auth_token.rb
|
366
367
|
- app/models/concerns/passkeys_rails/authenticatable.rb
|
368
|
+
- app/models/concerns/passkeys_rails/authenticatable_creator.rb
|
369
|
+
- app/models/concerns/passkeys_rails/debuggable.rb
|
367
370
|
- app/models/passkeys_rails/agent.rb
|
368
371
|
- app/models/passkeys_rails/application_record.rb
|
369
372
|
- app/models/passkeys_rails/error.rb
|
@@ -377,6 +380,7 @@ files:
|
|
377
380
|
- lib/generators/passkeys_rails/templates/README
|
378
381
|
- lib/generators/passkeys_rails/templates/passkeys_rails_config.rb
|
379
382
|
- lib/passkeys-rails.rb
|
383
|
+
- lib/passkeys_rails/configuration.rb
|
380
384
|
- lib/passkeys_rails/engine.rb
|
381
385
|
- lib/passkeys_rails/railtie.rb
|
382
386
|
- lib/passkeys_rails/test/integration_helpers.rb
|