rails_authentication 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +63 -8
  3. data/lib/generators/authentication/authentication_generator.rb +15 -0
  4. data/lib/generators/authentication/features/confirmable.rb +13 -12
  5. data/lib/generators/authentication/features/invitable.rb +14 -13
  6. data/lib/generators/authentication/features/lockable.rb +13 -12
  7. data/lib/generators/authentication/features/magic_link.rb +13 -12
  8. data/lib/generators/authentication/features/ott.rb +13 -12
  9. data/lib/generators/authentication/features/passkey.rb +36 -0
  10. data/lib/generators/authentication/features/recoverable.rb +13 -12
  11. data/lib/generators/authentication/features/registerable.rb +7 -6
  12. data/lib/generators/authentication/features/rememberable.rb +6 -5
  13. data/lib/generators/authentication/features/timeoutable.rb +7 -6
  14. data/lib/generators/authentication/features/trackable.rb +7 -6
  15. data/lib/generators/authentication/features/validatable.rb +5 -4
  16. data/lib/generators/authentication/templates/app/controllers/passkey_sessions_controller.rb.tt +68 -0
  17. data/lib/generators/authentication/templates/app/controllers/sessions_controller.rb.tt +4 -0
  18. data/lib/generators/authentication/templates/app/controllers/webauthn_credentials_controller.rb.tt +43 -0
  19. data/lib/generators/authentication/templates/app/models/concerns/confirmable_concern.rb.tt +18 -17
  20. data/lib/generators/authentication/templates/app/models/concerns/passkey_concern.rb.tt +19 -0
  21. data/lib/generators/authentication/templates/app/models/webauthn_credential.rb.tt +6 -0
  22. data/lib/generators/authentication/templates/app/views/registrations/edit.html.erb.tt +4 -0
  23. data/lib/generators/authentication/templates/app/views/sessions/new.html.erb.tt +65 -0
  24. data/lib/generators/authentication/templates/app/views/webauthn_credentials/index.html.erb.tt +19 -0
  25. data/lib/generators/authentication/templates/app/views/webauthn_credentials/new.html.erb.tt +71 -0
  26. data/lib/generators/authentication/templates/config/initializers/webauthn.rb.tt +11 -0
  27. data/lib/generators/authentication/templates/db/migrate/add_webauthn_id_to_users.rb.tt +7 -0
  28. data/lib/generators/authentication/templates/db/migrate/create_webauthn_credentials.rb.tt +15 -0
  29. data/lib/rails_authentication/version.rb +1 -1
  30. metadata +11 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f5c0f93e4dc6c9eb2562c7804920c82cff3a5a7d23d15758aaee3d63cdb60d99
4
- data.tar.gz: 60ee05fafe8ffd4d9f56c10aa57c9ea5d347b801e94d3651ba2b04b258545a13
3
+ metadata.gz: 6369064c5074eb057abc1ed169b7efbe7ae0758730dc6c764b86fee19f96b0d3
4
+ data.tar.gz: 5bdc5cb01eb60fed2449f0a1544d29102c79f917faf345f86a1a86bca6b6adc6
5
5
  SHA512:
6
- metadata.gz: bfd4e4526f964ef0ec6f0004b85061b64b86e6885c4e383adae282396b769f50e300de8e5424bd9892b59d5bae499d428be4719c6d8cdab0ec07fd76b64f5f3a
7
- data.tar.gz: d4c2cb6365fd97356977944566aa332772c751061fcf8b1ad4b9e57b6d4eb7f0cd65ee497493d79286b1efb8bf85b897933e635b914e38a8994e419af61c1961
6
+ metadata.gz: da49badfb4b8c58c2525329479154961cb10cbd6b6d683a4dbc56171198e9ad1d1f31d097455f106d47d19332a14391ee0e4370394aa23535e85b47be6ebf0f8
7
+ data.tar.gz: 4407fbc0f0c7daafc4324fb1e0ba7c366c4c810fa81b579bf464fd2f2e31b7d5e3da6c6e8741b189e0399a28660b4e0a728d80b1bfec63282eee3f8c0d204756
data/README.md CHANGED
@@ -19,6 +19,7 @@ gem extends that same command to also install:
19
19
  | **Invitable** | Invite users by email; blocks sign-in until they accept by choosing their own password |
20
20
  | **MagicLink** (opt-in) | Passwordless magic link sign-in via email — DB-backed, single-use, expiring tokens |
21
21
  | **Ott** (opt-in) | Passwordless sign-in via an emailed 6-digit one-time code — replaces the password sign-in form |
22
+ | **Passkey** (opt-in) | WebAuthn passkey sign-in alongside the password form — usernameless, no email required |
22
23
 
23
24
  Everything is **generated into your app** as plain, readable code — controllers, views, mailers,
24
25
  migrations, and one model concern per feature. There is no runtime dependency on this gem: after
@@ -57,12 +58,13 @@ Available flags: `--skip-confirmable`, `--skip-recoverable`, `--skip-registerabl
57
58
  `--skip-lockable`, `--skip-invitable`, and `--reconfirmable` (Confirmable: postpone email address
58
59
  changes until the new address is confirmed, via an `unconfirmed_email` column).
59
60
 
60
- MagicLink and Ott are **opt-in** features — they change the sign-in UX, so you have to ask for
61
- them:
61
+ MagicLink, Ott, and Passkey are **opt-in** features — they change the sign-in UX, so you have to
62
+ ask for them:
62
63
 
63
64
  ```sh
64
65
  bin/rails generate authentication --magic-link
65
66
  bin/rails generate authentication --ott
