rodauth-oauth 0.1.0 → 0.2.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 +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
|