rodauth-rails 0.14.0 → 0.15.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 +8 -0
- data/README.md +178 -58
- data/lib/generators/rodauth/templates/app/lib/rodauth_app.rb +3 -8
- data/lib/generators/rodauth/templates/app/models/account.rb +1 -0
- data/lib/generators/rodauth/views_generator.rb +3 -2
- data/lib/rodauth/rails.rb +27 -30
- data/lib/rodauth/rails/feature.rb +2 -0
- data/lib/rodauth/rails/feature/callbacks.rb +6 -2
- data/lib/rodauth/rails/feature/internal_request.rb +50 -0
- data/lib/rodauth/rails/model.rb +101 -0
- data/lib/rodauth/rails/model/associations.rb +195 -0
- data/lib/rodauth/rails/tasks.rake +5 -5
- data/lib/rodauth/rails/version.rb +1 -1
- data/rodauth-rails.gemspec +4 -1
- metadata +49 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '097625662e9cefbf7484ea775c9b1930968fbbc3a1b8ad24df9e229e2194e301'
|
4
|
+
data.tar.gz: fbbfe9dd849646e859aecbf3c600168fc0b65255f7b2bf933a2e42591f8b0b79
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 762d0c1725dcd0017cdd6722894e546dfdf246e245af688a8bc99f177e43765fe8bd7a79639e45d3146ca5bedadce9f34389f61bf3a6957b406cbc664cf93829
|
7
|
+
data.tar.gz: 8c6624c70668356b8434dde9bd237cced89f23d4d241b770989f1af68ee687b7186f305ada409885d619b9974e7d2d47b6fddc9faf6935653cab805ee8709167
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
## 0.15.0 (2021-07-29)
|
2
|
+
|
3
|
+
* Add `Rodauth::Rails::Model` mixin that defines password attribute and associations on the model (@janko)
|
4
|
+
|
5
|
+
* Add support for the new internal_request feature (@janko)
|
6
|
+
|
7
|
+
* Implement `Rodauth::Rails.rodauth` in terms of the internal_request feature (@janko)
|
8
|
+
|
1
9
|
## 0.14.0 (2021-07-10)
|
2
10
|
|
3
11
|
* Speed up template rendering by only searching formats accepted by the request (@janko)
|
data/README.md
CHANGED
@@ -49,7 +49,7 @@ For instructions on upgrading from previous rodauth-rails versions, see
|
|
49
49
|
Add the gem to your Gemfile:
|
50
50
|
|
51
51
|
```rb
|
52
|
-
gem "rodauth-rails", "~> 0.
|
52
|
+
gem "rodauth-rails", "~> 0.15"
|
53
53
|
|
54
54
|
# gem "jwt", require: false # for JWT feature
|
55
55
|
# gem "rotp", require: false # for OTP feature
|
@@ -453,6 +453,130 @@ class CreateRodauthOtpSmsCodesRecoveryCodes < ActiveRecord::Migration
|
|
453
453
|
end
|
454
454
|
```
|
455
455
|
|
456
|
+
### Model
|
457
|
+
|
458
|
+
The `Rodauth::Rails::Model` mixin can be included into the account model, which
|
459
|
+
defines a password attribute and associations for tables used by enabled
|
460
|
+
authentication features.
|
461
|
+
|
462
|
+
```rb
|
463
|
+
class Account < ApplicationRecord
|
464
|
+
include Rodauth::Rails.model # or `Rodauth::Rails.model(:admin)`
|
465
|
+
end
|
466
|
+
```
|
467
|
+
|
468
|
+
#### Password attribute
|
469
|
+
|
470
|
+
Regardless of whether you're storing the password hash in a column in the
|
471
|
+
accounts table, or in a separate table, the `#password` attribute can be used
|
472
|
+
to set or clear the password hash.
|
473
|
+
|
474
|
+
```rb
|
475
|
+
account = Account.create!(email: "user@example.com", password: "secret")
|
476
|
+
|
477
|
+
# when password hash is stored in a column on the accounts table
|
478
|
+
account.password_hash #=> "$2a$12$k/Ub1I2iomi84RacqY89Hu4.M0vK7klRnRtzorDyvOkVI.hKhkNw."
|
479
|
+
|
480
|
+
# when password hash is stored in a separate table
|
481
|
+
account.password_hash #=> #<Account::PasswordHash...> (record from `account_password_hashes` table)
|
482
|
+
account.password_hash.password_hash #=> "$2a$12$k/Ub1..." (inaccessible when using database authentication functions)
|
483
|
+
|
484
|
+
account.password = nil # clears password hash
|
485
|
+
account.password_hash #=> nil
|
486
|
+
```
|
487
|
+
|
488
|
+
Note that the password attribute doesn't come with validations, making it
|
489
|
+
unsuitable for forms. It was primarily intended to allow easily creating
|
490
|
+
accounts in development console and in tests.
|
491
|
+
|
492
|
+
#### Associations
|
493
|
+
|
494
|
+
The `Rodauth::Rails::Model` mixin defines associations for Rodauth tables
|
495
|
+
associated to the accounts table:
|
496
|
+
|
497
|
+
```rb
|
498
|
+
account.remember_key #=> #<Account::RememberKey> (record from `account_remember_keys` table)
|
499
|
+
account.active_session_keys #=> [#<Account::ActiveSessionKey>,...] (records from `account_active_session_keys` table)
|
500
|
+
```
|
501
|
+
|
502
|
+
You can also reference the associated models directly:
|
503
|
+
|
504
|
+
```rb
|
505
|
+
# model referencing the `account_authentication_audit_logs` table
|
506
|
+
Account::AuthenticationAuditLog.where(message: "login").group(:account_id)
|
507
|
+
```
|
508
|
+
|
509
|
+
The associated models define the inverse `belongs_to :account` association:
|
510
|
+
|
511
|
+
```rb
|
512
|
+
Account::ActiveSessionKey.includes(:account).map(&:account)
|
513
|
+
```
|
514
|
+
|
515
|
+
Here is an example of using associations to create a method that returns
|
516
|
+
whether the account has multifactor authentication enabled:
|
517
|
+
|
518
|
+
```rb
|
519
|
+
class Account < ApplicationRecord
|
520
|
+
include Rodauth::Rails.model
|
521
|
+
|
522
|
+
def mfa_enabled?
|
523
|
+
otp_key || (sms_code && sms_code.num_failures.nil?) || recovery_codes.any?
|
524
|
+
end
|
525
|
+
end
|
526
|
+
```
|
527
|
+
|
528
|
+
Here is another example of creating a query scope that selects accounts with
|
529
|
+
multifactor authentication enabled:
|
530
|
+
|
531
|
+
```rb
|
532
|
+
class Account < ApplicationRecord
|
533
|
+
include Rodauth::Rails.model
|
534
|
+
|
535
|
+
scope :otp_setup, -> { where(otp_key: OtpKey.all) }
|
536
|
+
scope :sms_codes_setup, -> { where(sms_code: SmsCode.where(num_failures: nil)) }
|
537
|
+
scope :recovery_codes_setup, -> { where(recovery_codes: RecoveryCode.all) }
|
538
|
+
scope :mfa_enabled, -> { merge(otp_setup.or(sms_codes_setup).or(recovery_codes_setup)) }
|
539
|
+
end
|
540
|
+
```
|
541
|
+
|
542
|
+
Below is a list of all associations defined depending on the features loaded:
|
543
|
+
|
544
|
+
| Feature | Association | Type | Model | Table (default) |
|
545
|
+
| :------ | :---------- | :--- | :---- | :---- |
|
546
|
+
| account_expiration | `:activity_time` | `has_one` | `ActivityTime` | `account_activity_times` |
|
547
|
+
| active_sessions | `:active_session_keys` | `has_many` | `ActiveSessionKey` | `account_active_session_keys` |
|
548
|
+
| audit_logging | `:authentication_audit_logs` | `has_many` | `AuthenticationAuditLog` | `account_authentication_audit_logs` |
|
549
|
+
| disallow_password_reuse | `:previous_password_hashes` | `has_many` | `PreviousPasswordHash` | `account_previous_password_hashes` |
|
550
|
+
| email_auth | `:email_auth_key` | `has_one` | `EmailAuthKey` | `account_email_auth_keys` |
|
551
|
+
| jwt_refresh | `:jwt_refresh_keys` | `has_many` | `JwtRefreshKey` | `account_jwt_refresh_keys` |
|
552
|
+
| lockout | `:lockout` | `has_one` | `Lockout` | `account_lockouts` |
|
553
|
+
| lockout | `:login_failure` | `has_one` | `LoginFailure` | `account_login_failures` |
|
554
|
+
| otp | `:otp_key` | `has_one` | `OtpKey` | `account_otp_keys` |
|
555
|
+
| password_expiration | `:password_change_time` | `has_one` | `PasswordChangeTime` | `account_password_change_times` |
|
556
|
+
| recovery_codes | `:recovery_codes` | `has_many` | `RecoveryCode` | `account_recovery_codes` |
|
557
|
+
| remember | `:remember_key` | `has_one` | `RememberKey` | `account_remember_keys` |
|
558
|
+
| reset_password | `:password_reset_key` | `has_one` | `PasswordResetKey` | `account_password_reset_keys` |
|
559
|
+
| single_session | `:session_key` | `has_one` | `SessionKey` | `account_session_keys` |
|
560
|
+
| sms_codes | `:sms_code` | `has_one` | `SmsCode` | `account_sms_codes` |
|
561
|
+
| verify_account | `:verification_key` | `has_one` | `VerificationKey` | `account_verification_keys` |
|
562
|
+
| verify_login_change | `:login_change_key` | `has_one` | `LoginChangeKey` | `account_login_change_keys` |
|
563
|
+
| webauthn | `:webauthn_keys` | `has_many` | `WebauthnKey` | `account_webauthn_keys` |
|
564
|
+
| webauthn | `:webauthn_user_id` | `has_one` | `WebauthnUserId` | `account_webauthn_user_ids` |
|
565
|
+
|
566
|
+
By default, all associations except for audit logs have `dependent: :destroy`
|
567
|
+
set, to allow for easy deletion of account records in the console. You can use
|
568
|
+
`:association_options` to modify global or per-association options:
|
569
|
+
|
570
|
+
```rb
|
571
|
+
# don't auto-delete associations when account model is deleted
|
572
|
+
Rodauth::Rails.model(association_options: { dependent: nil })
|
573
|
+
|
574
|
+
# require authentication audit logs to be eager loaded before retrieval
|
575
|
+
Rodauth::Rails.model(association_options: -> (name) {
|
576
|
+
{ strict_loading: true } if name == :authentication_audit_logs
|
577
|
+
})
|
578
|
+
```
|
579
|
+
|
456
580
|
### Multiple configurations
|
457
581
|
|
458
582
|
If you need to handle multiple types of accounts that require different
|
@@ -656,37 +780,53 @@ class RodauthApp < Rodauth::Rails::App
|
|
656
780
|
end
|
657
781
|
```
|
658
782
|
|
659
|
-
###
|
783
|
+
### Outside of a request
|
784
|
+
|
785
|
+
In some cases you might need to use Rodauth more programmatically. If you would
|
786
|
+
like to perform Rodauth operations outside of request context, Rodauth ships
|
787
|
+
with the [internal_request] feature just for that. The rodauth-rails gem
|
788
|
+
additionally updates the internal rack env hash with your
|
789
|
+
`config.action_mailer.default_url_options`, which is used for generating URLs.
|
660
790
|
|
661
|
-
|
662
|
-
Rodauth
|
663
|
-
|
791
|
+
If you need to access Rodauth methods not exposed as internal requests, you can
|
792
|
+
use `Rodauth::Rails.rodauth` to retrieve the Rodauth instance used by the
|
793
|
+
internal_request feature:
|
664
794
|
|
665
795
|
```rb
|
666
|
-
|
796
|
+
# app/lib/rodauth_app.rb
|
797
|
+
class RodauthApp < Rodauth::Rails::App
|
798
|
+
configure do
|
799
|
+
enable :internal_request # this is required
|
800
|
+
end
|
801
|
+
end
|
802
|
+
```
|
803
|
+
```rb
|
804
|
+
account = Account.find_by!(email: "user@example.com")
|
805
|
+
rodauth = Rodauth::Rails.rodauth(account: account)
|
667
806
|
|
668
|
-
rodauth.
|
669
|
-
rodauth.
|
670
|
-
rodauth.
|
671
|
-
rodauth.
|
672
|
-
rodauth.
|
807
|
+
rodauth.compute_hmac("token") #=> "TpEJTKfKwqYvIDKWsuZhkhKlhaBXtR1aodskBAflD8U"
|
808
|
+
rodauth.open_account? #=> true
|
809
|
+
rodauth.two_factor_authentication_setup? #=> true
|
810
|
+
rodauth.password_meets_requirements?("foo") #=> false
|
811
|
+
rodauth.locked_out? #=> false
|
673
812
|
```
|
674
813
|
|
675
|
-
|
676
|
-
|
677
|
-
|
814
|
+
In addition to the `:account` option, the `Rodauth::Rails.rodauth`
|
815
|
+
method accepts any options supported by the internal_request feature.
|
816
|
+
|
817
|
+
```rb
|
818
|
+
Rodauth::Rails.rodauth(
|
819
|
+
env: { "HTTP_USER_AGENT" => "programmatic" },
|
820
|
+
session: { two_factor_auth_setup: true },
|
821
|
+
params: { "param" => "value" }
|
822
|
+
)
|
823
|
+
```
|
678
824
|
|
679
|
-
|
680
|
-
|
681
|
-
* `:session` – set any session values
|
682
|
-
* `:env` – set any additional Rack env values
|
825
|
+
Secondary Rodauth configurations are specified by passing the configuration
|
826
|
+
name:
|
683
827
|
|
684
828
|
```rb
|
685
|
-
Rodauth::Rails.rodauth(
|
686
|
-
Rodauth::Rails.rodauth(query: { "param" => "value" })
|
687
|
-
Rodauth::Rails.rodauth(form: { "param" => "value" })
|
688
|
-
Rodauth::Rails.rodauth(session: { two_factor_auth_setup: true })
|
689
|
-
Rodauth::Rails.rodauth(env: { "HTTP_USER_AGENT" => "programmatic" })
|
829
|
+
Rodauth::Rails.rodauth(:admin)
|
690
830
|
```
|
691
831
|
|
692
832
|
## How it works
|
@@ -825,21 +965,23 @@ class RodauthApp < Rodauth::Rails::App
|
|
825
965
|
end
|
826
966
|
```
|
827
967
|
|
828
|
-
|
829
|
-
|
968
|
+
The JWT token will be returned after each request to Rodauth routes. To also
|
969
|
+
return the JWT token on requests to your app's routes, you can add the
|
970
|
+
following code to your base controller:
|
830
971
|
|
831
|
-
```sh
|
832
|
-
$ rails generate rodauth:migration jwt_refresh
|
833
|
-
$ rails db:migrate
|
834
|
-
```
|
835
972
|
```rb
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
973
|
+
class ApplicationController < ActionController::Base
|
974
|
+
# ...
|
975
|
+
after_action :set_jwt_token
|
976
|
+
|
977
|
+
private
|
978
|
+
|
979
|
+
def set_jwt_token
|
980
|
+
if rodauth.use_jwt? && rodauth.valid_jwt?
|
981
|
+
response.headers["Authorization"] = rodauth.session_jwt
|
982
|
+
end
|
842
983
|
end
|
984
|
+
# ...
|
843
985
|
end
|
844
986
|
```
|
845
987
|
|
@@ -1147,29 +1289,6 @@ Rails.application.configure do |config|
|
|
1147
1289
|
end
|
1148
1290
|
```
|
1149
1291
|
|
1150
|
-
If you need to create an account record with a password directly, you can do it
|
1151
|
-
as follows:
|
1152
|
-
|
1153
|
-
```rb
|
1154
|
-
# app/models/account.rb
|
1155
|
-
class Account < ApplicationRecord
|
1156
|
-
has_one :password_hash, foreign_key: :id
|
1157
|
-
end
|
1158
|
-
```
|
1159
|
-
```rb
|
1160
|
-
# app/models/account/password_hash.rb
|
1161
|
-
class Account::PasswordHash < ApplicationRecord
|
1162
|
-
belongs_to :account, foreign_key: :id
|
1163
|
-
end
|
1164
|
-
```
|
1165
|
-
```rb
|
1166
|
-
require "bcrypt"
|
1167
|
-
|
1168
|
-
account = Account.create!(email: "user@example.com", status: "verified")
|
1169
|
-
password_hash = BCrypt::Password.create("secret", cost: BCrypt::Engine::MIN_COST)
|
1170
|
-
account.create_password_hash!(id: account.id, password_hash: password_hash)
|
1171
|
-
```
|
1172
|
-
|
1173
1292
|
## Rodauth defaults
|
1174
1293
|
|
1175
1294
|
rodauth-rails changes some of the default Rodauth settings for easier setup:
|
@@ -1303,3 +1422,4 @@ conduct](https://github.com/janko/rodauth-rails/blob/master/CODE_OF_CONDUCT.md).
|
|
1303
1422
|
[single_session]: http://rodauth.jeremyevans.net/rdoc/files/doc/single_session_rdoc.html
|
1304
1423
|
[account_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/account_expiration_rdoc.html
|
1305
1424
|
[simple_ldap_authenticator]: https://github.com/jeremyevans/simple_ldap_authenticator
|
1425
|
+
[internal_request]: http://rodauth.jeremyevans.net/rdoc/files/doc/internal_request_rdoc.html
|
@@ -154,7 +154,7 @@ class RodauthApp < Rodauth::Rails::App
|
|
154
154
|
<% end -%>
|
155
155
|
end
|
156
156
|
|
157
|
-
# ==>
|
157
|
+
# ==> Secondary configurations
|
158
158
|
# configure(:admin) do
|
159
159
|
# # ... enable features ...
|
160
160
|
# prefix "/admin"
|
@@ -163,11 +163,6 @@ class RodauthApp < Rodauth::Rails::App
|
|
163
163
|
#
|
164
164
|
# # search views in `app/views/admin/rodauth` directory
|
165
165
|
# rails_controller { Admin::RodauthController }
|
166
|
-
#
|
167
|
-
# # use separate tables (requires creating the new tables)
|
168
|
-
# methods.grep(/_table$/) do |table_method|
|
169
|
-
# public_send(table_method) { :"admin_#{super()}" }
|
170
|
-
# end
|
171
166
|
# end
|
172
167
|
|
173
168
|
route do |r|
|
@@ -189,7 +184,7 @@ class RodauthApp < Rodauth::Rails::App
|
|
189
184
|
# rodauth.require_authentication
|
190
185
|
# end
|
191
186
|
|
192
|
-
# ==>
|
187
|
+
# ==> Secondary configurations
|
193
188
|
# r.on "admin" do
|
194
189
|
# r.rodauth(:admin)
|
195
190
|
#
|
@@ -197,7 +192,7 @@ class RodauthApp < Rodauth::Rails::App
|
|
197
192
|
# rodauth(:admin).require_http_basic_auth
|
198
193
|
# end
|
199
194
|
#
|
200
|
-
#
|
195
|
+
# break # allow the Rails app to handle other "/admin/*" requests
|
201
196
|
# end
|
202
197
|
end
|
203
198
|
end
|
@@ -129,8 +129,9 @@ module Rodauth
|
|
129
129
|
end
|
130
130
|
|
131
131
|
def controller
|
132
|
-
rodauth = Rodauth::Rails.rodauth(configuration_name)
|
133
|
-
rodauth.
|
132
|
+
rodauth = Rodauth::Rails.app.rodauth(configuration_name)
|
133
|
+
fail ArgumentError, "unknown rodauth configuration: #{configuration_name.inspect}" unless rodauth
|
134
|
+
rodauth.allocate.rails_controller
|
134
135
|
end
|
135
136
|
|
136
137
|
def configuration_name
|
data/lib/rodauth/rails.rb
CHANGED
@@ -1,9 +1,6 @@
|
|
1
1
|
require "rodauth/rails/version"
|
2
2
|
require "rodauth/rails/railtie"
|
3
3
|
|
4
|
-
require "rack/utils"
|
5
|
-
require "stringio"
|
6
|
-
|
7
4
|
module Rodauth
|
8
5
|
module Rails
|
9
6
|
class Error < StandardError
|
@@ -12,49 +9,49 @@ module Rodauth
|
|
12
9
|
# This allows the developer to avoid loading Rodauth at boot time.
|
13
10
|
autoload :App, "rodauth/rails/app"
|
14
11
|
autoload :Auth, "rodauth/rails/auth"
|
12
|
+
autoload :Model, "rodauth/rails/model"
|
15
13
|
|
16
14
|
@app = nil
|
17
15
|
@middleware = true
|
18
16
|
|
17
|
+
LOCK = Mutex.new
|
18
|
+
|
19
19
|
class << self
|
20
|
-
def rodauth(name = nil, query:
|
21
|
-
|
20
|
+
def rodauth(name = nil, query: nil, form: nil, account: nil, **options)
|
21
|
+
auth_class = app.rodauth(name)
|
22
|
+
|
23
|
+
unless auth_class
|
22
24
|
fail ArgumentError, "undefined rodauth configuration: #{name.inspect}"
|
23
25
|
end
|
24
26
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
host += ":#{port}" if port
|
32
|
-
|
33
|
-
content_type = "application/x-www-form-urlencoded" if form.any?
|
34
|
-
|
35
|
-
rack_env = {
|
36
|
-
"QUERY_STRING" => Rack::Utils.build_nested_query(query),
|
37
|
-
"rack.input" => StringIO.new(Rack::Utils.build_nested_query(form)),
|
38
|
-
"CONTENT_TYPE" => content_type,
|
39
|
-
"rack.session" => {},
|
40
|
-
"HTTP_HOST" => host,
|
41
|
-
"rack.url_scheme" => scheme,
|
42
|
-
}.merge(env)
|
43
|
-
|
44
|
-
scope = app.new(rack_env)
|
45
|
-
instance = scope.rodauth(name)
|
27
|
+
LOCK.synchronize do
|
28
|
+
unless auth_class.features.include?(:internal_request)
|
29
|
+
auth_class.configure { enable :internal_request }
|
30
|
+
warn "Rodauth::Rails.rodauth requires the internal_request feature to be enabled. For now it was enabled automatically, but this behaviour will be removed in version 1.0."
|
31
|
+
end
|
32
|
+
end
|
46
33
|
|
47
|
-
|
48
|
-
|
34
|
+
if query || form
|
35
|
+
warn "The :query and :form keyword arguments for Rodauth::Rails.rodauth have been deprecated. Please use the :params argument supported by internal_request feature instead."
|
36
|
+
options[:params] = query || form
|
37
|
+
end
|
49
38
|
|
50
39
|
if account
|
51
|
-
|
52
|
-
|
40
|
+
options[:account_id] = account.id
|
41
|
+
end
|
42
|
+
|
43
|
+
instance = auth_class.internal_request_eval(options) do
|
44
|
+
@account = account.attributes.symbolize_keys if account
|
45
|
+
self
|
53
46
|
end
|
54
47
|
|
55
48
|
instance
|
56
49
|
end
|
57
50
|
|
51
|
+
def model(name = nil, **options)
|
52
|
+
Rodauth::Rails::Model.new(app.rodauth(name), **options)
|
53
|
+
end
|
54
|
+
|
58
55
|
# routing constraint that requires authentication
|
59
56
|
def authenticated(name = nil, &condition)
|
60
57
|
lambda do |request|
|
@@ -10,6 +10,7 @@ module Rodauth
|
|
10
10
|
require "rodauth/rails/feature/render"
|
11
11
|
require "rodauth/rails/feature/email"
|
12
12
|
require "rodauth/rails/feature/instrumentation"
|
13
|
+
require "rodauth/rails/feature/internal_request"
|
13
14
|
|
14
15
|
include Rodauth::Rails::Feature::Base
|
15
16
|
include Rodauth::Rails::Feature::Callbacks
|
@@ -17,5 +18,6 @@ module Rodauth
|
|
17
18
|
include Rodauth::Rails::Feature::Render
|
18
19
|
include Rodauth::Rails::Feature::Email
|
19
20
|
include Rodauth::Rails::Feature::Instrumentation
|
21
|
+
include Rodauth::Rails::Feature::InternalRequest
|
20
22
|
end
|
21
23
|
end
|
@@ -4,13 +4,17 @@ module Rodauth
|
|
4
4
|
module Callbacks
|
5
5
|
private
|
6
6
|
|
7
|
+
def _around_rodauth
|
8
|
+
rails_controller_around { super }
|
9
|
+
end
|
10
|
+
|
7
11
|
# Runs controller callbacks and rescue handlers around Rodauth actions.
|
8
|
-
def
|
12
|
+
def rails_controller_around
|
9
13
|
result = nil
|
10
14
|
|
11
15
|
rails_controller_rescue do
|
12
16
|
rails_controller_callbacks do
|
13
|
-
result = catch(:halt) {
|
17
|
+
result = catch(:halt) { yield }
|
14
18
|
end
|
15
19
|
end
|
16
20
|
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Rodauth
|
2
|
+
module Rails
|
3
|
+
module Feature
|
4
|
+
module InternalRequest
|
5
|
+
def post_configure
|
6
|
+
super
|
7
|
+
return unless internal_request?
|
8
|
+
|
9
|
+
self.class.define_singleton_method(:internal_request) do |route, opts = {}, &blk|
|
10
|
+
url_options = ::Rails.application.config.action_mailer.default_url_options || {}
|
11
|
+
|
12
|
+
scheme = url_options[:protocol]
|
13
|
+
port = url_options[:port]
|
14
|
+
port||= Rack::Request::DEFAULT_PORTS[scheme] if Rack.release < "2"
|
15
|
+
host = url_options[:host]
|
16
|
+
host_with_port = host && port ? "#{host}:#{port}" : host
|
17
|
+
|
18
|
+
env = {
|
19
|
+
"HTTP_HOST" => host_with_port,
|
20
|
+
"rack.url_scheme" => scheme,
|
21
|
+
"SERVER_NAME" => host,
|
22
|
+
"SERVER_PORT" => port,
|
23
|
+
}.compact
|
24
|
+
|
25
|
+
opts = opts.merge(env: env) { |k, v1, v2| v2.merge(v1) }
|
26
|
+
|
27
|
+
super(route, opts, &blk)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def rails_controller_around
|
34
|
+
return yield if internal_request?
|
35
|
+
super
|
36
|
+
end
|
37
|
+
|
38
|
+
def rails_instrument_request
|
39
|
+
return yield if internal_request?
|
40
|
+
super
|
41
|
+
end
|
42
|
+
|
43
|
+
def rails_instrument_redirection
|
44
|
+
return yield if internal_request?
|
45
|
+
super
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Rodauth
|
2
|
+
module Rails
|
3
|
+
class Model < Module
|
4
|
+
require "rodauth/rails/model/associations"
|
5
|
+
|
6
|
+
def initialize(auth_class, association_options: {})
|
7
|
+
@auth_class = auth_class
|
8
|
+
@association_options = association_options
|
9
|
+
|
10
|
+
define_methods
|
11
|
+
end
|
12
|
+
|
13
|
+
def included(model)
|
14
|
+
fail Rodauth::Rails::Error, "must be an Active Record model" unless model < ActiveRecord::Base
|
15
|
+
|
16
|
+
define_associations(model)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def define_methods
|
22
|
+
rodauth = @auth_class.allocate.freeze
|
23
|
+
|
24
|
+
attr_reader :password
|
25
|
+
|
26
|
+
define_method(:password=) do |password|
|
27
|
+
@password = password
|
28
|
+
password_hash = rodauth.send(:password_hash, password) if password
|
29
|
+
set_password_hash(password_hash)
|
30
|
+
end
|
31
|
+
|
32
|
+
define_method(:set_password_hash) do |password_hash|
|
33
|
+
if rodauth.account_password_hash_column
|
34
|
+
public_send(:"#{rodauth.account_password_hash_column}=", password_hash)
|
35
|
+
else
|
36
|
+
if password_hash
|
37
|
+
record = self.password_hash || build_password_hash
|
38
|
+
record.public_send(:"#{rodauth.password_hash_column}=", password_hash)
|
39
|
+
else
|
40
|
+
self.password_hash&.mark_for_destruction
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def define_associations(model)
|
47
|
+
define_password_hash_association(model) unless rodauth.account_password_hash_column
|
48
|
+
|
49
|
+
feature_associations.each do |association|
|
50
|
+
define_association(model, **association)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def define_password_hash_association(model)
|
55
|
+
password_hash_id_column = rodauth.password_hash_id_column
|
56
|
+
scope = -> { select(password_hash_id_column) } if rodauth.send(:use_database_authentication_functions?)
|
57
|
+
|
58
|
+
define_association model,
|
59
|
+
type: :has_one,
|
60
|
+
name: :password_hash,
|
61
|
+
table: rodauth.password_hash_table,
|
62
|
+
foreign_key: password_hash_id_column,
|
63
|
+
scope: scope,
|
64
|
+
autosave: true
|
65
|
+
end
|
66
|
+
|
67
|
+
def define_association(model, type:, name:, table:, foreign_key:, scope: nil, **options)
|
68
|
+
associated_model = Class.new(model.superclass)
|
69
|
+
associated_model.table_name = table
|
70
|
+
associated_model.belongs_to :account,
|
71
|
+
class_name: model.name,
|
72
|
+
foreign_key: foreign_key,
|
73
|
+
inverse_of: name
|
74
|
+
|
75
|
+
model.const_set(name.to_s.singularize.camelize, associated_model)
|
76
|
+
|
77
|
+
model.public_send type, name, scope,
|
78
|
+
class_name: associated_model.name,
|
79
|
+
foreign_key: foreign_key,
|
80
|
+
dependent: :destroy,
|
81
|
+
inverse_of: :account,
|
82
|
+
**options,
|
83
|
+
**association_options(name)
|
84
|
+
end
|
85
|
+
|
86
|
+
def feature_associations
|
87
|
+
Rodauth::Rails::Model::Associations.call(rodauth)
|
88
|
+
end
|
89
|
+
|
90
|
+
def association_options(name)
|
91
|
+
options = @association_options
|
92
|
+
options = options.call(name) if options.respond_to?(:call)
|
93
|
+
options || {}
|
94
|
+
end
|
95
|
+
|
96
|
+
def rodauth
|
97
|
+
@auth_class.allocate
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
module Rodauth
|
2
|
+
module Rails
|
3
|
+
class Model
|
4
|
+
class Associations
|
5
|
+
attr_reader :rodauth
|
6
|
+
|
7
|
+
def self.call(rodauth)
|
8
|
+
new(rodauth).call
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(rodauth)
|
12
|
+
@rodauth = rodauth
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
rodauth.features
|
17
|
+
.select { |feature| respond_to?(feature, true) }
|
18
|
+
.flat_map { |feature| send(feature) }
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def remember
|
24
|
+
{
|
25
|
+
name: :remember_key,
|
26
|
+
type: :has_one,
|
27
|
+
table: rodauth.remember_table,
|
28
|
+
foreign_key: rodauth.remember_id_column,
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def verify_account
|
33
|
+
{
|
34
|
+
name: :verification_key,
|
35
|
+
type: :has_one,
|
36
|
+
table: rodauth.verify_account_table,
|
37
|
+
foreign_key: rodauth.verify_account_id_column,
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def reset_password
|
42
|
+
{
|
43
|
+
name: :password_reset_key,
|
44
|
+
type: :has_one,
|
45
|
+
table: rodauth.reset_password_table,
|
46
|
+
foreign_key: rodauth.reset_password_id_column,
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def verify_login_change
|
51
|
+
{
|
52
|
+
name: :login_change_key,
|
53
|
+
type: :has_one,
|
54
|
+
table: rodauth.verify_login_change_table,
|
55
|
+
foreign_key: rodauth.verify_login_change_id_column,
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
def lockout
|
60
|
+
[
|
61
|
+
{
|
62
|
+
name: :lockout,
|
63
|
+
type: :has_one,
|
64
|
+
table: rodauth.account_lockouts_table,
|
65
|
+
foreign_key: rodauth.account_lockouts_id_column,
|
66
|
+
},
|
67
|
+
{
|
68
|
+
name: :login_failure,
|
69
|
+
type: :has_one,
|
70
|
+
table: rodauth.account_login_failures_table,
|
71
|
+
foreign_key: rodauth.account_login_failures_id_column,
|
72
|
+
}
|
73
|
+
]
|
74
|
+
end
|
75
|
+
|
76
|
+
def email_auth
|
77
|
+
{
|
78
|
+
name: :email_auth_key,
|
79
|
+
type: :has_one,
|
80
|
+
table: rodauth.email_auth_table,
|
81
|
+
foreign_key: rodauth.email_auth_id_column,
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
def account_expiration
|
86
|
+
{
|
87
|
+
name: :activity_time,
|
88
|
+
type: :has_one,
|
89
|
+
table: rodauth.account_activity_table,
|
90
|
+
foreign_key: rodauth.account_activity_id_column,
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
94
|
+
def active_sessions
|
95
|
+
{
|
96
|
+
name: :active_session_keys,
|
97
|
+
type: :has_many,
|
98
|
+
table: rodauth.active_sessions_table,
|
99
|
+
foreign_key: rodauth.active_sessions_account_id_column,
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
def audit_logging
|
104
|
+
{
|
105
|
+
name: :authentication_audit_logs,
|
106
|
+
type: :has_many,
|
107
|
+
table: rodauth.audit_logging_table,
|
108
|
+
foreign_key: rodauth.audit_logging_account_id_column,
|
109
|
+
dependent: nil,
|
110
|
+
}
|
111
|
+
end
|
112
|
+
|
113
|
+
def disallow_password_reuse
|
114
|
+
{
|
115
|
+
name: :previous_password_hashes,
|
116
|
+
type: :has_many,
|
117
|
+
table: rodauth.previous_password_hash_table,
|
118
|
+
foreign_key: rodauth.previous_password_account_id_column,
|
119
|
+
}
|
120
|
+
end
|
121
|
+
|
122
|
+
def jwt_refresh
|
123
|
+
{
|
124
|
+
name: :jwt_refresh_keys,
|
125
|
+
type: :has_many,
|
126
|
+
table: rodauth.jwt_refresh_token_table,
|
127
|
+
foreign_key: rodauth.jwt_refresh_token_account_id_column,
|
128
|
+
}
|
129
|
+
end
|
130
|
+
|
131
|
+
def password_expiration
|
132
|
+
{
|
133
|
+
name: :password_change_time,
|
134
|
+
type: :has_one,
|
135
|
+
table: rodauth.password_expiration_table,
|
136
|
+
foreign_key: rodauth.password_expiration_id_column,
|
137
|
+
}
|
138
|
+
end
|
139
|
+
|
140
|
+
def single_session
|
141
|
+
{
|
142
|
+
name: :session_key,
|
143
|
+
type: :has_one,
|
144
|
+
table: rodauth.single_session_table,
|
145
|
+
foreign_key: rodauth.single_session_id_column,
|
146
|
+
}
|
147
|
+
end
|
148
|
+
|
149
|
+
def otp
|
150
|
+
{
|
151
|
+
name: :otp_key,
|
152
|
+
type: :has_one,
|
153
|
+
table: rodauth.otp_keys_table,
|
154
|
+
foreign_key: rodauth.otp_keys_id_column,
|
155
|
+
}
|
156
|
+
end
|
157
|
+
|
158
|
+
def sms_codes
|
159
|
+
{
|
160
|
+
name: :sms_code,
|
161
|
+
type: :has_one,
|
162
|
+
table: rodauth.sms_codes_table,
|
163
|
+
foreign_key: rodauth.sms_id_column,
|
164
|
+
}
|
165
|
+
end
|
166
|
+
|
167
|
+
def recovery_codes
|
168
|
+
{
|
169
|
+
name: :recovery_codes,
|
170
|
+
type: :has_many,
|
171
|
+
table: rodauth.recovery_codes_table,
|
172
|
+
foreign_key: rodauth.recovery_codes_id_column,
|
173
|
+
}
|
174
|
+
end
|
175
|
+
|
176
|
+
def webauthn
|
177
|
+
[
|
178
|
+
{
|
179
|
+
name: :webauthn_user_id,
|
180
|
+
type: :has_one,
|
181
|
+
table: rodauth.webauthn_user_ids_table,
|
182
|
+
foreign_key: rodauth.webauthn_user_ids_account_id_column,
|
183
|
+
},
|
184
|
+
{
|
185
|
+
name: :webauthn_keys,
|
186
|
+
type: :has_many,
|
187
|
+
table: rodauth.webauthn_keys_table,
|
188
|
+
foreign_key: rodauth.webauthn_keys_account_id_column,
|
189
|
+
}
|
190
|
+
]
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
@@ -4,15 +4,15 @@ namespace :rodauth do
|
|
4
4
|
|
5
5
|
puts "Routes handled by #{app}:"
|
6
6
|
|
7
|
-
app.opts[:rodauths].
|
8
|
-
|
7
|
+
app.opts[:rodauths].each do |configuration_name, auth_class|
|
8
|
+
auth_class.configure { enable :path_class_methods }
|
9
9
|
|
10
|
-
routes =
|
10
|
+
routes = auth_class.routes.map do |handle_method|
|
11
11
|
path_method = "#{handle_method.to_s.sub(/\Ahandle_/, "")}_path"
|
12
12
|
|
13
13
|
[
|
14
|
-
|
15
|
-
"rodauth#{
|
14
|
+
auth_class.public_send(path_method),
|
15
|
+
"rodauth#{configuration_name && "(:#{configuration_name})"}.#{path_method}",
|
16
16
|
]
|
17
17
|
end
|
18
18
|
|
data/rodauth-rails.gemspec
CHANGED
@@ -17,10 +17,13 @@ Gem::Specification.new do |spec|
|
|
17
17
|
spec.require_paths = ["lib"]
|
18
18
|
|
19
19
|
spec.add_dependency "railties", ">= 4.2", "< 7"
|
20
|
-
spec.add_dependency "rodauth", "~> 2.
|
20
|
+
spec.add_dependency "rodauth", "~> 2.15"
|
21
21
|
spec.add_dependency "sequel-activerecord_connection", "~> 1.1"
|
22
22
|
spec.add_dependency "tilt"
|
23
23
|
spec.add_dependency "bcrypt"
|
24
24
|
|
25
25
|
spec.add_development_dependency "jwt"
|
26
|
+
spec.add_development_dependency "rotp"
|
27
|
+
spec.add_development_dependency "rqrcode"
|
28
|
+
spec.add_development_dependency "webauthn" unless RUBY_ENGINE == "jruby"
|
26
29
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rodauth-rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.15.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Janko Marohnić
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-07-
|
11
|
+
date: 2021-07-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: railties
|
@@ -36,14 +36,14 @@ dependencies:
|
|
36
36
|
requirements:
|
37
37
|
- - "~>"
|
38
38
|
- !ruby/object:Gem::Version
|
39
|
-
version: '2.
|
39
|
+
version: '2.15'
|
40
40
|
type: :runtime
|
41
41
|
prerelease: false
|
42
42
|
version_requirements: !ruby/object:Gem::Requirement
|
43
43
|
requirements:
|
44
44
|
- - "~>"
|
45
45
|
- !ruby/object:Gem::Version
|
46
|
-
version: '2.
|
46
|
+
version: '2.15'
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: sequel-activerecord_connection
|
49
49
|
requirement: !ruby/object:Gem::Requirement
|
@@ -100,6 +100,48 @@ dependencies:
|
|
100
100
|
- - ">="
|
101
101
|
- !ruby/object:Gem::Version
|
102
102
|
version: '0'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: rotp
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
type: :development
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
- !ruby/object:Gem::Dependency
|
118
|
+
name: rqrcode
|
119
|
+
requirement: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
type: :development
|
125
|
+
prerelease: false
|
126
|
+
version_requirements: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
- !ruby/object:Gem::Dependency
|
132
|
+
name: webauthn
|
133
|
+
requirement: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
type: :development
|
139
|
+
prerelease: false
|
140
|
+
version_requirements: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - ">="
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0'
|
103
145
|
description: Provides Rails integration for Rodauth.
|
104
146
|
email:
|
105
147
|
- janko.marohnic@gmail.com
|
@@ -212,8 +254,11 @@ files:
|
|
212
254
|
- lib/rodauth/rails/feature/csrf.rb
|
213
255
|
- lib/rodauth/rails/feature/email.rb
|
214
256
|
- lib/rodauth/rails/feature/instrumentation.rb
|
257
|
+
- lib/rodauth/rails/feature/internal_request.rb
|
215
258
|
- lib/rodauth/rails/feature/render.rb
|
216
259
|
- lib/rodauth/rails/middleware.rb
|
260
|
+
- lib/rodauth/rails/model.rb
|
261
|
+
- lib/rodauth/rails/model/associations.rb
|
217
262
|
- lib/rodauth/rails/railtie.rb
|
218
263
|
- lib/rodauth/rails/tasks.rake
|
219
264
|
- lib/rodauth/rails/version.rb
|