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.
@@ -10,7 +10,7 @@ module Rodauth::OAuth::Rails
10
10
  include ::Rails::Generators::Migration
11
11
 
12
12
  source_root "#{__dir__}/templates"
13
- namespace "roda:oauth:install"
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 "roda:oauth:views"
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 # 5 minutes
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, false
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
- else
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 :json_request_accept_regexp, %r{\bapplication/(?:vnd\.api\+)?json\b}i
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 json_request?
203
- return @json_request if defined?(@json_request)
209
+ def accepts_json?
210
+ return true if only_json?
204
211
 
205
- @json_request = request.get_header("HTTP_ACCEPT") =~ json_request_accept_regexp
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[:scopes].split(",")
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
- set_field_error(key, invalid_url_message) unless URI::DEFAULT_PARSER.make_regexp(%w[http https]).match?(value)
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
- if (access_type = param_or_nil(access_type_param))
552
- create_params[oauth_grants_access_type_column] = access_type
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 (code_challenge = param_or_nil(code_challenge_param))
557
- code_challenge_method = param_or_nil(code_challenge_method_param)
592
+ if use_oauth_pkce?
558
593
 
559
- create_params[oauth_grants_code_challenge_column] = code_challenge
560
- create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
561
- elsif oauth_require_pkce
562
- redirect_response_error("code_challenge_required")
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
- # fetch oauth grant
618
- oauth_grant = db[oauth_grants_table].where(
619
- oauth_grants_code_column => param(code_param),
620
- oauth_grants_redirect_uri_column => param(redirect_uri_param),
621
- oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
622
- oauth_grants_revoked_at_column => nil
623
- ).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
624
- .first
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
- redirect_response_error("invalid_grant") unless oauth_grant
674
+ redirect_response_error("invalid_grant") unless oauth_grant
627
675
 
628
- # PKCE
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
- create_params = {
640
- oauth_tokens_account_id_column => oauth_grant[oauth_grants_account_id_column],
641
- oauth_tokens_oauth_application_id_column => oauth_grant[oauth_grants_oauth_application_id_column],
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
- # revoke oauth grant
647
- db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
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
- generate_oauth_token(create_params, oauth_grant[oauth_grants_access_type_column] == "offline")
703
+ generate_oauth_token(create_params, should_generate_refresh_token)
704
+ end
651
705
 
652
- when "refresh_token"
653
- # fetch oauth token
654
- oauth_token = oauth_token_by_refresh_token(param(refresh_token_param)).where(
655
- oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column]
656
- ).where(oauth_grants_revoked_at_column => nil).first
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
- redirect_response_error("invalid_grant") unless oauth_token
712
+ redirect_response_error("invalid_grant") unless oauth_token
659
713
 
660
- token = oauth_unique_id_generator
714
+ token = oauth_unique_id_generator
661
715
 
662
- update_params = {
663
- oauth_tokens_oauth_application_id_column => oauth_token[oauth_grants_oauth_application_id_column],
664
- oauth_tokens_expires_in_column => Time.now + oauth_token_expires_in
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
- if oauth_tokens_token_hash_column
668
- update_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
669
- else
670
- update_params[oauth_tokens_token_column] = token
671
- end
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
- ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
727
+ ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
674
728
 
675
- oauth_token = begin
676
- if ds.supports_returning?(:update)
677
- ds.returning.update(update_params)
678
- else
679
- ds.update(update_params)
680
- ds.first
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
- oauth_token[oauth_tokens_token_column] = token
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 json_request?
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"] = "Bearer" if status == 401
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 json_request?
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
- json_response = {
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 json_request?
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
- fragment_params << ["access_token=#{oauth_token[oauth_tokens_token_column]}"]
972
- fragment_params << ["token_type=#{oauth_token_type}"]
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 << ["code=#{code}"]
1028
+ query_params << "code=#{code}"
977
1029
  else
978
1030
  redirect_response_error("invalid_request")
979
1031
  end