rodauth-rails 0.4.2 → 0.8.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +39 -1
- data/README.md +419 -83
- data/lib/generators/rodauth/install_generator.rb +38 -23
- data/lib/generators/rodauth/migration/account_expiration.erb +7 -0
- data/lib/generators/rodauth/migration/active_sessions.erb +7 -0
- data/lib/generators/rodauth/migration/audit_logging.erb +16 -0
- data/lib/generators/rodauth/migration/base.erb +19 -0
- data/lib/generators/rodauth/migration/disallow_password_reuse.erb +5 -0
- data/lib/generators/rodauth/migration/email_auth.erb +7 -0
- data/lib/generators/rodauth/migration/jwt_refresh.erb +7 -0
- data/lib/generators/rodauth/migration/lockout.erb +11 -0
- data/lib/generators/rodauth/migration/otp.erb +7 -0
- data/lib/generators/rodauth/migration/password_expiration.erb +5 -0
- data/lib/generators/rodauth/migration/recovery_codes.erb +6 -0
- data/lib/generators/rodauth/migration/remember.erb +6 -0
- data/lib/generators/rodauth/migration/reset_password.erb +7 -0
- data/lib/generators/rodauth/migration/single_session.erb +5 -0
- data/lib/generators/rodauth/migration/sms_codes.erb +8 -0
- data/lib/generators/rodauth/migration/verify_account.erb +7 -0
- data/lib/generators/rodauth/migration/verify_login_change.erb +7 -0
- data/lib/generators/rodauth/migration/webauthn.erb +12 -0
- data/lib/generators/rodauth/migration_generator.rb +32 -0
- data/lib/generators/rodauth/migration_helpers.rb +69 -0
- data/lib/generators/rodauth/templates/app/controllers/rodauth_controller.rb +2 -1
- data/lib/generators/rodauth/templates/app/lib/rodauth_app.rb +18 -20
- data/lib/generators/rodauth/templates/config/initializers/sequel.rb +1 -5
- data/lib/generators/rodauth/templates/db/migrate/create_rodauth.rb +2 -176
- data/lib/rodauth/rails.rb +33 -4
- data/lib/rodauth/rails/app.rb +6 -2
- data/lib/rodauth/rails/app/flash.rb +1 -1
- data/lib/rodauth/rails/app/middleware.rb +26 -0
- data/lib/rodauth/rails/feature.rb +100 -30
- data/lib/rodauth/rails/railtie.rb +6 -0
- data/lib/rodauth/rails/tasks.rake +28 -0
- data/lib/rodauth/rails/version.rb +1 -1
- data/rodauth-rails.gemspec +1 -1
- metadata +26 -5
- data/lib/rodauth/features/rails.rb +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0f1312dbd1bb4dc0d954c77a5ff350b5c9e1ff3fc4dd45b8834cd3e7d0280a22
|
4
|
+
data.tar.gz: 5dda5720126361589a428add9b8256b35aa53644166ca7a8a6d14c5baef53f02
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f70e5a44db25c016fe92169be342d1f489cd0e3307fe6c06dbe822c28c05f55dc696b26721836d315daabbbfb0889d18357cec3bb7aa52932649f5ecb08ceedb
|
7
|
+
data.tar.gz: 6af3cd43f9266729049d984c9da58beff019dd2f0148465d65c8f814602d9a9678308d752ef469f08ab381a1373b919d1e61b62b204751d95ca57d10ed05de2a
|
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,42 @@
|
|
1
|
-
|
1
|
+
## 0.8.0 (2021-01-03)
|
2
|
+
|
3
|
+
* Add `--api` option to `rodauth:install` generator for choosing JSON-only configuration (@janko)
|
4
|
+
|
5
|
+
* Don't blow up when a Rodauth request is made using an unsupported HTTP verb (@janko)
|
6
|
+
|
7
|
+
## 0.7.0 (2020-11-27)
|
8
|
+
|
9
|
+
* Add `#rails_controller_eval` method for running code in context of a controller instance (@janko)
|
10
|
+
|
11
|
+
* Detect `secret_key_base` from credentials and `$SECRET_KEY_BASE` environment variable (@janko)
|
12
|
+
|
13
|
+
## 0.6.1 (2020-11-25)
|
14
|
+
|
15
|
+
* Generate the Rodauth controller for API-only Rails apps as well (@janko)
|
16
|
+
|
17
|
+
* Fix remember cookie deadline not extending in remember feature (@janko)
|
18
|
+
|
19
|
+
## 0.6.0 (2020-11-22)
|
20
|
+
|
21
|
+
* Add `Rodauth::Rails.rodauth` method for retrieving Rodauth instance outside of request context (@janko)
|
22
|
+
|
23
|
+
* Add default Action Dispatch response headers in Rodauth responses (@janko)
|
24
|
+
|
25
|
+
* Run controller rescue handlers around Rodauth actions (@janko)
|
26
|
+
|
27
|
+
* Run controller action callbacks around Rodauth actions (@janko)
|
28
|
+
|
29
|
+
## 0.5.0 (2020-11-16)
|
30
|
+
|
31
|
+
* Support more Active Record adapters in `rodauth:install` generator (@janko)
|
32
|
+
|
33
|
+
* Add `rodauth:migration` generator for creating tables of specified features (@janko)
|
34
|
+
|
35
|
+
* Use UUIDs for primary keys if so configured in Rails generators (@janko)
|
36
|
+
|
37
|
+
* Add `rodauth:routes` rake task for printing routes handled by Rodauth middleware (@janko)
|
38
|
+
|
39
|
+
## 0.4.2 (2020-11-08)
|
2
40
|
|
3
41
|
* Drop support for Ruby 2.2 (@janko)
|
4
42
|
|
data/README.md
CHANGED
@@ -4,16 +4,57 @@ Provides Rails integration for the [Rodauth] authentication framework.
|
|
4
4
|
|
5
5
|
## Resources
|
6
6
|
|
7
|
+
Useful links:
|
8
|
+
|
7
9
|
* [Rodauth documentation](http://rodauth.jeremyevans.net/documentation.html)
|
8
|
-
* [rodauth-rails wiki](https://github.com/janko/rodauth-rails/wiki)
|
9
10
|
* [Rails demo](https://github.com/janko/rodauth-demo-rails)
|
10
11
|
|
12
|
+
Articles:
|
13
|
+
|
14
|
+
* [Rodauth: A Refreshing Authentication Solution for Ruby](https://janko.io/rodauth-a-refreshing-authentication-solution-for-ruby/)
|
15
|
+
* [Adding Authentication in Rails with Rodauth](https://janko.io/adding-authentication-in-rails-with-rodauth/)
|
16
|
+
* [Adding Multifactor Authentication in Rails with Rodauth](https://janko.io/adding-multifactor-authentication-in-rails-with-rodauth/)
|
17
|
+
|
18
|
+
## Why Rodauth?
|
19
|
+
|
20
|
+
There are already several popular authentication solutions for Rails (Devise,
|
21
|
+
Sorcery, Clearance, Authlogic), so why would you choose Rodauth? Well, because
|
22
|
+
it has many advantages over the mentioned alternatives:
|
23
|
+
|
24
|
+
* multifactor authentication ([TOTP][otp], [SMS codes][sms_codes], [recovery codes][recovery_codes], [WebAuthn][webauthn])
|
25
|
+
* standardized [JSON API support][jwt] (for every feature)
|
26
|
+
* enterprise security features ([password complexity][password_complexity], [disallow password reuse][disallow_password_reuse], [password expiration][password_expiration], [session expiration][session_expiration], [single session][single_session], [account expiration][account_expiration])
|
27
|
+
* [email authentication][email_auth] (aka "passwordless")
|
28
|
+
* [audit logging][audit_logging] (for any action)
|
29
|
+
* ability to protect password hashes even in case of SQL injection ([more details][password protection])
|
30
|
+
* additional bruteforce protection for tokens ([more details][bruteforce tokens])
|
31
|
+
* uniform configuration DSL (any setting can be static or dynamic)
|
32
|
+
* consistent before/after hooks around everything
|
33
|
+
* dedicated object encapsulating all authentication logic
|
34
|
+
|
35
|
+
## Upgrading
|
36
|
+
|
37
|
+
### Upgrading to 0.7.0
|
38
|
+
|
39
|
+
Starting from version 0.7.0, rodauth-rails now correctly detects Rails
|
40
|
+
application's `secret_key_base` when setting default `hmac_secret`, including
|
41
|
+
when it's set via credentials or `$SECRET_KEY_BASE` environment variable. This
|
42
|
+
means that your authentication will now be more secure by default, and Rodauth
|
43
|
+
features that require `hmac_secret` should now work automatically as well.
|
44
|
+
|
45
|
+
However, if you've already been using rodauth-rails in production, where the
|
46
|
+
`secret_key_base` is set via credentials or environment variable and `hmac_secret`
|
47
|
+
was not explicitly set, the fact that your authentication will now start using
|
48
|
+
HMACs has backwards compatibility considerations. See the [Rodauth
|
49
|
+
documentation][hmac] for instructions on how to safely transition, or just set
|
50
|
+
`hmac_secret nil` in your Rodauth configuration.
|
51
|
+
|
11
52
|
## Installation
|
12
53
|
|
13
54
|
Add the gem to your Gemfile:
|
14
55
|
|
15
56
|
```rb
|
16
|
-
gem "rodauth-rails", "~> 0.
|
57
|
+
gem "rodauth-rails", "~> 0.6"
|
17
58
|
|
18
59
|
# gem "jwt", require: false # for JWT feature
|
19
60
|
# gem "rotp", require: false # for OTP feature
|
@@ -25,10 +66,17 @@ Then run `bundle install`.
|
|
25
66
|
|
26
67
|
Next, run the install generator:
|
27
68
|
|
28
|
-
```
|
69
|
+
```sh
|
29
70
|
$ rails generate rodauth:install
|
30
71
|
```
|
31
72
|
|
73
|
+
Or if you want Rodauth endpoints to be exposed via JSON API:
|
74
|
+
|
75
|
+
```sh
|
76
|
+
$ rails generate rodauth:install --api
|
77
|
+
$ bundle add jwt
|
78
|
+
```
|
79
|
+
|
32
80
|
The generator will create the following files:
|
33
81
|
|
34
82
|
* Rodauth migration at `db/migrate/*_create_rodauth.rb`
|
@@ -54,7 +102,6 @@ class CreateRodauth < ActiveRecord::Migration
|
|
54
102
|
create_table :account_verification_keys do |t| ... end
|
55
103
|
create_table :account_login_change_keys do |t| ... end
|
56
104
|
create_table :account_remember_keys do |t| ... end
|
57
|
-
# ...
|
58
105
|
end
|
59
106
|
end
|
60
107
|
```
|
@@ -88,7 +135,7 @@ ActiveRecord connection.
|
|
88
135
|
require "sequel/core"
|
89
136
|
|
90
137
|
# initialize Sequel and have it reuse Active Record's database connection
|
91
|
-
DB = Sequel.
|
138
|
+
DB = Sequel.connect("postgresql://", extensions: :activerecord_connection)
|
92
139
|
```
|
93
140
|
|
94
141
|
### Rodauth app
|
@@ -112,8 +159,9 @@ end
|
|
112
159
|
|
113
160
|
### Controller
|
114
161
|
|
115
|
-
Your Rodauth app will by default use `RodauthController` for view rendering
|
116
|
-
and
|
162
|
+
Your Rodauth app will by default use `RodauthController` for view rendering,
|
163
|
+
CSRF protection, and running controller callbacks and rescue handlers around
|
164
|
+
Rodauth actions.
|
117
165
|
|
118
166
|
```rb
|
119
167
|
# app/controllers/rodauth_controller.rb
|
@@ -121,7 +169,7 @@ class RodauthController < ApplicationController
|
|
121
169
|
end
|
122
170
|
```
|
123
171
|
|
124
|
-
### Account
|
172
|
+
### Account model
|
125
173
|
|
126
174
|
Rodauth stores user accounts in the `accounts` table, so the generator will
|
127
175
|
also create an `Account` model for custom use.
|
@@ -132,56 +180,83 @@ class Account < ApplicationRecord
|
|
132
180
|
end
|
133
181
|
```
|
134
182
|
|
135
|
-
##
|
183
|
+
## Usage
|
136
184
|
|
137
|
-
|
138
|
-
|
185
|
+
### Routes
|
186
|
+
|
187
|
+
We can see the list of routes our Rodauth middleware handles:
|
188
|
+
|
189
|
+
```sh
|
190
|
+
$ rails rodauth:routes
|
191
|
+
```
|
192
|
+
```
|
193
|
+
Routes handled by RodauthApp:
|
194
|
+
|
195
|
+
/login rodauth.login_path
|
196
|
+
/create-account rodauth.create_account_path
|
197
|
+
/verify-account-resend rodauth.verify_account_resend_path
|
198
|
+
/verify-account rodauth.verify_account_path
|
199
|
+
/change-password rodauth.change_password_path
|
200
|
+
/change-login rodauth.change_login_path
|
201
|
+
/logout rodauth.logout_path
|
202
|
+
/remember rodauth.remember_path
|
203
|
+
/reset-password-request rodauth.reset_password_request_path
|
204
|
+
/reset-password rodauth.reset_password_path
|
205
|
+
/verify-login-change rodauth.verify_login_change_path
|
206
|
+
/close-account rodauth.close_account_path
|
207
|
+
```
|
208
|
+
|
209
|
+
Using this information, we could add some basic authentication links to our
|
210
|
+
navigation header:
|
139
211
|
|
140
212
|
```erb
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
<% end %>
|
148
|
-
</ul>
|
213
|
+
<% if rodauth.logged_in? %>
|
214
|
+
<%= link_to "Sign out", rodauth.logout_path, method: :post %>
|
215
|
+
<% else %>
|
216
|
+
<%= link_to "Sign in", rodauth.login_path %>
|
217
|
+
<%= link_to "Sign up", rodauth.create_account_path %>
|
218
|
+
<% end %>
|
149
219
|
```
|
150
220
|
|
151
|
-
These
|
221
|
+
These routes are fully functional, feel free to visit them and interact with the
|
152
222
|
pages. The templates that ship with Rodauth aim to provide a complete
|
153
223
|
authentication experience, and the forms use [Bootstrap] markup.
|
154
224
|
|
155
|
-
|
156
|
-
|
225
|
+
### Current account
|
226
|
+
|
227
|
+
To be able to fetch currently authenticated account, let's define a
|
228
|
+
`#current_account` method that fetches the account id from session and
|
229
|
+
retrieves the corresponding account record:
|
157
230
|
|
158
231
|
```rb
|
159
232
|
# app/controllers/application_controller.rb
|
160
233
|
class ApplicationController < ActionController::Base
|
161
|
-
before_action :
|
234
|
+
before_action :current_account, if: -> { rodauth.logged_in? }
|
162
235
|
|
163
236
|
private
|
164
237
|
|
165
|
-
def
|
166
|
-
@current_account
|
238
|
+
def current_account
|
239
|
+
@current_account ||= Account.find(rodauth.session_value)
|
167
240
|
rescue ActiveRecord::RecordNotFound
|
168
241
|
rodauth.logout
|
169
242
|
rodauth.login_required
|
170
243
|
end
|
171
|
-
|
172
|
-
attr_reader :current_account
|
173
244
|
helper_method :current_account
|
174
245
|
end
|
175
246
|
```
|
247
|
+
|
248
|
+
This allows us to access the current account in controllers and views:
|
249
|
+
|
176
250
|
```erb
|
177
251
|
<p>Authenticated as: <%= current_account.email %></p>
|
178
252
|
```
|
179
253
|
|
180
254
|
### Requiring authentication
|
181
255
|
|
182
|
-
|
183
|
-
|
184
|
-
the authentication logic
|
256
|
+
We'll likely want to require authentication for certain parts of our app,
|
257
|
+
redirecting the user to the login page if they're not logged in. We can do this
|
258
|
+
in our Rodauth app's routing block, which helps keep the authentication logic
|
259
|
+
encapsulated:
|
185
260
|
|
186
261
|
```rb
|
187
262
|
# app/lib/rodauth_app.rb
|
@@ -239,9 +314,9 @@ end
|
|
239
314
|
|
240
315
|
### Views
|
241
316
|
|
242
|
-
The templates built into Rodauth are useful when getting started, but
|
243
|
-
|
244
|
-
|
317
|
+
The templates built into Rodauth are useful when getting started, but soon
|
318
|
+
you'll want to start editing the markup. You can run the following command to
|
319
|
+
copy Rodauth templates into your Rails app:
|
245
320
|
|
246
321
|
```sh
|
247
322
|
$ rails generate rodauth:views
|
@@ -265,7 +340,7 @@ $ rails generate rodauth:views --all
|
|
265
340
|
```
|
266
341
|
|
267
342
|
You can also tell the generator to create views into another directory (in this
|
268
|
-
case
|
343
|
+
case make sure to rename the Rodauth controller accordingly):
|
269
344
|
|
270
345
|
```sh
|
271
346
|
# generates views into app/views/authentication
|
@@ -300,8 +375,8 @@ end
|
|
300
375
|
|
301
376
|
### Mailer
|
302
377
|
|
303
|
-
Rodauth may send emails as part of
|
304
|
-
can be customized:
|
378
|
+
Depending on the features you've enabled, Rodauth may send emails as part of
|
379
|
+
the authentication flow. Most email settings can be customized:
|
305
380
|
|
306
381
|
```rb
|
307
382
|
# app/lib/rodauth_app.rb
|
@@ -330,7 +405,7 @@ $ rails generate rodauth:mailer
|
|
330
405
|
```
|
331
406
|
|
332
407
|
This will create a `RodauthMailer` with the associated mailer views in
|
333
|
-
`app/views/rodauth_mailer` directory
|
408
|
+
`app/views/rodauth_mailer` directory:
|
334
409
|
|
335
410
|
```rb
|
336
411
|
# app/mailers/rodauth_mailer.rb
|
@@ -345,8 +420,8 @@ end
|
|
345
420
|
```
|
346
421
|
|
347
422
|
You can then uncomment the lines in your Rodauth configuration to have it call
|
348
|
-
your mailer. If you've enabled additional authentication features
|
349
|
-
override their `
|
423
|
+
your mailer. If you've enabled additional authentication features that send
|
424
|
+
emails, make sure to override their `create_*_email` methods as well.
|
350
425
|
|
351
426
|
```rb
|
352
427
|
# app/lib/rodauth_app.rb
|
@@ -354,67 +429,102 @@ class RodauthApp < Rodauth::Rails::App
|
|
354
429
|
# ...
|
355
430
|
configure do
|
356
431
|
# ...
|
357
|
-
|
358
|
-
|
432
|
+
create_reset_password_email do
|
433
|
+
RodauthMailer.reset_password(email_to, reset_password_email_link)
|
359
434
|
end
|
360
|
-
|
361
|
-
|
435
|
+
create_verify_account_email do
|
436
|
+
RodauthMailer.verify_account(email_to, verify_account_email_link)
|
362
437
|
end
|
363
|
-
|
364
|
-
|
438
|
+
create_verify_login_change_email do |login|
|
439
|
+
RodauthMailer.verify_login_change(login, verify_login_change_old_login, verify_login_change_new_login, verify_login_change_email_link)
|
365
440
|
end
|
366
|
-
|
367
|
-
|
441
|
+
create_password_changed_email do
|
442
|
+
RodauthMailer.password_changed(email_to)
|
368
443
|
end
|
369
|
-
#
|
370
|
-
#
|
444
|
+
# create_email_auth_email do
|
445
|
+
# RodauthMailer.email_auth(email_to, email_auth_email_link)
|
371
446
|
# end
|
372
|
-
#
|
373
|
-
#
|
447
|
+
# create_unlock_account_email do
|
448
|
+
# RodauthMailer.unlock_account(email_to, unlock_account_email_link)
|
374
449
|
# end
|
375
|
-
|
450
|
+
send_email do |email|
|
376
451
|
# queue email delivery on the mailer after the transaction commits
|
377
|
-
|
378
|
-
db.after_commit do
|
379
|
-
RodauthMailer.public_send(type, *args).deliver_later
|
380
|
-
end
|
381
|
-
end
|
452
|
+
db.after_commit { email.deliver_later }
|
382
453
|
end
|
383
454
|
# ...
|
384
455
|
end
|
385
456
|
end
|
386
457
|
```
|
387
458
|
|
388
|
-
|
459
|
+
This approach can be used even if you're using a 3rd-party service for
|
460
|
+
transactional emails, where emails are sent via HTTP instead of SMTP. Whatever
|
461
|
+
the `create_*_email` block returns will be passed to `send_email`, so you can
|
462
|
+
be creative.
|
463
|
+
|
464
|
+
### Migrations
|
389
465
|
|
390
|
-
|
391
|
-
|
466
|
+
The install generator will create a migration for tables used by the Rodauth
|
467
|
+
features enabled by default. For any additional features, you can use the
|
468
|
+
migration generator to create the corresponding tables:
|
392
469
|
|
470
|
+
```sh
|
471
|
+
$ rails generate rodauth:migration otp sms_codes recovery_codes
|
472
|
+
```
|
393
473
|
```rb
|
394
|
-
|
474
|
+
# db/migration/*_create_rodauth_otp_sms_codes_recovery_codes.rb
|
475
|
+
class CreateRodauthOtpSmsCodesRecoveryCodes < ActiveRecord::Migration
|
476
|
+
def change
|
477
|
+
create_table :account_otp_keys do |t| ... end
|
478
|
+
create_table :account_sms_codes do |t| ... end
|
479
|
+
create_table :account_recovery_codes do |t| ... end
|
480
|
+
end
|
481
|
+
end
|
395
482
|
```
|
396
483
|
|
397
|
-
|
398
|
-
via JSON requests (in addition to HTML requests):
|
484
|
+
### Calling controller methods
|
399
485
|
|
486
|
+
When using Rodauth before/after hooks or generally overriding your Rodauth
|
487
|
+
configuration, in some cases you might want to call methods defined on your
|
488
|
+
controllers. You can do so with `rails_controller_eval`, for example:
|
489
|
+
|
490
|
+
```rb
|
491
|
+
# app/controllers/application_controller.rb
|
492
|
+
class ApplicationController < ActionController::Base
|
493
|
+
private
|
494
|
+
def setup_tracking(account_id)
|
495
|
+
# ... some implementation ...
|
496
|
+
end
|
497
|
+
end
|
498
|
+
```
|
400
499
|
```rb
|
401
500
|
# app/lib/rodauth_app.rb
|
402
501
|
class RodauthApp < Rodauth::Rails::App
|
403
|
-
configure
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
# ...
|
502
|
+
configure do
|
503
|
+
after_create_account do
|
504
|
+
rails_controller_eval { setup_tracking(account_id) }
|
505
|
+
end
|
408
506
|
end
|
409
507
|
end
|
410
508
|
```
|
411
509
|
|
412
|
-
|
413
|
-
|
414
|
-
|
510
|
+
### Rodauth instance
|
511
|
+
|
512
|
+
In some cases you might need to use Rodauth more programmatically, and perform
|
513
|
+
Rodauth operations outside of the request context. rodauth-rails gives you the
|
514
|
+
ability to retrieve the Rodauth instance:
|
515
|
+
|
516
|
+
```rb
|
517
|
+
rodauth = Rodauth::Rails.rodauth # or Rodauth::Rails.rodauth(:secondary)
|
518
|
+
|
519
|
+
rodauth.login_url #=> "https://example.com/login"
|
520
|
+
rodauth.account_from_login("user@example.com") # loads user by email
|
521
|
+
rodauth.password_match?("secret") #=> true
|
522
|
+
rodauth.setup_account_verification
|
523
|
+
rodauth.close_account
|
524
|
+
```
|
415
525
|
|
416
|
-
|
417
|
-
|
526
|
+
This Rodauth instance will be initialized with basic Rack env that allows is it
|
527
|
+
to generate URLs, using `config.action_mailer.default_url_options` options.
|
418
528
|
|
419
529
|
## How it works
|
420
530
|
|
@@ -460,19 +570,45 @@ end
|
|
460
570
|
The `Rodauth::Rails::App` class is a [Roda] subclass that provides Rails
|
461
571
|
integration for Rodauth:
|
462
572
|
|
463
|
-
* uses
|
464
|
-
* uses
|
573
|
+
* uses Action Dispatch flash instead of Roda's
|
574
|
+
* uses Action Dispatch CSRF protection instead of Roda's
|
465
575
|
* sets [HMAC] secret to Rails' secret key base
|
466
|
-
* uses
|
467
|
-
*
|
576
|
+
* uses Action Controller for rendering templates
|
577
|
+
* runs Action Controller callbacks & rescue handlers around Rodauth actions
|
578
|
+
* uses Action Mailer for sending emails
|
468
579
|
|
469
|
-
The `configure
|
580
|
+
The `configure` method wraps configuring the Rodauth plugin, forwarding
|
470
581
|
any additional [plugin options].
|
471
582
|
|
472
583
|
```rb
|
473
|
-
|
474
|
-
configure
|
475
|
-
configure(:
|
584
|
+
class RodauthApp < Rodauth::Rails::App
|
585
|
+
configure { ... } # defining default Rodauth configuration
|
586
|
+
configure(json: true) { ... } # passing options to the Rodauth plugin
|
587
|
+
configure(:secondary) { ... } # defining multiple Rodauth configurations
|
588
|
+
end
|
589
|
+
```
|
590
|
+
|
591
|
+
The `route` block is provided by Roda, and it's called on each request before
|
592
|
+
it reaches the Rails router.
|
593
|
+
|
594
|
+
```rb
|
595
|
+
class RodauthApp < Rodauth::Rails::App
|
596
|
+
route do |r|
|
597
|
+
# ... called before each request ...
|
598
|
+
end
|
599
|
+
end
|
600
|
+
```
|
601
|
+
|
602
|
+
Since `Rodauth::Rails::App` is just a Roda subclass, you can do anything you
|
603
|
+
would with a Roda app, such as loading additional Roda plugins:
|
604
|
+
|
605
|
+
```rb
|
606
|
+
class RodauthApp < Rodauth::Rails::App
|
607
|
+
plugin :request_headers # easier access to request headers
|
608
|
+
plugin :typecast_params # methods for conversion of request params
|
609
|
+
plugin :default_headers, { "Foo" => "Bar" }
|
610
|
+
# ...
|
611
|
+
end
|
476
612
|
```
|
477
613
|
|
478
614
|
### Sequel
|
@@ -488,6 +624,142 @@ connection (using the [sequel-activerecord_connection] gem).
|
|
488
624
|
This means that, from the usage perspective, Sequel can be considered just
|
489
625
|
as an implementation detail of Rodauth.
|
490
626
|
|
627
|
+
## JSON API
|
628
|
+
|
629
|
+
JSON API support in Rodauth is provided by the [JWT feature][jwt]. You'll need
|
630
|
+
to install the [JWT gem], enable JSON support and enable the JWT feature:
|
631
|
+
|
632
|
+
```sh
|
633
|
+
$ bundle add jwt
|
634
|
+
```
|
635
|
+
```rb
|
636
|
+
# app/lib/rodauth_app.rb
|
637
|
+
class RodauthApp < Rodauth::Rails::App
|
638
|
+
configure(json: :only) do
|
639
|
+
# ...
|
640
|
+
enable :jwt
|
641
|
+
# make sure to store the JWT secret below in a safe place
|
642
|
+
jwt_secret "...your secret key..."
|
643
|
+
# ...
|
644
|
+
end
|
645
|
+
end
|
646
|
+
```
|
647
|
+
|
648
|
+
With the above configuration, Rodauth routes will only be accessible via JSON
|
649
|
+
requests. If you still want to allow HTML access alongside JSON, change `json:
|
650
|
+
:only` to `json: true`.
|
651
|
+
|
652
|
+
Emails will automatically work in JSON-only mode, because `Rodauth::Rails::App`
|
653
|
+
comes with Roda's `render` plugin loaded. They are customized the same as in
|
654
|
+
the non-JSON case.
|
655
|
+
|
656
|
+
## OmniAuth
|
657
|
+
|
658
|
+
While Rodauth doesn't yet come with [OmniAuth] integration, we can build one
|
659
|
+
ourselves using the existing Rodauth API.
|
660
|
+
|
661
|
+
In order to allow the user to login via multiple external providers, let's
|
662
|
+
create an `account_identities` table that will have a many-to-one relationship
|
663
|
+
with the `accounts` table:
|
664
|
+
|
665
|
+
```sh
|
666
|
+
$ rails generate model AccountIdentity
|
667
|
+
```
|
668
|
+
```rb
|
669
|
+
# db/migrate/*_create_account_identities.rb
|
670
|
+
class CreateAccountIdentities < ActiveRecord::Migration
|
671
|
+
def change
|
672
|
+
create_table :account_identities do |t|
|
673
|
+
t.references :account, null: false, foreign_key: { on_delete: :cascade }
|
674
|
+
t.string :provider, null: false
|
675
|
+
t.string :uid, null: false
|
676
|
+
t.jsonb :info, null: false, default: {} # adjust JSON column type for your database
|
677
|
+
|
678
|
+
t.timestamps
|
679
|
+
|
680
|
+
t.index [:provider, :uid], unique: true
|
681
|
+
end
|
682
|
+
end
|
683
|
+
end
|
684
|
+
```
|
685
|
+
```rb
|
686
|
+
# app/models/account_identity.rb
|
687
|
+
class AcccountIdentity < ApplicationRecord
|
688
|
+
belongs_to :account
|
689
|
+
end
|
690
|
+
```
|
691
|
+
```rb
|
692
|
+
# app/models/account.rb
|
693
|
+
class Account < ApplicationRecord
|
694
|
+
has_many :identities, class_name: "AccountIdentity"
|
695
|
+
end
|
696
|
+
```
|
697
|
+
|
698
|
+
Let's assume we want to implement Facebook login, and have added the
|
699
|
+
corresponding OmniAuth strategy to the middleware stack, together with an
|
700
|
+
authorization link on the login form:
|
701
|
+
|
702
|
+
```rb
|
703
|
+
Rails.application.config.middleware.use OmniAuth::Builder do
|
704
|
+
provider :facebook, ENV["FACEBOOK_APP_ID"], ENV["FACEBOOK_APP_SECRET"],
|
705
|
+
scope: "email", callback_path: "/auth/facebook/callback"
|
706
|
+
end
|
707
|
+
```
|
708
|
+
```erb
|
709
|
+
<%= link_to "Login via Facebook", "/auth/facebook" %>
|
710
|
+
```
|
711
|
+
|
712
|
+
Let's implement the OmniAuth callback endpoint on our Rodauth controller:
|
713
|
+
|
714
|
+
```rb
|
715
|
+
# config/routes.rb
|
716
|
+
Rails.application.routes.draw do
|
717
|
+
# ...
|
718
|
+
get "/auth/:provider/callback", to: "rodauth#omniauth"
|
719
|
+
end
|
720
|
+
```
|
721
|
+
```rb
|
722
|
+
# app/controllres/rodauth_controller.rb
|
723
|
+
class RodauthController < ApplicationController
|
724
|
+
def omniauth
|
725
|
+
auth = request.env["omniauth.auth"]
|
726
|
+
|
727
|
+
# attempt to find existing identity directly
|
728
|
+
identity = AccountIdentity.find_by(provider: auth["provider"], uid: auth["uid"])
|
729
|
+
|
730
|
+
if identity
|
731
|
+
# update any external info changes
|
732
|
+
identity.update!(info: auth["info"])
|
733
|
+
# set account from identity
|
734
|
+
account = identity.account
|
735
|
+
end
|
736
|
+
|
737
|
+
# attempt to find an existing account by email
|
738
|
+
account ||= Account.find_by(email: auth["info"]["email"])
|
739
|
+
|
740
|
+
# disallow login if account is not verified
|
741
|
+
if account && account.status != rodauth.account_open_status_value
|
742
|
+
redirect_to rodauth.login_path, alert: rodauth.unverified_account_message
|
743
|
+
return
|
744
|
+
end
|
745
|
+
|
746
|
+
# create new account if it doesn't exist
|
747
|
+
unless account
|
748
|
+
account = Account.create!(email: auth["info"]["email"])
|
749
|
+
end
|
750
|
+
|
751
|
+
# create new identity if it doesn't exist
|
752
|
+
unless identity
|
753
|
+
account.identities.create!(provider: auth["provider"], uid: auth["uid"], info: auth["info"])
|
754
|
+
end
|
755
|
+
|
756
|
+
# login with Rodauth
|
757
|
+
rodauth.account_from_login(account.email)
|
758
|
+
rodauth.login("omniauth")
|
759
|
+
end
|
760
|
+
end
|
761
|
+
```
|
762
|
+
|
491
763
|
## Configuring
|
492
764
|
|
493
765
|
For the list of configuration methods provided by Rodauth, see the [feature
|
@@ -521,6 +793,37 @@ Rodauth::Rails.configure do |config|
|
|
521
793
|
end
|
522
794
|
```
|
523
795
|
|
796
|
+
## Custom extensions
|
797
|
+
|
798
|
+
When developing custom extensions for Rodauth inside your Rails project, it's
|
799
|
+
better to use plain modules (at least in the beginning), because Rodauth
|
800
|
+
feature API doesn't yet support Zeitwerk reloading well.
|
801
|
+
|
802
|
+
```rb
|
803
|
+
# app/lib/rodauth_argon2.rb
|
804
|
+
module RodauthArgon2
|
805
|
+
def password_hash(password)
|
806
|
+
Argon2::Password.create(password, t_cost: password_hash_cost, m_cost: password_hash_cost)
|
807
|
+
end
|
808
|
+
|
809
|
+
def password_hash_match?(hash, password)
|
810
|
+
Argon2::Password.verify_password(password, hash)
|
811
|
+
end
|
812
|
+
end
|
813
|
+
```
|
814
|
+
```rb
|
815
|
+
# app/lib/rodauth_app.rb
|
816
|
+
class RodauthApp < Rodauth::Rails::App
|
817
|
+
configure do
|
818
|
+
# ...
|
819
|
+
auth_class_eval do
|
820
|
+
include RodauthArgon2
|
821
|
+
end
|
822
|
+
# ...
|
823
|
+
end
|
824
|
+
end
|
825
|
+
```
|
826
|
+
|
524
827
|
## Testing
|
525
828
|
|
526
829
|
If you're writing system tests, it's generally better to go through the actual
|
@@ -588,6 +891,24 @@ disables the use of database functions, though you can always turn it back on.
|
|
588
891
|
use_database_authentication_functions? true
|
589
892
|
```
|
590
893
|
|
894
|
+
To create the database functions, pass the Sequel database object into the
|
895
|
+
Rodauth method for creating database functions:
|
896
|
+
|
897
|
+
```rb
|
898
|
+
# db/migrate/*_create_rodauth_database_functions.rb
|
899
|
+
require "rodauth/migrations"
|
900
|
+
|
901
|
+
class CreateRodauthDatabaseFunctions < ActiveRecord::Migration
|
902
|
+
def up
|
903
|
+
Rodauth.create_database_authentication_functions(DB)
|
904
|
+
end
|
905
|
+
|
906
|
+
def down
|
907
|
+
Rodauth.drop_database_authentication_functions(DB)
|
908
|
+
end
|
909
|
+
end
|
910
|
+
```
|
911
|
+
|
591
912
|
### Account statuses
|
592
913
|
|
593
914
|
The recommended [Rodauth migration] stores possible account status values in a
|
@@ -640,9 +961,7 @@ conduct](https://github.com/janko/rodauth-rails/blob/master/CODE_OF_CONDUCT.md).
|
|
640
961
|
|
641
962
|
[Rodauth]: https://github.com/jeremyevans/rodauth
|
642
963
|
[Sequel]: https://github.com/jeremyevans/sequel
|
643
|
-
[rendering views outside of controllers]: https://blog.bigbinary.com/2016/01/08/rendering-views-outside-of-controllers-in-rails-5.html
|
644
964
|
[feature documentation]: http://rodauth.jeremyevans.net/documentation.html
|
645
|
-
[JWT feature]: http://rodauth.jeremyevans.net/rdoc/files/doc/jwt_rdoc.html
|
646
965
|
[JWT gem]: https://github.com/jwt/ruby-jwt
|
647
966
|
[Bootstrap]: https://getbootstrap.com/
|
648
967
|
[Roda]: http://roda.jeremyevans.net/
|
@@ -651,3 +970,20 @@ conduct](https://github.com/janko/rodauth-rails/blob/master/CODE_OF_CONDUCT.md).
|
|
651
970
|
[Rodauth migration]: http://rodauth.jeremyevans.net/rdoc/files/README_rdoc.html#label-Creating+tables
|
652
971
|
[sequel-activerecord_connection]: https://github.com/janko/sequel-activerecord_connection
|
653
972
|
[plugin options]: http://rodauth.jeremyevans.net/rdoc/files/README_rdoc.html#label-Plugin+Options
|
973
|
+
[hmac]: http://rodauth.jeremyevans.net/rdoc/files/README_rdoc.html#label-HMAC
|
974
|
+
[OmniAuth]: https://github.com/omniauth/omniauth
|
975
|
+
[otp]: http://rodauth.jeremyevans.net/rdoc/files/doc/otp_rdoc.html
|
976
|
+
[sms_codes]: http://rodauth.jeremyevans.net/rdoc/files/doc/sms_codes_rdoc.html
|
977
|
+
[recovery_codes]: http://rodauth.jeremyevans.net/rdoc/files/doc/recovery_codes_rdoc.html
|
978
|
+
[webauthn]: http://rodauth.jeremyevans.net/rdoc/files/doc/webauthn_rdoc.html
|
979
|
+
[jwt]: http://rodauth.jeremyevans.net/rdoc/files/doc/jwt_rdoc.html
|
980
|
+
[email_auth]: http://rodauth.jeremyevans.net/rdoc/files/doc/email_auth_rdoc.html
|
981
|
+
[audit_logging]: http://rodauth.jeremyevans.net/rdoc/files/doc/audit_logging_rdoc.html
|
982
|
+
[password protection]: https://github.com/jeremyevans/rodauth#label-Password+Hash+Access+Via+Database+Functions
|
983
|
+
[bruteforce tokens]: https://github.com/jeremyevans/rodauth#label-Tokens
|
984
|
+
[password_complexity]: http://rodauth.jeremyevans.net/rdoc/files/doc/password_complexity_rdoc.html
|
985
|
+
[disallow_password_reuse]: http://rodauth.jeremyevans.net/rdoc/files/doc/disallow_password_reuse_rdoc.html
|
986
|
+
[password_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/password_expiration_rdoc.html
|
987
|
+
[session_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/session_expiration_rdoc.html
|
988
|
+
[single_session]: http://rodauth.jeremyevans.net/rdoc/files/doc/single_session_rdoc.html
|
989
|
+
[account_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/account_expiration_rdoc.html
|