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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 666859b1227428b580202f3c2f9822cf937db0ecaf26381dddefd45b6d8e9b3e
4
- data.tar.gz: 7cbce30a6f2efb7eb15aa4a747156da1e355c795ca346fdc8af2ecad6af837d6
3
+ metadata.gz: a137b9b4f2ab7aac24fef0747ff112588215d496034f0542983df3790bc32de7
4
+ data.tar.gz: 412185131a984a883a11af7fae1bbbb69806819a978871a08e0929921c766fe4
5
5
  SHA512:
6
- metadata.gz: d0bff5cc099209252fa1853c9e75e8f23edce8de4ac910f2e958e9279ec81a4452074fa6611d42424e4809fc010a129072e7d719e9de8b4e0a3e40e7619ad7f9
7
- data.tar.gz: 98e62cd971c37781aecadc0dbe09b1c32d6aa2e0837f24c816971c660d8d00c19c738a339a43f021a4a27e843b79b04b3128f65f7ff21b971289fa232dd0df75
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
- # PasskeysRails
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
- Devise is awesome, but we don't need all that UI/UX for PassKeys, especially for an API back end.
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 an `Agent` model and related `Passkeys`. If you have a user model, add `include PasskeysRails::Authenticatable` to your model and include the name of that class (e.g. `"User"`) in the `authenticatable_class` param when calling the register API or set the `PasskeysRails.default_class` to the name of that class.
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 that you supply your own model, but it's often useful to do so. For example, if you have a User model that you would like to have created at registration, you can supply the model name in the `finishRegistration` API call.
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 `"user"` models. Whatever model name you supply will be created during a successful the `finishRegiration` API call. When created, it will be provided an opportunity to do any initialization at that time.
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` - see below.
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 "passkeys_rails"
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
- 1. Add `before_action :authenticate_passkey!`
80
+ <a id="rails-Integration-standard"></a>
81
+ ## Rails Integration <p><small>Adding to a standard rails project</small></p>
64
82
 
65
- To prevent access to controller actions, add `before_action :authenticate_passkey!`. If an action is attempted without an authenticated entity, an error will be rendered in JSON with an :unauthorized result code.
83
+ - ### Add `before_action :authenticate_passkey!`
66
84
 
67
- 1. Use `current_agent` and `current_agent.authenticatable`
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
- 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.
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
- 1. Add `include PasskeysRails::Authenticatable` to model class(es)
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
- ### Adding to a Grape API rails project
95
+ <a id="rails-Integration-grape"></a>
96
+ ## Rails Integration - <p><small>Adding to a Grape API rails project</small></p>
76
97
 
77
- 1. Call `PasskeysRails.authenticate(request)` to authenticate the request.
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
- 1. Consider adding the following helpers to your base API class:
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
- 1. Use `current_agent` and `current_agent.authenticatable`
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
- ### Authentication Failure
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
- 1. In the event of authentication failure, PasskeysRails returns an error code and message.
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
- ### Test Helpers
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
- ### Mobile Application Integration
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
- There are n groups of API endpoints that your mobile application may consume.
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 and authentication.
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
- #### POST /passkeys/challenge
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
- #### POST /passkeys/register
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 persiste the passkey
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
- #### POST /passkeys/authenticate
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
- #### POST /passkeys/refresh
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's expired, to get a new token, use the /authentication API.
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
- #### POST /passkeys/debug_login
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 login (authenticate) a username while bypassing the normal challenge/response sequence.
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/authenticate. The response is identicial to that of /passkeys/authenticate.
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/authenticate.
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
- - agent_not_found - No agent found with that username
347
-
348
- ## Reference/Example Mobile Applications
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
- ### Contributing Guidelines
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
- To ensure a smooth collaboration, please follow the guidelines below when submitting your contributions:
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
- 4. Before submitting a pull request, ensure that your changes pass all existing tests and add relevant tests if applicable.
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
- 5. Update the documentation if your changes introduce new features, modify existing behavior, or require user instructions.
430
+ To use this endpoint:
375
431
 
376
- 6. Squash your commits into a single logical commit if needed. Keep your commit history clean and focused.
432
+ 1. Manually create one or more PasskeysRails::Agent records in the database. A unique username is required for each.
377
433
 
378
- 7. Submit a pull request against the `main` branch of the original repository.
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
- 8. Add a comment at the top of the CHANGELOG.md describing the change.
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
- #### Pull Request Guidelines
438
+ 1. Use the response as if it was from /passkeys/authenticate.
383
439
 
384
- When submitting a pull request, please include the following details:
440
+ If you supply a username that doesn't match the DEBUG_LOGIN_REGEX, the endpoint will respond with an error.
385
441
 
386
- - A clear description of the changes you made and the problem it solves.
442
+ Supply the following JSON structure:
387
443
 
388
- - Any relevant issue numbers that your pull request addresses or fixes.
444
+ ```JSON
445
+ # POST body
446
+ {
447
+ "username": String
448
+ }
449
+ ```
450
+ On **success**, the response is an `AuthResponse`.
389
451
 
390
- - The steps to test your changes, so the project maintainers can verify them.
452
+ Possible **failure codes** (using the `ErrorResponse` structure) are:
391
453
 
392
- - Ensure that your pull request title and description are descriptive and informative.
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
- #### Code Review Process
457
+ ## Reference/Example Mobile Applications
395
458
 
396
- All pull requests will undergo a code review process by the project maintainers. We appreciate your patience during this review process. Constructive feedback may be provided, and further changes might be requested.
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
- #### Contributor License Agreement
461
+ Check out the [PasskeysRailsDemo](https://github.com/alliedcode/PasskeysRailsDemo) app.
399
462
 
400
- By submitting a pull request, you acknowledge that your contributions will be licensed under the project's [MIT License](https://github.com/alliedcode/passkeys-rails/blob/main/MIT-LICENSE).
463
+ ## Contributing
401
464
 
402
- #### Reporting Issues
465
+ ### Contribution Guidelines
403
466
 
404
- If you encounter any bugs, problems, or have suggestions for improvement, please create an issue on the GitHub repository. Provide clear and detailed information about the issue to help us address it efficiently.
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
- #### Thank You
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
- Your contributions are valuable, and we sincerely appreciate your efforts to improve PasskeysRails. Together, we can build a better software ecosystem for the community. Thank you for your support and happy contributing!
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
- session[:passkeys_rails] = result.session_data
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: authenticatable_info&.to_h,
15
- username: session.dig(:passkeys_rails, :username),
16
- challenge: session.dig(:passkeys_rails, :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: { username: result.username, auth_token: result.auth_token }
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: session.dig(:passkeys_rails, :challenge))
30
+ challenge: cookie_data["challenge"])
31
+
32
+ broadcast(:did_authenticate, agent: result.agent)
24
33
 
25
- render json: { username: result.username, auth_token: result.auth_token }
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
- render json: { username: result.username, auth_token: result.auth_token }
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
- render json: { username: result.username, auth_token: result.auth_token }
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 authenticatable_info
56
- params.require[:authenticatable].permit(:class, :params) if params[:authenticatable].present?
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.session_data = session_data(options)
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 session_data(options)
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
@@ -8,6 +8,7 @@ module PasskeysRails
8
8
  def call
9
9
  verify_credential!
10
10
 
11
+ context.agent = agent
11
12
  context.username = agent.username
12
13
  context.auth_token = GenerateAuthToken.call!(agent:).auth_token
13
14
  rescue Interactor::Failure => e
@@ -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
- context.fail!(code: :error, message: e.message)
26
- end
27
-
28
- def store_passkey_and_register_agent!
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 create_authenticatable!
81
- authenticatable = aux_class.create! do |obj|
82
- obj.agent = agent if obj.respond_to?(:agent=)
83
- obj.registering_with(authenticatable_params) if obj.respond_to?(:registering_with)
84
- end
85
-
86
- agent.update!(authenticatable:)
87
- rescue ActiveRecord::RecordInvalid => e
88
- context.fail!(code: :record_invalid, message: e.message)
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 session value: \"#{username}\"") if agent.blank?
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
@@ -8,6 +8,7 @@ module PasskeysRails
8
8
  def call
9
9
  agent = ValidateAuthToken.call!(auth_token: token).agent
10
10
 
11
+ context.agent = agent
11
12
  context.username = agent.username
12
13
  context.auth_token = GenerateAuthToken.call!(agent:).auth_token
13
14
  rescue Interactor::Failure => e
@@ -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/challenge'
3
- post 'passkeys/register'
4
- post 'passkeys/authenticate'
5
- post 'passkeys/refresh'
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
- # This route exists to allow easier mobile app debugging as it may not
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/debug_login'
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
@@ -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
- # Secret used to encode the auth token.
12
- # Rails.application.secret_key_base is used if none is defined here.
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
- # Algorithm used to generate the auth token.
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
- # How long the auth token is valid before requiring a refresh or new login.
23
- # Set it to 0 for no expiration (not recommended in production).
24
- mattr_accessor :auth_token_expires_in, default: 30.days
18
+ def config
19
+ @config ||= begin
20
+ config = Configuration.new
21
+ yield(config) if block_given?
22
+ apply_webauthn_configuration(config)
25
23
 
26
- # Model to use when creating or authenticating a passkey.
27
- # This can be overridden when calling the API, but if no
28
- # value is supplied when calling the API, this value is used.
29
- # If nil, there is no default, and if none is supplied when
30
- # calling the API, no resource is created other than
31
- # a PaskeysRails::Agent that is used to track the passkey.
32
- #
33
- # This library doesn't assume that there will only be one
34
- # model, but it is a common use case, so setting the
35
- # default_class simplifies the use of the API in that case.
36
- mattr_accessor :default_class, default: "User"
24
+ config
25
+ end
26
+ end
27
+ end
37
28
 
38
- # By providing a class_whitelist, the API will require that
39
- # any supplied class is in the whitelist. If it is not, the
40
- # auth API will return an error. This prevents a caller from
41
- # attempting to create an unintended record on registration.
42
- # If nil, any model will be allowed.
43
- # If [], no model will be allowed.
44
- # This should be an array of symbols or strings,
45
- # for example: %w[User AdminUser]
46
- mattr_accessor :class_whitelist, default: nil
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
@@ -1,3 +1,3 @@
1
1
  module PasskeysRails
2
- VERSION = "0.2.1".freeze
2
+ VERSION = "0.3.1".freeze
3
3
  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.2.1
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-07-29 00:00:00.000000000 Z
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