67
+ bin/rails generate authentication --passkey
66
68
  ```
67
69
 
68
70
  MagicLink adds a "Sign in with magic link" link to the sign-in page, leading to an email-only
@@ -79,8 +81,16 @@ and are voided after 5 wrong entries (`OttConcern::OTT_MAX_ATTEMPTS`). The passw
79
81
  (SessionsController#create, Recoverable, Registerable) stays generated and functional — only the
80
82
  sign-in UI changes.
81
83
 
82
- Both flows honor the other enabled features: locked, unconfirmed, or invitation-pending accounts
83
- still can't sign in, and Trackable records the attempt.
84
+ Passkey adds a "Sign in with a passkey" button to the sign-in page (no email field — the
85
+ browser's own discoverable-credential picker takes its place) plus an authenticated
86
+ `/webauthn_credentials` section where a signed-in user can register and remove passkeys (linked
87
+ from the account page when Registerable is enabled). Unlike every other feature here, this one
88
+ brings a real runtime dependency: the generator adds `gem "webauthn"` to your Gemfile for you (with
89
+ a reminder to run `bundle install`), since ceremony verification is delegated to it. A user can
90
+ register any number of passkeys, each stored as its own row in `webauthn_credentials`.
91
+
92
+ All three flows honor the other enabled features: locked, unconfirmed, or invitation-pending
93
+ accounts still can't sign in, and Trackable records the attempt.
84
94
 
85
95
  Each feature adds a single `include <Feature>Concern` line to `app/models/user.rb`; all of its
86
96
  model behavior lives in `app/models/concerns/<feature>_concern.rb`. Tunables are plain constants in
@@ -98,6 +108,8 @@ resources :unlocks, only: %i[ new create show ], param: :token
98
108
  resources :invitations, only: %i[ new create edit update ], param: :token
99
109
  resources :magic_links, only: %i[ new create show ], param: :token # with --magic-link
100
110
  resource :ott, only: %i[ create edit update ] # with --ott
111
+ resources :webauthn_credentials, only: %i[ index new create destroy ] # with --passkey
112
+ resource :passkey_session, only: %i[ create ] # with --passkey
101
113
  resource :session # from the base generator
102
114
  resources :passwords, param: :token # from the base generator
103
115
  ```
@@ -114,10 +126,6 @@ resources :passwords, param: :token # from the base generator
114
126
  sessions view with versions tailored to the features you selected. Run the generator once, up
115
127
  front — re-running it after you've customized those files will prompt to overwrite them.
116
128
 
117
- ## Future Plans
118
-
119
- - Passkey
120
-
121
129
  ## Development
122
130
 
