rodauth-oauth 0.0.2 → 0.0.3
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 +34 -2
- data/LICENSE.txt +191 -0
- data/README.md +145 -13
- data/lib/generators/roda/oauth/install_generator.rb +1 -1
- data/lib/generators/roda/oauth/views_generator.rb +1 -6
- data/lib/rodauth/features/oauth.rb +148 -96
- data/lib/rodauth/features/oauth_http_mac.rb +111 -0
- data/lib/rodauth/features/oauth_jwt.rb +228 -0
- data/lib/rodauth/oauth/version.rb +1 -1
- metadata +6 -2
@@ -10,7 +10,7 @@ module Rodauth::OAuth::Rails
|
|
10
10
|
include ::Rails::Generators::Migration
|
11
11
|
|
12
12
|
source_root "#{__dir__}/templates"
|
13
|
-
namespace "
|
13
|
+
namespace "rodauth:oauth:install"
|
14
14
|
|
15
15
|
def create_rodauth_migration
|
16
16
|
return unless defined?(ActiveRecord::Base)
|
@@ -7,7 +7,7 @@ module Rodauth::OAuth
|
|
7
7
|
module Generators
|
8
8
|
class ViewsGenerator < ::Rails::Generators::Base
|
9
9
|
source_root "#{__dir__}/templates"
|
10
|
-
namespace "
|
10
|
+
namespace "rodauth:oauth:views"
|
11
11
|
|
12
12
|
DEFAULT = %w[oauth_authorize].freeze
|
13
13
|
VIEWS = {
|
@@ -16,11 +16,6 @@ module Rodauth::OAuth
|
|
16
16
|
}.freeze
|
17
17
|
|
18
18
|
DEPENDENCIES = {
|
19
|
-
active_sessions: :logout,
|
20
|
-
otp: :two_factor_base,
|
21
|
-
sms_codes: :two_factor_base,
|
22
|
-
recovery_codes: :two_factor_base,
|
23
|
-
webauthn: :two_factor_base
|
24
19
|
}.freeze
|
25
20
|
|
26
21
|
class_option :features, type: :array,
|
@@ -33,8 +33,6 @@ module Rodauth
|
|
33
33
|
|
34
34
|
SCOPES = %w[profile.read].freeze
|
35
35
|
|
36
|
-
depends :login
|
37
|
-
|
38
36
|
before "authorize"
|
39
37
|
after "authorize"
|
40
38
|
after "authorize_failure"
|
@@ -64,13 +62,17 @@ module Rodauth
|
|
64
62
|
|
65
63
|
auth_value_method :json_response_content_type, "application/json"
|
66
64
|
|
67
|
-
auth_value_method :oauth_grant_expires_in, 60 * 5 #
|
65
|
+
auth_value_method :oauth_grant_expires_in, 60 * 5 # 60 minutes
|
68
66
|
auth_value_method :oauth_token_expires_in, 60 * 60 # 60 minutes
|
69
|
-
auth_value_method :use_oauth_implicit_grant_type
|
67
|
+
auth_value_method :use_oauth_implicit_grant_type?, false
|
68
|
+
auth_value_method :use_oauth_pkce?, true
|
69
|
+
auth_value_method :use_oauth_access_type?, true
|
70
70
|
|
71
71
|
auth_value_method :oauth_require_pkce, false
|
72
72
|
auth_value_method :oauth_pkce_challenge_method, "S256"
|
73
73
|
|
74
|
+
auth_value_method :oauth_valid_uri_schemes, %w[http https]
|
75
|
+
|
74
76
|
# URL PARAMS
|
75
77
|
|
76
78
|
# Authorize / token
|
@@ -168,6 +170,8 @@ module Rodauth
|
|
168
170
|
:secret_hash
|
169
171
|
)
|
170
172
|
|
173
|
+
auth_value_methods(:only_json?)
|
174
|
+
|
171
175
|
redirect(:oauth_application) do |id|
|
172
176
|
"/#{oauth_applications_path}/#{id}"
|
173
177
|
end
|
@@ -175,13 +179,14 @@ module Rodauth
|
|
175
179
|
redirect(:require_authorization) do
|
176
180
|
if logged_in?
|
177
181
|
oauth_authorize_path
|
178
|
-
|
182
|
+
elsif respond_to?(:login_redirect)
|
179
183
|
login_redirect
|
184
|
+
else
|
185
|
+
default_redirect
|
180
186
|
end
|
181
187
|
end
|
182
188
|
|
183
|
-
auth_value_method :
|
184
|
-
auth_methods(:json_request?)
|
189
|
+
auth_value_method :json_request_regexp, %r{\bapplication/(?:vnd\.api\+)?json\b}i
|
185
190
|
|
186
191
|
def check_csrf?
|
187
192
|
case request.path
|
@@ -189,6 +194,8 @@ module Rodauth
|
|
189
194
|
false
|
190
195
|
when oauth_revoke_path
|
191
196
|
!json_request?
|
197
|
+
when oauth_authorize_path, %r{/#{oauth_applications_path}}
|
198
|
+
only_json? ? false : super
|
192
199
|
else
|
193
200
|
super
|
194
201
|
end
|
@@ -199,10 +206,19 @@ module Rodauth
|
|
199
206
|
super || authorization_token
|
200
207
|
end
|
201
208
|
|
202
|
-
def
|
203
|
-
return
|
209
|
+
def accepts_json?
|
210
|
+
return true if only_json?
|
204
211
|
|
205
|
-
|
212
|
+
(accept = request.env["HTTP_ACCEPT"]) && accept =~ json_request_regexp
|
213
|
+
end
|
214
|
+
|
215
|
+
unless method_defined?(:json_request?)
|
216
|
+
# copied from the jwt feature
|
217
|
+
def json_request?
|
218
|
+
return @json_request if defined?(@json_request)
|
219
|
+
|
220
|
+
@json_request = request.content_type =~ json_request_regexp
|
221
|
+
end
|
206
222
|
end
|
207
223
|
|
208
224
|
attr_reader :oauth_application
|
@@ -275,7 +291,7 @@ module Rodauth
|
|
275
291
|
|
276
292
|
scopes << oauth_application_default_scope if scopes.empty?
|
277
293
|
|
278
|
-
token_scopes = authorization_token[
|
294
|
+
token_scopes = authorization_token[oauth_tokens_scopes_column].split(",")
|
279
295
|
|
280
296
|
authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
|
281
297
|
end
|
@@ -426,6 +442,18 @@ module Rodauth
|
|
426
442
|
end
|
427
443
|
end
|
428
444
|
|
445
|
+
def json_access_token_payload(oauth_token)
|
446
|
+
payload = {
|
447
|
+
"access_token" => oauth_token[oauth_tokens_token_column],
|
448
|
+
"token_type" => oauth_token_type.downcase,
|
449
|
+
"expires_in" => oauth_token_expires_in
|
450
|
+
}
|
451
|
+
if oauth_token[oauth_tokens_refresh_token_column]
|
452
|
+
payload["refresh_token"] = oauth_token[oauth_tokens_refresh_token_column]
|
453
|
+
end
|
454
|
+
payload
|
455
|
+
end
|
456
|
+
|
429
457
|
# Oauth Application
|
430
458
|
|
431
459
|
def oauth_application_params
|
@@ -444,7 +472,9 @@ module Rodauth
|
|
444
472
|
if key == oauth_application_homepage_url_param ||
|
445
473
|
key == oauth_application_redirect_uri_param
|
446
474
|
|
447
|
-
|
475
|
+
unless URI::DEFAULT_PARSER.make_regexp(oauth_valid_uri_schemes).match?(value)
|
476
|
+
set_field_error(key, invalid_url_message)
|
477
|
+
end
|
448
478
|
|
449
479
|
elsif key == oauth_application_scopes_param
|
450
480
|
|
@@ -509,6 +539,9 @@ module Rodauth
|
|
509
539
|
end
|
510
540
|
|
511
541
|
# Authorize
|
542
|
+
def before_authorize
|
543
|
+
require_account
|
544
|
+
end
|
512
545
|
|
513
546
|
def validate_oauth_grant_params
|
514
547
|
unless oauth_application && check_valid_redirect_uri? && check_valid_access_type? &&
|
@@ -517,7 +550,7 @@ module Rodauth
|
|
517
550
|
end
|
518
551
|
redirect_response_error("invalid_scope") unless check_valid_scopes?
|
519
552
|
|
520
|
-
validate_pkce_challenge_params
|
553
|
+
validate_pkce_challenge_params if use_oauth_pkce?
|
521
554
|
end
|
522
555
|
|
523
556
|
def try_approval_prompt
|
@@ -548,18 +581,24 @@ module Rodauth
|
|
548
581
|
oauth_grants_scopes_column => scopes.join(",")
|
549
582
|
}
|
550
583
|
|
551
|
-
|
552
|
-
|
584
|
+
# Access Type flow
|
585
|
+
if use_oauth_access_type?
|
586
|
+
if (access_type = param_or_nil(access_type_param))
|
587
|
+
create_params[oauth_grants_access_type_column] = access_type
|
588
|
+
end
|
553
589
|
end
|
554
590
|
|
555
591
|
# PKCE flow
|
556
|
-
if
|
557
|
-
code_challenge_method = param_or_nil(code_challenge_method_param)
|
592
|
+
if use_oauth_pkce?
|
558
593
|
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
594
|
+
if (code_challenge = param_or_nil(code_challenge_param))
|
595
|
+
code_challenge_method = param_or_nil(code_challenge_method_param)
|
596
|
+
|
597
|
+
create_params[oauth_grants_code_challenge_column] = code_challenge
|
598
|
+
create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
|
599
|
+
elsif oauth_require_pkce
|
600
|
+
redirect_response_error("code_challenge_required")
|
601
|
+
end
|
563
602
|
end
|
564
603
|
|
565
604
|
ds = db[oauth_grants_table]
|
@@ -613,19 +652,29 @@ module Rodauth
|
|
613
652
|
|
614
653
|
case param(grant_type_param)
|
615
654
|
when "authorization_code"
|
655
|
+
create_oauth_token_from_authorization_code(oauth_application)
|
656
|
+
when "refresh_token"
|
657
|
+
create_oauth_token_from_token(oauth_application)
|
658
|
+
else
|
659
|
+
redirect_response_error("invalid_grant")
|
660
|
+
end
|
661
|
+
end
|
616
662
|
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
663
|
+
def create_oauth_token_from_authorization_code(oauth_application)
|
664
|
+
# fetch oauth grant
|
665
|
+
oauth_grant = db[oauth_grants_table].where(
|
666
|
+
oauth_grants_code_column => param(code_param),
|
667
|
+
oauth_grants_redirect_uri_column => param(redirect_uri_param),
|
668
|
+
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
669
|
+
oauth_grants_revoked_at_column => nil
|
670
|
+
).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
|
671
|
+
.for_update
|
672
|
+
.first
|
625
673
|
|
626
|
-
|
674
|
+
redirect_response_error("invalid_grant") unless oauth_grant
|
627
675
|
|
628
|
-
|
676
|
+
# PKCE
|
677
|
+
if use_oauth_pkce?
|
629
678
|
if oauth_grant[oauth_grants_code_challenge_column]
|
630
679
|
code_verifier = param_or_nil(code_verifier_param)
|
631
680
|
|
@@ -635,63 +684,69 @@ module Rodauth
|
|
635
684
|
elsif oauth_require_pkce
|
636
685
|
redirect_response_error("code_challenge_required")
|
637
686
|
end
|
687
|
+
end
|
688
|
+
|
689
|
+
create_params = {
|
690
|
+
oauth_tokens_account_id_column => oauth_grant[oauth_grants_account_id_column],
|
691
|
+
oauth_tokens_oauth_application_id_column => oauth_grant[oauth_grants_oauth_application_id_column],
|
692
|
+
oauth_tokens_oauth_grant_id_column => oauth_grant[oauth_grants_id_column],
|
693
|
+
oauth_tokens_scopes_column => oauth_grant[oauth_grants_scopes_column]
|
694
|
+
}
|
638
695
|
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
oauth_tokens_oauth_grant_id_column => oauth_grant[oauth_grants_id_column],
|
643
|
-
oauth_tokens_scopes_column => oauth_grant[oauth_grants_scopes_column]
|
644
|
-
}
|
696
|
+
# revoke oauth grant
|
697
|
+
db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
|
698
|
+
.update(oauth_grants_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
|
645
699
|
|
646
|
-
|
647
|
-
|
648
|
-
.update(oauth_grants_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
|
700
|
+
should_generate_refresh_token = !use_oauth_access_type? ||
|
701
|
+
oauth_grant[oauth_grants_access_type_column] == "offline"
|
649
702
|
|
650
|
-
|
703
|
+
generate_oauth_token(create_params, should_generate_refresh_token)
|
704
|
+
end
|
651
705
|
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
706
|
+
def create_oauth_token_from_token(oauth_application)
|
707
|
+
# fetch oauth token
|
708
|
+
oauth_token = oauth_token_by_refresh_token(param(refresh_token_param)).where(
|
709
|
+
oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column]
|
710
|
+
).where(oauth_grants_revoked_at_column => nil).for_update.first
|
657
711
|
|
658
|
-
|
712
|
+
redirect_response_error("invalid_grant") unless oauth_token
|
659
713
|
|
660
|
-
|
714
|
+
token = oauth_unique_id_generator
|
661
715
|
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
716
|
+
update_params = {
|
717
|
+
oauth_tokens_oauth_application_id_column => oauth_token[oauth_grants_oauth_application_id_column],
|
718
|
+
oauth_tokens_expires_in_column => Time.now + oauth_token_expires_in
|
719
|
+
}
|
666
720
|
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
721
|
+
if oauth_tokens_token_hash_column
|
722
|
+
update_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
|
723
|
+
else
|
724
|
+
update_params[oauth_tokens_token_column] = token
|
725
|
+
end
|
672
726
|
|
673
|
-
|
727
|
+
ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
|
674
728
|
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
end
|
682
|
-
rescue Sequel::UniqueConstraintViolation
|
683
|
-
retry
|
729
|
+
oauth_token = begin
|
730
|
+
if ds.supports_returning?(:update)
|
731
|
+
ds.returning.update(update_params)
|
732
|
+
else
|
733
|
+
ds.update(update_params)
|
734
|
+
ds.first
|
684
735
|
end
|
685
|
-
|
686
|
-
|
687
|
-
oauth_token
|
688
|
-
else
|
689
|
-
redirect_response_error("invalid_grant")
|
736
|
+
rescue Sequel::UniqueConstraintViolation
|
737
|
+
retry
|
690
738
|
end
|
739
|
+
|
740
|
+
oauth_token[oauth_tokens_token_column] = token
|
741
|
+
oauth_token
|
691
742
|
end
|
692
743
|
|
693
744
|
# Token revocation
|
694
745
|
|
746
|
+
def before_revoke
|
747
|
+
require_account
|
748
|
+
end
|
749
|
+
|
695
750
|
TOKEN_HINT_TYPES = %w[access_token refresh_token].freeze
|
696
751
|
|
697
752
|
def validate_oauth_revoke_params
|
@@ -719,7 +774,7 @@ module Rodauth
|
|
719
774
|
oauth_applications_account_id_column => account_id
|
720
775
|
).select(oauth_applications_id_column)
|
721
776
|
)
|
722
|
-
).first
|
777
|
+
).for_update.first
|
723
778
|
|
724
779
|
redirect_response_error("invalid_request") unless oauth_token
|
725
780
|
|
@@ -749,7 +804,7 @@ module Rodauth
|
|
749
804
|
# Response helpers
|
750
805
|
|
751
806
|
def redirect_response_error(error_code, redirect_url = request.referer || default_redirect)
|
752
|
-
if
|
807
|
+
if accepts_json?
|
753
808
|
throw_json_response_error(invalid_oauth_response_status, error_code)
|
754
809
|
else
|
755
810
|
redirect_url = URI.parse(redirect_url)
|
@@ -783,7 +838,7 @@ module Rodauth
|
|
783
838
|
payload["error_description"] = send(:"#{error_code}_message") if respond_to?(:"#{error_code}_message")
|
784
839
|
json_payload = _json_response_body(payload)
|
785
840
|
response["Content-Type"] ||= json_response_content_type
|
786
|
-
response["WWW-Authenticate"] =
|
841
|
+
response["WWW-Authenticate"] = oauth_token_type if status == 401
|
787
842
|
response.write(json_payload)
|
788
843
|
request.halt
|
789
844
|
end
|
@@ -799,7 +854,7 @@ module Rodauth
|
|
799
854
|
end
|
800
855
|
|
801
856
|
def authorization_required
|
802
|
-
if
|
857
|
+
if accepts_json?
|
803
858
|
throw_json_response_error(authorization_required_error_status, "invalid_client")
|
804
859
|
else
|
805
860
|
set_redirect_error_flash(require_authorization_error_flash)
|
@@ -820,6 +875,8 @@ module Rodauth
|
|
820
875
|
ACCESS_TYPES = %w[offline online].freeze
|
821
876
|
|
822
877
|
def check_valid_access_type?
|
878
|
+
return true unless use_oauth_access_type?
|
879
|
+
|
823
880
|
access_type = param_or_nil(access_type_param)
|
824
881
|
!access_type || ACCESS_TYPES.include?(access_type)
|
825
882
|
end
|
@@ -827,6 +884,8 @@ module Rodauth
|
|
827
884
|
APPROVAL_PROMPTS = %w[force auto].freeze
|
828
885
|
|
829
886
|
def check_valid_approval_prompt?
|
887
|
+
return true unless use_oauth_access_type?
|
888
|
+
|
830
889
|
approval_prompt = param_or_nil(approval_prompt_param)
|
831
890
|
!approval_prompt || APPROVAL_PROMPTS.include?(approval_prompt)
|
832
891
|
end
|
@@ -836,7 +895,7 @@ module Rodauth
|
|
836
895
|
|
837
896
|
return true if response_type.nil? || response_type == "code"
|
838
897
|
|
839
|
-
return use_oauth_implicit_grant_type if response_type == "token"
|
898
|
+
return use_oauth_implicit_grant_type? if response_type == "token"
|
840
899
|
|
841
900
|
false
|
842
901
|
end
|
@@ -873,28 +932,21 @@ module Rodauth
|
|
873
932
|
|
874
933
|
# /oauth-token
|
875
934
|
route(:oauth_token) do |r|
|
935
|
+
before_token
|
936
|
+
|
876
937
|
r.post do
|
877
938
|
catch_error do
|
878
939
|
validate_oauth_token_params
|
879
940
|
|
880
941
|
oauth_token = nil
|
881
942
|
transaction do
|
882
|
-
before_token
|
883
943
|
oauth_token = create_oauth_token
|
884
944
|
after_token
|
885
945
|
end
|
886
946
|
|
887
947
|
response.status = 200
|
888
948
|
response["Content-Type"] ||= json_response_content_type
|
889
|
-
|
890
|
-
"token" => oauth_token[oauth_tokens_token_column],
|
891
|
-
"token_type" => oauth_token_type,
|
892
|
-
"expires_in" => oauth_token_expires_in
|
893
|
-
}
|
894
|
-
|
895
|
-
json_response["refresh_token"] = oauth_token[oauth_tokens_refresh_token_column] if oauth_token[:refresh_token]
|
896
|
-
|
897
|
-
json_payload = _json_response_body(json_response)
|
949
|
+
json_payload = _json_response_body(json_access_token_payload(oauth_token))
|
898
950
|
response.write(json_payload)
|
899
951
|
request.halt
|
900
952
|
end
|
@@ -906,6 +958,7 @@ module Rodauth
|
|
906
958
|
# /oauth-revoke
|
907
959
|
route(:oauth_revoke) do |r|
|
908
960
|
require_account
|
961
|
+
before_revoke
|
909
962
|
|
910
963
|
# access-token
|
911
964
|
r.post do
|
@@ -914,12 +967,11 @@ module Rodauth
|
|
914
967
|
|
915
968
|
oauth_token = nil
|
916
969
|
transaction do
|
917
|
-
before_revoke
|
918
970
|
oauth_token = revoke_oauth_token
|
919
971
|
after_revoke
|
920
972
|
end
|
921
973
|
|
922
|
-
if
|
974
|
+
if accepts_json?
|
923
975
|
response.status = 200
|
924
976
|
response["Content-Type"] ||= json_response_content_type
|
925
977
|
json_response = {
|
@@ -944,7 +996,9 @@ module Rodauth
|
|
944
996
|
route(:oauth_authorize) do |r|
|
945
997
|
require_account
|
946
998
|
validate_oauth_grant_params
|
947
|
-
try_approval_prompt if request.get?
|
999
|
+
try_approval_prompt if use_oauth_access_type? && request.get?
|
1000
|
+
|
1001
|
+
before_authorize
|
948
1002
|
|
949
1003
|
r.get do
|
950
1004
|
authorize_view
|
@@ -956,10 +1010,9 @@ module Rodauth
|
|
956
1010
|
fragment_params = []
|
957
1011
|
|
958
1012
|
transaction do
|
959
|
-
before_authorize
|
960
1013
|
case param(response_type_param)
|
961
1014
|
when "token"
|
962
|
-
redirect_response_error("invalid_request", redirect_uri) unless use_oauth_implicit_grant_type
|
1015
|
+
redirect_response_error("invalid_request", redirect_uri) unless use_oauth_implicit_grant_type?
|
963
1016
|
|
964
1017
|
create_params = {
|
965
1018
|
oauth_tokens_account_id_column => account_id,
|
@@ -968,12 +1021,11 @@ module Rodauth
|
|
968
1021
|
}
|
969
1022
|
oauth_token = generate_oauth_token(create_params, false)
|
970
1023
|
|
971
|
-
|
972
|
-
fragment_params
|
973
|
-
fragment_params << ["expires_in=#{oauth_token_expires_in}"]
|
1024
|
+
token_payload = json_access_token_payload(oauth_token)
|
1025
|
+
fragment_params.replace(token_payload.map { |k, v| "#{k}=#{v}" })
|
974
1026
|
when "code", "", nil
|
975
1027
|
code = create_oauth_grant
|
976
|
-
query_params <<
|
1028
|
+
query_params << "code=#{code}"
|
977
1029
|
else
|
978
1030
|
redirect_response_error("invalid_request")
|
979
1031
|
end
|