rodauth-rails 0.4.2 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|