rodauth-oauth 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|