rodauth-rails 0.8.1 → 0.11.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 +44 -0
- data/README.md +446 -108
- data/lib/generators/rodauth/install_generator.rb +26 -15
- data/lib/generators/rodauth/migration/base.erb +2 -2
- data/lib/generators/rodauth/templates/app/lib/rodauth_app.rb +50 -49
- data/lib/generators/rodauth/templates/app/mailers/rodauth_mailer.rb +3 -3
- data/lib/generators/rodauth/templates/app/views/rodauth_mailer/unlock_account.text.erb +1 -1
- data/lib/rodauth/rails.rb +20 -0
- data/lib/rodauth/rails/app.rb +23 -31
- data/lib/rodauth/rails/app/flash.rb +7 -11
- data/lib/rodauth/rails/app/middleware.rb +20 -10
- data/lib/rodauth/rails/auth.rb +40 -0
- data/lib/rodauth/rails/controller_methods.rb +1 -5
- data/lib/rodauth/rails/feature.rb +43 -10
- data/lib/rodauth/rails/log_subscriber.rb +34 -0
- data/lib/rodauth/rails/railtie.rb +5 -0
- data/lib/rodauth/rails/version.rb +1 -1
- data/rodauth-rails.gemspec +1 -1
- metadata +10 -9
- data/lib/generators/rodauth/mailer_generator.rb +0 -37
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b8063be8ad00634114f74f0eb549c672e2b62cd1fa81cb7f124cc9cd12505e3f
|
4
|
+
data.tar.gz: 6f466e29420f9e4bacb58c855e942cc20289d2c3fc69a12638b97628d25dbbfb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8cc0af59c6ce29837fbc8a3401d456fd407ef76b74493b08ee9b4f2dfc8807d4a95c86f9bb0266401013d5162c009d46b7d07e3f741654af2cc267c0ee2c135e
|
7
|
+
data.tar.gz: 78c098dbaed458d5764ca2e7ee61f4710e01b2386d0cc04831b1732b9883d76c4b9f56c35c0a1e557c40951086d72bb0ed264f769313c1b45be30b2dd760024a
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,47 @@
|
|
1
|
+
## 0.11.0 (2021-05-06)
|
2
|
+
|
3
|
+
* Add controller-like logging for requests to Rodauth endpoints (@janko)
|
4
|
+
|
5
|
+
* Add `#rails_routes` to Roda and Rodauth instance for accessing Rails route helpers (@janko)
|
6
|
+
|
7
|
+
* Add `#rails_request` to Roda and Rodauth instance for retrieving an `ActionDispatch::Request` instance (@janko)
|
8
|
+
|
9
|
+
## 0.10.0 (2021-03-23)
|
10
|
+
|
11
|
+
* Add `Rodauth::Rails::Auth` superclass for moving configurations into separate files (@janko)
|
12
|
+
|
13
|
+
* Load the `pass` Roda plugin and recommend calling `r.pass` on prefixed routes (@janko)
|
14
|
+
|
15
|
+
* Improve Roda middleware inspect output (@janko)
|
16
|
+
|
17
|
+
* Create `RodauthMailer` and email templates in `rodauth:install`, and remove `rodauth:mailer` (@janko)
|
18
|
+
|
19
|
+
* Raise `KeyError` in `#rodauth` method when the Rodauth instance doesn't exist (@janko)
|
20
|
+
|
21
|
+
* Add `Rodauth::Rails.authenticated` routing constraint for requiring authentication (@janko)
|
22
|
+
|
23
|
+
## 0.9.1 (2021-02-10)
|
24
|
+
|
25
|
+
* Fix flash integration being loaded for API-only apps and causing an error (@dmitryzuev)
|
26
|
+
|
27
|
+
* Change account status column default to `unverified` in migration to match Rodauth's default (@basabin54)
|
28
|
+
|
29
|
+
## 0.9.0 (2021-02-07)
|
30
|
+
|
31
|
+
* Load Roda's JSON support by default, so that enabling `json`/`jwt` feature is all that's needed (@janko)
|
32
|
+
|
33
|
+
* Bump Rodauth dependency to 2.9+ (@janko)
|
34
|
+
|
35
|
+
* Add `--json` option for `rodauth:install` generator for configuring `json` feature (@janko)
|
36
|
+
|
37
|
+
* Add `--jwt` option for `rodauth:install` generator for configuring `jwt` feature (@janko)
|
38
|
+
|
39
|
+
* Remove the `--api` option from `rodauth:install` generator (@janko)
|
40
|
+
|
41
|
+
## 0.8.2 (2021-01-10)
|
42
|
+
|
43
|
+
* Reset Rails session on `#clear_session`, protecting from potential session fixation attacks (@janko)
|
44
|
+
|
1
45
|
## 0.8.1 (2021-01-04)
|
2
46
|
|
3
47
|
* Fix blank email body when `json: true` and `ActionController::API` descendant are used (@janko)
|
data/README.md
CHANGED
@@ -14,15 +14,16 @@ Articles:
|
|
14
14
|
* [Rodauth: A Refreshing Authentication Solution for Ruby](https://janko.io/rodauth-a-refreshing-authentication-solution-for-ruby/)
|
15
15
|
* [Adding Authentication in Rails with Rodauth](https://janko.io/adding-authentication-in-rails-with-rodauth/)
|
16
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)
|
17
18
|
|
18
19
|
## Why Rodauth?
|
19
20
|
|
20
21
|
There are already several popular authentication solutions for Rails (Devise,
|
21
|
-
Sorcery, Clearance, Authlogic), so why would you choose Rodauth?
|
22
|
-
|
22
|
+
Sorcery, Clearance, Authlogic), so why would you choose Rodauth? Here are some
|
23
|
+
of the advantages that stand out for me:
|
23
24
|
|
24
25
|
* multifactor authentication ([TOTP][otp], [SMS codes][sms_codes], [recovery codes][recovery_codes], [WebAuthn][webauthn])
|
25
|
-
* standardized [JSON API support][
|
26
|
+
* standardized [JSON API support][json] for every feature (including [JWT][jwt])
|
26
27
|
* 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
28
|
* [email authentication][email_auth] (aka "passwordless")
|
28
29
|
* [audit logging][audit_logging] (for any action)
|
@@ -32,6 +33,12 @@ it has many advantages over the mentioned alternatives:
|
|
32
33
|
* consistent before/after hooks around everything
|
33
34
|
* dedicated object encapsulating all authentication logic
|
34
35
|
|
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].
|
41
|
+
|
35
42
|
## Upgrading
|
36
43
|
|
37
44
|
### Upgrading to 0.7.0
|
@@ -54,7 +61,7 @@ documentation][hmac] for instructions on how to safely transition, or just set
|
|
54
61
|
Add the gem to your Gemfile:
|
55
62
|
|
56
63
|
```rb
|
57
|
-
gem "rodauth-rails", "~> 0.
|
64
|
+
gem "rodauth-rails", "~> 0.10"
|
58
65
|
|
59
66
|
# gem "jwt", require: false # for JWT feature
|
60
67
|
# gem "rotp", require: false # for OTP feature
|
@@ -73,7 +80,9 @@ $ rails generate rodauth:install
|
|
73
80
|
Or if you want Rodauth endpoints to be exposed via JSON API:
|
74
81
|
|
75
82
|
```sh
|
76
|
-
$ rails generate rodauth:install --
|
83
|
+
$ rails generate rodauth:install --json # regular authentication using the Rails session
|
84
|
+
# or
|
85
|
+
$ rails generate rodauth:install --jwt # token authentication via the "Authorization" header
|
77
86
|
$ bundle add jwt
|
78
87
|
```
|
79
88
|
|
@@ -85,6 +94,7 @@ The generator will create the following files:
|
|
85
94
|
* Rodauth app at `app/lib/rodauth_app.rb`
|
86
95
|
* Rodauth controller at `app/controllers/rodauth_controller.rb`
|
87
96
|
* Account model at `app/models/account.rb`
|
97
|
+
* Rodauth mailer at `app/mailers/rodauth_mailer.rb` with views
|
88
98
|
|
89
99
|
### Migration
|
90
100
|
|
@@ -180,6 +190,23 @@ class Account < ApplicationRecord
|
|
180
190
|
end
|
181
191
|
```
|
182
192
|
|
193
|
+
### Rodauth mailer
|
194
|
+
|
195
|
+
The default Rodauth app is configured to use `RodauthMailer` mailer
|
196
|
+
for sending authentication emails.
|
197
|
+
|
198
|
+
```rb
|
199
|
+
# app/mailers/rodauth_mailer.rb
|
200
|
+
class RodauthMailer < ApplicationMailer
|
201
|
+
def verify_account(recipient, email_link) ... end
|
202
|
+
def reset_password(recipient, email_link) ... end
|
203
|
+
def verify_login_change(recipient, old_login, new_login, email_link) ... end
|
204
|
+
def password_changed(recipient) ... end
|
205
|
+
# def email_auth(recipient, email_link) ... end
|
206
|
+
# def unlock_account(recipient, email_link) ... end
|
207
|
+
end
|
208
|
+
```
|
209
|
+
|
183
210
|
## Usage
|
184
211
|
|
185
212
|
### Routes
|
@@ -222,6 +249,19 @@ These routes are fully functional, feel free to visit them and interact with the
|
|
222
249
|
pages. The templates that ship with Rodauth aim to provide a complete
|
223
250
|
authentication experience, and the forms use [Bootstrap] markup.
|
224
251
|
|
252
|
+
Inside Rodauth configuration and the `route` block you can access Rails route
|
253
|
+
helpers through `#rails_routes`:
|
254
|
+
|
255
|
+
```rb
|
256
|
+
class RodauthApp < Rodauth::Rails::App
|
257
|
+
configure do
|
258
|
+
# ...
|
259
|
+
login_redirect { rails_routes.activity_path }
|
260
|
+
# ...
|
261
|
+
end
|
262
|
+
end
|
263
|
+
```
|
264
|
+
|
225
265
|
### Current account
|
226
266
|
|
227
267
|
To be able to fetch currently authenticated account, let's define a
|
@@ -241,7 +281,7 @@ class ApplicationController < ActionController::Base
|
|
241
281
|
rodauth.logout
|
242
282
|
rodauth.login_required
|
243
283
|
end
|
244
|
-
helper_method :current_account
|
284
|
+
helper_method :current_account # skip if inheriting from ActionController:API
|
245
285
|
end
|
246
286
|
```
|
247
287
|
|
@@ -299,15 +339,52 @@ class PostsController < ApplicationController
|
|
299
339
|
end
|
300
340
|
```
|
301
341
|
|
302
|
-
|
342
|
+
#### Routing constraints
|
343
|
+
|
344
|
+
You can also require authentication at the Rails router level by
|
345
|
+
using a built-in `authenticated` routing constraint:
|
303
346
|
|
304
347
|
```rb
|
305
348
|
# config/routes.rb
|
306
349
|
Rails.application.routes.draw do
|
307
|
-
constraints
|
308
|
-
|
309
|
-
|
310
|
-
|
350
|
+
constraints Rodauth::Rails.authenticated do
|
351
|
+
# ... authenticated routes ...
|
352
|
+
end
|
353
|
+
end
|
354
|
+
```
|
355
|
+
|
356
|
+
If you want additional conditions, you can pass in a block, which is
|
357
|
+
called with the Rodauth instance:
|
358
|
+
|
359
|
+
```rb
|
360
|
+
# config/routes.rb
|
361
|
+
Rails.application.routes.draw do
|
362
|
+
# require multifactor authentication to be setup
|
363
|
+
constraints Rodauth::Rails.authenticated { |rodauth| rodauth.uses_two_factor_authentication? } do
|
364
|
+
# ...
|
365
|
+
end
|
366
|
+
end
|
367
|
+
```
|
368
|
+
|
369
|
+
You can specify the Rodauth configuration by passing the configuration name:
|
370
|
+
|
371
|
+
```rb
|
372
|
+
# config/routes.rb
|
373
|
+
Rails.application.routes.draw do
|
374
|
+
constraints Rodauth::Rails.authenticated(:admin) do
|
375
|
+
# ...
|
376
|
+
end
|
377
|
+
end
|
378
|
+
```
|
379
|
+
|
380
|
+
If you need something more custom, you can always create the routing constraint
|
381
|
+
manually:
|
382
|
+
|
383
|
+
```rb
|
384
|
+
# config/routes.rb
|
385
|
+
Rails.application.routes.draw do
|
386
|
+
constraints -> (r) { !r.env["rodauth"].logged_in? } do # or "rodauth.admin"
|
387
|
+
# routes when the user is not logged in
|
311
388
|
end
|
312
389
|
end
|
313
390
|
```
|
@@ -375,58 +452,36 @@ end
|
|
375
452
|
|
376
453
|
### Mailer
|
377
454
|
|
378
|
-
|
379
|
-
|
455
|
+
The install generator will create `RodauthMailer` with default email templates,
|
456
|
+
and configure Rodauth features that send emails as part of the authentication
|
457
|
+
flow to use it.
|
380
458
|
|
381
459
|
```rb
|
382
|
-
# app/
|
383
|
-
class
|
384
|
-
|
385
|
-
configure do
|
460
|
+
# app/mailers/rodauth_mailer.rb
|
461
|
+
class RodauthMailer < ApplicationMailer
|
462
|
+
def verify_account(recipient, email_link)
|
386
463
|
# ...
|
387
|
-
|
388
|
-
|
389
|
-
email_subject_prefix "[MyApp] "
|
390
|
-
send_email(&:deliver_later)
|
464
|
+
end
|
465
|
+
def reset_password(recipient, email_link)
|
391
466
|
# ...
|
392
|
-
|
393
|
-
|
394
|
-
verify_account_email_body { "Verify your account by visting this link: #{verify_account_email_link}" }
|
467
|
+
end
|
468
|
+
def verify_login_change(recipient, old_login, new_login, email_link)
|
395
469
|
# ...
|
396
470
|
end
|
471
|
+
def password_changed(recipient)
|
472
|
+
# ...
|
473
|
+
end
|
474
|
+
# def email_auth(recipient, email_link)
|
475
|
+
# ...
|
476
|
+
# end
|
477
|
+
# def unlock_account(recipient, email_link)
|
478
|
+
# ...
|
479
|
+
# end
|
397
480
|
end
|
398
481
|
```
|
399
|
-
|
400
|
-
This is convenient when starting out, but eventually you might want to use your
|
401
|
-
own mailer. You can start by running the following command:
|
402
|
-
|
403
|
-
```sh
|
404
|
-
$ rails generate rodauth:mailer
|
405
|
-
```
|
406
|
-
|
407
|
-
This will create a `RodauthMailer` with the associated mailer views in
|
408
|
-
`app/views/rodauth_mailer` directory:
|
409
|
-
|
410
|
-
```rb
|
411
|
-
# app/mailers/rodauth_mailer.rb
|
412
|
-
class RodauthMailer < ApplicationMailer
|
413
|
-
def verify_account(recipient, email_link) ... end
|
414
|
-
def reset_password(recipient, email_link) ... end
|
415
|
-
def verify_login_change(recipient, old_login, new_login, email_link) ... end
|
416
|
-
def password_changed(recipient) ... end
|
417
|
-
# def email_auth(recipient, email_link) ... end
|
418
|
-
# def unlock_account(recipient, email_link) ... end
|
419
|
-
end
|
420
|
-
```
|
421
|
-
|
422
|
-
You can then uncomment the lines in your Rodauth configuration to have it call
|
423
|
-
your mailer. If you've enabled additional authentication features that send
|
424
|
-
emails, make sure to override their `create_*_email` methods as well.
|
425
|
-
|
426
482
|
```rb
|
427
483
|
# app/lib/rodauth_app.rb
|
428
484
|
class RodauthApp < Rodauth::Rails::App
|
429
|
-
# ...
|
430
485
|
configure do
|
431
486
|
# ...
|
432
487
|
create_reset_password_email do
|
@@ -456,10 +511,17 @@ class RodauthApp < Rodauth::Rails::App
|
|
456
511
|
end
|
457
512
|
```
|
458
513
|
|
459
|
-
This
|
460
|
-
|
461
|
-
|
462
|
-
|
514
|
+
This configuration calls `#deliver_later`, which uses Active Job to deliver
|
515
|
+
emails in a background job. It's generally recommended to send emails
|
516
|
+
asynchronously for better request throughput and the ability to retry
|
517
|
+
deliveries. However, if you want to send emails synchronously, modify the
|
518
|
+
configuration to call `#deliver_now` instead.
|
519
|
+
|
520
|
+
If you're using a background processing library without an Active Job adapter,
|
521
|
+
or a 3rd-party service for sending transactional emails, this two-phase API
|
522
|
+
might not be suitable. In this case, instead of overriding `#create_*_email`
|
523
|
+
and `#send_email`, override the `#send_*_email` methods instead, which are
|
524
|
+
required to send the email immediately.
|
463
525
|
|
464
526
|
### Migrations
|
465
527
|
|
@@ -481,6 +543,143 @@ class CreateRodauthOtpSmsCodesRecoveryCodes < ActiveRecord::Migration
|
|
481
543
|
end
|
482
544
|
```
|
483
545
|
|
546
|
+
### Multiple configurations
|
547
|
+
|
548
|
+
If you need to handle multiple types of accounts that require different
|
549
|
+
authentication logic, you can create different configurations for them:
|
550
|
+
|
551
|
+
```rb
|
552
|
+
# app/lib/rodauth_app.rb
|
553
|
+
class RodauthApp < Rodauth::Rails::App
|
554
|
+
# primary configuration
|
555
|
+
configure do
|
556
|
+
# ...
|
557
|
+
end
|
558
|
+
|
559
|
+
# alternative configuration
|
560
|
+
configure(:admin) do
|
561
|
+
# ... enable features ...
|
562
|
+
prefix "/admin"
|
563
|
+
session_key_prefix "admin_"
|
564
|
+
remember_cookie_key "_admin_remember" # if using remember feature
|
565
|
+
|
566
|
+
# if you want separate tables
|
567
|
+
accounts_table :admin_accounts
|
568
|
+
password_hash_table :admin_account_password_hashes
|
569
|
+
# ...
|
570
|
+
end
|
571
|
+
|
572
|
+
route do |r|
|
573
|
+
r.rodauth
|
574
|
+
|
575
|
+
r.on "admin" do
|
576
|
+
r.rodauth(:admin)
|
577
|
+
r.pass # allow the Rails app to handle other "/admin/*" requests
|
578
|
+
end
|
579
|
+
|
580
|
+
# ...
|
581
|
+
end
|
582
|
+
end
|
583
|
+
```
|
584
|
+
|
585
|
+
Then in your application you can reference the secondary Rodauth instance:
|
586
|
+
|
587
|
+
```rb
|
588
|
+
rodauth(:admin).login_path #=> "/admin/login"
|
589
|
+
```
|
590
|
+
|
591
|
+
#### Named auth classes
|
592
|
+
|
593
|
+
A `configure` block inside `Rodauth::Rails::App` will internally create an
|
594
|
+
anonymous `Rodauth::Auth` subclass, and register it under the given name.
|
595
|
+
However, you can also define the auth classes explicitly, by creating
|
596
|
+
subclasses of `Rodauth::Rails::Auth`:
|
597
|
+
|
598
|
+
```rb
|
599
|
+
# app/lib/rodauth_main.rb
|
600
|
+
class RodauthMain < Rodauth::Rails::Auth
|
601
|
+
configure do
|
602
|
+
# ... main configuration ...
|
603
|
+
end
|
604
|
+
end
|
605
|
+
```
|
606
|
+
```rb
|
607
|
+
# app/lib/rodauth_admin.rb
|
608
|
+
class RodauthAdmin < Rodauth::Rails::Auth
|
609
|
+
configure do
|
610
|
+
# ...
|
611
|
+
prefix "/admin"
|
612
|
+
session_key_prefix "admin_"
|
613
|
+
# ...
|
614
|
+
end
|
615
|
+
end
|
616
|
+
```
|
617
|
+
```rb
|
618
|
+
# app/lib/rodauth_app.rb
|
619
|
+
class RodauthApp < Rodauth::Rails::App
|
620
|
+
configure RodauthMain
|
621
|
+
configure RodauthAdmin, :admin
|
622
|
+
# ...
|
623
|
+
end
|
624
|
+
```
|
625
|
+
|
626
|
+
This allows having each configuration in a dedicated file, and named constants
|
627
|
+
improve introspection and error messages. You can also use inheritance to share
|
628
|
+
common settings:
|
629
|
+
|
630
|
+
```rb
|
631
|
+
# app/lib/rodauth_base.rb
|
632
|
+
class RodauthBase < Rodauth::Rails::Auth
|
633
|
+
# common settings that can be shared between multiple configurations
|
634
|
+
configure do
|
635
|
+
enable :login, :logout
|
636
|
+
login_return_to_requested_location? true
|
637
|
+
logout_redirect "/"
|
638
|
+
# ...
|
639
|
+
end
|
640
|
+
end
|
641
|
+
```
|
642
|
+
```rb
|
643
|
+
# app/lib/rodauth_main.rb
|
644
|
+
class RodauthMain < RodauthBase # inherit common settings
|
645
|
+
configure do
|
646
|
+
# ... customize main ...
|
647
|
+
end
|
648
|
+
end
|
649
|
+
```
|
650
|
+
```rb
|
651
|
+
# app/lib/rodauth_admin.rb
|
652
|
+
class RodauthAdmin < RodauthBase # inherit common settings
|
653
|
+
configure do
|
654
|
+
# ... customize admin ...
|
655
|
+
end
|
656
|
+
end
|
657
|
+
```
|
658
|
+
|
659
|
+
Another benefit is that you can define custom methods directly on the class
|
660
|
+
instead of through `auth_class_eval`:
|
661
|
+
|
662
|
+
```rb
|
663
|
+
# app/lib/rodauth_admin.rb
|
664
|
+
class RodauthAdmin < Rodauth::Rails::Auth
|
665
|
+
configure do
|
666
|
+
# ...
|
667
|
+
end
|
668
|
+
|
669
|
+
def superadmin?
|
670
|
+
Role.where(account_id: session_id, type: "superadmin").any?
|
671
|
+
end
|
672
|
+
end
|
673
|
+
```
|
674
|
+
```rb
|
675
|
+
# config/routes.rb
|
676
|
+
Rails.application.routes.draw do
|
677
|
+
constraints Rodauth::Rails.authenticated(:admin) { |rodauth| rodauth.superadmin? } do
|
678
|
+
mount Sidekiq::Web => "sidekiq"
|
679
|
+
end
|
680
|
+
end
|
681
|
+
```
|
682
|
+
|
484
683
|
### Calling controller methods
|
485
684
|
|
486
685
|
When using Rodauth before/after hooks or generally overriding your Rodauth
|
@@ -514,7 +713,7 @@ Rodauth operations outside of the request context. rodauth-rails gives you the
|
|
514
713
|
ability to retrieve the Rodauth instance:
|
515
714
|
|
516
715
|
```rb
|
517
|
-
rodauth = Rodauth::Rails.rodauth # or Rodauth::Rails.rodauth(:
|
716
|
+
rodauth = Rodauth::Rails.rodauth # or Rodauth::Rails.rodauth(:admin)
|
518
717
|
|
519
718
|
rodauth.login_url #=> "https://example.com/login"
|
520
719
|
rodauth.account_from_login("user@example.com") # loads user by email
|
@@ -545,8 +744,8 @@ The Rodauth app stores the `Rodauth::Auth` instance in the Rack env hash, which
|
|
545
744
|
is then available in your Rails app:
|
546
745
|
|
547
746
|
```rb
|
548
|
-
request.env["rodauth"]
|
549
|
-
request.env["rodauth.
|
747
|
+
request.env["rodauth"] #=> #<Rodauth::Auth>
|
748
|
+
request.env["rodauth.admin"] #=> #<Rodauth::Auth> (if using multiple configurations)
|
550
749
|
```
|
551
750
|
|
552
751
|
For convenience, this object can be accessed via the `#rodauth` method in views
|
@@ -555,14 +754,14 @@ and controllers:
|
|
555
754
|
```rb
|
556
755
|
class MyController < ApplicationController
|
557
756
|
def my_action
|
558
|
-
rodauth
|
559
|
-
rodauth(:
|
757
|
+
rodauth #=> #<Rodauth::Auth>
|
758
|
+
rodauth(:admin) #=> #<Rodauth::Auth> (if using multiple configurations)
|
560
759
|
end
|
561
760
|
end
|
562
761
|
```
|
563
762
|
```erb
|
564
|
-
<% rodauth
|
565
|
-
<% rodauth(:
|
763
|
+
<% rodauth #=> #<Rodauth::Auth> %>
|
764
|
+
<% rodauth(:admin) #=> #<Rodauth::Auth> (if using multiple configurations) %>
|
566
765
|
```
|
567
766
|
|
568
767
|
### App
|
@@ -584,7 +783,7 @@ any additional [plugin options].
|
|
584
783
|
class RodauthApp < Rodauth::Rails::App
|
585
784
|
configure { ... } # defining default Rodauth configuration
|
586
785
|
configure(json: true) { ... } # passing options to the Rodauth plugin
|
587
|
-
configure(:
|
786
|
+
configure(:admin) { ... } # defining multiple Rodauth configurations
|
588
787
|
end
|
589
788
|
```
|
590
789
|
|
@@ -619,15 +818,32 @@ function calls).
|
|
619
818
|
|
620
819
|
If ActiveRecord is used in the application, the `rodauth:install` generator
|
621
820
|
will have automatically configured Sequel to reuse ActiveRecord's database
|
622
|
-
connection
|
821
|
+
connection, using the [sequel-activerecord_connection] gem.
|
623
822
|
|
624
823
|
This means that, from the usage perspective, Sequel can be considered just
|
625
824
|
as an implementation detail of Rodauth.
|
626
825
|
|
627
826
|
## JSON API
|
628
827
|
|
629
|
-
|
630
|
-
|
828
|
+
To make Rodauth endpoints accessible via JSON API, enable the [`json`][json]
|
829
|
+
feature:
|
830
|
+
|
831
|
+
```rb
|
832
|
+
# app/lib/rodauth_app.rb
|
833
|
+
class RodauthApp < Rodauth::Rails::App
|
834
|
+
configure do
|
835
|
+
# ...
|
836
|
+
enable :json
|
837
|
+
only_json? true # accept only JSON requests
|
838
|
+
# ...
|
839
|
+
end
|
840
|
+
end
|
841
|
+
```
|
842
|
+
|
843
|
+
This will store account session data into the Rails session. If you rather want
|
844
|
+
stateless token-based authentication via the `Authorization` header, enable the
|
845
|
+
[`jwt`][jwt] feature (which builds on top of the `json` feature) and add the
|
846
|
+
[JWT gem] to the Gemfile:
|
631
847
|
|
632
848
|
```sh
|
633
849
|
$ bundle add jwt
|
@@ -635,23 +851,33 @@ $ bundle add jwt
|
|
635
851
|
```rb
|
636
852
|
# app/lib/rodauth_app.rb
|
637
853
|
class RodauthApp < Rodauth::Rails::App
|
638
|
-
configure
|
854
|
+
configure do
|
639
855
|
# ...
|
640
856
|
enable :jwt
|
641
|
-
|
642
|
-
|
857
|
+
jwt_secret "<YOUR_SECRET_KEY>" # store the JWT secret in a safe place
|
858
|
+
only_json? true # accept only JSON requests
|
643
859
|
# ...
|
644
860
|
end
|
645
861
|
end
|
646
862
|
```
|
647
863
|
|
648
|
-
|
649
|
-
|
650
|
-
:only` to `json: true`.
|
864
|
+
If you need Cross-Origin Resource Sharing and/or JWT refresh tokens, enable the
|
865
|
+
corresponding Rodauth features and create the necessary tables:
|
651
866
|
|
652
|
-
|
653
|
-
|
654
|
-
|
867
|
+
```sh
|
868
|
+
$ rails generate rodauth:migration jwt_refresh
|
869
|
+
$ rails db:migrate
|
870
|
+
```
|
871
|
+
```rb
|
872
|
+
# app/lib/rodauth_app.rb
|
873
|
+
class RodauthApp < Rodauth::Rails::App
|
874
|
+
configure do
|
875
|
+
# ...
|
876
|
+
enable :jwt, :jwt_cors, :jwt_refresh
|
877
|
+
# ...
|
878
|
+
end
|
879
|
+
end
|
880
|
+
```
|
655
881
|
|
656
882
|
## OmniAuth
|
657
883
|
|
@@ -745,7 +971,7 @@ class RodauthController < ApplicationController
|
|
745
971
|
|
746
972
|
# create new account if it doesn't exist
|
747
973
|
unless account
|
748
|
-
account = Account.create!(email: auth["info"]["email"])
|
974
|
+
account = Account.create!(email: auth["info"]["email"], status: rodauth.account_open_status_value)
|
749
975
|
end
|
750
976
|
|
751
977
|
# create new identity if it doesn't exist
|
@@ -797,17 +1023,19 @@ end
|
|
797
1023
|
|
798
1024
|
When developing custom extensions for Rodauth inside your Rails project, it's
|
799
1025
|
better to use plain modules (at least in the beginning), because Rodauth
|
800
|
-
feature
|
1026
|
+
feature design doesn't yet support Zeitwerk reloading well. Here is
|
1027
|
+
an example of an LDAP authentication extension that uses the
|
1028
|
+
[simple_ldap_authenticator] gem.
|
801
1029
|
|
802
1030
|
```rb
|
803
|
-
# app/lib/
|
804
|
-
module
|
805
|
-
def
|
806
|
-
|
1031
|
+
# app/lib/rodauth_ldap.rb
|
1032
|
+
module RodauthLdap
|
1033
|
+
def require_bcrypt?
|
1034
|
+
false
|
807
1035
|
end
|
808
1036
|
|
809
|
-
def
|
810
|
-
|
1037
|
+
def password_match?(password)
|
1038
|
+
SimpleLdapAuthenticator.valid?(account[:email], password)
|
811
1039
|
end
|
812
1040
|
end
|
813
1041
|
```
|
@@ -817,7 +1045,7 @@ class RodauthApp < Rodauth::Rails::App
|
|
817
1045
|
configure do
|
818
1046
|
# ...
|
819
1047
|
auth_class_eval do
|
820
|
-
include
|
1048
|
+
include RodauthLdap
|
821
1049
|
end
|
822
1050
|
# ...
|
823
1051
|
end
|
@@ -826,48 +1054,156 @@ end
|
|
826
1054
|
|
827
1055
|
## Testing
|
828
1056
|
|
829
|
-
|
830
|
-
authentication flow with tools like Capybara, and to not use any stubbing.
|
831
|
-
|
832
|
-
In functional and integration tests you can just make requests to Rodauth
|
833
|
-
routes:
|
1057
|
+
System (browser) tests for Rodauth actions could look something like this:
|
834
1058
|
|
835
1059
|
```rb
|
836
|
-
# test/
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
1060
|
+
# test/system/authentication_test.rb
|
1061
|
+
require "test_helper"
|
1062
|
+
|
1063
|
+
class AuthenticationTest < ActionDispatch::SystemTestCase
|
1064
|
+
include ActiveJob::TestHelper
|
1065
|
+
driven_by :rack_test
|
1066
|
+
|
1067
|
+
test "creating and verifying an account" do
|
1068
|
+
create_account
|
1069
|
+
assert_match "An email has been sent to you with a link to verify your account", page.text
|
1070
|
+
|
1071
|
+
verify_account
|
1072
|
+
assert_match "Your account has been verified", page.text
|
1073
|
+
end
|
1074
|
+
|
1075
|
+
test "logging in and logging out" do
|
1076
|
+
create_account(verify: true)
|
1077
|
+
|
1078
|
+
logout
|
1079
|
+
assert_match "You have been logged out", page.text
|
841
1080
|
|
842
1081
|
login
|
843
|
-
|
1082
|
+
assert_match "You have been logged in", page.text
|
1083
|
+
end
|
1084
|
+
|
1085
|
+
private
|
1086
|
+
|
1087
|
+
def create_account(email: "user@example.com", password: "secret", verify: false)
|
1088
|
+
visit "/create-account"
|
1089
|
+
fill_in "Login", with: email
|
1090
|
+
fill_in "Password", with: password
|
1091
|
+
fill_in "Confirm Password", with: password
|
1092
|
+
click_on "Create Account"
|
1093
|
+
verify_account if verify
|
1094
|
+
end
|
1095
|
+
|
1096
|
+
def verify_account
|
1097
|
+
perform_enqueued_jobs # run enqueued email deliveries
|
1098
|
+
email = ActionMailer::Base.deliveries.last
|
1099
|
+
verify_account_link = email.body.to_s[/\S+verify-account\S+/]
|
1100
|
+
visit verify_account_link
|
1101
|
+
click_on "Verify Account"
|
1102
|
+
end
|
1103
|
+
|
1104
|
+
def login(email: "user@example.com", password: "secret")
|
1105
|
+
visit "/login"
|
1106
|
+
fill_in "Login", with: email
|
1107
|
+
fill_in "Password", with: password
|
1108
|
+
click_on "Login"
|
1109
|
+
end
|
1110
|
+
|
1111
|
+
def logout
|
1112
|
+
visit "/logout"
|
1113
|
+
click_on "Logout"
|
1114
|
+
end
|
1115
|
+
end
|
1116
|
+
```
|
1117
|
+
|
1118
|
+
While request tests in JSON API mode with JWT tokens could look something like
|
1119
|
+
this:
|
1120
|
+
|
1121
|
+
```rb
|
1122
|
+
# test/integration/authentication_test.rb
|
1123
|
+
require "test_helper"
|
1124
|
+
|
1125
|
+
class AuthenticationTest < ActionDispatch::IntegrationTest
|
1126
|
+
test "creating and verifying an account" do
|
1127
|
+
create_account
|
844
1128
|
assert_response :success
|
1129
|
+
assert_match "An email has been sent to you with a link to verify your account", JSON.parse(body)["success"]
|
1130
|
+
|
1131
|
+
verify_account
|
1132
|
+
assert_response :success
|
1133
|
+
assert_match "Your account has been verified", JSON.parse(body)["success"]
|
1134
|
+
end
|
1135
|
+
|
1136
|
+
test "logging in and logging out" do
|
1137
|
+
create_account(verify: true)
|
845
1138
|
|
846
1139
|
logout
|
847
|
-
|
1140
|
+
assert_response :success
|
1141
|
+
assert_match "You have been logged out", JSON.parse(body)["success"]
|
1142
|
+
|
1143
|
+
login
|
1144
|
+
assert_response :success
|
1145
|
+
assert_match "You have been logged in", JSON.parse(body)["success"]
|
848
1146
|
end
|
849
1147
|
|
850
1148
|
private
|
851
1149
|
|
852
|
-
def
|
853
|
-
post "/create-account", params: {
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
|
1150
|
+
def create_account(email: "user@example.com", password: "secret", verify: false)
|
1151
|
+
post "/create-account", as: :json, params: { login: email, password: password, "password-confirm": password }
|
1152
|
+
verify_account if verify
|
1153
|
+
end
|
1154
|
+
|
1155
|
+
def verify_account
|
1156
|
+
perform_enqueued_jobs # run enqueued email deliveries
|
1157
|
+
email = ActionMailer::Base.deliveries.last
|
1158
|
+
verify_account_key = email.body.to_s[/verify-account\?key=(\S+)/, 1]
|
1159
|
+
post "/verify-account", as: :json, params: { key: verify_account_key }
|
1160
|
+
end
|
858
1161
|
|
859
|
-
|
860
|
-
|
861
|
-
"password" => password,
|
862
|
-
}
|
1162
|
+
def login(email: "user@example.com", password: "secret")
|
1163
|
+
post "/login", as: :json, params: { login: email, password: password }
|
863
1164
|
end
|
864
1165
|
|
865
1166
|
def logout
|
866
|
-
post "/logout"
|
1167
|
+
post "/logout", as: :json, headers: { "Authorization" => headers["Authorization"] }
|
867
1168
|
end
|
868
1169
|
end
|
869
1170
|
```
|
870
1171
|
|
1172
|
+
If you're delivering emails in the background, make sure to set Active Job
|
1173
|
+
queue adapter to `:test` or `:inline`:
|
1174
|
+
|
1175
|
+
```rb
|
1176
|
+
# config/environments/test.rb
|
1177
|
+
Rails.application.configure do |config|
|
1178
|
+
# ...
|
1179
|
+
config.active_job.queue_adapter = :test # or :inline
|
1180
|
+
# ...
|
1181
|
+
end
|
1182
|
+
```
|
1183
|
+
|
1184
|
+
If you need to create an account record with a password directly, you can do it
|
1185
|
+
as follows:
|
1186
|
+
|
1187
|
+
```rb
|
1188
|
+
# app/models/account.rb
|
1189
|
+
class Account < ApplicationRecord
|
1190
|
+
has_one :password_hash, foreign_key: :id
|
1191
|
+
end
|
1192
|
+
```
|
1193
|
+
```rb
|
1194
|
+
# app/models/account/password_hash.rb
|
1195
|
+
class Account::PasswordHash < ApplicationRecord
|
1196
|
+
belongs_to :account, foreign_key: :id
|
1197
|
+
end
|
1198
|
+
```
|
1199
|
+
```rb
|
1200
|
+
require "bcrypt"
|
1201
|
+
|
1202
|
+
account = Account.create!(email: "user@example.com", status: "verified")
|
1203
|
+
password_hash = BCrypt::Password.create("secret", cost: BCrypt::Engine::MIN_COST)
|
1204
|
+
account.create_password_hash!(id: account.id, password_hash: password_hash)
|
1205
|
+
```
|
1206
|
+
|
871
1207
|
## Rodauth defaults
|
872
1208
|
|
873
1209
|
rodauth-rails changes some of the default Rodauth settings for easier setup:
|
@@ -976,6 +1312,7 @@ conduct](https://github.com/janko/rodauth-rails/blob/master/CODE_OF_CONDUCT.md).
|
|
976
1312
|
[sms_codes]: http://rodauth.jeremyevans.net/rdoc/files/doc/sms_codes_rdoc.html
|
977
1313
|
[recovery_codes]: http://rodauth.jeremyevans.net/rdoc/files/doc/recovery_codes_rdoc.html
|
978
1314
|
[webauthn]: http://rodauth.jeremyevans.net/rdoc/files/doc/webauthn_rdoc.html
|
1315
|
+
[json]: http://rodauth.jeremyevans.net/rdoc/files/doc/json_rdoc.html
|
979
1316
|
[jwt]: http://rodauth.jeremyevans.net/rdoc/files/doc/jwt_rdoc.html
|
980
1317
|
[email_auth]: http://rodauth.jeremyevans.net/rdoc/files/doc/email_auth_rdoc.html
|
981
1318
|
[audit_logging]: http://rodauth.jeremyevans.net/rdoc/files/doc/audit_logging_rdoc.html
|
@@ -987,3 +1324,4 @@ conduct](https://github.com/janko/rodauth-rails/blob/master/CODE_OF_CONDUCT.md).
|
|
987
1324
|
[session_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/session_expiration_rdoc.html
|
988
1325
|
[single_session]: http://rodauth.jeremyevans.net/rdoc/files/doc/single_session_rdoc.html
|
989
1326
|
[account_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/account_expiration_rdoc.html
|
1327
|
+
[simple_ldap_authenticator]: https://github.com/jeremyevans/simple_ldap_authenticator
|