123
131
  ```sh
@@ -131,6 +139,53 @@ bundle exec rspec spec/generators/lockable_spec.rb:12 # a single example
131
139
  `spec/dummy` is generated (and gitignored), never hand-maintained, so the request specs always
132
140
  exercise exactly what the templates produce.
133
141
 
142
+ To run the app using the dummy app generated in `spec/dummy`, run `bin/dev`
143
+
144
+ ## Appraisal
145
+
146
+ Install Appraisal dependencies or when a new Appraisal is added, generate the .gemfile
147
+
148
+ ```sh
149
+ bundle exec appraisal generate-install
150
+ ```
151
+
152
+ Run tests with Appraisal
153
+
154
+ ```sh
155
+ bin/appraisal
156
+ ```
157
+
158
+ Run a specific Appraisal test
159
+
160
+ ```sh
161
+ # rails-8.0-json-2
162
+ bundle exec appraisal rails-8.0-json-2 rake spec:generators
163
+ bundle exec appraisal rails-8.0-json-2 rake dummy:prepare
164
+ bundle exec appraisal rails-8.0-json-2 rake spec:requests
165
+ # rails-8.0-json-3
166
+ bundle exec appraisal rails-8.0-json-3 rake spec:generators
167
+ bundle exec appraisal rails-8.0-json-3 rake dummy:prepare
168
+ bundle exec appraisal rails-8.0-json-3 rake spec:requests
169
+ # rails-8.1-json-2
170
+ bundle exec appraisal rails-8.1-json-2 rake spec:generators
171
+ bundle exec appraisal rails-8.1-json-2 rake dummy:prepare
172
+ bundle exec appraisal rails-8.1-json-2 rake spec:requests
173
+ # rails-8.1-json-3
174
+ bundle exec appraisal rails-8.1-json-3 rake spec:generators
175
+ bundle exec appraisal rails-8.1-json-3 rake dummy:prepare
176
+ bundle exec appraisal rails-8.1-json-3 rake spec:requests
177
+ ```
178
+
179
+ ## json 3.0 gem compatibility
180
+
181
+ `json` 3.0 gem removed the `quirks_mode:` keyword and made `JSON.generate`/`JSON.parse`'s second argument keyword-only. Rails' `ActiveSupport::JSON` still calls both with `quirks_mode: true` positionally, so every
182
+ cookie/session read or write throws `ArgumentError` under json 3. `bin/prepare_dummy`
183
+ writes `spec/dummy/config/initializers/json_quirks_mode_compat.rb`, which prepends a shim onto
184
+ `JSON.generate`/`.parse` that strips `quirks_mode` and re-passes remaining options via `**opts`
185
+ (so it works whether the resolved `JSON.parse` still takes a positional options hash, json 2.x,
186
+ or keywords only, json 3.x). It no-ops under json 2.x (`Gem::Version.new(::JSON::VERSION) >=
187
+ Gem::Version.new("3.0")` gates it) and is safe to delete once Rails or json fixes this upstream.
188
+
134
189
  ## License
135
190
 
136
191
  MIT
@@ -13,6 +13,7 @@ require_relative "features/lockable"
13
13
  require_relative "features/invitable"
14
14
  require_relative "features/magic_link"
15
15
  require_relative "features/ott"
16
+ require_relative "features/passkey"
16
17
 
17
18
  module RailsAuthentication
18
19
  module Generators
@@ -46,6 +47,7 @@ module RailsAuthentication
46
47
  include Features::Invitable
47
48
  include Features::MagicLink
48
49
  include Features::Ott
50
+ include Features::Passkey
49
51
 
50
52
  source_root File.expand_path("templates", __dir__)
51
53
 
@@ -68,6 +70,11 @@ module RailsAuthentication
68
70
  class_option :ott, type: :boolean, default: false,
69
71
  desc: "Add one-time token (emailed 6-digit code) sign-in (opt-in)"
70
72
 
73
+ # Also opt-in: WebAuthn passkey sign-in alongside password sign-in.
74
+ # Requires the host app to add the `webauthn` gem itself.
75
+ class_option :passkey, type: :boolean, default: false,
76
+ desc: "Add WebAuthn passkey sign-in (opt-in; requires the webauthn gem)"
77
+
71
78
  def install_base_authentication
72
79
  say "Running Rails' built-in authentication generator", :green
73
80
  Rails::Generators.invoke("rails:authentication", [], behavior: behavior, destination_root: destination_root)
@@ -117,6 +124,10 @@ module RailsAuthentication
117
124
  generate_ott if ott?
118
125
  end
119
126
 
127
+ def install_passkey
128
+ generate_passkey if passkey?
129
+ end
130
+
120
131
  # Runs after every feature install so the blank line separates the concern
121
132
  # includes (if any) from the rest of the class body, no matter which features
122
133
  # are enabled.
@@ -151,6 +162,10 @@ module RailsAuthentication
151
162
  options[:ott]
152
163
  end
153
164
 
165
+ def passkey?
166
+ options[:passkey]
167
+ end
168
+
154
169
  def include_concern_in_user(concern)
155
170
  inject_into_class "app/models/user.rb", "User", " include #{concern}\n"
156
171
  end
@@ -5,20 +5,21 @@ module RailsAuthentication
5
5
  module Features
6
6
  module Confirmable
7
7
  private
8
- def generate_confirmable
9
- template "app/models/concerns/confirmable_concern.rb"
10
- include_concern_in_user "ConfirmableConcern"
11
- migration_template "db/migrate/add_confirmable_to_users.rb", "db/migrate/add_confirmable_to_users.rb"
12
- template "app/controllers/confirmations_controller.rb"
13
- template "app/views/confirmations/new.html.erb"
14
- route "resources :confirmations, only: %i[ new create show ], param: :token"
15
8
 
16
- if defined?(ActionMailer::Railtie)
17
- template "app/mailers/confirmations_mailer.rb"
18
- template "app/views/confirmations_mailer/confirmation_instructions.html.erb"
19
- template "app/views/confirmations_mailer/confirmation_instructions.text.erb"
20
- end
9
+ def generate_confirmable
10
+ template "app/models/concerns/confirmable_concern.rb"
11
+ include_concern_in_user "ConfirmableConcern"
12
+ migration_template "db/migrate/add_confirmable_to_users.rb", "db/migrate/add_confirmable_to_users.rb"
13
+ template "app/controllers/confirmations_controller.rb"
14
+ template "app/views/confirmations/new.html.erb"
15
+ route "resources :confirmations, only: %i[ new create show ], param: :token"
16
+
17
+ if defined?(ActionMailer::Railtie)
18
+ template "app/mailers/confirmations_mailer.rb"
19
+ template "app/views/confirmations_mailer/confirmation_instructions.html.erb"
20
+ template "app/views/confirmations_mailer/confirmation_instructions.text.erb"
21
21
  end
22
+ end
22
23
  end
23
24
  end
24
25
  end
@@ -5,21 +5,22 @@ module RailsAuthentication
5
5
  module Features
6
6
  module Invitable
7
7
  private
8
- def generate_invitable
9
- template "app/models/concerns/invitable_concern.rb"
10
- include_concern_in_user "InvitableConcern"
11
- migration_template "db/migrate/add_invitable_to_users.rb", "db/migrate/add_invitable_to_users.rb"
12
- template "app/controllers/invitations_controller.rb"
13
- template "app/views/invitations/new.html.erb"
14
- template "app/views/invitations/edit.html.erb"
15
- route "resources :invitations, only: %i[ new create edit update ], param: :token"
16
8
 
17
- if defined?(ActionMailer::Railtie)
18
- template "app/mailers/invitations_mailer.rb"
19
- template "app/views/invitations_mailer/invitation_instructions.html.erb"
20
- template "app/views/invitations_mailer/invitation_instructions.text.erb"
21
- end
9
+ def generate_invitable
10
+ template "app/models/concerns/invitable_concern.rb"
11
+ include_concern_in_user "InvitableConcern"
12
+ migration_template "db/migrate/add_invitable_to_users.rb", "db/migrate/add_invitable_to_users.rb"
13
+ template "app/controllers/invitations_controller.rb"
14
+ template "app/views/invitations/new.html.erb"
15
+ template "app/views/invitations/edit.html.erb"
16
+ route "resources :invitations, only: %i[ new create edit update ], param: :token"
17
+
18
+ if defined?(ActionMailer::Railtie)
19
+ template "app/mailers/invitations_mailer.rb"
20
+ template "app/views/invitations_mailer/invitation_instructions.html.erb"
21
+ template "app/views/invitations_mailer/invitation_instructions.text.erb"
22
22
  end
23
+ end
23
24
  end
24
25
  end
25
26
  end
@@ -5,20 +5,21 @@ module RailsAuthentication
5
5
  module Features
6
6
  module Lockable
7
7
  private
8
- def generate_lockable
9
- template "app/models/concerns/lockable_concern.rb"
10
- include_concern_in_user "LockableConcern"
11
- migration_template "db/migrate/add_lockable_to_users.rb", "db/migrate/add_lockable_to_users.rb"
12
- template "app/controllers/unlocks_controller.rb"
13
- template "app/views/unlocks/new.html.erb"
14
- route "resources :unlocks, only: %i[ new create show ], param: :token"
15
8
 
16
- if defined?(ActionMailer::Railtie)
17
- template "app/mailers/unlocks_mailer.rb"
18
- template "app/views/unlocks_mailer/unlock_instructions.html.erb"
19
- template "app/views/unlocks_mailer/unlock_instructions.text.erb"
20
- end
9
+ def generate_lockable
10
+ template "app/models/concerns/lockable_concern.rb"
11
+ include_concern_in_user "LockableConcern"
12
+ migration_template "db/migrate/add_lockable_to_users.rb", "db/migrate/add_lockable_to_users.rb"
13
+ template "app/controllers/unlocks_controller.rb"
14
+ template "app/views/unlocks/new.html.erb"
15
+ route "resources :unlocks, only: %i[ new create show ], param: :token"
16
+
17
+ if defined?(ActionMailer::Railtie)
18
+ template "app/mailers/unlocks_mailer.rb"
19
+ template "app/views/unlocks_mailer/unlock_instructions.html.erb"
20
+ template "app/views/unlocks_mailer/unlock_instructions.text.erb"
21
21
  end
22
+ end
22
23
  end
23
24
  end
24
25
  end
@@ -8,20 +8,21 @@ module RailsAuthentication
8
8
  # Recoverable rework.
9
9
  module MagicLink
10
10
  private
11
- def generate_magic_link
12
- template "app/models/concerns/magic_link_concern.rb"
13
- include_concern_in_user "MagicLinkConcern"
14
- migration_template "db/migrate/add_magic_link_to_users.rb", "db/migrate/add_magic_link_to_users.rb"
15
- template "app/controllers/magic_links_controller.rb"
16
- template "app/views/magic_links/new.html.erb"
17
- route "resources :magic_links, only: %i[ new create show ], param: :token"
18
11
 
19
- if defined?(ActionMailer::Railtie)
20
- template "app/mailers/magic_links_mailer.rb"
21
- template "app/views/magic_links_mailer/magic_link.html.erb"
22
- template "app/views/magic_links_mailer/magic_link.text.erb"
23
- end
12
+ def generate_magic_link
13
+ template "app/models/concerns/magic_link_concern.rb"
14
+ include_concern_in_user "MagicLinkConcern"
15
+ migration_template "db/migrate/add_magic_link_to_users.rb", "db/migrate/add_magic_link_to_users.rb"
16
+ template "app/controllers/magic_links_controller.rb"
17
+ template "app/views/magic_links/new.html.erb"
18
+ route "resources :magic_links, only: %i[ new create show ], param: :token"
19
+
20
+ if defined?(ActionMailer::Railtie)
21
+ template "app/mailers/magic_links_mailer.rb"
22
+ template "app/views/magic_links_mailer/magic_link.html.erb"
23
+ template "app/views/magic_links_mailer/magic_link.text.erb"
24
24
  end
25
+ end
25
26
  end
26
27
  end
27
28
  end
@@ -10,20 +10,21 @@ module RailsAuthentication
10
10
  # Registerable, SessionsController#create) is left intact.
11
11
  module Ott
12
12
  private
13
- def generate_ott
14
- template "app/models/concerns/ott_concern.rb"
15
- include_concern_in_user "OttConcern"
16
- migration_template "db/migrate/add_ott_to_users.rb", "db/migrate/add_ott_to_users.rb"
17
- template "app/controllers/otts_controller.rb"
18
- template "app/views/otts/edit.html.erb"
19
- route "resource :ott, only: %i[ create edit update ]"
20
13
 
21
- if defined?(ActionMailer::Railtie)
22
- template "app/mailers/otts_mailer.rb"
23
- template "app/views/otts_mailer/ott.html.erb"
24
- template "app/views/otts_mailer/ott.text.erb"
25
- end
14
+ def generate_ott
15
+ template "app/models/concerns/ott_concern.rb"
16
+ include_concern_in_user "OttConcern"
17
+ migration_template "db/migrate/add_ott_to_users.rb", "db/migrate/add_ott_to_users.rb"
18
+ template "app/controllers/otts_controller.rb"
19
+ template "app/views/otts/edit.html.erb"
20
+ route "resource :ott, only: %i[ create edit update ]"
21
+
22
+ if defined?(ActionMailer::Railtie)
23
+ template "app/mailers/otts_mailer.rb"
24
+ template "app/views/otts_mailer/ott.html.erb"
25
+ template "app/views/otts_mailer/ott.text.erb"
26
26
  end
27
+ end
27
28
  end
28
29
  end
29
30
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAuthentication
4
+ module Generators
5
+ module Features
6
+ # Opt-in (--passkey): WebAuthn-based passkey sign-in, in addition to
7
+ # password sign-in. A user may register any number of passkeys
8
+ # (platform authenticators, security keys); sign-in is usernameless —
9
+ # the browser's discoverable-credential picker stands in for an email
10
+ # field. Ceremony verification is delegated to the `webauthn` gem —
11
+ # unlike everything else this gem generates, that's a real runtime
12
+ # dependency, so it's added straight to the host app's Gemfile.
13
+ module Passkey
14
+ private
15
+
16
+ def generate_passkey
17
+ gem "webauthn", comment: "Verifies WebAuthn passkey registration/authentication ceremonies"
18
+ say "Added the `webauthn` gem to your Gemfile — run `bundle install`.", :yellow
19
+
20
+ template "config/initializers/webauthn.rb"
21
+ template "app/models/concerns/passkey_concern.rb"
22
+ include_concern_in_user "PasskeyConcern"
23
+ template "app/models/webauthn_credential.rb"
24
+ migration_template "db/migrate/add_webauthn_id_to_users.rb", "db/migrate/add_webauthn_id_to_users.rb"
25
+ migration_template "db/migrate/create_webauthn_credentials.rb", "db/migrate/create_webauthn_credentials.rb"
26
+ template "app/controllers/webauthn_credentials_controller.rb"
27
+ template "app/controllers/passkey_sessions_controller.rb"
28
+ template "app/views/webauthn_credentials/index.html.erb"
29
+ template "app/views/webauthn_credentials/new.html.erb"
30
+ route "resources :webauthn_credentials, only: %i[ index new create destroy ]"
31
+ route "resource :passkey_session, only: %i[ create ], controller: \"passkey_sessions\""
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -5,20 +5,21 @@ module RailsAuthentication
5
5
  module Features
6
6
  module Recoverable
7
7
  private
8
- # Replaces the base generator's stateless generates_token_for password reset
9
- # with DB-backed, revocable tokens.
10
- def generate_recoverable
11
- template "app/models/concerns/recoverable_concern.rb"
12
- include_concern_in_user "RecoverableConcern"
13
- migration_template "db/migrate/add_recoverable_to_users.rb", "db/migrate/add_recoverable_to_users.rb"
14
- template "app/controllers/passwords_controller.rb", force: true
15
8
 
16
- if defined?(ActionMailer::Railtie)
17
- template "app/mailers/passwords_mailer.rb", force: true
18
- template "app/views/passwords_mailer/reset.html.erb", force: true
19
- template "app/views/passwords_mailer/reset.text.erb", force: true
20
- end
9
+ # Replaces the base generator's stateless generates_token_for password reset
10
+ # with DB-backed, revocable tokens.
11
+ def generate_recoverable
12
+ template "app/models/concerns/recoverable_concern.rb"
13
+ include_concern_in_user "RecoverableConcern"
14
+ migration_template "db/migrate/add_recoverable_to_users.rb", "db/migrate/add_recoverable_to_users.rb"
15
+ template "app/controllers/passwords_controller.rb", force: true
16
+
17
+ if defined?(ActionMailer::Railtie)
18
+ template "app/mailers/passwords_mailer.rb", force: true
19
+ template "app/views/passwords_mailer/reset.html.erb", force: true
20
+ template "app/views/passwords_mailer/reset.text.erb", force: true
21
21
  end
22
+ end
22
23
  end
23
24
  end
24
25
  end
@@ -5,12 +5,13 @@ module RailsAuthentication
5
5
  module Features
6
6
  module Registerable
7
7
  private
8
- def generate_registerable
9
- template "app/controllers/registrations_controller.rb"
10
- template "app/views/registrations/new.html.erb"
11
- template "app/views/registrations/edit.html.erb"
12
- route "resource :registration, only: %i[ new create edit update destroy ]"
13
- end
8
+
9
+ def generate_registerable
10
+ template "app/controllers/registrations_controller.rb"
11
+ template "app/views/registrations/new.html.erb"
12
+ template "app/views/registrations/edit.html.erb"
13
+ route "resource :registration, only: %i[ new create edit update destroy ]"
14
+ end
14
15
  end
15
16
  end
16
17
  end
@@ -5,11 +5,12 @@ module RailsAuthentication
5
5
  module Features
6
6
  module Rememberable
7
7
  private
8
- def generate_rememberable
9
- template "app/models/concerns/rememberable_concern.rb"
10
- include_concern_in_user "RememberableConcern"
11
- migration_template "db/migrate/add_rememberable_to_users.rb", "db/migrate/add_rememberable_to_users.rb"
12
- end
8
+
9
+ def generate_rememberable
10
+ template "app/models/concerns/rememberable_concern.rb"
11
+ include_concern_in_user "RememberableConcern"
12
+ migration_template "db/migrate/add_rememberable_to_users.rb", "db/migrate/add_rememberable_to_users.rb"
13
+ end
13
14
  end
14
15
  end
15
16
  end
@@ -5,12 +5,13 @@ module RailsAuthentication
5
5
  module Features
6
6
  module Timeoutable
7
7
  private
8
- # No migration: inactivity is measured against sessions.updated_at, which the
9
- # base generator's sessions table already has.
10
- def generate_timeoutable
11
- template "app/models/concerns/timeoutable_concern.rb"
12
- include_concern_in_user "TimeoutableConcern"
13
- end
8
+
9
+ # No migration: inactivity is measured against sessions.updated_at, which the
10
+ # base generator's sessions table already has.
11
+ def generate_timeoutable
12
+ template "app/models/concerns/timeoutable_concern.rb"
13
+ include_concern_in_user "TimeoutableConcern"
14
+ end
14
15
  end
15
16
  end
16
17
  end
@@ -5,12 +5,13 @@ module RailsAuthentication
5
5
  module Features
6
6
  module Trackable
7
7
  private
8
- def generate_trackable
9
- template "app/models/concerns/trackable_concern.rb"
10
- include_concern_in_user "TrackableConcern"
11
- template "app/models/user_auth.rb"
12
- migration_template "db/migrate/create_user_auths.rb", "db/migrate/create_user_auths.rb"
13
- end
8
+
9
+ def generate_trackable
10
+ template "app/models/concerns/trackable_concern.rb"
11
+ include_concern_in_user "TrackableConcern"
12
+ template "app/models/user_auth.rb"
13
+ migration_template "db/migrate/create_user_auths.rb", "db/migrate/create_user_auths.rb"
14
+ end
14
15
  end
15
16
  end
16
17
  end
@@ -5,10 +5,11 @@ module RailsAuthentication
5
5
  module Features
6
6
  module Validatable
7
7
  private
8
- def generate_validatable
9
- template "app/models/concerns/validatable_concern.rb"
10
- include_concern_in_user "ValidatableConcern"
11
- end
8
+
9
+ def generate_validatable
10
+ template "app/models/concerns/validatable_concern.rb"
11
+ include_concern_in_user "ValidatableConcern"
12
+ end
12
13
  end
13
14
  end
14
15
  end
@@ -0,0 +1,68 @@
1
+ class PasskeySessionsController < ApplicationController
2
+ allow_unauthenticated_access
3
+ rate_limit to: 10, within: 3.minutes, only: :create, with: -> { render json: { error: "Try again later." }, status: :too_many_requests }
4
+
5
+ def create
6
+ webauthn_credential = WebAuthn::Credential.from_get(params)
7
+ stored_credential = WebauthnCredential.find_by(external_id: webauthn_credential.id)
8
+
9
+ if stored_credential.nil?
10
+ return render json: { error: "Passkey not recognized." }, status: :unprocessable_content
11
+ end
12
+
13
+ begin
14
+ webauthn_credential.verify(
15
+ session[:webauthn_authentication_challenge],
16
+ public_key: stored_credential.public_key,
17
+ sign_count: stored_credential.sign_count
18
+ )
19
+ rescue WebAuthn::Error
20
+ return render json: { error: "Passkey verification failed." }, status: :unprocessable_content
21
+ end
22
+
23
+ stored_credential.update!(sign_count: webauthn_credential.sign_count)
24
+ user = stored_credential.user
25
+ session.delete(:webauthn_authentication_challenge)
26
+
27
+ <% if lockable? -%>
28
+ if user.locked?
29
+ <% if trackable? -%>
30
+ record_authentication_attempt(user, success: false, failure_reason: "locked")
31
+ <% end -%>
32
+ return render json: { error: "Your account is locked. Check your email for unlock instructions." }, status: :unprocessable_content
33
+ end
34
+ <% end -%>
35
+ <% if confirmable? -%>
36
+ unless user.confirmed?
37
+ <% if trackable? -%>
38
+ record_authentication_attempt(user, success: false, failure_reason: "unconfirmed")
39
+ <% end -%>
40
+ return render json: { error: "You must confirm your email address before signing in." }, status: :unprocessable_content
41
+ end
42
+ <% end -%>
43
+ <% if invitable? -%>
44
+ if user.invitation_pending?
45
+ <% if trackable? -%>
46
+ record_authentication_attempt(user, success: false, failure_reason: "invitation_pending")
47
+ <% end -%>
48
+ return render json: { error: "You must accept your invitation before signing in." }, status: :unprocessable_content
49
+ end
50
+ <% end -%>
51
+ <% if lockable? -%>
52
+ user.reset_failed_attempts!
53
+ <% end -%>
54
+ <% if trackable? -%>
55
+ record_authentication_attempt(user, success: true)
56
+ <% end -%>
57
+ start_new_session_for user
58
+ render json: { redirect_to: after_authentication_url }
59
+ end
60
+ <% if trackable? -%>
61
+
62
+ private
63
+
64
+ def record_authentication_attempt(user, success:, failure_reason: nil)
65
+ UserAuth.record(user, request, success: success, failure_reason: failure_reason)
66
+ end
67
+ <% end -%>
68
+ end
@@ -3,6 +3,10 @@ class SessionsController < ApplicationController
3
3
  rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." }
4
4
 
5
5
  def new
6
+ <% if passkey? -%>
7
+ @webauthn_credential_options = WebAuthn::Credential.options_for_get(user_verification: "required")
8
+ session[:webauthn_authentication_challenge] = @webauthn_credential_options.challenge
9
+ <% end -%>
6
10
  end
7
11
 
8
12
  def create
@@ -0,0 +1,43 @@
1
+ class WebauthnCredentialsController < ApplicationController
2
+ before_action :set_webauthn_credential, only: :destroy
3
+
4
+ def index
5
+ @webauthn_credentials = current_user.webauthn_credentials.order(:created_at)
6
+ end
7
+
8
+ def new
9
+ @webauthn_credential_options = WebAuthn::Credential.options_for_create(
10
+ user: { id: current_user.webauthn_id, name: current_user.email_address },
11
+ exclude: current_user.webauthn_credentials.pluck(:external_id)
12
+ )
13
+ session[:webauthn_registration_challenge] = @webauthn_credential_options.challenge
14
+ end
15
+
16
+ def create
17
+ webauthn_credential = WebAuthn::Credential.from_create(params)
18
+ webauthn_credential.verify(session[:webauthn_registration_challenge])
19
+
20
+ current_user.webauthn_credentials.create!(
21
+ external_id: webauthn_credential.id,
22
+ public_key: webauthn_credential.public_key,
23
+ sign_count: webauthn_credential.sign_count,
24
+ nickname: params[:nickname].presence || "Passkey"
25
+ )
26
+ session.delete(:webauthn_registration_challenge)
27
+
28
+ render json: { redirect_to: webauthn_credentials_path }
29
+ rescue WebAuthn::Error => e
30
+ render json: { error: e.message }, status: :unprocessable_content
31
+ end
32
+
33
+ def destroy
34
+ @webauthn_credential.destroy
35
+ redirect_to webauthn_credentials_path, notice: "Passkey removed."
36
+ end
37
+
38
+ private
39
+
40
+ def set_webauthn_credential
41
+ @webauthn_credential = current_user.webauthn_credentials.find(params[:id])
42
+ end
43
+ end
@@ -47,28 +47,29 @@ module ConfirmableConcern
47
47
  def generate_confirmation_token!
48
48
  update!(confirmation_token: generate_confirmable_token, confirmation_sent_at: Time.current)
49
49
  end
50
+
51
+ protected
52
+
53
+ def generate_confirmable_token
54
+ SecureRandom.urlsafe_base64(32)
55
+ end
50
56
  <% if reconfirmable? -%>
51
57
 
52
58
  private
53
- def postponing_email_address_change?
54
- !@confirming && email_address_changed? && confirmed?
55
- end
56
-
57
- def postpone_email_address_change
58
- self.unconfirmed_email = email_address
59
- self.email_address = email_address_in_database
60
- self.confirmation_token = generate_confirmable_token
61
- self.confirmation_sent_at = Time.current
62
- end
63
59
 
64
- def deliver_reconfirmation_instructions
65
- ConfirmationsMailer.confirmation_instructions(self).deliver_later
66
- end
67
- <% end -%>
60
+ def postponing_email_address_change?
61
+ !@confirming && email_address_changed? && confirmed?
62
+ end
68
63
 
69
- protected
64
+ def postpone_email_address_change
65
+ self.unconfirmed_email = email_address
66
+ self.email_address = email_address_in_database
67
+ self.confirmation_token = generate_confirmable_token
68
+ self.confirmation_sent_at = Time.current
69
+ end
70
70
 
71
- def generate_confirmable_token
72
- SecureRandom.urlsafe_base64(32)
71
+ def deliver_reconfirmation_instructions
72
+ ConfirmationsMailer.confirmation_instructions(self).deliver_later
73
73
  end
74
+ <% end -%>
74
75
  end
@@ -0,0 +1,19 @@
1
+ module PasskeyConcern
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ has_many :webauthn_credentials, dependent: :destroy
6
+
7
+ before_validation :ensure_webauthn_id, on: :create
8
+ end
9
+
10
+ def passkeys?
11
+ webauthn_credentials.exists?
12
+ end
13
+
14
+ private
15
+
16
+ def ensure_webauthn_id
17
+ self.webauthn_id ||= WebAuthn.generate_user_id
18
+ end
19
+ end
@@ -0,0 +1,6 @@
1
+ class WebauthnCredential < ApplicationRecord
2
+ belongs_to :user
3
+
4
+ validates :external_id, presence: true, uniqueness: true
5
+ validates :public_key, presence: true
6
+ end
@@ -19,4 +19,8 @@
19
19
  <%% end %>
20
20
  <br>
21
21
 
22
+ <% if passkey? -%>
23
+ <%%= link_to "Manage your passkeys", webauthn_credentials_path %>
24
+ <br><br>
25
+ <% end -%>
22
26
  <%%= button_to "Delete my account", registration_path, method: :delete, data: { turbo_confirm: "Are you sure?" } %>
@@ -30,6 +30,71 @@
30
30
  <% if magic_link? -%>
31
31
  <br><%%= link_to "Sign in with magic link", new_magic_link_path %>
32
32
  <% end -%>
33
+ <% if passkey? -%>
34
+ <br><%%= button_tag "Sign in with a passkey", type: "button", id: "passkey-sign-in-button" %>
35
+ <%%= tag.span "", id: "passkey-sign-in-error", style: "color:red" %>
36
+
37
+ <%%= javascript_tag do %>
38
+ (function () {
39
+ function bufferEncode(value) {
40
+ return btoa(String.fromCharCode.apply(null, new Uint8Array(value)))
41
+ .replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
42
+ }
43
+
44
+ function bufferDecode(value) {
45
+ const padded = value.replace(/-/g, "+").replace(/_/g, "/");
46
+ const raw = window.atob(padded);
47
+ const buffer = new Uint8Array(raw.length);
48
+ for (let i = 0; i < raw.length; i++) buffer[i] = raw.charCodeAt(i);
49
+ return buffer.buffer;
50
+ }
51
+
52
+ document.getElementById("passkey-sign-in-button").addEventListener("click", async function () {
53
+ const options = <%%= raw @webauthn_credential_options.to_json %>;
54
+ options.challenge = bufferDecode(options.challenge);
55
+ if (options.allowCredentials) {
56
+ options.allowCredentials = options.allowCredentials.map(function (cred) {
57
+ return Object.assign({}, cred, { id: bufferDecode(cred.id) });
58
+ });
59
+ }
60
+
61
+ let credential;
62
+ try {
63
+ credential = await navigator.credentials.get({ publicKey: options });
64
+ } catch (error) {
65
+ document.getElementById("passkey-sign-in-error").textContent = error.message;
66
+ return;
67
+ }
68
+
69
+ const response = await fetch("<%%= passkey_session_path %>", {
70
+ method: "POST",
71
+ headers: {
72
+ "Content-Type": "application/json",
73
+ "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content
74
+ },
75
+ body: JSON.stringify({
76
+ id: credential.id,
77
+ rawId: bufferEncode(credential.rawId),
78
+ type: credential.type,
79
+ response: {
80
+ clientDataJSON: bufferEncode(credential.response.clientDataJSON),
81
+ authenticatorData: bufferEncode(credential.response.authenticatorData),
82
+ signature: bufferEncode(credential.response.signature),
83
+ userHandle: credential.response.userHandle ? bufferEncode(credential.response.userHandle) : null
84
+ }
85
+ })
86
+ });
87
+
88
+ const result = await response.json();
89
+ if (response.ok) {
90
+ window.location.href = result.redirect_to;
91
+ } else {
92
+ document.getElementById("passkey-sign-in-error").textContent = result.error || "Could not sign in with passkey.";
93
+ }
94
+ });
95
+ })();
96
+ <%% end %>
97
+ <% end -%>
33
98
  <br><%%= link_to "Forgot password?", new_password_path %>
34
99
  <% if registerable? -%>
35
100
  <br><%%= link_to "Sign up", new_registration_path %>
@@ -0,0 +1,19 @@
1
+ <h1>Your passkeys</h1>
2
+
3
+ <%%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
4
+ <%%= tag.div(flash[:notice], style: "color:green") if flash[:notice] %>
5
+
6
+ <%% if @webauthn_credentials.empty? %>
7
+ <p>You haven't added a passkey yet.</p>
8
+ <%% else %>
9
+ <ul>
10
+ <%% @webauthn_credentials.each do |credential| %>
11
+ <li>
12
+ <%%= credential.nickname %> &mdash; added <%%= credential.created_at.to_date %>
13
+ <%%= button_to "Remove", webauthn_credential_path(credential), method: :delete, data: { turbo_confirm: "Remove this passkey?" } %>
14
+ </li>
15
+ <%% end %>
16
+ </ul>
17
+ <%% end %>
18
+
19
+ <br><%%= link_to "Add a passkey", new_webauthn_credential_path %>
@@ -0,0 +1,71 @@
1
+ <h1>Add a passkey</h1>
2
+
3
+ <%%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
4
+ <%%= tag.span "", id: "passkey-error", style: "color:red" %>
5
+
6
+ <%%= label_tag :passkey_nickname, "Name this passkey (e.g. \"MacBook\" or \"iPhone\")" %>
7
+ <%%= text_field_tag :passkey_nickname, nil, placeholder: "Passkey" %><br><br>
8
+
9
+ <%%= button_tag "Create passkey", type: "button", id: "create-passkey-button" %>
10
+
11
+ <%%= javascript_tag do %>
12
+ (function () {
13
+ function bufferEncode(value) {
14
+ return btoa(String.fromCharCode.apply(null, new Uint8Array(value)))
15
+ .replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
16
+ }
17
+
18
+ function bufferDecode(value) {
19
+ const padded = value.replace(/-/g, "+").replace(/_/g, "/");
20
+ const raw = window.atob(padded);
21
+ const buffer = new Uint8Array(raw.length);
22
+ for (let i = 0; i < raw.length; i++) buffer[i] = raw.charCodeAt(i);
23
+ return buffer.buffer;
24
+ }
25
+
26
+ document.getElementById("create-passkey-button").addEventListener("click", async function () {
27
+ const options = <%%= raw @webauthn_credential_options.to_json %>;
28
+
29
+ options.challenge = bufferDecode(options.challenge);
30
+ options.user.id = bufferDecode(options.user.id);
31
+ if (options.excludeCredentials) {
32
+ options.excludeCredentials = options.excludeCredentials.map(function (cred) {
33
+ return Object.assign({}, cred, { id: bufferDecode(cred.id) });
34
+ });
35
+ }
36
+
37
+ let credential;
38
+ try {
39
+ credential = await navigator.credentials.create({ publicKey: options });
40
+ } catch (error) {
41
+ document.getElementById("passkey-error").textContent = error.message;
42
+ return;
43
+ }
44
+
45
+ const response = await fetch("<%%= webauthn_credentials_path %>", {
46
+ method: "POST",
47
+ headers: {
48
+ "Content-Type": "application/json",
49
+ "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content
50
+ },
51
+ body: JSON.stringify({
52
+ id: credential.id,
53
+ rawId: bufferEncode(credential.rawId),
54
+ type: credential.type,
55
+ nickname: document.getElementById("passkey_nickname").value,
56
+ response: {
57
+ clientDataJSON: bufferEncode(credential.response.clientDataJSON),
58
+ attestationObject: bufferEncode(credential.response.attestationObject)
59
+ }
60
+ })
61
+ });
62
+
63
+ const result = await response.json();
64
+ if (response.ok) {
65
+ window.location.href = result.redirect_to;
66
+ } else {
67
+ document.getElementById("passkey-error").textContent = result.error || "Could not save passkey.";
68
+ }
69
+ });
70
+ })();
71
+ <%% end %>
@@ -0,0 +1,11 @@
1
+ WebAuthn.configure do |config|
2
+ # This value needs to match `window.location.hostname` where the API is called from in
3
+ # your client-side (front-end) code.
4
+ # config.rp_id = "example.com"
5
+
6
+ # This value needs to match `window.location.origin` where the API is called from in
7
+ # your client-side (front-end) code.
8
+ # config.allowed_origins = ["https://example.com"]
9
+
10
+ config.rp_name = Rails.application.class.module_parent_name
11
+ end
@@ -0,0 +1,7 @@
1
+ class AddWebauthnIdToUsers < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ add_column :users, :webauthn_id, :string
4
+
5
+ add_index :users, :webauthn_id, unique: true
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ class CreateWebauthnCredentials < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :webauthn_credentials do |t|
4
+ t.references :user, null: false, foreign_key: true
5
+ t.string :external_id, null: false
6
+ t.string :public_key, null: false
7
+ t.string :nickname
8
+ t.bigint :sign_count, null: false, default: 0
9
+
10
+ t.timestamps
11
+ end
12
+
13
+ add_index :webauthn_credentials, :external_id, unique: true
14
+ end
15
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsAuthentication
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_authentication
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Freerksen
@@ -39,6 +39,7 @@ files:
39
39
  - lib/generators/authentication/features/lockable.rb
40
40
  - lib/generators/authentication/features/magic_link.rb
41
41
  - lib/generators/authentication/features/ott.rb
42
+ - lib/generators/authentication/features/passkey.rb
42
43
  - lib/generators/authentication/features/recoverable.rb
43
44
  - lib/generators/authentication/features/registerable.rb
44
45
  - lib/generators/authentication/features/rememberable.rb
@@ -50,10 +51,12 @@ files:
50
51
  - lib/generators/authentication/templates/app/controllers/invitations_controller.rb.tt
51
52
  - lib/generators/authentication/templates/app/controllers/magic_links_controller.rb.tt
52
53
  - lib/generators/authentication/templates/app/controllers/otts_controller.rb.tt
54
+ - lib/generators/authentication/templates/app/controllers/passkey_sessions_controller.rb.tt
53
55
  - lib/generators/authentication/templates/app/controllers/passwords_controller.rb.tt
54
56
  - lib/generators/authentication/templates/app/controllers/registrations_controller.rb.tt
55
57
  - lib/generators/authentication/templates/app/controllers/sessions_controller.rb.tt
56
58
  - lib/generators/authentication/templates/app/controllers/unlocks_controller.rb.tt
59
+ - lib/generators/authentication/templates/app/controllers/webauthn_credentials_controller.rb.tt
57
60
  - lib/generators/authentication/templates/app/mailers/confirmations_mailer.rb.tt
58
61
  - lib/generators/authentication/templates/app/mailers/invitations_mailer.rb.tt
59
62
  - lib/generators/authentication/templates/app/mailers/magic_links_mailer.rb.tt
@@ -65,12 +68,14 @@ files:
65
68
  - lib/generators/authentication/templates/app/models/concerns/lockable_concern.rb.tt
66
69
  - lib/generators/authentication/templates/app/models/concerns/magic_link_concern.rb.tt
67
70
  - lib/generators/authentication/templates/app/models/concerns/ott_concern.rb.tt
71
+ - lib/generators/authentication/templates/app/models/concerns/passkey_concern.rb.tt
68
72
  - lib/generators/authentication/templates/app/models/concerns/recoverable_concern.rb.tt
69
73
  - lib/generators/authentication/templates/app/models/concerns/rememberable_concern.rb.tt
70
74
  - lib/generators/authentication/templates/app/models/concerns/timeoutable_concern.rb.tt
71
75
  - lib/generators/authentication/templates/app/models/concerns/trackable_concern.rb.tt
72
76
  - lib/generators/authentication/templates/app/models/concerns/validatable_concern.rb.tt
73
77
  - lib/generators/authentication/templates/app/models/user_auth.rb.tt
78
+ - lib/generators/authentication/templates/app/models/webauthn_credential.rb.tt
74
79
  - lib/generators/authentication/templates/app/views/confirmations/new.html.erb.tt
75
80
  - lib/generators/authentication/templates/app/views/confirmations_mailer/confirmation_instructions.html.erb.tt
76
81
  - lib/generators/authentication/templates/app/views/confirmations_mailer/confirmation_instructions.text.erb.tt
@@ -92,6 +97,9 @@ files:
92
97
  - lib/generators/authentication/templates/app/views/unlocks/new.html.erb.tt
93
98
  - lib/generators/authentication/templates/app/views/unlocks_mailer/unlock_instructions.html.erb.tt
94
99
  - lib/generators/authentication/templates/app/views/unlocks_mailer/unlock_instructions.text.erb.tt
100
+ - lib/generators/authentication/templates/app/views/webauthn_credentials/index.html.erb.tt
101
+ - lib/generators/authentication/templates/app/views/webauthn_credentials/new.html.erb.tt
102
+ - lib/generators/authentication/templates/config/initializers/webauthn.rb.tt
95
103
  - lib/generators/authentication/templates/db/migrate/add_confirmable_to_users.rb.tt
96
104
  - lib/generators/authentication/templates/db/migrate/add_invitable_to_users.rb.tt
97
105
  - lib/generators/authentication/templates/db/migrate/add_lockable_to_users.rb.tt
@@ -99,7 +107,9 @@ files:
99
107
  - lib/generators/authentication/templates/db/migrate/add_ott_to_users.rb.tt
100
108
  - lib/generators/authentication/templates/db/migrate/add_recoverable_to_users.rb.tt
101
109
  - lib/generators/authentication/templates/db/migrate/add_rememberable_to_users.rb.tt
110
+ - lib/generators/authentication/templates/db/migrate/add_webauthn_id_to_users.rb.tt
102
111
  - lib/generators/authentication/templates/db/migrate/create_user_auths.rb.tt
112
+ - lib/generators/authentication/templates/db/migrate/create_webauthn_credentials.rb.tt
103
113
  - lib/rails_authentication.rb
104
114
  - lib/rails_authentication/version.rb
105
115
  homepage: https://github.com/dfreerksen/rails_authentication