rodauth-oauth 0.1.0 → 0.2.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 +1 -0
- data/lib/generators/roda/oauth/templates/db/migrate/create_rodauth_oauth.rb +4 -4
- data/lib/rodauth/features/oauth.rb +148 -142
- data/lib/rodauth/features/oauth_http_mac.rb +0 -2
- data/lib/rodauth/features/oauth_jwt.rb +56 -38
- data/lib/rodauth/features/oauth_saml.rb +104 -0
- data/lib/rodauth/oauth/database_extensions.rb +73 -0
- data/lib/rodauth/oauth/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 02d69464053b6809900da774b4c9957d642b003d0a0de7aa076e57a5eb8895bc
|
4
|
+
data.tar.gz: e1dd94b69aa4bdf051b1c28d684a0ec2a1435da9f99ca6bf77da9d537474f9a6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cfe325a2e8daa96a72b4577566ae84772dcf114411ebbd9d0115f0f33f5b104f7c138a1b1ef7813d46f0fd2226c6560db5d974e51ec92b33ce7e393726005b2d
|
7
|
+
data.tar.gz: df5866c1cd089c0d361a00d1ffd1db02df9b6551940dbf70e7390657fa8558ae0fb3c8eb2fcff6ec6fb9fd52406a99615574305d5a3bacdbd20f5fc22bacd63f
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,50 @@
|
|
2
2
|
|
3
3
|
## master
|
4
4
|
|
5
|
+
### 0.2.0
|
6
|
+
|
7
|
+
#### Features
|
8
|
+
|
9
|
+
##### SAML Assertion Grant Type
|
10
|
+
|
11
|
+
`rodauth-auth` now supports using a SAML Assertion to request for an Access token.In order to enable, you have to:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
plugin :rodauth do
|
15
|
+
enable :oauth_saml
|
16
|
+
end
|
17
|
+
```
|
18
|
+
|
19
|
+
For more info about integrating it, [check the wiki](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/wikis/SAML-Assertion-Access-Tokens).
|
20
|
+
|
21
|
+
##### Supporting rotating keys
|
22
|
+
|
23
|
+
At some point, you'll want to replace the pkeys and algorithm used to generate and verify the JWT access tokens, but you want to keep validating previously-distributed JWT tokens, at least until they expire. Now you can, via two new options, `oauth_jwt_legacy_public_key` and `oauth_jwt_legacy_algorithm`, which will be declared in the JWKs URI and used to verify access tokens.
|
24
|
+
|
25
|
+
|
26
|
+
##### Reuse access tokens
|
27
|
+
|
28
|
+
If the `oauth_reuse_access_token` is set, if there's already an existing valid access token, any new grant for the same application / account / scope will keep the same access token. This can be helpful in scenarios where one wants the same access token distributed across devices.
|
29
|
+
|
30
|
+
##### require_authorizable_account
|
31
|
+
|
32
|
+
The method used to verify access to the authorize flow is called `require_authorizable_account`. By default, it checks if a user is logged in by using rodauth's own `require_account`. This is the method you'd want to redefine in order to augment these requirements, i.e. request 2fa authentication.
|
33
|
+
|
34
|
+
#### Improvements
|
35
|
+
|
36
|
+
Expired and revoked access tokens end up generating a lot of garbage, which will have to be periodically cleaned up. You can mitigate this now by setting a uniqueness index for a group of columns, i.e. if you set a uniqueness index for the `oauth_application_id/account_id/scopes` column, `rodauth-oauth` will transparently reuse the same db entry to store the new access token. If setting some other type of uniqueness index, make sure to update the option `oauth_tokens_unique_columns` (the array of columns from the uniqueness index).
|
37
|
+
|
38
|
+
#### Bugfixes
|
39
|
+
|
40
|
+
Calling `before_*_route` callbacks appropriately.
|
41
|
+
|
42
|
+
Fixed some mishandling of HTTP headers when in in resource-server mode.
|
43
|
+
|
44
|
+
#### Chore
|
45
|
+
|
46
|
+
* 97.7% test coverage;
|
47
|
+
* `rodauth-oauth` CI tests run against sqlite, postgresql and mysql.
|
48
|
+
|
5
49
|
### 0.1.0
|
6
50
|
|
7
51
|
(31/7/2020)
|
data/README.md
CHANGED
@@ -21,6 +21,7 @@ This gem implements the following RFCs and features of OAuth:
|
|
21
21
|
* Access Type (Token refresh online and offline);
|
22
22
|
* [MAC Authentication Scheme](https://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-02);
|
23
23
|
* [JWT Acess Tokens](https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-07);
|
24
|
+
* [SAML 2.0 Assertion Access Tokens](https://tools.ietf.org/html/draft-ietf-oauth-saml2-bearer-03);
|
24
25
|
* [JWT Secured Authorization Requests](https://tools.ietf.org/html/draft-ietf-oauth-jwsreq-20);
|
25
26
|
* OAuth application and token management dashboards;
|
26
27
|
|
@@ -43,14 +43,14 @@ class CreateRodauthOAuth < ActiveRecord::Migration<%= migration_version %>
|
|
43
43
|
t.foreign_key :oauth_tokens, column: :oauth_token_id
|
44
44
|
t.integer :oauth_application_id
|
45
45
|
t.foreign_key :oauth_applications, column: :oauth_application_id
|
46
|
-
t.string :token, null: false, token: true
|
46
|
+
t.string :token, null: false, token: true, unique: true
|
47
47
|
# uncomment if setting oauth_tokens_token_hash_column
|
48
48
|
# and delete the token column
|
49
|
-
# t.string :token_hash, token: true
|
50
|
-
t.string :refresh_token
|
49
|
+
# t.string :token_hash, token: true, unique: true
|
50
|
+
t.string :refresh_token, unique: true
|
51
51
|
# uncomment if setting oauth_tokens_refresh_token_hash_column
|
52
52
|
# and delete the refresh_token column
|
53
|
-
# t.string :refresh_token_hash, token: true
|
53
|
+
# t.string :refresh_token_hash, token: true, unique: true
|
54
54
|
t.datetime :expires_in, null: false
|
55
55
|
t.datetime :revoked_at
|
56
56
|
t.string :scopes, null: false
|
@@ -1,16 +1,21 @@
|
|
1
1
|
# frozen-string-literal: true
|
2
2
|
|
3
|
+
require "time"
|
3
4
|
require "base64"
|
4
5
|
require "securerandom"
|
5
6
|
require "net/http"
|
6
7
|
|
7
8
|
require "rodauth/oauth/ttl_store"
|
9
|
+
require "rodauth/oauth/database_extensions"
|
8
10
|
|
9
11
|
module Rodauth
|
10
12
|
Feature.define(:oauth) do
|
11
13
|
# RUBY EXTENSIONS
|
12
|
-
# :nocov:
|
13
14
|
unless Regexp.method_defined?(:match?)
|
15
|
+
# If you wonder why this is there: the oauth feature uses a refinement to enhance the
|
16
|
+
# Regexp class locally with #match? , but this is never tested, because ActiveSupport
|
17
|
+
# monkey-patches the same method... Please ActiveSupport, stop being so intrusive!
|
18
|
+
# :nocov:
|
14
19
|
module RegexpExtensions
|
15
20
|
refine(Regexp) do
|
16
21
|
def match?(*args)
|
@@ -19,6 +24,7 @@ module Rodauth
|
|
19
24
|
end
|
20
25
|
end
|
21
26
|
using(RegexpExtensions)
|
27
|
+
# :nocov:
|
22
28
|
end
|
23
29
|
|
24
30
|
unless String.method_defined?(:delete_suffix!)
|
@@ -37,7 +43,6 @@ module Rodauth
|
|
37
43
|
end
|
38
44
|
using(SuffixExtensions)
|
39
45
|
end
|
40
|
-
# :nocov:
|
41
46
|
|
42
47
|
SCOPES = %w[profile.read].freeze
|
43
48
|
|
@@ -110,6 +115,8 @@ module Rodauth
|
|
110
115
|
auth_value_method :oauth_tokens_token_hash_column, nil
|
111
116
|
auth_value_method :oauth_tokens_refresh_token_hash_column, nil
|
112
117
|
|
118
|
+
# Access Token reuse
|
119
|
+
auth_value_method :oauth_reuse_access_token, false
|
113
120
|
# OAuth Grants
|
114
121
|
auth_value_method :oauth_grants_table, :oauth_grants
|
115
122
|
auth_value_method :oauth_grants_id_column, :id
|
@@ -124,6 +131,7 @@ module Rodauth
|
|
124
131
|
|
125
132
|
auth_value_method :authorization_required_error_status, 401
|
126
133
|
auth_value_method :invalid_oauth_response_status, 400
|
134
|
+
auth_value_method :already_in_use_response_status, 409
|
127
135
|
|
128
136
|
# OAuth Applications
|
129
137
|
auth_value_method :oauth_applications_path, "oauth-applications"
|
@@ -155,6 +163,8 @@ module Rodauth
|
|
155
163
|
|
156
164
|
auth_value_method :unique_error_message, "is already in use"
|
157
165
|
auth_value_method :null_error_message, "is not filled"
|
166
|
+
auth_value_method :already_in_use_message, "error generating unique token"
|
167
|
+
auth_value_method :already_in_use_error_code, "invalid_request"
|
158
168
|
|
159
169
|
# PKCE
|
160
170
|
auth_value_method :code_challenge_required_error_code, "invalid_request"
|
@@ -172,6 +182,8 @@ module Rodauth
|
|
172
182
|
# Only required to use if the plugin is to be used in a resource server
|
173
183
|
auth_value_method :is_authorization_server?, true
|
174
184
|
|
185
|
+
auth_value_method :oauth_unique_id_generation_retries, 3
|
186
|
+
|
175
187
|
auth_value_methods(
|
176
188
|
:fetch_access_token,
|
177
189
|
:oauth_unique_id_generator,
|
@@ -179,7 +191,9 @@ module Rodauth
|
|
179
191
|
:secret_hash,
|
180
192
|
:generate_token_hash,
|
181
193
|
:authorization_server_url,
|
182
|
-
:before_introspection_request
|
194
|
+
:before_introspection_request,
|
195
|
+
:require_authorizable_account,
|
196
|
+
:oauth_tokens_unique_columns
|
183
197
|
)
|
184
198
|
|
185
199
|
auth_value_methods(:only_json?)
|
@@ -213,14 +227,12 @@ module Rodauth
|
|
213
227
|
end
|
214
228
|
|
215
229
|
unless method_defined?(:json_request?)
|
216
|
-
# :nocov:
|
217
230
|
# copied from the jwt feature
|
218
231
|
def json_request?
|
219
232
|
return @json_request if defined?(@json_request)
|
220
233
|
|
221
234
|
@json_request = request.content_type =~ json_request_regexp
|
222
235
|
end
|
223
|
-
# :nocov:
|
224
236
|
end
|
225
237
|
|
226
238
|
def initialize(scope)
|
@@ -343,7 +355,7 @@ module Rodauth
|
|
343
355
|
end
|
344
356
|
|
345
357
|
request.get do
|
346
|
-
scope.instance_variable_set(:@oauth_applications, db[
|
358
|
+
scope.instance_variable_set(:@oauth_applications, db[oauth_applications_table])
|
347
359
|
oauth_applications_view
|
348
360
|
end
|
349
361
|
|
@@ -375,8 +387,42 @@ module Rodauth
|
|
375
387
|
end
|
376
388
|
end
|
377
389
|
|
390
|
+
def post_configure
|
391
|
+
super
|
392
|
+
self.class.__send__(:include, Rodauth::OAuth::ExtendDatabase(db))
|
393
|
+
|
394
|
+
# Check whether we can reutilize db entries for the same account / application pair
|
395
|
+
one_oauth_token_per_account = begin
|
396
|
+
db.indexes(oauth_tokens_table).values.any? do |definition|
|
397
|
+
definition[:unique] &&
|
398
|
+
definition[:columns] == oauth_tokens_unique_columns
|
399
|
+
end
|
400
|
+
end
|
401
|
+
self.class.send(:define_method, :__one_oauth_token_per_account) { one_oauth_token_per_account }
|
402
|
+
end
|
403
|
+
|
378
404
|
private
|
379
405
|
|
406
|
+
def rescue_from_uniqueness_error(&block)
|
407
|
+
retries = oauth_unique_id_generation_retries
|
408
|
+
begin
|
409
|
+
transaction(savepoint: :only, &block)
|
410
|
+
rescue Sequel::UniqueConstraintViolation
|
411
|
+
redirect_response_error("already_in_use") if retries.zero?
|
412
|
+
retries -= 1
|
413
|
+
retry
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
# OAuth Token Unique/Reuse
|
418
|
+
def oauth_tokens_unique_columns
|
419
|
+
[
|
420
|
+
oauth_tokens_oauth_application_id_column,
|
421
|
+
oauth_tokens_account_id_column,
|
422
|
+
oauth_tokens_scopes_column
|
423
|
+
]
|
424
|
+
end
|
425
|
+
|
380
426
|
def authorization_server_url
|
381
427
|
base_url
|
382
428
|
end
|
@@ -399,10 +445,10 @@ module Rodauth
|
|
399
445
|
|
400
446
|
# time-to-live
|
401
447
|
ttl = if response.key?("cache-control")
|
402
|
-
cache_control = response["
|
448
|
+
cache_control = response["cache-control"]
|
403
449
|
cache_control[/max-age=(\d+)/, 1]
|
404
450
|
elsif response.key?("expires")
|
405
|
-
|
451
|
+
DateTime.httpdate(response["expires"]).utc.to_i - Time.now.utc.to_i
|
406
452
|
end
|
407
453
|
|
408
454
|
[JSON.parse(response.body, symbolize_names: true), ttl]
|
@@ -482,22 +528,11 @@ module Rodauth
|
|
482
528
|
end
|
483
529
|
|
484
530
|
unless method_defined?(:password_hash)
|
485
|
-
# :nocov:
|
486
531
|
# From login_requirements_base feature
|
487
|
-
if ENV["RACK_ENV"] == "test"
|
488
|
-
def password_hash_cost
|
489
|
-
BCrypt::Engine::MIN_COST
|
490
|
-
end
|
491
|
-
else
|
492
|
-
def password_hash_cost
|
493
|
-
BCrypt::Engine::DEFAULT_COST
|
494
|
-
end
|
495
|
-
end
|
496
532
|
|
497
533
|
def password_hash(password)
|
498
|
-
BCrypt::Password.create(password, cost:
|
534
|
+
BCrypt::Password.create(password, cost: BCrypt::Engine::DEFAULT_COST)
|
499
535
|
end
|
500
|
-
# :nocov:
|
501
536
|
end
|
502
537
|
|
503
538
|
def generate_oauth_token(params = {}, should_generate_refresh_token = true)
|
@@ -505,43 +540,60 @@ module Rodauth
|
|
505
540
|
oauth_grants_expires_in_column => Time.now + oauth_token_expires_in
|
506
541
|
}.merge(params)
|
507
542
|
|
508
|
-
|
509
|
-
|
543
|
+
rescue_from_uniqueness_error do
|
544
|
+
token = oauth_unique_id_generator
|
510
545
|
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
546
|
+
if oauth_tokens_token_hash_column
|
547
|
+
create_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
|
548
|
+
else
|
549
|
+
create_params[oauth_tokens_token_column] = token
|
550
|
+
end
|
516
551
|
|
517
|
-
|
518
|
-
|
552
|
+
refresh_token = nil
|
553
|
+
if should_generate_refresh_token
|
554
|
+
refresh_token = oauth_unique_id_generator
|
519
555
|
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
556
|
+
if oauth_tokens_refresh_token_hash_column
|
557
|
+
create_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
|
558
|
+
else
|
559
|
+
create_params[oauth_tokens_refresh_token_column] = refresh_token
|
560
|
+
end
|
524
561
|
end
|
562
|
+
oauth_token = _generate_oauth_token(create_params)
|
563
|
+
oauth_token[oauth_tokens_token_column] = token
|
564
|
+
oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
|
565
|
+
oauth_token
|
525
566
|
end
|
526
|
-
oauth_token = _generate_oauth_token(create_params)
|
527
|
-
|
528
|
-
oauth_token[oauth_tokens_token_column] = token
|
529
|
-
oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
|
530
|
-
oauth_token
|
531
567
|
end
|
532
568
|
|
533
569
|
def _generate_oauth_token(params = {})
|
534
570
|
ds = db[oauth_tokens_table]
|
535
571
|
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
572
|
+
if __one_oauth_token_per_account
|
573
|
+
|
574
|
+
token = __insert_or_update_and_return__(
|
575
|
+
ds,
|
576
|
+
oauth_tokens_id_column,
|
577
|
+
oauth_tokens_unique_columns,
|
578
|
+
params,
|
579
|
+
Sequel.expr(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column]) > Sequel::CURRENT_TIMESTAMP,
|
580
|
+
([oauth_tokens_token_column, oauth_tokens_refresh_token_column] if oauth_reuse_access_token)
|
581
|
+
)
|
582
|
+
|
583
|
+
# if the previous operation didn't return a row, it means that the conditions
|
584
|
+
# invalidated the update, and the existing token is still valid.
|
585
|
+
token || ds.where(
|
586
|
+
oauth_tokens_account_id_column => params[oauth_tokens_account_id_column],
|
587
|
+
oauth_tokens_oauth_application_id_column => params[oauth_tokens_oauth_application_id_column]
|
588
|
+
).first
|
589
|
+
else
|
590
|
+
if oauth_reuse_access_token
|
591
|
+
unique_conds = Hash[oauth_tokens_unique_columns.map { |column| [column, params[column]] }]
|
592
|
+
valid_token = ds.where(Sequel.expr(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column]) > Sequel::CURRENT_TIMESTAMP)
|
593
|
+
.where(unique_conds).first
|
594
|
+
return valid_token if valid_token
|
542
595
|
end
|
543
|
-
|
544
|
-
retry
|
596
|
+
__insert_and_return__(ds, oauth_tokens_id_column, params)
|
545
597
|
end
|
546
598
|
end
|
547
599
|
|
@@ -633,7 +685,6 @@ module Rodauth
|
|
633
685
|
# set client ID/secret pairs
|
634
686
|
|
635
687
|
create_params.merge! \
|
636
|
-
oauth_applications_client_id_column => oauth_unique_id_generator,
|
637
688
|
oauth_applications_client_secret_column => \
|
638
689
|
secret_hash(oauth_application_params[oauth_application_client_secret_param])
|
639
690
|
|
@@ -643,29 +694,14 @@ module Rodauth
|
|
643
694
|
oauth_application_default_scope
|
644
695
|
end
|
645
696
|
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
false
|
650
|
-
rescue Sequel::ConstraintViolation => e
|
651
|
-
e
|
652
|
-
end
|
653
|
-
|
654
|
-
if raised
|
655
|
-
field = raised.message[/\.(.*)$/, 1]
|
656
|
-
case raised
|
657
|
-
when Sequel::UniqueConstraintViolation
|
658
|
-
throw_error(field, unique_error_message)
|
659
|
-
when Sequel::NotNullConstraintViolation
|
660
|
-
throw_error(field, null_error_message)
|
661
|
-
end
|
697
|
+
rescue_from_uniqueness_error do
|
698
|
+
create_params[oauth_applications_client_id_column] = oauth_unique_id_generator
|
699
|
+
db[oauth_applications_table].insert(create_params)
|
662
700
|
end
|
663
|
-
|
664
|
-
!raised && id
|
665
701
|
end
|
666
702
|
|
667
703
|
# Authorize
|
668
|
-
def
|
704
|
+
def require_authorizable_account
|
669
705
|
require_account
|
670
706
|
end
|
671
707
|
|
@@ -709,35 +745,25 @@ module Rodauth
|
|
709
745
|
)
|
710
746
|
|
711
747
|
# Access Type flow
|
712
|
-
if use_oauth_access_type?
|
713
|
-
|
714
|
-
create_params[oauth_grants_access_type_column] = access_type
|
715
|
-
end
|
748
|
+
if use_oauth_access_type? && (access_type = param_or_nil("access_type"))
|
749
|
+
create_params[oauth_grants_access_type_column] = access_type
|
716
750
|
end
|
717
751
|
|
718
752
|
# PKCE flow
|
719
|
-
if use_oauth_pkce?
|
753
|
+
if use_oauth_pkce? && (code_challenge = param_or_nil("code_challenge"))
|
754
|
+
code_challenge_method = param_or_nil("code_challenge_method")
|
720
755
|
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
create_params[oauth_grants_code_challenge_column] = code_challenge
|
725
|
-
create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
|
726
|
-
elsif oauth_require_pkce
|
727
|
-
redirect_response_error("code_challenge_required")
|
728
|
-
end
|
756
|
+
create_params[oauth_grants_code_challenge_column] = code_challenge
|
757
|
+
create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
|
729
758
|
end
|
730
759
|
|
731
760
|
ds = db[oauth_grants_table]
|
732
761
|
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
ds.insert(create_params)
|
737
|
-
authorization_code
|
738
|
-
rescue Sequel::UniqueConstraintViolation
|
739
|
-
retry
|
762
|
+
rescue_from_uniqueness_error do
|
763
|
+
create_params[oauth_grants_code_column] = oauth_unique_id_generator
|
764
|
+
__insert_and_return__(ds, oauth_grants_id_column, create_params)
|
740
765
|
end
|
766
|
+
create_params[oauth_grants_code_column]
|
741
767
|
end
|
742
768
|
|
743
769
|
def do_authorize(redirect_url, query_params = [], fragment_params = [])
|
@@ -781,10 +807,6 @@ module Rodauth
|
|
781
807
|
|
782
808
|
# Access Tokens
|
783
809
|
|
784
|
-
def before_token
|
785
|
-
require_oauth_application
|
786
|
-
end
|
787
|
-
|
788
810
|
def validate_oauth_token_params
|
789
811
|
unless (grant_type = param_or_nil("grant_type"))
|
790
812
|
redirect_response_error("invalid_request")
|
@@ -834,8 +856,6 @@ module Rodauth
|
|
834
856
|
oauth_tokens_expires_in_column => Time.now + oauth_token_expires_in
|
835
857
|
}
|
836
858
|
create_oauth_token_from_token(oauth_token, update_params)
|
837
|
-
else
|
838
|
-
redirect_response_error("invalid_grant")
|
839
859
|
end
|
840
860
|
end
|
841
861
|
|
@@ -864,29 +884,21 @@ module Rodauth
|
|
864
884
|
def create_oauth_token_from_token(oauth_token, update_params)
|
865
885
|
redirect_response_error("invalid_grant") unless token_from_application?(oauth_token, oauth_application)
|
866
886
|
|
867
|
-
|
868
|
-
|
869
|
-
if oauth_tokens_token_hash_column
|
870
|
-
update_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
|
871
|
-
else
|
872
|
-
update_params[oauth_tokens_token_column] = token
|
873
|
-
end
|
887
|
+
rescue_from_uniqueness_error do
|
888
|
+
token = oauth_unique_id_generator
|
874
889
|
|
875
|
-
|
876
|
-
|
877
|
-
oauth_token = begin
|
878
|
-
if ds.supports_returning?(:update)
|
879
|
-
ds.returning.update(update_params).first
|
890
|
+
if oauth_tokens_token_hash_column
|
891
|
+
update_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
|
880
892
|
else
|
881
|
-
|
882
|
-
ds.first
|
893
|
+
update_params[oauth_tokens_token_column] = token
|
883
894
|
end
|
884
|
-
rescue Sequel::UniqueConstraintViolation
|
885
|
-
retry
|
886
|
-
end
|
887
895
|
|
888
|
-
|
889
|
-
|
896
|
+
ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
|
897
|
+
|
898
|
+
oauth_token = __update_and_return__(ds, update_params)
|
899
|
+
oauth_token[oauth_tokens_token_column] = token
|
900
|
+
oauth_token
|
901
|
+
end
|
890
902
|
end
|
891
903
|
|
892
904
|
TOKEN_HINT_TYPES = %w[access_token refresh_token].freeze
|
@@ -895,8 +907,8 @@ module Rodauth
|
|
895
907
|
|
896
908
|
def validate_oauth_introspect_params
|
897
909
|
# check if valid token hint type
|
898
|
-
if param_or_nil("token_type_hint")
|
899
|
-
redirect_response_error("unsupported_token_type")
|
910
|
+
if param_or_nil("token_type_hint") && !TOKEN_HINT_TYPES.include?(param("token_type_hint"))
|
911
|
+
redirect_response_error("unsupported_token_type")
|
900
912
|
end
|
901
913
|
|
902
914
|
redirect_response_error("invalid_request") unless param_or_nil("token")
|
@@ -914,18 +926,12 @@ module Rodauth
|
|
914
926
|
}
|
915
927
|
end
|
916
928
|
|
917
|
-
def before_introspect; end
|
918
|
-
|
919
929
|
# Token revocation
|
920
930
|
|
921
|
-
def before_revoke
|
922
|
-
require_oauth_application
|
923
|
-
end
|
924
|
-
|
925
931
|
def validate_oauth_revoke_params
|
926
932
|
# check if valid token hint type
|
927
|
-
if param_or_nil("token_type_hint")
|
928
|
-
redirect_response_error("unsupported_token_type")
|
933
|
+
if param_or_nil("token_type_hint") && !TOKEN_HINT_TYPES.include?(param("token_type_hint"))
|
934
|
+
redirect_response_error("unsupported_token_type")
|
929
935
|
end
|
930
936
|
|
931
937
|
redirect_response_error("invalid_request") unless param_or_nil("token")
|
@@ -942,23 +948,13 @@ module Rodauth
|
|
942
948
|
|
943
949
|
redirect_response_error("invalid_request") unless oauth_token
|
944
950
|
|
945
|
-
|
946
|
-
redirect_response_error("invalid_request") unless token_from_application?(oauth_token, oauth_application)
|
947
|
-
else
|
948
|
-
@oauth_application = db[oauth_applications_table].where(oauth_applications_id_column =>
|
949
|
-
oauth_token[oauth_tokens_oauth_application_id_column]).first
|
950
|
-
end
|
951
|
+
redirect_response_error("invalid_request") unless token_from_application?(oauth_token, oauth_application)
|
951
952
|
|
952
953
|
update_params = { oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP }
|
953
954
|
|
954
955
|
ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
|
955
956
|
|
956
|
-
oauth_token =
|
957
|
-
ds.returning.update(update_params).first
|
958
|
-
else
|
959
|
-
ds.update(update_params)
|
960
|
-
ds.first
|
961
|
-
end
|
957
|
+
oauth_token = __update_and_return__(ds, update_params)
|
962
958
|
|
963
959
|
oauth_token[oauth_tokens_token_column] = token
|
964
960
|
oauth_token
|
@@ -976,7 +972,13 @@ module Rodauth
|
|
976
972
|
|
977
973
|
def redirect_response_error(error_code, redirect_url = redirect_uri || request.referer || default_redirect)
|
978
974
|
if accepts_json?
|
979
|
-
|
975
|
+
status_code = if respond_to?(:"#{error_code}_response_status")
|
976
|
+
send(:"#{error_code}_response_status")
|
977
|
+
else
|
978
|
+
invalid_oauth_response_status
|
979
|
+
end
|
980
|
+
|
981
|
+
throw_json_response_error(status_code, error_code)
|
980
982
|
else
|
981
983
|
redirect_url = URI.parse(redirect_url)
|
982
984
|
query_params = []
|
@@ -1023,7 +1025,6 @@ module Rodauth
|
|
1023
1025
|
end
|
1024
1026
|
|
1025
1027
|
unless method_defined?(:_json_response_body)
|
1026
|
-
# :nocov:
|
1027
1028
|
def _json_response_body(hash)
|
1028
1029
|
if request.respond_to?(:convert_to_json)
|
1029
1030
|
request.send(:convert_to_json, hash)
|
@@ -1031,7 +1032,6 @@ module Rodauth
|
|
1031
1032
|
JSON.dump(hash)
|
1032
1033
|
end
|
1033
1034
|
end
|
1034
|
-
# :nocov:
|
1035
1035
|
end
|
1036
1036
|
|
1037
1037
|
def authorization_required
|
@@ -1156,7 +1156,8 @@ module Rodauth
|
|
1156
1156
|
route(:token) do |r|
|
1157
1157
|
next unless is_authorization_server?
|
1158
1158
|
|
1159
|
-
|
1159
|
+
before_token_route
|
1160
|
+
require_oauth_application
|
1160
1161
|
|
1161
1162
|
r.post do
|
1162
1163
|
catch_error do
|
@@ -1164,6 +1165,7 @@ module Rodauth
|
|
1164
1165
|
|
1165
1166
|
oauth_token = nil
|
1166
1167
|
transaction do
|
1168
|
+
before_token
|
1167
1169
|
oauth_token = create_oauth_token
|
1168
1170
|
end
|
1169
1171
|
|
@@ -1178,12 +1180,13 @@ module Rodauth
|
|
1178
1180
|
route(:introspect) do |r|
|
1179
1181
|
next unless is_authorization_server?
|
1180
1182
|
|
1181
|
-
|
1183
|
+
before_introspect_route
|
1182
1184
|
|
1183
1185
|
r.post do
|
1184
1186
|
catch_error do
|
1185
1187
|
validate_oauth_introspect_params
|
1186
1188
|
|
1189
|
+
before_introspect
|
1187
1190
|
oauth_token = case param("token_type_hint")
|
1188
1191
|
when "access_token"
|
1189
1192
|
oauth_token_by_token(param("token"))
|
@@ -1211,7 +1214,8 @@ module Rodauth
|
|
1211
1214
|
route(:revoke) do |r|
|
1212
1215
|
next unless is_authorization_server?
|
1213
1216
|
|
1214
|
-
|
1217
|
+
before_revoke_route
|
1218
|
+
require_oauth_application
|
1215
1219
|
|
1216
1220
|
r.post do
|
1217
1221
|
catch_error do
|
@@ -1219,6 +1223,7 @@ module Rodauth
|
|
1219
1223
|
|
1220
1224
|
oauth_token = nil
|
1221
1225
|
transaction do
|
1226
|
+
before_revoke
|
1222
1227
|
oauth_token = revoke_oauth_token
|
1223
1228
|
after_revoke
|
1224
1229
|
end
|
@@ -1242,12 +1247,12 @@ module Rodauth
|
|
1242
1247
|
route(:authorize) do |r|
|
1243
1248
|
next unless is_authorization_server?
|
1244
1249
|
|
1245
|
-
|
1250
|
+
before_authorize_route
|
1251
|
+
require_authorizable_account
|
1252
|
+
|
1246
1253
|
validate_oauth_grant_params
|
1247
1254
|
try_approval_prompt if use_oauth_access_type? && request.get?
|
1248
1255
|
|
1249
|
-
before_authorize
|
1250
|
-
|
1251
1256
|
r.get do
|
1252
1257
|
authorize_view
|
1253
1258
|
end
|
@@ -1256,6 +1261,7 @@ module Rodauth
|
|
1256
1261
|
redirect_url = URI.parse(redirect_uri)
|
1257
1262
|
|
1258
1263
|
transaction do
|
1264
|
+
before_authorize
|
1259
1265
|
do_authorize(redirect_url)
|
1260
1266
|
end
|
1261
1267
|
redirect(redirect_url.to_s)
|
@@ -2,7 +2,6 @@
|
|
2
2
|
|
3
3
|
module Rodauth
|
4
4
|
Feature.define(:oauth_http_mac) do
|
5
|
-
# :nocov:
|
6
5
|
unless String.method_defined?(:delete_prefix)
|
7
6
|
module PrefixExtensions
|
8
7
|
refine(String) do
|
@@ -28,7 +27,6 @@ module Rodauth
|
|
28
27
|
end
|
29
28
|
using(PrefixExtensions)
|
30
29
|
end
|
31
|
-
# :nocov:
|
32
30
|
|
33
31
|
depends :oauth
|
34
32
|
|
@@ -22,6 +22,10 @@ module Rodauth
|
|
22
22
|
auth_value_method :oauth_jwt_jwe_algorithm, nil
|
23
23
|
auth_value_method :oauth_jwt_jwe_encryption_method, nil
|
24
24
|
|
25
|
+
# values used for rotating keys
|
26
|
+
auth_value_method :oauth_jwt_legacy_public_key, nil
|
27
|
+
auth_value_method :oauth_jwt_legacy_algorithm, nil
|
28
|
+
|
25
29
|
auth_value_method :oauth_jwt_jwe_copyright, nil
|
26
30
|
auth_value_method :oauth_jwt_audience, nil
|
27
31
|
|
@@ -88,9 +92,7 @@ module Rodauth
|
|
88
92
|
jws_jwk = if oauth_application[oauth_application_jws_jwk_column]
|
89
93
|
jwk = oauth_application[oauth_application_jws_jwk_column]
|
90
94
|
|
91
|
-
if jwk
|
92
|
-
jwk = JSON.parse(jwk, symbolize_names: true) if jwk.is_a?(String)
|
93
|
-
end
|
95
|
+
jwk = JSON.parse(jwk, symbolize_names: true) if jwk && jwk.is_a?(String)
|
94
96
|
else
|
95
97
|
redirect_response_error("invalid_request_object")
|
96
98
|
end
|
@@ -105,8 +107,8 @@ module Rodauth
|
|
105
107
|
# [RFC7519] specification. The value of "aud" should be the value of
|
106
108
|
# the Authorization Server (AS) "issuer" as defined in RFC8414
|
107
109
|
# [RFC8414].
|
108
|
-
claims.delete(
|
109
|
-
audience = claims.delete(
|
110
|
+
claims.delete("iss")
|
111
|
+
audience = claims.delete("aud")
|
110
112
|
|
111
113
|
redirect_response_error("invalid_request_object") if audience && audience != authorization_server_url
|
112
114
|
|
@@ -119,11 +121,17 @@ module Rodauth
|
|
119
121
|
|
120
122
|
# /token
|
121
123
|
|
122
|
-
def
|
124
|
+
def require_oauth_application
|
123
125
|
# requset authentication optional for assertions
|
124
|
-
return
|
126
|
+
return super unless param("grant_type") == "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
125
127
|
|
126
|
-
|
128
|
+
claims = jwt_decode(param("assertion"))
|
129
|
+
|
130
|
+
redirect_response_error("invalid_grant") unless claims
|
131
|
+
|
132
|
+
@oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["client_id"]).first
|
133
|
+
|
134
|
+
authorization_required unless @oauth_application
|
127
135
|
end
|
128
136
|
|
129
137
|
def validate_oauth_token_params
|
@@ -145,10 +153,6 @@ module Rodauth
|
|
145
153
|
def create_oauth_token_from_assertion
|
146
154
|
claims = jwt_decode(param("assertion"))
|
147
155
|
|
148
|
-
redirect_response_error("invalid_grant") unless claims
|
149
|
-
|
150
|
-
@oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["client_id"]).first
|
151
|
-
|
152
156
|
account = account_ds(claims["sub"]).first
|
153
157
|
|
154
158
|
redirect_response_error("invalid_client") unless oauth_application && account
|
@@ -167,17 +171,19 @@ module Rodauth
|
|
167
171
|
oauth_grants_expires_in_column => Time.now + oauth_token_expires_in
|
168
172
|
}.merge(params)
|
169
173
|
|
170
|
-
|
171
|
-
|
174
|
+
oauth_token = rescue_from_uniqueness_error do
|
175
|
+
if should_generate_refresh_token
|
176
|
+
refresh_token = oauth_unique_id_generator
|
172
177
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
178
|
+
if oauth_tokens_refresh_token_hash_column
|
179
|
+
create_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
|
180
|
+
else
|
181
|
+
create_params[oauth_tokens_refresh_token_column] = refresh_token
|
182
|
+
end
|
177
183
|
end
|
178
|
-
end
|
179
184
|
|
180
|
-
|
185
|
+
_generate_oauth_token(create_params)
|
186
|
+
end
|
181
187
|
|
182
188
|
claims = jwt_claims(oauth_token)
|
183
189
|
|
@@ -293,10 +299,10 @@ module Rodauth
|
|
293
299
|
|
294
300
|
# time-to-live
|
295
301
|
ttl = if response.key?("cache-control")
|
296
|
-
cache_control = response["
|
302
|
+
cache_control = response["cache-control"]
|
297
303
|
cache_control[/max-age=(\d+)/, 1]
|
298
304
|
elsif response.key?("expires")
|
299
|
-
|
305
|
+
DateTime.httpdate(response["expires"]).utc.to_i - Time.now.utc.to_i
|
300
306
|
end
|
301
307
|
|
302
308
|
[JSON.parse(response.body, symbolize_names: true), ttl]
|
@@ -304,7 +310,6 @@ module Rodauth
|
|
304
310
|
end
|
305
311
|
|
306
312
|
if defined?(JSON::JWT)
|
307
|
-
# :nocov:
|
308
313
|
|
309
314
|
def jwk_import(data)
|
310
315
|
JSON::JWK.new(data)
|
@@ -330,23 +335,27 @@ module Rodauth
|
|
330
335
|
def jwt_decode(token, jws_key: oauth_jwt_public_key || _jwt_key, **)
|
331
336
|
token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if oauth_jwt_jwe_key
|
332
337
|
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
+
if is_authorization_server?
|
339
|
+
if oauth_jwt_legacy_public_key
|
340
|
+
JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks_set }))
|
341
|
+
elsif jws_key
|
342
|
+
JSON::JWT.decode(token, jws_key)
|
343
|
+
end
|
344
|
+
elsif (jwks = auth_server_jwks_set)
|
345
|
+
JSON::JWT.decode(token, JSON::JWK::Set.new(jwks))
|
346
|
+
end
|
338
347
|
rescue JSON::JWT::Exception
|
339
348
|
nil
|
340
349
|
end
|
341
350
|
|
342
351
|
def jwks_set
|
343
|
-
[
|
352
|
+
@jwks_set ||= [
|
344
353
|
(JSON::JWK.new(oauth_jwt_public_key).merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
|
354
|
+
(JSON::JWK.new(oauth_jwt_legacy_public_key).merge(use: "sig", alg: oauth_jwt_legacy_algorithm) if oauth_jwt_legacy_public_key),
|
345
355
|
(JSON::JWK.new(oauth_jwt_jwe_public_key).merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key)
|
346
356
|
].compact
|
347
357
|
end
|
348
358
|
|
349
|
-
# :nocov:
|
350
359
|
elsif defined?(JWT)
|
351
360
|
|
352
361
|
# ruby-jwt
|
@@ -391,21 +400,30 @@ module Rodauth
|
|
391
400
|
def jwt_decode(token, jws_key: oauth_jwt_public_key || _jwt_key, jws_algorithm: oauth_jwt_algorithm)
|
392
401
|
# decrypt jwe
|
393
402
|
token = JWE.decrypt(token, oauth_jwt_jwe_key) if oauth_jwt_jwe_key
|
394
|
-
|
395
403
|
# decode jwt
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
404
|
+
if is_authorization_server?
|
405
|
+
if oauth_jwt_legacy_public_key
|
406
|
+
algorithms = jwks_set.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
|
407
|
+
JWT.decode(token, nil, true, jwks: { keys: jwks_set }, algorithms: algorithms).first
|
408
|
+
elsif jws_key
|
409
|
+
JWT.decode(token, jws_key, true, algorithms: [jws_algorithm]).first
|
410
|
+
end
|
411
|
+
elsif (jwks = auth_server_jwks_set)
|
412
|
+
algorithms = jwks[:keys].select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
|
413
|
+
JWT.decode(token, nil, true, jwks: jwks, algorithms: algorithms).first
|
414
|
+
end
|
402
415
|
rescue JWT::DecodeError, JWT::JWKError
|
403
416
|
nil
|
404
417
|
end
|
405
418
|
|
406
419
|
def jwks_set
|
407
|
-
[
|
420
|
+
@jwks_set ||= [
|
408
421
|
(JWT::JWK.new(oauth_jwt_public_key).export.merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
|
422
|
+
(
|
423
|
+
if oauth_jwt_legacy_public_key
|
424
|
+
JWT::JWK.new(oauth_jwt_legacy_public_key).export.merge(use: "sig", alg: oauth_jwt_legacy_algorithm)
|
425
|
+
end
|
426
|
+
),
|
409
427
|
(JWT::JWK.new(oauth_jwt_jwe_public_key).export.merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key)
|
410
428
|
].compact
|
411
429
|
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
require "onelogin/ruby-saml"
|
4
|
+
|
5
|
+
module Rodauth
|
6
|
+
Feature.define(:oauth_saml) do
|
7
|
+
depends :oauth
|
8
|
+
|
9
|
+
auth_value_method :oauth_saml_cert_fingerprint, "9E:65:2E:03:06:8D:80:F2:86:C7:6C:77:A1:D9:14:97:0A:4D:F4:4D"
|
10
|
+
auth_value_method :oauth_saml_cert_fingerprint_algorithm, nil
|
11
|
+
auth_value_method :oauth_saml_name_identifier_format, "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
|
12
|
+
|
13
|
+
auth_value_method :oauth_saml_security_authn_requests_signed, false
|
14
|
+
auth_value_method :oauth_saml_security_metadata_signed, false
|
15
|
+
auth_value_method :oauth_saml_security_digest_method, XMLSecurity::Document::SHA1
|
16
|
+
auth_value_method :oauth_saml_security_signature_method, XMLSecurity::Document::RSA_SHA1
|
17
|
+
|
18
|
+
SAML_GRANT_TYPE = "http://oauth.net/grant_type/assertion/saml/2.0/bearer"
|
19
|
+
|
20
|
+
# /token
|
21
|
+
|
22
|
+
def require_oauth_application
|
23
|
+
# requset authentication optional for assertions
|
24
|
+
return super unless param("grant_type") == SAML_GRANT_TYPE && !param_or_nil("client_id")
|
25
|
+
|
26
|
+
# TODO: invalid grant
|
27
|
+
authorization_required unless saml_assertion
|
28
|
+
|
29
|
+
redirect_uri = saml_assertion.destination
|
30
|
+
|
31
|
+
@oauth_application = db[oauth_applications_table].where(
|
32
|
+
oauth_applications_homepage_url_column => saml_assertion.audiences,
|
33
|
+
oauth_applications_redirect_uri_column => redirect_uri
|
34
|
+
).first
|
35
|
+
|
36
|
+
# The Assertion's <Issuer> element MUST contain a unique identifier
|
37
|
+
# for the entity that issued the Assertion.
|
38
|
+
authorization_required unless saml_assertion.issuers.all? do |issuer|
|
39
|
+
issuer.start_with?(@oauth_application[oauth_applications_homepage_url_column])
|
40
|
+
end
|
41
|
+
|
42
|
+
authorization_required unless @oauth_application
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def secret_matches?(oauth_application, secret)
|
48
|
+
return super unless param_or_nil("assertion")
|
49
|
+
|
50
|
+
true
|
51
|
+
end
|
52
|
+
|
53
|
+
def saml_assertion
|
54
|
+
return @saml_assertion if defined?(@saml_assertion)
|
55
|
+
|
56
|
+
@saml_assertion = begin
|
57
|
+
settings = OneLogin::RubySaml::Settings.new
|
58
|
+
settings.idp_cert_fingerprint = oauth_saml_cert_fingerprint
|
59
|
+
settings.idp_cert_fingerprint_algorithm = oauth_saml_cert_fingerprint_algorithm
|
60
|
+
settings.name_identifier_format = oauth_saml_name_identifier_format
|
61
|
+
settings.security[:authn_requests_signed] = oauth_saml_security_authn_requests_signed
|
62
|
+
settings.security[:metadata_signed] = oauth_saml_security_metadata_signed
|
63
|
+
settings.security[:digest_method] = oauth_saml_security_digest_method
|
64
|
+
settings.security[:signature_method] = oauth_saml_security_signature_method
|
65
|
+
|
66
|
+
response = OneLogin::RubySaml::Response.new(param("assertion"), settings: settings, skip_recipient_check: true)
|
67
|
+
|
68
|
+
return unless response.is_valid?
|
69
|
+
|
70
|
+
response
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def validate_oauth_token_params
|
75
|
+
return super unless param("grant_type") == SAML_GRANT_TYPE
|
76
|
+
|
77
|
+
redirect_response_error("invalid_client") unless param_or_nil("assertion")
|
78
|
+
|
79
|
+
redirect_response_error("invalid_scope") unless check_valid_scopes?
|
80
|
+
end
|
81
|
+
|
82
|
+
def create_oauth_token
|
83
|
+
if param("grant_type") == SAML_GRANT_TYPE
|
84
|
+
create_oauth_token_from_saml_assertion
|
85
|
+
else
|
86
|
+
super
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def create_oauth_token_from_saml_assertion
|
91
|
+
account = db[accounts_table].where(login_column => saml_assertion.nameid).first
|
92
|
+
|
93
|
+
redirect_response_error("invalid_client") unless oauth_application && account
|
94
|
+
|
95
|
+
create_params = {
|
96
|
+
oauth_tokens_account_id_column => account[account_id_column],
|
97
|
+
oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
98
|
+
oauth_tokens_scopes_column => (param_or_nil("scope") || oauth_application[oauth_applications_scopes_column])
|
99
|
+
}
|
100
|
+
|
101
|
+
generate_oauth_token(create_params, false)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rodauth
|
4
|
+
module OAuth
|
5
|
+
# rubocop:disable Naming/MethodName, Metrics/ParameterLists
|
6
|
+
def self.ExtendDatabase(db)
|
7
|
+
Module.new do
|
8
|
+
dataset = db.dataset
|
9
|
+
|
10
|
+
if dataset.supports_returning?(:insert)
|
11
|
+
def __insert_and_return__(dataset, _pkey, params)
|
12
|
+
dataset.returning.insert(params).first
|
13
|
+
end
|
14
|
+
else
|
15
|
+
def __insert_and_return__(dataset, pkey, params)
|
16
|
+
id = dataset.insert(params)
|
17
|
+
dataset.where(pkey => id).first
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
if dataset.supports_returning?(:update)
|
22
|
+
def __update_and_return__(dataset, params)
|
23
|
+
dataset.returning.update(params).first
|
24
|
+
end
|
25
|
+
else
|
26
|
+
def __update_and_return__(dataset, params)
|
27
|
+
dataset.update(params)
|
28
|
+
dataset.first
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
if dataset.respond_to?(:supports_insert_conflict?) && dataset.supports_insert_conflict?
|
33
|
+
def __insert_or_update_and_return__(dataset, pkey, unique_columns, params, conds = nil, exclude_on_update = nil)
|
34
|
+
to_update = params.keys - unique_columns
|
35
|
+
to_update -= exclude_on_update if exclude_on_update
|
36
|
+
|
37
|
+
dataset = dataset.insert_conflict(
|
38
|
+
target: unique_columns,
|
39
|
+
update: Hash[ to_update.map { |attribute| [attribute, Sequel[:excluded][attribute]] } ],
|
40
|
+
update_where: conds
|
41
|
+
)
|
42
|
+
|
43
|
+
__insert_and_return__(dataset, pkey, params)
|
44
|
+
end
|
45
|
+
else
|
46
|
+
def __insert_or_update_and_return__(dataset, pkey, unique_columns, params, conds = nil, exclude_on_update = nil)
|
47
|
+
find_params, update_params = params.partition { |key, _| unique_columns.include?(key) }.map { |h| Hash[h] }
|
48
|
+
|
49
|
+
dataset_where = dataset.where(find_params)
|
50
|
+
record = if conds
|
51
|
+
dataset_where_conds = dataset_where.where(conds)
|
52
|
+
|
53
|
+
# this means that there's still a valid entry there, so return early
|
54
|
+
return if dataset_where.count != dataset_where_conds.count
|
55
|
+
|
56
|
+
dataset_where_conds.first
|
57
|
+
else
|
58
|
+
dataset_where.first
|
59
|
+
end
|
60
|
+
|
61
|
+
if record
|
62
|
+
update_params.reject! { |k, _v| exclude_on_update.include?(k) } if exclude_on_update
|
63
|
+
__update_and_return__(dataset_where, update_params)
|
64
|
+
else
|
65
|
+
__insert_and_return__(dataset, pkey, params)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
# rubocop:enable Naming/MethodName, Metrics/ParameterLists
|
72
|
+
end
|
73
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rodauth-oauth
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tiago Cardoso
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-09-09 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Implementation of the OAuth 2.0 protocol on top of rodauth.
|
14
14
|
email:
|
@@ -32,8 +32,10 @@ files:
|
|
32
32
|
- lib/rodauth/features/oauth.rb
|
33
33
|
- lib/rodauth/features/oauth_http_mac.rb
|
34
34
|
- lib/rodauth/features/oauth_jwt.rb
|
35
|
+
- lib/rodauth/features/oauth_saml.rb
|
35
36
|
- lib/rodauth/features/oidc.rb
|
36
37
|
- lib/rodauth/oauth.rb
|
38
|
+
- lib/rodauth/oauth/database_extensions.rb
|
37
39
|
- lib/rodauth/oauth/railtie.rb
|
38
40
|
- lib/rodauth/oauth/ttl_store.rb
|
39
41
|
- lib/rodauth/oauth/version.rb
|