rodauth-rails 0.9.1 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -0
  3. data/README.md +467 -249
  4. data/lib/generators/rodauth/install_generator.rb +17 -0
  5. data/lib/generators/rodauth/templates/app/lib/rodauth_app.rb +41 -30
  6. data/lib/generators/rodauth/templates/app/mailers/rodauth_mailer.rb +3 -3
  7. data/lib/generators/rodauth/templates/app/views/rodauth/_email_auth_request_form.html.erb +1 -1
  8. data/lib/generators/rodauth/templates/app/views/rodauth/_field.html.erb +2 -2
  9. data/lib/generators/rodauth/templates/app/views/rodauth/_field_error.html.erb +2 -2
  10. data/lib/generators/rodauth/templates/app/views/rodauth/_global_logout_field.html.erb +2 -2
  11. data/lib/generators/rodauth/templates/app/views/rodauth/_login_confirm_field.html.erb +3 -3
  12. data/lib/generators/rodauth/templates/app/views/rodauth/_login_display.html.erb +3 -3
  13. data/lib/generators/rodauth/templates/app/views/rodauth/_login_field.html.erb +3 -3
  14. data/lib/generators/rodauth/templates/app/views/rodauth/_login_form.html.erb +3 -3
  15. data/lib/generators/rodauth/templates/app/views/rodauth/_login_form_footer.html.erb +2 -2
  16. data/lib/generators/rodauth/templates/app/views/rodauth/_login_form_header.html.erb +2 -2
  17. data/lib/generators/rodauth/templates/app/views/rodauth/_login_hidden_field.html.erb +1 -1
  18. data/lib/generators/rodauth/templates/app/views/rodauth/_new_password_field.html.erb +3 -3
  19. data/lib/generators/rodauth/templates/app/views/rodauth/_otp_auth_code_field.html.erb +3 -3
  20. data/lib/generators/rodauth/templates/app/views/rodauth/_password_confirm_field.html.erb +3 -3
  21. data/lib/generators/rodauth/templates/app/views/rodauth/_password_field.html.erb +3 -3
  22. data/lib/generators/rodauth/templates/app/views/rodauth/_recovery_code_field.html.erb +3 -3
  23. data/lib/generators/rodauth/templates/app/views/rodauth/_recovery_codes_form.html.erb +4 -4
  24. data/lib/generators/rodauth/templates/app/views/rodauth/_sms_code_field.html.erb +3 -3
  25. data/lib/generators/rodauth/templates/app/views/rodauth/_sms_phone_field.html.erb +3 -3
  26. data/lib/generators/rodauth/templates/app/views/rodauth/_submit.html.erb +1 -1
  27. data/lib/generators/rodauth/templates/app/views/rodauth/add_recovery_codes.html.erb +2 -2
  28. data/lib/generators/rodauth/templates/app/views/rodauth/change_login.html.erb +3 -3
  29. data/lib/generators/rodauth/templates/app/views/rodauth/change_password.html.erb +3 -3
  30. data/lib/generators/rodauth/templates/app/views/rodauth/close_account.html.erb +2 -2
  31. data/lib/generators/rodauth/templates/app/views/rodauth/confirm_password.html.erb +1 -1
  32. data/lib/generators/rodauth/templates/app/views/rodauth/create_account.html.erb +4 -4
  33. data/lib/generators/rodauth/templates/app/views/rodauth/email_auth.html.erb +1 -1
  34. data/lib/generators/rodauth/templates/app/views/rodauth/logout.html.erb +2 -2
  35. data/lib/generators/rodauth/templates/app/views/rodauth/multi_phase_login.html.erb +1 -1
  36. data/lib/generators/rodauth/templates/app/views/rodauth/otp_auth.html.erb +1 -1
  37. data/lib/generators/rodauth/templates/app/views/rodauth/otp_disable.html.erb +2 -2
  38. data/lib/generators/rodauth/templates/app/views/rodauth/otp_setup.html.erb +9 -9
  39. data/lib/generators/rodauth/templates/app/views/rodauth/recovery_auth.html.erb +1 -1
  40. data/lib/generators/rodauth/templates/app/views/rodauth/remember.html.erb +5 -5
  41. data/lib/generators/rodauth/templates/app/views/rodauth/reset_password.html.erb +2 -2
  42. data/lib/generators/rodauth/templates/app/views/rodauth/reset_password_request.html.erb +2 -2
  43. data/lib/generators/rodauth/templates/app/views/rodauth/sms_auth.html.erb +1 -1
  44. data/lib/generators/rodauth/templates/app/views/rodauth/sms_confirm.html.erb +1 -1
  45. data/lib/generators/rodauth/templates/app/views/rodauth/sms_disable.html.erb +2 -2
  46. data/lib/generators/rodauth/templates/app/views/rodauth/sms_request.html.erb +1 -1
  47. data/lib/generators/rodauth/templates/app/views/rodauth/sms_setup.html.erb +2 -2
  48. data/lib/generators/rodauth/templates/app/views/rodauth/two_factor_auth.html.erb +1 -1
  49. data/lib/generators/rodauth/templates/app/views/rodauth/two_factor_disable.html.erb +2 -2
  50. data/lib/generators/rodauth/templates/app/views/rodauth/two_factor_manage.html.erb +6 -6
  51. data/lib/generators/rodauth/templates/app/views/rodauth/unlock_account.html.erb +2 -2
  52. data/lib/generators/rodauth/templates/app/views/rodauth/unlock_account_request.html.erb +1 -1
  53. data/lib/generators/rodauth/templates/app/views/rodauth/verify_account.html.erb +3 -3
  54. data/lib/generators/rodauth/templates/app/views/rodauth/verify_account_resend.html.erb +2 -2
  55. data/lib/generators/rodauth/templates/app/views/rodauth/verify_login_change.html.erb +1 -1
  56. data/lib/generators/rodauth/templates/app/views/rodauth/webauthn_auth.html.erb +7 -7
  57. data/lib/generators/rodauth/templates/app/views/rodauth/webauthn_remove.html.erb +6 -6
  58. data/lib/generators/rodauth/templates/app/views/rodauth/webauthn_setup.html.erb +7 -7
  59. data/lib/generators/rodauth/templates/app/views/rodauth_mailer/unlock_account.text.erb +1 -1
  60. data/lib/generators/rodauth/views_generator.rb +25 -4
  61. data/lib/rodauth/rails.rb +36 -4
  62. data/lib/rodauth/rails/app.rb +19 -21
  63. data/lib/rodauth/rails/app/flash.rb +2 -8
  64. data/lib/rodauth/rails/app/middleware.rb +20 -10
  65. data/lib/rodauth/rails/auth.rb +37 -0
  66. data/lib/rodauth/rails/controller_methods.rb +1 -5
  67. data/lib/rodauth/rails/feature.rb +17 -210
  68. data/lib/rodauth/rails/feature/base.rb +62 -0
  69. data/lib/rodauth/rails/feature/callbacks.rb +61 -0
  70. data/lib/rodauth/rails/feature/csrf.rb +65 -0
  71. data/lib/rodauth/rails/feature/email.rb +30 -0
  72. data/lib/rodauth/rails/feature/instrumentation.rb +71 -0
  73. data/lib/rodauth/rails/feature/render.rb +48 -0
  74. data/lib/rodauth/rails/version.rb +1 -1
  75. data/rodauth-rails.gemspec +1 -1
  76. metadata +12 -6
  77. data/lib/generators/rodauth/mailer_generator.rb +0 -37
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b8f8aec1dbdc745a530aabec0d63bc2681499dd36f8185faed9ea09e7184636e
4
- data.tar.gz: fbc5a75976a922978a6e37fee3bef8e7f04bb0a9a324066afdf79172b33f00e9
3
+ metadata.gz: d447d09fef8c29feb6240523286b8906049e85965f20a6410d1a475f913d9051
4
+ data.tar.gz: bca9b6eadec6b32f2193291c6922467a554105d290ffd7b34bc2606d62121926
5
5
  SHA512:
6
- metadata.gz: 89d2f6ad377ba8e3f18bc747c3bfdf53e97c1a29f2731036987e5f7c1fde14db89732cda2d09026a153d81eabe26e51e021a129f02517d4d5582fcaf392876ca
7
- data.tar.gz: 648b1297a9569b436113b5921a9ae37944d808ed42a03ef57a75452a74143dcc493e7d9c34a12f31f780745db5d2b1365d5a7b602dfa303571961730566852f4
6
+ metadata.gz: 1f512f9fe9a3e22dcddf477d8906d1ea63a548241fd93b43bbcaf274ff39e0104e20f64c6a2836e5b243e812ffde654deae55a0beca69f4ba917cd5943da8a3c
7
+ data.tar.gz: dbbd99959dfd42134cd3374f1f9767cf3e8d49327c195d4c35c4ecf281d0c3dad52db76b7e2fbf030c9e3ea2131bfcb1b6a120cc4a310983d8db564e63b97cda
data/CHANGELOG.md CHANGED
@@ -1,3 +1,57 @@
1
+ ## 0.14.0 (2021-07-10)
2
+
3
+ * Speed up template rendering by only searching formats accepted by the request (@janko)
4
+
5
+ * Add `--name` option to `rodauth:views` generator for specifying different rodauth configuration (@janko)
6
+
7
+ * Infer correct template path from configured controller in `rodauth:views` generator (@janko)
8
+
9
+ * Raise `ArgumentError` if undefined rodauth configuration is passed to `Rodauth::Rails.app` (@janko)
10
+
11
+ * Make `#rails_controller` method on the rodauth instance public (@janko)
12
+
13
+ * Remove `--directory` option from `rodauth:views` generator (@janko)
14
+
15
+ * Remove `#features` and `#routes` writer and `#configuration` reader from `Rodauth::Rails::Auth` (@janko)
16
+
17
+ ## 0.13.0 (2021-06-10)
18
+
19
+ * Add `:query`, `:form`, `:session`, `:account`, and `:env` options to `Rodauth::Rails.rodauth` (@janko)
20
+
21
+ ## 0.12.0 (2021-05-15)
22
+
23
+ * Include total view render time in logs for Rodauth requests (@janko)
24
+
25
+ * Instrument redirects (@janko)
26
+
27
+ * Instrument Rodauth requests on `action_controller` namespace (@janko)
28
+
29
+ * Update templates for Boostrap 5 compatibility (@janko)
30
+
31
+ * Log request parameters for Rodauth requests (@janko)
32
+
33
+ ## 0.11.0 (2021-05-06)
34
+
35
+ * Add controller-like logging for requests to Rodauth endpoints (@janko)
36
+
37
+ * Add `#rails_routes` to Roda and Rodauth instance for accessing Rails route helpers (@janko)
38
+
39
+ * Add `#rails_request` to Roda and Rodauth instance for retrieving an `ActionDispatch::Request` instance (@janko)
40
+
41
+ ## 0.10.0 (2021-03-23)
42
+
43
+ * Add `Rodauth::Rails::Auth` superclass for moving configurations into separate files (@janko)
44
+
45
+ * Load the `pass` Roda plugin and recommend calling `r.pass` on prefixed routes (@janko)
46
+
47
+ * Improve Roda middleware inspect output (@janko)
48
+
49
+ * Create `RodauthMailer` and email templates in `rodauth:install`, and remove `rodauth:mailer` (@janko)
50
+
51
+ * Raise `KeyError` in `#rodauth` method when the Rodauth instance doesn't exist (@janko)
52
+
53
+ * Add `Rodauth::Rails.authenticated` routing constraint for requiring authentication (@janko)
54
+
1
55
  ## 0.9.1 (2021-02-10)
2
56
 
3
57
  * Fix flash integration being loaded for API-only apps and causing an error (@dmitryzuev)
data/README.md CHANGED
@@ -2,35 +2,6 @@
2
2
 
3
3
  Provides Rails integration for the [Rodauth] authentication framework.
4
4
 
