passkeys-rails 0.3.0 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Gem Version](https://badge.fury.io/rb/passkeys-rails.svg?cachebust=0.2.1)](https://badge.fury.io/rb/passkeys-rails)
|
2
4
|
[![Build Status](https://app.travis-ci.com/alliedcode/passkeys-rails.svg?branch=main)](https://travis-ci.org/alliedcode/passkeys-rails)
|
3
5
|
[![codecov](https://codecov.io/gh/alliedcode/passkeys-rails/branch/main/graph/badge.svg?token=UHSNJDUL21)](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
|