passkeys-rails 0.3.0 → 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 +7 -0
- data/README.md +129 -100
- data/app/controllers/passkeys_rails/passkeys_controller.rb +6 -4
- 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/finish_registration.rb +6 -2
- data/lib/generators/passkeys_rails/templates/passkeys_rails_config.rb +41 -0
- data/lib/passkeys-rails.rb +25 -42
- data/lib/passkeys_rails/configuration.rb +62 -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: 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,10 @@
|
|
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
|
+
|
1
8
|
### 0.3.0
|
2
9
|
|
3
10
|
* Added debug_register endpoint.
|
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:
|
@@ -58,29 +76,32 @@ $ rails generate passkeys_rails:install
|
|
58
76
|
|
59
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`
|
70
88
|
|
71
|
-
|
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.
|
90
|
+
|
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,15 +130,17 @@ This will add the `config/initializers/passkeys_rails.rb` configuration file, pa
|
|
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`.
|
117
140
|
|
118
|
-
|
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.
|
119
142
|
|
120
|
-
|
143
|
+
### Events
|
121
144
|
|
122
145
|
- `:did_register ` - a new agent has registered
|
123
146
|
|
@@ -125,7 +148,7 @@ Certain actions trigger notifications that can be subscribed. See `subscribe` i
|
|
125
148
|
|
126
149
|
- `:did_refresh` - an agent's auth token has been refreshed
|
127
150
|
|
128
|
-
A convenient place to set these up in is in `passkeys_rails.rb`
|
151
|
+
A convenient place to set these up in is in `config/initializers/passkeys_rails.rb`
|
129
152
|
|
130
153
|
```ruby
|
131
154
|
PasskeysRails.config do |c|
|
@@ -147,10 +170,9 @@ PasskeysRails.subscribe(:did_register) do |event, agent, request|
|
|
147
170
|
end
|
148
171
|
```
|
149
172
|
|
173
|
+
## Failure Codes
|
150
174
|
|
151
|
-
|
152
|
-
|
153
|
-
1. In the event of authentication failure, PasskeysRails returns an error code and message.
|
175
|
+
1. In the event of authentication failure, **PasskeysRails** API endpoints render an error code and message.
|
154
176
|
|
155
177
|
1. In a standard rails controller, the error code and message are rendered in JSON if `before_action :authenticate_passkey!` fails.
|
156
178
|
|
@@ -168,12 +190,10 @@ end
|
|
168
190
|
|
169
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.
|
170
192
|
|
171
|
-
|
193
|
+
## Testing
|
172
194
|
|
173
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.
|
174
196
|
|
175
|
-
### Integration tests
|
176
|
-
|
177
197
|
Integration test helpers are available by including the `PasskeysRails::IntegrationHelpers` module.
|
178
198
|
|
179
199
|
```ruby
|
@@ -210,20 +230,38 @@ RSpec.describe 'Posts', type: :request do
|
|
210
230
|
end
|
211
231
|
```
|
212
232
|
|
213
|
-
|
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)
|
238
|
+
|
214
239
|
|
215
|
-
|
240
|
+
### Mobile API Endpoints
|
241
|
+
|
242
|
+
There are 3 groups of API endpoints that your mobile application may consume.
|
216
243
|
|
217
244
|
1. Unauthenticated (public) endpoints
|
218
245
|
1. Authenticated (private) endpoints
|
219
246
|
1. Passey endpoints (for supporting authentication)
|
220
247
|
|
221
|
-
**Unauthenticated endpoints** can be consumed without
|
248
|
+
**Unauthenticated endpoints** can be consumed without any authentication.
|
222
249
|
|
223
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.
|
224
251
|
|
225
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.
|
226
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
|
+
|
227
265
|
All Passkey endpoints accept and respond with JSON.
|
228
266
|
|
229
267
|
On **success**, they will respond with a 200 or 201 response code and relevant JSON.
|
@@ -249,7 +287,7 @@ Some endpoints return an `AuthResponse`, which has this JSON structure:
|
|
249
287
|
}
|
250
288
|
```
|
251
289
|
|
252
|
-
|
290
|
+
### POST /passkeys/challenge
|
253
291
|
|
254
292
|
Submit this to begin registration or authentication.
|
255
293
|
|
@@ -260,7 +298,7 @@ If the username is already in use, or anything else goes wrong, an error with co
|
|
260
298
|
Omit the `username` when authenticating (logging in).
|
261
299
|
The JSON response will be the `options_for_get` from webauthn.
|
262
300
|
|
263
|
-
|
301
|
+
### POST /passkeys/register
|
264
302
|
|
265
303
|
After calling the `challenge` endpoint with a `username`, and handling its response, finish registering by calling this endpoint.
|
266
304
|
|
@@ -291,16 +329,16 @@ On **success**, the response is an `AuthResponse`.
|
|
291
329
|
|
292
330
|
Possible **failure codes** (using the `ErrorResponse` structure) are:
|
293
331
|
|
294
|
-
- webauthn_error - something is wrong with the credential
|
295
|
-
- error - something else went wrong during credentail validation - see the `message` in the `ErrorResponse`
|
296
|
-
- passkey_error - unable to
|
297
|
-
- invalid_authenticatable_class - the supplied authenticatable class can't be created/found (check spelling & capitalization)
|
298
|
-
- invalid_class_whitelist - the whitelist in the passkeys_rails.rb configuration is invalid - be sure it's nil or an array
|
299
|
-
- invalid_authenticatable_class - the supplied authenticatable class is not allowed - maybe it's not in the whitelist
|
300
|
-
- record_invalid - the object of the supplied authenticatable class cannot be saved due to validation errors
|
301
|
-
- 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
|
302
340
|
|
303
|
-
|
341
|
+
### POST /passkeys/authenticate
|
304
342
|
|
305
343
|
After calling the `challenge` endpoint without a `username`, and handling its response, finish authenticating by calling this endpoint.
|
306
344
|
|
@@ -325,12 +363,12 @@ On **success**, the response is an `AuthResponse`.
|
|
325
363
|
|
326
364
|
Possible **failure codes** (using the `ErrorResponse` structure) are:
|
327
365
|
|
328
|
-
- webauthn_error - something is wrong with the credential
|
329
|
-
- 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
|
330
368
|
|
331
|
-
|
369
|
+
### POST /passkeys/refresh
|
332
370
|
|
333
|
-
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.
|
334
372
|
|
335
373
|
Supply the following JSON structure:
|
336
374
|
|
@@ -345,28 +383,28 @@ On **success**, the response is an `AuthResponse` with a new, refreshed token.
|
|
345
383
|
|
346
384
|
Possible **failure codes** (using the `ErrorResponse` structure) are:
|
347
385
|
|
348
|
-
- invalid_token - the token data is invalid
|
349
|
-
- expired_token - the token is expired
|
350
|
-
- 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
|
351
389
|
|
352
|
-
|
390
|
+
### POST /passkeys/debug_register
|
353
391
|
|
354
|
-
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.
|
355
393
|
|
356
|
-
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.
|
357
395
|
|
358
396
|
To use this endpoint:
|
359
397
|
|
360
|
-
1. Manually create one or more PasskeysRails::Agent records in the database. A unique username is required for each.
|
361
|
-
|
362
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.
|
363
399
|
|
364
|
-
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.
|
365
401
|
|
366
|
-
1. Use the response as if it was from /passkeys/
|
402
|
+
1. Use the response as if it was from /passkeys/register.
|
367
403
|
|
368
404
|
If you supply a username that doesn't match the DEBUG_LOGIN_REGEX, the endpoint will respond with an error.
|
369
405
|
|
406
|
+
Supply the following JSON structure:
|
407
|
+
|
370
408
|
```JSON
|
371
409
|
# POST body
|
372
410
|
{
|
@@ -377,71 +415,62 @@ On **success**, the response is an `AuthResponse`.
|
|
377
415
|
|
378
416
|
Possible **failure codes** (using the `ErrorResponse` structure) are:
|
379
417
|
|
380
|
-
- not_allowed - Invalid username (the username doesn't match the regex)
|
381
|
-
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
**TODO**: Point to the soon-to-be-created reference mobile applications for how to use **passkeys-rails** for passkey authentication.
|
386
|
-
|
387
|
-
## Contributing
|
388
|
-
|
389
|
-
### Contributing Guidelines
|
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
|
390
423
|
|
391
|
-
|
392
|
-
|
393
|
-
To ensure a smooth collaboration, please follow the guidelines below when submitting your contributions:
|
394
|
-
|
395
|
-
#### Code of Conduct
|
396
|
-
|
397
|
-
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.
|
398
|
-
|
399
|
-
#### How to Contribute
|
400
|
-
|
401
|
-
1. Fork the repository on GitHub.
|
424
|
+
### POST /passkeys/debug_login
|
402
425
|
|
403
|
-
|
404
|
-
|
405
|
-
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.
|
406
427
|
|
407
|
-
|
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.
|
408
429
|
|
409
|
-
|
430
|
+
To use this endpoint:
|
410
431
|
|
411
|
-
|
432
|
+
1. Manually create one or more PasskeysRails::Agent records in the database. A unique username is required for each.
|
412
433
|
|
413
|
-
|
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.
|
414
435
|
|
415
|
-
|
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.
|
416
437
|
|
417
|
-
|
438
|
+
1. Use the response as if it was from /passkeys/authenticate.
|
418
439
|
|
419
|
-
|
440
|
+
If you supply a username that doesn't match the DEBUG_LOGIN_REGEX, the endpoint will respond with an error.
|
420
441
|
|
421
|
-
|
442
|
+
Supply the following JSON structure:
|
422
443
|
|
423
|
-
|
444
|
+
```JSON
|
445
|
+
# POST body
|
446
|
+
{
|
447
|
+
"username": String
|
448
|
+
}
|
449
|
+
```
|
450
|
+
On **success**, the response is an `AuthResponse`.
|
424
451
|
|
425
|
-
|
452
|
+
Possible **failure codes** (using the `ErrorResponse` structure) are:
|
426
453
|
|
427
|
-
-
|
454
|
+
- `not_allowed` - Invalid username (the username doesn't match the regex)
|
455
|
+
- `agent_not_found` - No agent found with that username
|
428
456
|
|
429
|
-
|
457
|
+
## Reference/Example Mobile Applications
|
430
458
|
|
431
|
-
|
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.
|
432
460
|
|
433
|
-
|
461
|
+
Check out the [PasskeysRailsDemo](https://github.com/alliedcode/PasskeysRailsDemo) app.
|
434
462
|
|
435
|
-
|
463
|
+
## Contributing
|
436
464
|
|
437
|
-
|
465
|
+
### Contribution Guidelines
|
438
466
|
|
439
|
-
|
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.
|
440
468
|
|
441
|
-
|
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.
|
442
470
|
|
443
|
-
|
471
|
+
### Code of Conduct
|
444
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.
|
445
474
|
|
446
475
|
## License
|
447
476
|
|
@@ -7,16 +7,17 @@ module PasskeysRails
|
|
7
7
|
result = PasskeysRails::BeginChallenge.call!(username: challenge_params[:username])
|
8
8
|
|
9
9
|
# Store the challenge so we can verify the future register or authentication request
|
10
|
-
|
10
|
+
cookies[:passkeys_rails] = result.cookie_data
|
11
11
|
|
12
12
|
render json: result.response.as_json
|
13
13
|
end
|
14
14
|
|
15
15
|
def register
|
16
|
+
cookie_data = cookies["passkeys_rails"] || {}
|
16
17
|
result = PasskeysRails::FinishRegistration.call!(credential: attestation_credential_params.to_h,
|
17
18
|
authenticatable_info: authenticatable_params&.to_h,
|
18
|
-
username:
|
19
|
-
challenge:
|
19
|
+
username: cookie_data["username"],
|
20
|
+
challenge: cookie_data["challenge"])
|
20
21
|
|
21
22
|
broadcast(:did_register, agent: result.agent)
|
22
23
|
|
@@ -24,8 +25,9 @@ module PasskeysRails
|
|
24
25
|
end
|
25
26
|
|
26
27
|
def authenticate
|
28
|
+
cookie_data = cookies["passkeys_rails"] || {}
|
27
29
|
result = PasskeysRails::FinishAuthentication.call!(credential: authentication_params.to_h,
|
28
|
-
challenge:
|
30
|
+
challenge: cookie_data["challenge"])
|
29
31
|
|
30
32
|
broadcast(:did_authenticate, agent: result.agent)
|
31
33
|
|
@@ -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)
|
@@ -28,7 +28,11 @@ module PasskeysRails
|
|
28
28
|
rescue WebAuthn::Error => e
|
29
29
|
context.fail!(code: :webauthn_error, message: e.message)
|
30
30
|
rescue StandardError => e
|
31
|
-
|
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)
|
35
|
+
end
|
32
36
|
end
|
33
37
|
|
34
38
|
def store_passkey_and_register_agent!
|
@@ -51,7 +55,7 @@ module PasskeysRails
|
|
51
55
|
def agent
|
52
56
|
@agent ||= begin
|
53
57
|
agent = Agent.find_by(username:)
|
54
|
-
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?
|
55
59
|
|
56
60
|
agent
|
57
61
|
end
|
@@ -60,4 +60,45 @@ PasskeysRails.config do |c|
|
|
60
60
|
# c.subscribe(:did_register) do |event, agent, request|
|
61
61
|
# puts("#{event} | #{agent.id} | #{request.headers}")
|
62
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"
|
63
104
|
end
|
data/lib/passkeys-rails.rb
CHANGED
@@ -1,49 +1,42 @@
|
|
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
|
47
40
|
|
48
41
|
# Convenience method to subscribe to various events in PasskeysRails.
|
49
42
|
#
|
@@ -104,16 +97,6 @@ module PasskeysRails
|
|
104
97
|
message: auth.message)
|
105
98
|
end
|
106
99
|
|
107
|
-
class << self
|
108
|
-
def config
|
109
|
-
yield self
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
100
|
require 'passkeys_rails/railtie' if defined?(Rails)
|
114
101
|
end
|
115
|
-
|
116
|
-
ActiveSupport.on_load(:before_initialize) do
|
117
|
-
PasskeysRails.auth_token_secret ||= Rails.application.secret_key_base
|
118
|
-
end
|
119
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.3.
|
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
|
@@ -380,6 +380,7 @@ files:
|
|
380
380
|
- lib/generators/passkeys_rails/templates/README
|
381
381
|
- lib/generators/passkeys_rails/templates/passkeys_rails_config.rb
|
382
382
|
- lib/passkeys-rails.rb
|
383
|
+
- lib/passkeys_rails/configuration.rb
|
383
384
|
- lib/passkeys_rails/engine.rb
|
384
385
|
- lib/passkeys_rails/railtie.rb
|
385
386
|
- lib/passkeys_rails/test/integration_helpers.rb
|