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.
@@ -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