passkeys-rails 0.2.1 → 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 +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
|
[![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:
|
@@ -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
|