5
- ## Table of contents
6
-
7
- * [Resources](#resources)
8
- * [Why Rodauth?](#why-rodauth)
9
- * [Upgrading](#upgrading)
10
- * [Installation](#installation)
11
- * [Usage](#usage)
12
- - [Routes](#routes)
13
- - [Current account](#current-account)
14
- - [Requiring authentication](#requiring-authentication)
15
- - [Views](#views)
16
- - [Mailer](#mailer)
17
- - [Migrations](#migrations)
18
- - [Multiple configurations](#multiple-configurations)
19
- - [Calling controller methods](#calling-controller-methods)
20
- - [Rodauth instance](#rodauth-instance)
21
- * [How it works](#how-it-works)
22
- - [Middleware](#middleware)
23
- - [App](#app)
24
- - [Sequel](#sequel)
25
- * [JSON API](#json-api)
26
- * [OmniAuth](#omniauth)
27
- * [Configuring](#configuring)
28
- * [Custom extensions](#custom-extensions)
29
- * [Testing](#testing)
30
- * [Rodauth defaults](#rodauth-defaults)
31
- - [Database functions](#database-functions)
32
- - [Account statuses](#account-statuses)
33
-
34
5
  ## Resources
35
6
 
36
7
  Useful links:
@@ -43,6 +14,7 @@ Articles:
43
14
  * [Rodauth: A Refreshing Authentication Solution for Ruby](https://janko.io/rodauth-a-refreshing-authentication-solution-for-ruby/)
44
15
  * [Adding Authentication in Rails with Rodauth](https://janko.io/adding-authentication-in-rails-with-rodauth/)
45
16
  * [Adding Multifactor Authentication in Rails with Rodauth](https://janko.io/adding-multifactor-authentication-in-rails-with-rodauth/)
17
+ * [How to build an OIDC provider using rodauth-oauth on Rails](https://honeyryderchuck.gitlab.io/httpx/2021/03/15/oidc-provider-on-rails-using-rodauth-oauth.html)
46
18
 
47
19
  ## Why Rodauth?
48
20
 
@@ -61,29 +33,23 @@ of the advantages that stand out for me:
61
33
  * consistent before/after hooks around everything
62
34
  * dedicated object encapsulating all authentication logic
63
35
 
64
- ## Upgrading
65
-
66
- ### Upgrading to 0.7.0
36
+ One commmon concern is the fact that, unlike most other authentication
37
+ frameworks for Rails, Rodauth uses [Sequel] for database interaction instead of
38
+ Active Record. There are good reasons for this, and to make Rodauth work
39
+ smoothly alongside Active Record, rodauth-rails configures Sequel to [reuse
40
+ Active Record's database connection][sequel-activerecord_connection].
67
41
 
68
- Starting from version 0.7.0, rodauth-rails now correctly detects Rails
69
- application's `secret_key_base` when setting default `hmac_secret`, including
70
- when it's set via credentials or `$SECRET_KEY_BASE` environment variable. This
71
- means that your authentication will now be more secure by default, and Rodauth
72
- features that require `hmac_secret` should now work automatically as well.
42
+ ## Upgrading
73
43
 
74
- However, if you've already been using rodauth-rails in production, where the
75
- `secret_key_base` is set via credentials or environment variable and `hmac_secret`
76
- was not explicitly set, the fact that your authentication will now start using
77
- HMACs has backwards compatibility considerations. See the [Rodauth
78
- documentation][hmac] for instructions on how to safely transition, or just set
79
- `hmac_secret nil` in your Rodauth configuration.
44
+ For instructions on upgrading from previous rodauth-rails versions, see
45
+ [UPGRADING.md](/UPGRADING.md).
80
46
 
81
47
  ## Installation
82
48
 
83
49
  Add the gem to your Gemfile:
84
50
 
85
51
  ```rb
86
- gem "rodauth-rails", "~> 0.9"
52
+ gem "rodauth-rails", "~> 0.14"
87
53
 
88
54
  # gem "jwt", require: false # for JWT feature
89
55
  # gem "rotp", require: false # for OTP feature
@@ -108,114 +74,22 @@ $ rails generate rodauth:install --jwt # token authentication via the "Authoriza
108
74
  $ bundle add jwt
109
75
  ```
110
76
 
111
- The generator will create the following files:
112
-
113
- * Rodauth migration at `db/migrate/*_create_rodauth.rb`
114
- * Rodauth initializer at `config/initializers/rodauth.rb`
115
- * Sequel initializer at `config/initializers/sequel.rb` for ActiveRecord integration
116
- * Rodauth app at `app/lib/rodauth_app.rb`
117
- * Rodauth controller at `app/controllers/rodauth_controller.rb`
118
- * Account model at `app/models/account.rb`
119
-
120
- ### Migration
121
-
122
- The migration file creates tables required by Rodauth. You're encouraged to
123
- review the migration, and modify it to only create tables for features you
124
- intend to use.
125
-
126
- ```rb
127
- # db/migrate/*_create_rodauth.rb
128
- class CreateRodauth < ActiveRecord::Migration
129
- def change
130
- create_table :accounts do |t| ... end
131
- create_table :account_password_hashes do |t| ... end
132
- create_table :account_password_reset_keys do |t| ... end
133
- create_table :account_verification_keys do |t| ... end
134
- create_table :account_login_change_keys do |t| ... end
135
- create_table :account_remember_keys do |t| ... end
136
- end
137
- end
138
- ```
77
+ This generator will create a Rodauth app with common authentication features
78
+ enabled, a database migration with tables required by those features, a mailer
79
+ with default templates, and a few other files.
139
80
 
140
- Once you're done, you can run the migration:
81
+ Feel free to remove any features you don't need, along with their corresponding
82
+ tables. Afterwards, run the migration:
141
83
 
142
- ```
84
+ ```sh
143
85
  $ rails db:migrate
144
86
  ```
145
87
 
146
- ### Rodauth initializer
147
-
148
- The Rodauth initializer assigns the constant for your Rodauth app, which will
149
- be called by the Rack middleware that's added in front of your Rails router.
150
-
151
- ```rb
152
- # config/initializers/rodauth.rb
153
- Rodauth::Rails.configure do |config|
154
- config.app = "RodauthApp"
155
- end
156
- ```
157
-
158
- ### Sequel initializer
159
-
160
- Rodauth uses [Sequel] for database interaction. If you're using ActiveRecord,
161
- an additional initializer will be created which configures Sequel to use the
162
- ActiveRecord connection.
163
-
164
- ```rb
165
- # config/initializers/sequel.rb
166
- require "sequel/core"
167
-
168
- # initialize Sequel and have it reuse Active Record's database connection
169
- DB = Sequel.connect("postgresql://", extensions: :activerecord_connection)
170
- ```
171
-
172
- ### Rodauth app
173
-
174
- Your Rodauth app is created in the `app/lib/` directory, and comes with a
175
- default set of authentication features enabled, as well as extensive examples
176
- on ways you can configure authentication behaviour.
177
-
178
- ```rb
179
- # app/lib/rodauth_app.rb
180
- class RodauthApp < Rodauth::Rails::App
181
- configure do
182
- # authentication configuration
183
- end
184
-
185
- route do |r|
186
- # request handling
187
- end
188
- end
189
- ```
190
-
191
- ### Controller
192
-
193
- Your Rodauth app will by default use `RodauthController` for view rendering,
194
- CSRF protection, and running controller callbacks and rescue handlers around
195
- Rodauth actions.
196
-
197
- ```rb
198
- # app/controllers/rodauth_controller.rb
199
- class RodauthController < ApplicationController
200
- end
201
- ```
202
-
203
- ### Account model
204
-
205
- Rodauth stores user accounts in the `accounts` table, so the generator will
206
- also create an `Account` model for custom use.
207
-
208
- ```rb
209
- # app/models/account.rb
210
- class Account < ApplicationRecord
211
- end
212
- ```
213
-
214
88
  ## Usage
215
89
 
216
90
  ### Routes
217
91
 
218
- We can see the list of routes our Rodauth middleware handles:
92
+ You can see the list of routes our Rodauth middleware handles:
219
93
 
220
94
  ```sh
221
95
  $ rails rodauth:routes
@@ -237,7 +111,7 @@ Routes handled by RodauthApp:
237
111
  /close-account rodauth.close_account_path
238
112
  ```
239
113
 
240
- Using this information, we could add some basic authentication links to our
114
+ Using this information, you can add some basic authentication links to your
241
115
  navigation header:
242
116
 
243
117
  ```erb
@@ -253,9 +127,22 @@ These routes are fully functional, feel free to visit them and interact with the
253
127
  pages. The templates that ship with Rodauth aim to provide a complete
254
128
  authentication experience, and the forms use [Bootstrap] markup.
255
129
 
130
+ Inside Rodauth configuration and the `route` block you can access Rails route
131
+ helpers through `#rails_routes`:
132
+
133
+ ```rb
134
+ class RodauthApp < Rodauth::Rails::App
135
+ configure do
136
+ # ...
137
+ login_redirect { rails_routes.activity_path }
138
+ # ...
139
+ end
140
+ end
141
+ ```
142
+
256
143
  ### Current account
257
144
 
258
- To be able to fetch currently authenticated account, let's define a
145
+ To be able to fetch currently authenticated account, you can define a
259
146
  `#current_account` method that fetches the account id from session and
260
147
  retrieves the corresponding account record:
261
148
 
@@ -272,11 +159,11 @@ class ApplicationController < ActionController::Base
272
159
  rodauth.logout
273
160
  rodauth.login_required
274
161
  end
275
- helper_method :current_account
162
+ helper_method :current_account # skip if inheriting from ActionController::API
276
163
  end
277
164
  ```
278
165
 
279
- This allows us to access the current account in controllers and views:
166
+ This allows you to access the current account in controllers and views:
280
167
 
281
168
  ```erb
282
169
  <p>Authenticated as: <%= current_account.email %></p>
@@ -284,9 +171,9 @@ This allows us to access the current account in controllers and views:
284
171
 
285
172
  ### Requiring authentication
286
173
 
287
- We'll likely want to require authentication for certain parts of our app,
288
- redirecting the user to the login page if they're not logged in. We can do this
289
- in our Rodauth app's routing block, which helps keep the authentication logic
174
+ You'll likely want to require authentication for certain parts of your app,
175
+ redirecting the user to the login page if they're not logged in. You can do this
176
+ in your Rodauth app's routing block, which helps keep the authentication logic
290
177
  encapsulated:
291
178
 
292
179
  ```rb
@@ -305,7 +192,7 @@ class RodauthApp < Rodauth::Rails::App
305
192
  end
306
193
  ```
307
194
 
308
- We can also require authentication at the controller layer:
195
+ You can also require authentication at the controller layer:
309
196
 
310
197
  ```rb
311
198
  # app/controllers/application_controller.rb
@@ -330,15 +217,52 @@ class PostsController < ApplicationController
330
217
  end
331
218
  ```
332
219
 
333
- Or at the Rails router level:
220
+ #### Routing constraints
221
+
222
+ In some cases it makes sense to require authentication at the Rails router
223
+ level. You can do this via the built-in `authenticated` routing constraint:
334
224
 
335
225
  ```rb
336
226
  # config/routes.rb
337
227
  Rails.application.routes.draw do
338
- constraints -> (r) { r.env["rodauth"].require_authentication } do
339
- namespace :admin do
340
- # ...
341
- end
228
+ constraints Rodauth::Rails.authenticated do
229
+ # ... authenticated routes ...
230
+ end
231
+ end
232
+ ```
233
+
234
+ If you want additional conditions, you can pass in a block, which is
235
+ called with the Rodauth instance:
236
+
237
+ ```rb
238
+ # config/routes.rb
239
+ Rails.application.routes.draw do
240
+ # require multifactor authentication to be setup
241
+ constraints Rodauth::Rails.authenticated { |rodauth| rodauth.uses_two_factor_authentication? } do
242
+ # ...
243
+ end
244
+ end
245
+ ```
246
+
247
+ You can specify the Rodauth configuration by passing the configuration name:
248
+
249
+ ```rb
250
+ # config/routes.rb
251
+ Rails.application.routes.draw do
252
+ constraints Rodauth::Rails.authenticated(:admin) do
253
+ # ...
254
+ end
255
+ end
256
+ ```
257
+
258
+ If you need something more custom, you can always create the routing constraint
259
+ manually:
260
+
261
+ ```rb
262
+ # config/routes.rb
263
+ Rails.application.routes.draw do
264
+ constraints -> (r) { !r.env["rodauth"].logged_in? } do # or "rodauth.admin"
265
+ # routes when the user is not logged in
342
266
  end
343
267
  end
344
268
  ```
@@ -354,11 +278,11 @@ $ rails generate rodauth:views
354
278
  ```
355
279
 
356
280
  This will generate views for the default set of Rodauth features into the
357
- `app/views/rodauth` directory, which will be automatically picked up by the
358
- `RodauthController`.
281
+ `app/views/rodauth` directory, provided that `RodauthController` is set for the
282
+ main configuration.
359
283
 
360
284
  You can pass a list of Rodauth features to the generator to create views for
361
- these features (this will not remove any existing views):
285
+ these features (this will not remove or overwrite any existing views):
362
286
 
363
287
  ```sh
364
288
  $ rails generate rodauth:views login create_account lockout otp
@@ -370,12 +294,10 @@ Or you can generate views for all features:
370
294
  $ rails generate rodauth:views --all
371
295
  ```
372
296
 
373
- You can also tell the generator to create views into another directory (in this
374
- case make sure to rename the Rodauth controller accordingly):
297
+ Use `--name` to generate views for a different Rodauth configuration:
375
298
 
376
299
  ```sh
377
- # generates views into app/views/authentication
378
- $ rails generate rodauth:views --name authentication
300
+ $ rails generate rodauth:views --name admin
379
301
  ```
380
302
 
381
303
  #### Layout
@@ -406,58 +328,36 @@ end
406
328
 
407
329
  ### Mailer
408
330
 
409
- Depending on the features you've enabled, Rodauth may send emails as part of
410
- the authentication flow. Most email settings can be customized:
331
+ The install generator will create `RodauthMailer` with default email templates,
332
+ and configure Rodauth features that send emails as part of the authentication
333
+ flow to use it.
411
334
 
412
335
  ```rb
413
- # app/lib/rodauth_app.rb
414
- class RodauthApp < Rodauth::Rails::App
415
- # ...
416
- configure do
336
+ # app/mailers/rodauth_mailer.rb
337
+ class RodauthMailer < ApplicationMailer
338
+ def verify_account(recipient, email_link)
417
339
  # ...
418
- # general settings
419
- email_from "no-reply@myapp.com"
420
- email_subject_prefix "[MyApp] "
421
- send_email(&:deliver_later)
340
+ end
341
+ def reset_password(recipient, email_link)
422
342
  # ...
423
- # feature settings
424
- verify_account_email_subject "Verify your account"
425
- verify_account_email_body { "Verify your account by visting this link: #{verify_account_email_link}" }
343
+ end
344
+ def verify_login_change(recipient, old_login, new_login, email_link)
426
345
  # ...
427
346
  end
347
+ def password_changed(recipient)
348
+ # ...
349
+ end
350
+ # def email_auth(recipient, email_link)
351
+ # ...
352
+ # end
353
+ # def unlock_account(recipient, email_link)
354
+ # ...
355
+ # end
428
356
  end
429
357
  ```
430
-
431
- This is convenient when starting out, but eventually you might want to use your
432
- own mailer. You can start by running the following command:
433
-
434
- ```sh
435
- $ rails generate rodauth:mailer
436
- ```
437
-
438
- This will create a `RodauthMailer` with the associated mailer views in
439
- `app/views/rodauth_mailer` directory:
440
-
441
- ```rb
442
- # app/mailers/rodauth_mailer.rb
443
- class RodauthMailer < ApplicationMailer
444
- def verify_account(recipient, email_link) ... end
445
- def reset_password(recipient, email_link) ... end
446
- def verify_login_change(recipient, old_login, new_login, email_link) ... end
447
- def password_changed(recipient) ... end
448
- # def email_auth(recipient, email_link) ... end
449
- # def unlock_account(recipient, email_link) ... end
450
- end
451
- ```
452
-
453
- You can then uncomment the lines in your Rodauth configuration to have it call
454
- your mailer. If you've enabled additional authentication features that send
455
- emails, make sure to override their `create_*_email` methods as well.
456
-
457
358
  ```rb
458
359
  # app/lib/rodauth_app.rb
459
360
  class RodauthApp < Rodauth::Rails::App
460
- # ...
461
361
  configure do
462
362
  # ...
463
363
  create_reset_password_email do
@@ -487,10 +387,51 @@ class RodauthApp < Rodauth::Rails::App
487
387
  end
488
388
  ```
489
389
 
490
- This approach can be used even if you're using a 3rd-party service for
491
- transactional emails, where emails are sent via HTTP instead of SMTP. Whatever
492
- the `create_*_email` block returns will be passed to `send_email`, so you can
493
- be creative.
390
+ This configuration calls `#deliver_later`, which uses Active Job to deliver
391
+ emails in a background job. It's generally recommended to send emails
392
+ asynchronously for better request throughput and the ability to retry
393
+ deliveries. However, if you want to send emails synchronously, you can modify
394
+ the configuration to call `#deliver_now` instead.
395
+
396
+ If you're using a background processing library without an Active Job adapter,
397
+ or a 3rd-party service for sending transactional emails, this two-phase API
398
+ might not be suitable. In this case, instead of overriding `#create_*_email`
399
+ and `#send_email`, override the `#send_*_email` methods instead, which are
400
+ required to send the email immediately. For example:
401
+
402
+ ```rb
403
+ # app/workers/rodauth_mailer_worker.rb
404
+ class RodauthMailerWorker
405
+ include Sidekiq::Worker
406
+
407
+ def perform(name, *args)
408
+ email = RodauthMailer.public_send(name, *args)
409
+ email.deliver_now
410
+ end
411
+ end
412
+ ```
413
+ ```rb
414
+ # app/lib/rodauth_app.rb
415
+ class RodauthApp < Rodauth::Rails::App
416
+ configure do
417
+ # ...
418
+ # use `#send_*_email` method to be able to immediately enqueue email delivery
419
+ send_reset_password_email do
420
+ enqueue_email(:reset_password, email_to, reset_password_email_link)
421
+ end
422
+ # ...
423
+ auth_class_eval do
424
+ # custom method for enqueuing email delivery using our worker
425
+ def enqueue_email(name, *args)
426
+ db.after_commit do
427
+ RodauthMailerWorker.perform_async(name, *args)
428
+ end
429
+ end
430
+ end
431
+ # ...
432
+ end
433
+ end
434
+ ```
494
435
 
495
436
  ### Migrations
496
437
 
@@ -515,7 +456,7 @@ end
515
456
  ### Multiple configurations
516
457
 
517
458
  If you need to handle multiple types of accounts that require different
518
- authentication logic, you can create different configurations for them:
459
+ authentication logic, you can create additional configurations for them:
519
460
 
520
461
  ```rb
521
462
  # app/lib/rodauth_app.rb
@@ -536,7 +477,12 @@ class RodauthApp < Rodauth::Rails::App
536
477
 
537
478
  route do |r|
538
479
  r.rodauth
539
- r.on("admin") { r.rodauth(:admin) }
480
+
481
+ r.on "admin" do
482
+ r.rodauth(:admin)
483
+ break # allow routing of other /admin/* requests to continue to Rails
484
+ end
485
+
540
486
  # ...
541
487
  end
542
488
  end
@@ -548,6 +494,142 @@ Then in your application you can reference the secondary Rodauth instance:
548
494
  rodauth(:admin).login_path #=> "/admin/login"
549
495
  ```
550
496
 
497
+ You'll likely want to save the information of which account belongs to which
498
+ configuration to the database. One way would be to have a separate table that
499
+ stores account types:
500
+
501
+ ```sh
502
+ $ rails generate migration create_account_types
503
+ ```
504
+ ```rb
505
+ # db/migrate/*_create_account_types.rb
506
+ class CreateAccountTypes < ActiveRecord::Migration
507
+ def change
508
+ create_table :account_types do |t|
509
+ t.references :account, foreign_key: { on_delete: :cascade }, null: false
510
+ t.string :type, null: false
511
+ end
512
+ end
513
+ end
514
+ ```
515
+ ```sh
516
+ $ rails db:migrate
517
+ ```
518
+
519
+ Then an entry would be inserted after account creation, and optionally whenever
520
+ Rodauth retrieves accounts you could filter only those belonging to the current
521
+ configuration:
522
+
523
+ ```rb
524
+ # app/lib/rodauth_app.rb
525
+ class RodauthApp < Rodauth::Rails::App
526
+ configure(:admin) do
527
+ # ...
528
+ after_create_account do
529
+ db[:account_types].insert(account_id: account_id, type: "admin")
530
+ end
531
+ auth_class_eval do
532
+ def account_ds(*)
533
+ super.join(:account_types, account_id: :id).where(type: "admin")
534
+ end
535
+ end
536
+ # ...
537
+ end
538
+ end
539
+ ```
540
+
541
+ #### Named auth classes
542
+
543
+ A `configure` block inside `Rodauth::Rails::App` will internally create an
544
+ anonymous `Rodauth::Auth` subclass, and register it under the given name.
545
+ However, you can also define the auth classes explicitly, by creating
546
+ subclasses of `Rodauth::Rails::Auth`:
547
+
548
+ ```rb
549
+ # app/lib/rodauth_main.rb
550
+ class RodauthMain < Rodauth::Rails::Auth
551
+ configure do
552
+ # ... main configuration ...
553
+ end
554
+ end
555
+ ```
556
+ ```rb
557
+ # app/lib/rodauth_admin.rb
558
+ class RodauthAdmin < Rodauth::Rails::Auth
559
+ configure do
560
+ # ...
561
+ prefix "/admin"
562
+ session_key_prefix "admin_"
563
+ # ...
564
+ end
565
+ end
566
+ ```
567
+ ```rb
568
+ # app/lib/rodauth_app.rb
569
+ class RodauthApp < Rodauth::Rails::App
570
+ configure RodauthMain
571
+ configure RodauthAdmin, :admin
572
+ # ...
573
+ end
574
+ ```
575
+
576
+ This allows having each configuration in a dedicated file, and named constants
577
+ improve introspection and error messages. You can also use inheritance to share
578
+ common settings:
579
+
580
+ ```rb
581
+ # app/lib/rodauth_base.rb
582
+ class RodauthBase < Rodauth::Rails::Auth
583
+ # common settings that can be shared between multiple configurations
584
+ configure do
585
+ enable :login, :logout
586
+ login_return_to_requested_location? true
587
+ logout_redirect "/"
588
+ # ...
589
+ end
590
+ end
591
+ ```
592
+ ```rb
593
+ # app/lib/rodauth_main.rb
594
+ class RodauthMain < RodauthBase # inherit common settings
595
+ configure do
596
+ # ... customize main ...
597
+ end
598
+ end
599
+ ```
600
+ ```rb
601
+ # app/lib/rodauth_admin.rb
602
+ class RodauthAdmin < RodauthBase # inherit common settings
603
+ configure do
604
+ # ... customize admin ...
605
+ end
606
+ end
607
+ ```
608
+
609
+ Another benefit of explicit classes is that you can define custom methods
610
+ directly at the class level instead of inside an `auth_class_eval`:
611
+
612
+ ```rb
613
+ # app/lib/rodauth_admin.rb
614
+ class RodauthAdmin < Rodauth::Rails::Auth
615
+ configure do
616
+ # ...
617
+ end
618
+
619
+ def superadmin?
620
+ Role.where(account_id: session_id, type: "superadmin").any?
621
+ end
622
+ end
623
+ ```
624
+ ```rb
625
+ # config/routes.rb
626
+ Rails.application.routes.draw do
627
+ constraints Rodauth::Rails.authenticated(:admin) { |rodauth| rodauth.superadmin? } do
628
+ mount Sidekiq::Web => "sidekiq"
629
+ end
630
+ end
631
+ ```
632
+
551
633
  ### Calling controller methods
552
634
 
553
635
  When using Rodauth before/after hooks or generally overriding your Rodauth
@@ -577,8 +659,8 @@ end
577
659
  ### Rodauth instance
578
660
 
579
661
  In some cases you might need to use Rodauth more programmatically, and perform
580
- Rodauth operations outside of the request context. rodauth-rails gives you the
581
- ability to retrieve the Rodauth instance:
662
+ Rodauth operations outside of the request context. rodauth-rails gives you a
663
+ helper method for building a Rodauth instance:
582
664
 
583
665
  ```rb
584
666
  rodauth = Rodauth::Rails.rodauth # or Rodauth::Rails.rodauth(:admin)
@@ -590,8 +672,22 @@ rodauth.setup_account_verification
590
672
  rodauth.close_account
591
673
  ```
592
674
 
593
- This Rodauth instance will be initialized with basic Rack env that allows is it
594
- to generate URLs, using `config.action_mailer.default_url_options` options.
675
+ The base URL is taken from Action Mailer's `default_url_options` setting if
676
+ configured. The `Rodauth::Rails.rodauth` method accepts additional keyword
677
+ arguments:
678
+
679
+ * `:account` – Active Record model instance from which to set `account` and `session[:account_id]`
680
+ * `:query` & `:form` – set specific query/form parameters
681
+ * `:session` – set any session values
682
+ * `:env` – set any additional Rack env values
683
+
684
+ ```rb
685
+ Rodauth::Rails.rodauth(account: Account.find(account_id))
686
+ Rodauth::Rails.rodauth(query: { "param" => "value" })
687
+ Rodauth::Rails.rodauth(form: { "param" => "value" })
688
+ Rodauth::Rails.rodauth(session: { two_factor_auth_setup: true })
689
+ Rodauth::Rails.rodauth(env: { "HTTP_USER_AGENT" => "programmatic" })
690
+ ```
595
691
 
596
692
  ## How it works
597
693
 
@@ -702,7 +798,7 @@ class RodauthApp < Rodauth::Rails::App
702
798
  configure do
703
799
  # ...
704
800
  enable :json
705
- only_json? true # accept only JSON requests
801
+ only_json? true # accept only JSON requests (optional)
706
802
  # ...
707
803
  end
708
804
  end
@@ -723,7 +819,7 @@ class RodauthApp < Rodauth::Rails::App
723
819
  # ...
724
820
  enable :jwt
725
821
  jwt_secret "<YOUR_SECRET_KEY>" # store the JWT secret in a safe place
726
- only_json? true # accept only JSON requests
822
+ only_json? true # accept only JSON requests (optional)
727
823
  # ...
728
824
  end
729
825
  end
@@ -803,7 +899,8 @@ end
803
899
  <%= link_to "Login via Facebook", "/auth/facebook" %>
804
900
  ```
805
901
 
806
- Let's implement the OmniAuth callback endpoint on our Rodauth controller:
902
+ Finally, let's implement the OmniAuth callback endpoint on our Rodauth
903
+ controller:
807
904
 
808
905
  ```rb
809
906
  # config/routes.rb
@@ -856,11 +953,8 @@ end
856
953
 
857
954
  ## Configuring
858
955
 
859
- For the list of configuration methods provided by Rodauth, see the [feature
860
- documentation].
861
-
862
- The `rails` feature rodauth-rails loads is customizable as well, here is the
863
- list of its configuration methods:
956
+ The `rails` feature rodauth-rails loads provides the following configuration
957
+ methods:
864
958
 
865
959
  | Name | Description |
866
960
  | :---- | :---------- |
@@ -887,12 +981,16 @@ Rodauth::Rails.configure do |config|
887
981
  end
888
982
  ```
889
983
 
984
+ For the list of configuration methods provided by Rodauth, see the [feature
985
+ documentation].
986
+
890
987
  ## Custom extensions
891
988
 
892
989
  When developing custom extensions for Rodauth inside your Rails project, it's
893
- better to use plain modules (at least in the beginning), because Rodauth
894
- feature design doesn't yet support Zeitwerk reloading well. Here is
895
- an example of an LDAP authentication extension that uses the
990
+ probably better to use plain modules, at least in the beginning, as Rodauth
991
+ feature design doesn't yet work well with Zeitwerk reloading.
992
+
993
+ Here is an example of an LDAP authentication extension that uses the
896
994
  [simple_ldap_authenticator] gem.
897
995
 
898
996
  ```rb
@@ -922,48 +1020,156 @@ end
922
1020
 
923
1021
  ## Testing
924
1022
 
925
- If you're writing system tests, it's generally better to go through the actual
926
- authentication flow with tools like Capybara, and to not use any stubbing.
927
-
928
- In functional and integration tests you can just make requests to Rodauth
929
- routes:
1023
+ System (browser) tests for Rodauth actions could look something like this:
930
1024
 
931
1025
  ```rb
932
- # test/controllers/posts_controller_test.rb
933
- class PostsControllerTest < ActionDispatch::IntegrationTest
934
- test "should require authentication" do
935
- get posts_url
936
- assert_redirected_to "/login"
1026
+ # test/system/authentication_test.rb
1027
+ require "test_helper"
1028
+
1029
+ class AuthenticationTest < ActionDispatch::SystemTestCase
1030
+ include ActiveJob::TestHelper
1031
+ driven_by :rack_test
1032
+
1033
+ test "creating and verifying an account" do
1034
+ create_account
1035
+ assert_match "An email has been sent to you with a link to verify your account", page.text
1036
+
1037
+ verify_account
1038
+ assert_match "Your account has been verified", page.text
1039
+ end
1040
+
1041
+ test "logging in and logging out" do
1042
+ create_account(verify: true)
1043
+
1044
+ logout
1045
+ assert_match "You have been logged out", page.text
937
1046
 
938
1047
  login
939
- get posts_url
1048
+ assert_match "You have been logged in", page.text
1049
+ end
1050
+
1051
+ private
1052
+
1053
+ def create_account(email: "user@example.com", password: "secret", verify: false)
1054
+ visit "/create-account"
1055
+ fill_in "Login", with: email
1056
+ fill_in "Password", with: password
1057
+ fill_in "Confirm Password", with: password
1058
+ click_on "Create Account"
1059
+ verify_account if verify
1060
+ end
1061
+
1062
+ def verify_account
1063
+ perform_enqueued_jobs # run enqueued email deliveries
1064
+ email = ActionMailer::Base.deliveries.last
1065
+ verify_account_link = email.body.to_s[/\S+verify-account\S+/]
1066
+ visit verify_account_link
1067
+ click_on "Verify Account"
1068
+ end
1069
+
1070
+ def login(email: "user@example.com", password: "secret")
1071
+ visit "/login"
1072
+ fill_in "Login", with: email
1073
+ fill_in "Password", with: password
1074
+ click_on "Login"
1075
+ end
1076
+
1077
+ def logout
1078
+ visit "/logout"
1079
+ click_on "Logout"
1080
+ end
1081
+ end
1082
+ ```
1083
+
1084
+ While request tests in JSON API mode with JWT tokens could look something like
1085
+ this:
1086
+
1087
+ ```rb
1088
+ # test/integration/authentication_test.rb
1089
+ require "test_helper"
1090
+
1091
+ class AuthenticationTest < ActionDispatch::IntegrationTest
1092
+ test "creating and verifying an account" do
1093
+ create_account
940
1094
  assert_response :success
1095
+ assert_match "An email has been sent to you with a link to verify your account", JSON.parse(body)["success"]
1096
+
1097
+ verify_account
1098
+ assert_response :success
1099
+ assert_match "Your account has been verified", JSON.parse(body)["success"]
1100
+ end
1101
+
1102
+ test "logging in and logging out" do
1103
+ create_account(verify: true)
941
1104
 
942
1105
  logout
943
- assert_redirected_to "/login"
1106
+ assert_response :success
1107
+ assert_match "You have been logged out", JSON.parse(body)["success"]
1108
+
1109
+ login
1110
+ assert_response :success
1111
+ assert_match "You have been logged in", JSON.parse(body)["success"]
944
1112
  end
945
1113
 
946
1114
  private
947
1115
 
948
- def login(login: "user@example.com", password: "secret")
949
- post "/create-account", params: {
950
- "login" => login,
951
- "password" => password,
952
- "password-confirm" => password,
953
- }
954
-
955
- post "/login", params: {
956
- "login" => login,
957
- "password" => password,
958
- }
1116
+ def create_account(email: "user@example.com", password: "secret", verify: false)
1117
+ post "/create-account", as: :json, params: { login: email, password: password, "password-confirm": password }
1118
+ verify_account if verify
1119
+ end
1120
+
1121
+ def verify_account
1122
+ perform_enqueued_jobs # run enqueued email deliveries
1123
+ email = ActionMailer::Base.deliveries.last
1124
+ verify_account_key = email.body.to_s[/verify-account\?key=(\S+)/, 1]
1125
+ post "/verify-account", as: :json, params: { key: verify_account_key }
1126
+ end
1127
+
1128
+ def login(email: "user@example.com", password: "secret")
1129
+ post "/login", as: :json, params: { login: email, password: password }
959
1130
  end
960
1131
 
961
1132
  def logout
962
- post "/logout"
1133
+ post "/logout", as: :json, headers: { "Authorization" => headers["Authorization"] }
963
1134
  end
964
1135
  end
965
1136
  ```
966
1137
 
1138
+ If you're delivering emails in the background, make sure to set Active Job
1139
+ queue adapter to `:test` or `:inline`:
1140
+
1141
+ ```rb
1142
+ # config/environments/test.rb
1143
+ Rails.application.configure do |config|
1144
+ # ...
1145
+ config.active_job.queue_adapter = :test # or :inline
1146
+ # ...
1147
+ end
1148
+ ```
1149
+
1150
+ If you need to create an account record with a password directly, you can do it
1151
+ as follows:
1152
+
1153
+ ```rb
1154
+ # app/models/account.rb
1155
+ class Account < ApplicationRecord
1156
+ has_one :password_hash, foreign_key: :id
1157
+ end
1158
+ ```
1159
+ ```rb
1160
+ # app/models/account/password_hash.rb
1161
+ class Account::PasswordHash < ApplicationRecord
1162
+ belongs_to :account, foreign_key: :id
1163
+ end
1164
+ ```
1165
+ ```rb
1166
+ require "bcrypt"
1167
+
1168
+ account = Account.create!(email: "user@example.com", status: "verified")
1169
+ password_hash = BCrypt::Password.create("secret", cost: BCrypt::Engine::MIN_COST)
1170
+ account.create_password_hash!(id: account.id, password_hash: password_hash)
1171
+ ```
1172
+
967
1173
  ## Rodauth defaults
968
1174
 
969
1175
  rodauth-rails changes some of the default Rodauth settings for easier setup:
@@ -1044,6 +1250,18 @@ configure do
1044
1250
  end
1045
1251
  ```
1046
1252
 
1253
+ ### Deadline values
1254
+
1255
+ To simplify changes to the database schema, rodauth-rails configures Rodauth
1256
+ to set deadline values for various features in Ruby, instead of relying on
1257
+ the database to set default column values.
1258
+
1259
+ You can easily change this back:
1260
+
1261
+ ```rb
1262
+ set_deadline_values? false
1263
+ ```
1264
+
1047
1265
  ## License
1048
1266
 
1049
1267
  The gem is available as open source under the terms of the [MIT