rodauth-rails 0.14.0 → 0.15.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 +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
|