loading
Generated 2023-04-29T00:05:02+00:00

All Files ( 96.17% covered at 368.69 hits/line )

34 files in total.
2950 relevant lines, 2837 lines covered and 113 lines missed. ( 96.17% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
lib/rodauth/features/oauth_application_management.rb 100.00 % 210 117 117 0 57.44
lib/rodauth/features/oauth_assertion_base.rb 100.00 % 92 38 38 0 53.05
lib/rodauth/features/oauth_authorization_code_grant.rb 100.00 % 165 76 76 0 510.16
lib/rodauth/features/oauth_authorize_base.rb 98.45 % 260 129 127 2 576.87
lib/rodauth/features/oauth_base.rb 96.51 % 869 430 415 15 1187.80
lib/rodauth/features/oauth_client_credentials_grant.rb 100.00 % 35 17 17 0 33.18
lib/rodauth/features/oauth_device_code_grant.rb 95.33 % 208 107 102 5 50.02
lib/rodauth/features/oauth_dynamic_client_registration.rb 96.36 % 426 220 212 8 631.82
lib/rodauth/features/oauth_grant_management.rb 100.00 % 70 33 33 0 63.27
lib/rodauth/features/oauth_implicit_grant.rb 100.00 % 95 49 49 0 276.00
lib/rodauth/features/oauth_jwt.rb 100.00 % 128 57 57 0 259.51
lib/rodauth/features/oauth_jwt_base.rb 93.46 % 499 214 200 14 178.26
lib/rodauth/features/oauth_jwt_bearer_grant.rb 100.00 % 91 47 47 0 37.28
lib/rodauth/features/oauth_jwt_jwks.rb 100.00 % 47 23 23 0 36.52
lib/rodauth/features/oauth_jwt_secured_authorization_request.rb 98.28 % 124 58 57 1 142.34
lib/rodauth/features/oauth_jwt_secured_authorization_response_mode.rb 98.53 % 126 68 67 1 70.24
lib/rodauth/features/oauth_management_base.rb 100.00 % 72 33 33 0 149.09
lib/rodauth/features/oauth_pkce.rb 100.00 % 93 49 49 0 23.27
lib/rodauth/features/oauth_pushed_authorization_request.rb 95.16 % 135 62 59 3 31.55
lib/rodauth/features/oauth_resource_indicators.rb 93.90 % 166 82 77 5 53.85
lib/rodauth/features/oauth_resource_server.rb 96.30 % 59 27 26 1 59.56
lib/rodauth/features/oauth_saml_bearer_grant.rb 96.00 % 108 50 48 2 18.00
lib/rodauth/features/oauth_tls_client_auth.rb 89.16 % 170 83 74 9 125.69
lib/rodauth/features/oauth_token_introspection.rb 98.33 % 139 60 59 1 98.60
lib/rodauth/features/oauth_token_revocation.rb 100.00 % 124 60 60 0 76.80
lib/rodauth/features/oidc.rb 93.51 % 855 385 360 25 166.32
lib/rodauth/features/oidc_dynamic_client_registration.rb 89.60 % 273 125 112 13 103.49
lib/rodauth/features/oidc_rp_initiated_logout.rb 94.23 % 117 52 49 3 28.85
lib/rodauth/features/oidc_self_issued.rb 97.06 % 73 34 33 1 22.24
lib/rodauth/oauth.rb 100.00 % 35 18 18 0 2278.00
lib/rodauth/oauth/database_extensions.rb 100.00 % 88 41 41 0 1540.24
lib/rodauth/oauth/http_extensions.rb 95.56 % 74 45 43 2 113.87
lib/rodauth/oauth/jwe_extensions.rb 100.00 % 64 33 33 0 34.09
lib/rodauth/oauth/ttl_store.rb 92.86 % 67 28 26 2 12.43

lib/rodauth/features/oauth_application_management.rb

100.0% lines covered

117 relevant lines. 117 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 12 require "rodauth/oauth"
  3. 12 module Rodauth
  4. 12 Feature.define(:oauth_application_management, :OauthApplicationManagement) do
  5. 12 depends :oauth_management_base, :oauth_token_revocation
  6. 12 before "create_oauth_application"
  7. 12 after "create_oauth_application"
  8. 12 error_flash "There was an error registering your oauth application", "create_oauth_application"
  9. 12 notice_flash "Your oauth application has been registered", "create_oauth_application"
  10. 12 view "oauth_applications", "Oauth Applications", "oauth_applications"
  11. 12 view "oauth_application", "Oauth Application", "oauth_application"
  12. 12 view "new_oauth_application", "New Oauth Application", "new_oauth_application"
  13. 12 view "oauth_application_oauth_grants", "Oauth Application Grants", "oauth_application_oauth_grants"
  14. # Application
  15. 12 APPLICATION_REQUIRED_PARAMS = %w[name scopes homepage_url redirect_uri client_secret].freeze
  16. 12 auth_value_method :oauth_application_required_params, APPLICATION_REQUIRED_PARAMS
  17. 12 (APPLICATION_REQUIRED_PARAMS + %w[description client_id]).each do |param|
  18. 84 auth_value_method :"oauth_application_#{param}_param", param
  19. end
  20. 12 translatable_method :oauth_applications_name_label, "Name"
  21. 12 translatable_method :oauth_applications_description_label, "Description"
  22. 12 translatable_method :oauth_applications_scopes_label, "Default scopes"
  23. 12 translatable_method :oauth_applications_contacts_label, "Contacts"
  24. 12 translatable_method :oauth_applications_tos_uri_label, "Terms of service"
  25. 12 translatable_method :oauth_applications_policy_uri_label, "Policy"
  26. 12 translatable_method :oauth_applications_jwks_label, "JSON Web Keys"
  27. 12 translatable_method :oauth_applications_jwks_uri_label, "JSON Web Keys URI"
  28. 12 translatable_method :oauth_applications_homepage_url_label, "Homepage URL"
  29. 12 translatable_method :oauth_applications_redirect_uri_label, "Redirect URI"
  30. 12 translatable_method :oauth_applications_client_secret_label, "Client Secret"
  31. 12 translatable_method :oauth_applications_client_id_label, "Client ID"
  32. 12 %w[type token refresh_token expires_in revoked_at].each do |param|
  33. 60 translatable_method :"oauth_grants_#{param}_label", param.gsub("_", " ").capitalize
  34. end
  35. 12 button "Register", "oauth_application"
  36. 12 button "Revoke", "oauth_grant_revoke"
  37. 12 auth_value_method :oauth_applications_oauth_grants_path, "oauth-grants"
  38. 12 auth_value_method :oauth_applications_route, "oauth-applications"
  39. 12 auth_value_method :oauth_applications_per_page, 20
  40. 12 auth_value_method :oauth_applications_id_pattern, Integer
  41. 12 auth_value_method :oauth_grants_per_page, 20
  42. 12 translatable_method :invalid_url_message, "Invalid URL"
  43. 12 translatable_method :null_error_message, "is not filled"
  44. 12 translatable_method :oauth_no_applications_text, "No oauth applications yet!"
  45. 12 translatable_method :oauth_no_grants_text, "No oauth grants yet!"
  46. 12 auth_value_methods(
  47. :oauth_application_path
  48. )
  49. 12 def oauth_applications_path(opts = {})
  50. 1032 route_path(oauth_applications_route, opts)
  51. end
  52. 12 def oauth_application_path(id)
  53. 204 "#{oauth_applications_path}/#{id}"
  54. end
  55. # /oauth-applications routes
  56. 12 def load_oauth_application_management_routes
  57. 252 request.on(oauth_applications_route) do
  58. 252 check_csrf if check_csrf?
  59. 252 require_account
  60. 252 request.get "new" do
  61. 36 new_oauth_application_view
  62. end
  63. 216 request.on(oauth_applications_id_pattern) do |id|
  64. 84 oauth_application = db[oauth_applications_table]
  65. .where(oauth_applications_id_column => id)
  66. .where(oauth_applications_account_id_column => account_id)
  67. .first
  68. 84 next unless oauth_application
  69. 72 scope.instance_variable_set(:@oauth_application, oauth_application)
  70. 72 request.is do
  71. 24 request.get do
  72. 24 oauth_application_view
  73. end
  74. end
  75. 48 request.on(oauth_applications_oauth_grants_path) do
  76. 48 page = Integer(param_or_nil("page") || 1)
  77. 48 per_page = per_page_param(oauth_grants_per_page)
  78. 48 oauth_grants = db[oauth_grants_table]
  79. .where(oauth_grants_oauth_application_id_column => id)
  80. .order(Sequel.desc(oauth_grants_id_column))
  81. 48 scope.instance_variable_set(:@oauth_grants, oauth_grants.paginate(page, per_page))
  82. 48 request.is do
  83. 48 request.get do
  84. 48 oauth_application_oauth_grants_view
  85. end
  86. end
  87. end
  88. end
  89. 132 request.is do
  90. 132 request.get do
  91. 84 page = Integer(param_or_nil("page") || 1)
  92. 84 per_page = per_page_param(oauth_applications_per_page)
  93. 84 scope.instance_variable_set(:@oauth_applications, db[oauth_applications_table]
  94. .where(oauth_applications_account_id_column => account_id)
  95. .order(Sequel.desc(oauth_applications_id_column))
  96. .paginate(page, per_page))
  97. 84 oauth_applications_view
  98. end
  99. 48 request.post do
  100. 48 catch_error do
  101. 48 validate_oauth_application_params
  102. 24 transaction do
  103. 24 before_create_oauth_application
  104. 24 id = create_oauth_application
  105. 24 after_create_oauth_application
  106. 24 set_notice_flash create_oauth_application_notice_flash
  107. 24 redirect "#{request.path}/#{id}"
  108. end
  109. end
  110. 24 set_error_flash create_oauth_application_error_flash
  111. 24 new_oauth_application_view
  112. end
  113. end
  114. end
  115. end
  116. 12 private
  117. 12 def oauth_application_params
  118. 192 @oauth_application_params ||= oauth_application_required_params.each_with_object({}) do |param, params|
  119. 240 value = request.params[__send__(:"oauth_application_#{param}_param")]
  120. 240 if value && !value.empty?
  121. 156 params[param] = value
  122. else
  123. 84 set_field_error(param, null_error_message)
  124. end
  125. end
  126. end
  127. 12 def validate_oauth_application_params
  128. 48 oauth_application_params.each do |key, value|
  129. 156 if key == oauth_application_homepage_url_param
  130. 36 set_field_error(key, invalid_url_message) unless check_valid_uri?(value)
  131. 120 elsif key == oauth_application_redirect_uri_param
  132. 36 if value.respond_to?(:each)
  133. 12 value.each do |uri|
  134. 24 next if uri.empty?
  135. 24 set_field_error(key, invalid_url_message) unless check_valid_no_fragment_uri?(uri)
  136. end
  137. else
  138. 24 set_field_error(key, invalid_url_message) unless check_valid_no_fragment_uri?(value)
  139. end
  140. 84 elsif key == oauth_application_scopes_param
  141. 24 value.each do |scope|
  142. 48 set_field_error(key, oauth_invalid_scope_message) unless oauth_application_scopes.include?(scope)
  143. end
  144. end
  145. end
  146. 48 throw :rodauth_error if @field_errors && !@field_errors.empty?
  147. end
  148. 12 def create_oauth_application
  149. 8 create_params = {
  150. 16 oauth_applications_account_id_column => account_id,
  151. oauth_applications_name_column => oauth_application_params[oauth_application_name_param],
  152. oauth_applications_description_column => oauth_application_params[oauth_application_description_param],
  153. oauth_applications_scopes_column => oauth_application_params[oauth_application_scopes_param],
  154. oauth_applications_homepage_url_column => oauth_application_params[oauth_application_homepage_url_param]
  155. }
  156. 24 redirect_uris = oauth_application_params[oauth_application_redirect_uri_param]
  157. 24 redirect_uris = redirect_uris.to_a.reject(&:empty?).join(" ") if redirect_uris.respond_to?(:each)
  158. 24 create_params[oauth_applications_redirect_uri_column] = redirect_uris unless redirect_uris.empty?
  159. # set client ID/secret pairs
  160. 24 set_client_secret(create_params, oauth_application_params[oauth_application_client_secret_param])
  161. 24 if create_params[oauth_applications_scopes_column]
  162. 24 create_params[oauth_applications_scopes_column] = create_params[oauth_applications_scopes_column].join(oauth_scope_separator)
  163. end
  164. 24 rescue_from_uniqueness_error do
  165. 24 create_params[oauth_applications_client_id_column] = oauth_unique_id_generator
  166. 24 db[oauth_applications_table].insert(create_params)
  167. end
  168. end
  169. end
  170. end

lib/rodauth/features/oauth_assertion_base.rb

100.0% lines covered

38 relevant lines. 38 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 12 require "rodauth/oauth"
  3. 12 module Rodauth
  4. 12 Feature.define(:oauth_assertion_base, :OauthAssertionBase) do
  5. 12 depends :oauth_base
  6. 12 auth_value_methods(
  7. :assertion_grant_type?,
  8. :client_assertion_type?,
  9. :assertion_grant_type,
  10. :client_assertion_type
  11. )
  12. 12 private
  13. 12 def validate_token_params
  14. 96 return super unless assertion_grant_type?
  15. 48 redirect_response_error("invalid_grant") unless param_or_nil("assertion")
  16. end
  17. 12 def require_oauth_application
  18. 192 if assertion_grant_type?
  19. 48 @oauth_application = __send__(:"require_oauth_application_from_#{assertion_grant_type}_assertion_issuer", param("assertion"))
  20. 144 elsif client_assertion_type?
  21. 108 @oauth_application = __send__(:"require_oauth_application_from_#{client_assertion_type}_assertion_subject",
  22. param("client_assertion"))
  23. 72 if (client_id = param_or_nil("client_id")) &&
  24. client_id != @oauth_application[oauth_applications_client_id_column]
  25. # If present, the value of the
  26. # "client_id" parameter MUST identify the same client as is
  27. # identified by the client assertion.
  28. 24 redirect_response_error("invalid_grant")
  29. end
  30. else
  31. 36 super
  32. end
  33. end
  34. 12 def account_from_bearer_assertion_subject(subject)
  35. 48 __insert_or_do_nothing_and_return__(
  36. db[accounts_table],
  37. account_id_column,
  38. [login_column],
  39. login_column => subject
  40. )
  41. end
  42. 12 def create_token(grant_type)
  43. 60 return super unless assertion_grant_type?(grant_type) && supported_grant_type?(grant_type)
  44. 48 account = __send__(:"account_from_#{assertion_grant_type}_assertion", param("assertion"))
  45. 48 redirect_response_error("invalid_grant") unless account
  46. 48 grant_scopes = if param_or_nil("scope")
  47. 24 redirect_response_error("invalid_scope") unless check_valid_scopes?
  48. 12 scopes
  49. else
  50. 24 @oauth_application[oauth_applications_scopes_column]
  51. end
  52. 12 grant_params = {
  53. 24 oauth_grants_type_column => grant_type,
  54. oauth_grants_account_id_column => account[account_id_column],
  55. oauth_grants_oauth_application_id_column => @oauth_application[oauth_applications_id_column],
  56. oauth_grants_scopes_column => grant_scopes
  57. }
  58. 36 generate_token(grant_params, false)
  59. end
  60. 12 def assertion_grant_type?(grant_type = param("grant_type"))
  61. 348 grant_type.start_with?("urn:ietf:params:oauth:grant-type:")
  62. end
  63. 12 def client_assertion_type?(client_assertion_type = param("client_assertion_type"))
  64. 144 client_assertion_type.start_with?("urn:ietf:params:oauth:client-assertion-type:")
  65. end
  66. 12 def assertion_grant_type(grant_type = param("grant_type"))
  67. 96 grant_type.delete_prefix("urn:ietf:params:oauth:grant-type:").tr("-", "_")
  68. end
  69. 12 def client_assertion_type(assertion_type = param("client_assertion_type"))
  70. 108 assertion_type.delete_prefix("urn:ietf:params:oauth:client-assertion-type:").tr("-", "_")
  71. end
  72. end
  73. end

lib/rodauth/features/oauth_authorization_code_grant.rb

100.0% lines covered

76 relevant lines. 76 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 12 require "rodauth/oauth"
  3. 12 module Rodauth
  4. 12 Feature.define(:oauth_authorization_code_grant, :OauthAuthorizationCodeGrant) do
  5. 12 depends :oauth_authorize_base
  6. 12 auth_value_method :oauth_response_mode, "form_post"
  7. 12 def oauth_grant_types_supported
  8. 3660 super | %w[authorization_code]
  9. end
  10. 12 def oauth_response_types_supported
  11. 1596 super | %w[code]
  12. end
  13. 12 def oauth_response_modes_supported
  14. 2808 super | %w[query form_post]
  15. end
  16. 12 private
  17. 12 def validate_authorize_params
  18. 2088 super
  19. 1908 response_mode = param_or_nil("response_mode")
  20. 1908 return unless response_mode
  21. 900 redirect_response_error("invalid_request") unless oauth_response_modes_supported.include?(response_mode)
  22. 900 response_type = param_or_nil("response_type")
  23. 900 return unless response_type.nil? || response_type == "code"
  24. 780 redirect_response_error("invalid_request") unless oauth_response_modes_for_code_supported.include?(response_mode)
  25. end
  26. 12 def oauth_response_modes_for_code_supported
  27. 780 %w[query form_post]
  28. end
  29. 12 def validate_token_params
  30. 1608 redirect_response_error("invalid_request") if param_or_nil("grant_type") == "authorization_code" && !param_or_nil("code")
  31. 1608 super
  32. end
  33. 12 def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
  34. 708 response_mode ||= oauth_response_mode
  35. 708 redirect_response_error("invalid_request") unless response_mode.nil? || supported_response_mode?(response_mode)
  36. 708 response_type = param_or_nil("response_type")
  37. 708 redirect_response_error("invalid_request") unless response_type.nil? || supported_response_type?(response_type)
  38. 708 case response_type
  39. when "code", nil
  40. 492 response_params.replace(_do_authorize_code)
  41. end
  42. 696 response_params["state"] = param("state") if param_or_nil("state")
  43. 696 [response_params, response_mode]
  44. end
  45. 12 def _do_authorize_code
  46. 196 create_params = {
  47. 392 oauth_grants_type_column => "authorization_code",
  48. **resource_owner_params
  49. }
  50. 588 { "code" => create_oauth_grant(create_params) }
  51. end
  52. 12 def authorize_response(params, mode)
  53. 420 redirect_url = URI.parse(redirect_uri)
  54. 420 case mode
  55. when "query"
  56. 396 params = [URI.encode_www_form(params)]
  57. 396 params << redirect_url.query if redirect_url.query
  58. 396 redirect_url.query = params.join("&")
  59. 396 redirect(redirect_url.to_s)
  60. when "form_post"
  61. 24 inline_html = form_post_response_html(redirect_uri) do
  62. 16 params.map do |name, value|
  63. 24 "<input type=\"hidden\" name=\"#{scope.h(name)}\" value=\"#{scope.h(value)}\" />"
  64. 8 end.join
  65. end
  66. 24 scope.view layout: false, inline: inline_html
  67. end
  68. end
  69. 12 def _redirect_response_error(redirect_url, params)
  70. 336 response_mode = param_or_nil("response_mode") || oauth_response_mode
  71. 336 case response_mode
  72. when "form_post"
  73. 12 response["Content-Type"] = "text/html"
  74. 12 error_body = form_post_error_response_html(redirect_url) do
  75. 8 params.map do |name, value|
  76. 24 "<input type=\"hidden\" name=\"#{name}\" value=\"#{scope.h(value)}\" />"
  77. 4 end.join
  78. end
  79. 12 response.write(error_body)
  80. 12 request.halt
  81. else
  82. 324 super
  83. end
  84. end
  85. 12 def form_post_response_html(url)
  86. 36 <<-FORM
  87. <html>
  88. <head><title>Authorized</title></head>
  89. <body onload="javascript:document.forms[0].submit()">
  90. <form method="post" action="#{url}">
  91. #{yield}
  92. <input type="submit" class="btn btn-outline-primary" value="#{scope.h(oauth_authorize_post_button)}"/>
  93. </form>
  94. </body>
  95. </html>
  96. FORM
  97. end
  98. 12 def form_post_error_response_html(url)
  99. 12 <<-FORM
  100. <html>
  101. <head><title></title></head>
  102. <body onload="javascript:document.forms[0].submit()">
  103. <form method="post" action="#{url}">
  104. #{yield}
  105. </form>
  106. </body>
  107. </html>
  108. FORM
  109. end
  110. 12 def create_token(grant_type)
  111. 1404 return super unless supported_grant_type?(grant_type, "authorization_code")
  112. 364 grant_params = {
  113. 728 oauth_grants_code_column => param("code"),
  114. oauth_grants_redirect_uri_column => param("redirect_uri"),
  115. oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column]
  116. }
  117. 1092 create_token_from_authorization_code(grant_params)
  118. end
  119. 12 def check_valid_response_type?
  120. 1356 response_type = param_or_nil("response_type")
  121. 1356 response_type == "code" || response_type == "none" || super
  122. end
  123. 12 def oauth_server_metadata_body(*)
  124. 264 super.tap do |data|
  125. 264 data[:authorization_endpoint] = authorize_url
  126. end
  127. end
  128. end
  129. end

lib/rodauth/features/oauth_authorize_base.rb

98.45% lines covered

129 relevant lines. 127 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 12 require "ipaddr"
  3. 12 require "rodauth/oauth"
  4. 12 module Rodauth
  5. 12 Feature.define(:oauth_authorize_base, :OauthAuthorizeBase) do
  6. 12 depends :oauth_base
  7. 12 before "authorize"
  8. 12 after "authorize"
  9. 12 view "authorize", "Authorize", "authorize"
  10. 12 view "authorize_error", "Authorize Error", "authorize_error"
  11. 12 button "Authorize", "oauth_authorize"
  12. 12 button "Back to Client Application", "oauth_authorize_post"
  13. 12 auth_value_method :use_oauth_access_type?, false
  14. 12 auth_value_method :oauth_grants_access_type_column, :access_type
  15. 12 translatable_method :authorize_page_lead, "The application %<name>s would like to access your data"
  16. 12 translatable_method :oauth_grants_scopes_label, "Scopes"
  17. 12 translatable_method :oauth_applications_contacts_label, "Contacts"
  18. 12 translatable_method :oauth_applications_tos_uri_label, "Terms of service URL"
  19. 12 translatable_method :oauth_applications_policy_uri_label, "Policy URL"
  20. 12 translatable_method :oauth_unsupported_response_type_message, "Unsupported response type"
  21. 12 translatable_method :oauth_authorize_parameter_required, "Invalid or missing '%<parameter>s'"
  22. 12 auth_value_methods(
  23. :resource_owner_params,
  24. :oauth_grants_resource_owner_columns
  25. )
  26. # /authorize
  27. 12 auth_server_route(:authorize) do |r|
  28. 2388 require_authorizable_account
  29. 2268 before_authorize_route
  30. 2268 validate_authorize_params
  31. 1764 r.get do
  32. 1008 authorize_view
  33. end
  34. 756 r.post do
  35. 756 params, mode = transaction do
  36. 756 before_authorize
  37. 756 do_authorize
  38. end
  39. 744 authorize_response(params, mode)
  40. end
  41. end
  42. 12 def check_csrf?
  43. 9372 case request.path
  44. when authorize_path
  45. 2388 only_json? ? false : super
  46. else
  47. 6984 super
  48. end
  49. end
  50. 12 def authorize_scopes
  51. 1008 scopes || begin
  52. 168 oauth_application[oauth_applications_scopes_column].split(oauth_scope_separator)
  53. end
  54. end
  55. 12 private
  56. 12 def validate_authorize_params
  57. 2052 redirect_authorize_error("client_id") unless oauth_application
  58. 2004 redirect_uris = oauth_application[oauth_applications_redirect_uri_column].split(" ")
  59. 2004 if (redirect_uri = param_or_nil("redirect_uri"))
  60. 408 normalized_redirect_uri = normalize_redirect_uri_for_comparison(redirect_uri)
  61. 408 unless redirect_uris.include?(normalized_redirect_uri) || redirect_uris.include?(redirect_uri)
  62. 12 redirect_authorize_error("redirect_uri")
  63. end
  64. 1596 elsif redirect_uris.size > 1
  65. 12 redirect_authorize_error("redirect_uri")
  66. end
  67. 1980 redirect_response_error("unsupported_response_type") unless check_valid_response_type?
  68. 1956 redirect_response_error("invalid_request") unless check_valid_access_type? && check_valid_approval_prompt?
  69. 1956 try_approval_prompt if use_oauth_access_type? && request.get?
  70. 1956 redirect_response_error("invalid_scope") if (request.post? || param_or_nil("scope")) && !check_valid_scopes?
  71. 1944 response_mode = param_or_nil("response_mode")
  72. 1944 redirect_response_error("invalid_request") unless response_mode.nil? || oauth_response_modes_supported.include?(response_mode)
  73. end
  74. 12 def check_valid_scopes?(scp = scopes)
  75. 1716 super(scp - %w[offline_access])
  76. end
  77. 12 def check_valid_response_type?
  78. 24 false
  79. end
  80. 12 ACCESS_TYPES = %w[offline online].freeze
  81. 12 def check_valid_access_type?
  82. 1956 return true unless use_oauth_access_type?
  83. 36 access_type = param_or_nil("access_type")
  84. 36 !access_type || ACCESS_TYPES.include?(access_type)
  85. end
  86. 12 APPROVAL_PROMPTS = %w[force auto].freeze
  87. 12 def check_valid_approval_prompt?
  88. 1956 return true unless use_oauth_access_type?
  89. 36 approval_prompt = param_or_nil("approval_prompt")
  90. 36 !approval_prompt || APPROVAL_PROMPTS.include?(approval_prompt)
  91. end
  92. 12 def resource_owner_params
  93. 1056 { oauth_grants_account_id_column => account_id }
  94. end
  95. 12 def oauth_grants_resource_owner_columns
  96. 36 [oauth_grants_account_id_column]
  97. end
  98. 12 def try_approval_prompt
  99. 24 approval_prompt = param_or_nil("approval_prompt")
  100. 24 return unless approval_prompt && approval_prompt == "auto"
  101. 8 return if db[oauth_grants_table].where(resource_owner_params).where(
  102. oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
  103. oauth_grants_redirect_uri_column => redirect_uri,
  104. oauth_grants_scopes_column => scopes.join(oauth_scope_separator),
  105. oauth_grants_access_type_column => "online"
  106. 4 ).count.zero?
  107. # if there's a previous oauth grant for the params combo, it means that this user has approved before.
  108. 12 request.env["REQUEST_METHOD"] = "POST"
  109. end
  110. 12 def redirect_authorize_error(parameter, referer = request.referer || default_redirect)
  111. 96 error_message = oauth_authorize_parameter_required(parameter: parameter)
  112. 96 if accepts_json?
  113. status_code = oauth_invalid_response_status
  114. throw_json_response_error(status_code, "invalid_request", error_message)
  115. else
  116. 96 scope.instance_variable_set(:@error, error_message)
  117. 96 scope.instance_variable_set(:@back_url, referer)
  118. 96 return_response(authorize_error_view)
  119. end
  120. end
  121. 12 def authorization_required
  122. 372 if accepts_json?
  123. 360 throw_json_response_error(oauth_authorization_required_error_status, "invalid_client")
  124. else
  125. 12 set_redirect_error_flash(require_authorization_error_flash)
  126. 12 redirect(authorize_path)
  127. end
  128. end
  129. 12 def do_authorize(*args); end
  130. 12 def authorize_response(params, mode); end
  131. 12 def create_token_from_authorization_code(grant_params, should_generate_refresh_token = !use_oauth_access_type?, oauth_grant: nil)
  132. # fetch oauth grant
  133. 1056 oauth_grant ||= valid_locked_oauth_grant(grant_params)
  134. 864 should_generate_refresh_token ||= oauth_grant[oauth_grants_access_type_column] == "offline"
  135. 864 generate_token(oauth_grant, should_generate_refresh_token)
  136. end
  137. 12 def create_oauth_grant(create_params = {})
  138. 636 create_params[oauth_grants_oauth_application_id_column] ||= oauth_application[oauth_applications_id_column]
  139. 636 create_params[oauth_grants_redirect_uri_column] ||= redirect_uri
  140. 636 create_params[oauth_grants_expires_in_column] ||= Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_grant_expires_in)
  141. 636 create_params[oauth_grants_scopes_column] ||= scopes.join(oauth_scope_separator)
  142. 636 if use_oauth_access_type? && (access_type = param_or_nil("access_type"))
  143. 24 create_params[oauth_grants_access_type_column] = access_type
  144. end
  145. 636 ds = db[oauth_grants_table]
  146. 636 create_params[oauth_grants_code_column] = oauth_unique_id_generator
  147. 636 if oauth_reuse_access_token
  148. 384 unique_conds = Hash[oauth_grants_unique_columns.map { |column| [column, create_params[column]] }]
  149. 96 valid_grant = valid_oauth_grant_ds(unique_conds).select(oauth_grants_id_column).first
  150. 96 if valid_grant
  151. 96 create_params[oauth_grants_id_column] = valid_grant[oauth_grants_id_column]
  152. 96 rescue_from_uniqueness_error do
  153. 96 __insert_or_update_and_return__(
  154. ds,
  155. oauth_grants_id_column,
  156. [oauth_grants_id_column],
  157. create_params
  158. )
  159. end
  160. 96 return create_params[oauth_grants_code_column]
  161. end
  162. end
  163. 540 rescue_from_uniqueness_error do
  164. 576 if __one_oauth_token_per_account
  165. 288 __insert_or_update_and_return__(
  166. ds,
  167. oauth_grants_id_column,
  168. oauth_grants_unique_columns,
  169. create_params,
  170. nil,
  171. {
  172. oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_grant_expires_in),
  173. oauth_grants_revoked_at_column => nil
  174. }
  175. )
  176. else
  177. 288 __insert_and_return__(ds, oauth_grants_id_column, create_params)
  178. end
  179. end
  180. 528 create_params[oauth_grants_code_column]
  181. end
  182. 12 def normalize_redirect_uri_for_comparison(redirect_uri)
  183. 408 uri = URI(redirect_uri)
  184. 408 return redirect_uri unless uri.scheme == "http" && uri.port
  185. 48 hostname = uri.hostname
  186. # https://www.rfc-editor.org/rfc/rfc8252#section-7.3
  187. # ignore (potentially ephemeral) port number for native clients per RFC8252
  188. 16 begin
  189. 48 ip = IPAddr.new(hostname)
  190. 24 uri.port = nil if ip.loopback?
  191. rescue IPAddr::InvalidAddressError
  192. # https://www.rfc-editor.org/rfc/rfc8252#section-8.3
  193. # Although the use of localhost is NOT RECOMMENDED, it is still allowed.
  194. 24 uri.port = nil if hostname == "localhost"
  195. end
  196. 48 uri.to_s
  197. end
  198. end
  199. end

lib/rodauth/features/oauth_base.rb

96.51% lines covered

430 relevant lines. 415 lines covered and 15 lines missed.
    
  1. # frozen_string_literal: true
  2. 12 require "time"
  3. 12 require "base64"
  4. 12 require "securerandom"
  5. 12 require "cgi"
  6. 12 require "digest/sha2"
  7. 12 require "rodauth/version"
  8. 12 require "rodauth/oauth"
  9. 12 require "rodauth/oauth/database_extensions"
  10. 12 require "rodauth/oauth/http_extensions"
  11. 12 module Rodauth
  12. 12 Feature.define(:oauth_base, :OauthBase) do
  13. 12 include OAuth::HTTPExtensions
  14. 12 EMPTY_HASH = {}.freeze
  15. 12 auth_value_methods(:http_request)
  16. 12 auth_value_methods(:http_request_cache)
  17. 12 before "token"
  18. 12 error_flash "Please authorize to continue", "require_authorization"
  19. 12 error_flash "You are not authorized to revoke this token", "revoke_unauthorized_account"
  20. 12 button "Cancel", "oauth_cancel"
  21. 12 auth_value_method :json_response_content_type, "application/json"
  22. 12 auth_value_method :oauth_grant_expires_in, 60 * 5 # 5 minutes
  23. 12 auth_value_method :oauth_access_token_expires_in, 60 * 60 # 60 minutes
  24. 12 auth_value_method :oauth_refresh_token_expires_in, 60 * 60 * 24 * 360 # 1 year
  25. 12 auth_value_method :oauth_unique_id_generation_retries, 3
  26. 12 auth_value_method :oauth_token_endpoint_auth_methods_supported, %w[client_secret_basic client_secret_post]
  27. 12 auth_value_method :oauth_grant_types_supported, %w[refresh_token]
  28. 12 auth_value_method :oauth_response_types_supported, []
  29. 12 auth_value_method :oauth_response_modes_supported, []
  30. 12 auth_value_method :oauth_valid_uri_schemes, %w[https]
  31. 12 auth_value_method :oauth_scope_separator, " "
  32. # OAuth Grants
  33. 12 auth_value_method :oauth_grants_table, :oauth_grants
  34. 12 auth_value_method :oauth_grants_id_column, :id
  35. 8 %i[
  36. account_id oauth_application_id type
  37. redirect_uri code scopes
  38. expires_in revoked_at
  39. token refresh_token
  40. 4 ].each do |column|
  41. 120 auth_value_method :"oauth_grants_#{column}_column", column
  42. end
  43. # Enables Token Hash
  44. 12 auth_value_method :oauth_grants_token_hash_column, :token
  45. 12 auth_value_method :oauth_grants_refresh_token_hash_column, :refresh_token
  46. # Access Token reuse
  47. 12 auth_value_method :oauth_reuse_access_token, false
  48. 12 auth_value_method :oauth_applications_table, :oauth_applications
  49. 12 auth_value_method :oauth_applications_id_column, :id
  50. 8 %i[
  51. account_id
  52. name description scopes
  53. client_id client_secret
  54. homepage_url redirect_uri
  55. token_endpoint_auth_method grant_types response_types response_modes
  56. logo_uri tos_uri policy_uri jwks jwks_uri
  57. contacts software_id software_version
  58. 4 ].each do |column|
  59. 240 auth_value_method :"oauth_applications_#{column}_column", column
  60. end
  61. # Enables client secret Hash
  62. 12 auth_value_method :oauth_applications_client_secret_hash_column, :client_secret
  63. 12 auth_value_method :oauth_authorization_required_error_status, 401
  64. 12 auth_value_method :oauth_invalid_response_status, 400
  65. 12 auth_value_method :oauth_already_in_use_response_status, 409
  66. # Feature options
  67. 12 auth_value_method :oauth_application_scopes, []
  68. 12 auth_value_method :oauth_token_type, "bearer"
  69. 12 auth_value_method :oauth_refresh_token_protection_policy, "rotation" # can be: none, sender_constrained, rotation
  70. 12 translatable_method :oauth_invalid_client_message, "Invalid client"
  71. 12 translatable_method :oauth_invalid_grant_type_message, "Invalid grant type"
  72. 12 translatable_method :oauth_invalid_grant_message, "Invalid grant"
  73. 12 translatable_method :oauth_invalid_scope_message, "Invalid scope"
  74. 12 translatable_method :oauth_unsupported_token_type_message, "Invalid token type hint"
  75. 12 translatable_method :oauth_already_in_use_message, "error generating unique token"
  76. 12 auth_value_method :oauth_already_in_use_error_code, "invalid_request"
  77. 12 auth_value_method :oauth_invalid_grant_type_error_code, "unsupported_grant_type"
  78. 12 auth_value_method :is_authorization_server?, true
  79. 12 auth_value_methods(:only_json?)
  80. 12 auth_value_method :json_request_regexp, %r{\bapplication/(?:vnd\.api\+)?json\b}i
  81. # METADATA
  82. 12 auth_value_method :oauth_metadata_service_documentation, nil
  83. 12 auth_value_method :oauth_metadata_ui_locales_supported, nil
  84. 12 auth_value_method :oauth_metadata_op_policy_uri, nil
  85. 12 auth_value_method :oauth_metadata_op_tos_uri, nil
  86. 12 auth_value_methods(
  87. :fetch_access_token,
  88. :secret_hash,
  89. :generate_token_hash,
  90. :secret_matches?,
  91. :authorization_server_url,
  92. :oauth_unique_id_generator,
  93. :oauth_grants_unique_columns,
  94. :require_authorizable_account,
  95. :oauth_account_ds,
  96. :oauth_application_ds
  97. )
  98. # /token
  99. 12 auth_server_route(:token) do |r|
  100. 2112 require_oauth_application
  101. 1824 before_token_route
  102. 1824 r.post do
  103. 1824 catch_error do
  104. 1824 validate_token_params
  105. 1752 oauth_grant = nil
  106. 1752 transaction do
  107. 1752 before_token
  108. 1752 oauth_grant = create_token(param("grant_type"))
  109. end
  110. 1104 json_response_success(json_access_token_payload(oauth_grant))
  111. end
  112. throw_json_response_error(oauth_invalid_response_status, "invalid_request")
  113. end
  114. end
  115. 12 def load_oauth_server_metadata_route(issuer = nil)
  116. 216 request.on(".well-known") do
  117. 216 request.get("oauth-authorization-server") do
  118. 216 json_response_success(oauth_server_metadata_body(issuer), true)
  119. end
  120. end
  121. end
  122. 12 def check_csrf?
  123. 8892 case request.path
  124. when token_path
  125. 2112 false
  126. else
  127. 6780 super
  128. end
  129. end
  130. 12 def oauth_token_subject
  131. 132 return unless authorization_token
  132. 132 authorization_token[oauth_grants_account_id_column] ||
  133. db[oauth_applications_table].where(
  134. oauth_applications_id_column => authorization_token[oauth_grants_oauth_application_id_column]
  135. ).select_map(oauth_applications_client_id_column).first
  136. end
  137. 12 def current_oauth_account
  138. 132 account_id = authorization_token[oauth_grants_account_id_column]
  139. 132 return unless account_id
  140. 108 oauth_account_ds(account_id).first
  141. end
  142. 12 def current_oauth_application
  143. 156 oauth_application_ds(authorization_token[oauth_grants_oauth_application_id_column]).first
  144. end
  145. 12 def accepts_json?
  146. 1740 return true if only_json?
  147. 1728 (accept = request.env["HTTP_ACCEPT"]) && accept =~ json_request_regexp
  148. end
  149. # copied from the jwt feature
  150. 12 def json_request?
  151. 240 return super if features.include?(:jsonn)
  152. 240 return @json_request if defined?(@json_request)
  153. 240 @json_request = request.content_type =~ json_request_regexp
  154. end
  155. 12 def scopes
  156. 4584 scope = request.params["scope"]
  157. 4584 case scope
  158. when Array
  159. 1716 scope
  160. when String
  161. 2520 scope.split(" ")
  162. end
  163. end
  164. 12 def redirect_uri
  165. 4200 param_or_nil("redirect_uri") || begin
  166. 3204 return unless oauth_application
  167. 3204 redirect_uris = oauth_application[oauth_applications_redirect_uri_column].split(" ")
  168. 3204 redirect_uris.size == 1 ? redirect_uris.first : nil
  169. end
  170. end
  171. 12 def oauth_application
  172. 33294 return @oauth_application if defined?(@oauth_application)
  173. 888 @oauth_application = begin
  174. 2664 client_id = param_or_nil("client_id")
  175. 2664 return unless client_id
  176. 2580 db[oauth_applications_table].filter(oauth_applications_client_id_column => client_id).first
  177. end
  178. end
  179. 12 def fetch_access_token
  180. 780 if (token = request.params["access_token"])
  181. 24 if request.post? && !(request.content_type.start_with?("application/x-www-form-urlencoded") &&
  182. request.params.size == 1)
  183. return
  184. end
  185. else
  186. 756 value = request.env["HTTP_AUTHORIZATION"]
  187. 756 return unless value && !value.empty?
  188. 672 scheme, token = value.split(" ", 2)
  189. 672 return unless scheme.downcase == oauth_token_type
  190. end
  191. 696 return if token.nil? || token.empty?
  192. 612 token
  193. end
  194. 12 def authorization_token
  195. 1092 return @authorization_token if defined?(@authorization_token)
  196. # check if there is a token
  197. 348 access_token = fetch_access_token
  198. 348 return unless access_token
  199. 216 @authorization_token = oauth_grant_by_token(access_token)
  200. end
  201. 12 def require_oauth_authorization(*scopes)
  202. 324 authorization_required unless authorization_token
  203. 168 token_scopes = authorization_token[oauth_grants_scopes_column].split(oauth_scope_separator)
  204. 360 authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
  205. end
  206. 12 def use_date_arithmetic?
  207. 4890 true
  208. end
  209. # override
  210. 12 def translate(key, default, args = EMPTY_HASH)
  211. 23004 return i18n_translate(key, default, **args) if features.include?(:i18n)
  212. # do not attempt to translate by default
  213. 96 return default if args.nil?
  214. 96 default % args
  215. end
  216. 12 def post_configure
  217. 4902 super
  218. 4902 i18n_register(File.expand_path(File.join(__dir__, "..", "..", "..", "locales"))) if features.include?(:i18n)
  219. # all of the extensions below involve DB changes. Resource server mode doesn't use
  220. # database functions for OAuth though.
  221. 4902 return unless is_authorization_server?
  222. 4734 self.class.__send__(:include, Rodauth::OAuth::ExtendDatabase(db))
  223. # Check whether we can reutilize db entries for the same account / application pair
  224. 4734 one_oauth_token_per_account = db.indexes(oauth_grants_table).values.any? do |definition|
  225. 23676 definition[:unique] &&
  226. definition[:columns] == oauth_grants_unique_columns
  227. end
  228. 6234 self.class.send(:define_method, :__one_oauth_token_per_account) { one_oauth_token_per_account }
  229. end
  230. 12 private
  231. 12 def oauth_account_ds(account_id)
  232. 252 account_ds(account_id)
  233. end
  234. 12 def oauth_application_ds(oauth_application_id)
  235. 156 db[oauth_applications_table].where(oauth_applications_id_column => oauth_application_id)
  236. end
  237. 12 def require_authorizable_account
  238. 2616 require_account
  239. end
  240. 12 def rescue_from_uniqueness_error(&block)
  241. 2340 retries = oauth_unique_id_generation_retries
  242. 780 begin
  243. 2412 transaction(savepoint: :only, &block)
  244. 96 rescue Sequel::UniqueConstraintViolation
  245. 96 redirect_response_error("already_in_use") if retries.zero?
  246. 72 retries -= 1
  247. 72 retry
  248. end
  249. end
  250. # OAuth Token Unique/Reuse
  251. 12 def oauth_grants_unique_columns
  252. 8190 [
  253. 16380 oauth_grants_oauth_application_id_column,
  254. oauth_grants_account_id_column,
  255. oauth_grants_scopes_column
  256. ]
  257. end
  258. 12 def authorization_server_url
  259. 1350 base_url
  260. end
  261. 12 def template_path(page)
  262. 54078 path = File.join(File.dirname(__FILE__), "../../../templates", "#{page}.str")
  263. 54078 return super unless File.exist?(path)
  264. 1860 path
  265. end
  266. # to be used internally. Same semantics as require account, must:
  267. # fetch an authorization basic header
  268. # parse client id and secret
  269. #
  270. 12 def require_oauth_application
  271. 2028 @oauth_application = if (token = ((v = request.env["HTTP_AUTHORIZATION"]) && v[/\A *Basic (.*)\Z/, 1]))
  272. # client_secret_basic
  273. 588 require_oauth_application_from_client_secret_basic(token)
  274. 1440 elsif (client_id = param_or_nil("client_id"))
  275. 1356 if (client_secret = param_or_nil("client_secret"))
  276. # client_secret_post
  277. 888 require_oauth_application_from_client_secret_post(client_id, client_secret)
  278. else
  279. # none
  280. 468 require_oauth_application_from_none(client_id)
  281. end
  282. else
  283. 84 authorization_required
  284. end
  285. end
  286. 12 def require_oauth_application_from_client_secret_basic(token)
  287. 588 client_id, client_secret = Base64.decode64(token).split(/:/, 2)
  288. 588 authorization_required unless client_id
  289. 588 oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
  290. 392 authorization_required unless supports_auth_method?(oauth_application,
  291. 196 "client_secret_basic") && secret_matches?(oauth_application, client_secret)
  292. 564 oauth_application
  293. end
  294. 12 def require_oauth_application_from_client_secret_post(client_id, client_secret)
  295. 888 oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
  296. 592 authorization_required unless supports_auth_method?(oauth_application,
  297. 296 "client_secret_post") && secret_matches?(oauth_application, client_secret)
  298. 864 oauth_application
  299. end
  300. 12 def require_oauth_application_from_none(client_id)
  301. 468 oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
  302. 468 authorization_required unless supports_auth_method?(oauth_application, "none")
  303. 360 oauth_application
  304. end
  305. 12 def supports_auth_method?(oauth_application, auth_method)
  306. 2208 return false unless oauth_application
  307. 2172 supported_auth_methods = if oauth_application[oauth_applications_token_endpoint_auth_method_column]
  308. 660 oauth_application[oauth_applications_token_endpoint_auth_method_column].split(/ +/)
  309. else
  310. 1512 oauth_token_endpoint_auth_methods_supported
  311. end
  312. 2172 supported_auth_methods.include?(auth_method)
  313. end
  314. 12 def require_oauth_application_from_account
  315. 12 ds = db[oauth_applications_table]
  316. .join(oauth_grants_table, Sequel[oauth_grants_table][oauth_grants_oauth_application_id_column] =>
  317. Sequel[oauth_applications_table][oauth_applications_id_column])
  318. .where(oauth_grant_by_token_ds(param("token")).opts.fetch(:where, true))
  319. .where(Sequel[oauth_applications_table][oauth_applications_account_id_column] => account_id)
  320. 12 @oauth_application = ds.qualify.first
  321. 12 return if @oauth_application
  322. set_redirect_error_flash revoke_unauthorized_account_error_flash
  323. redirect request.referer || "/"
  324. end
  325. 12 def secret_matches?(oauth_application, secret)
  326. 1428 if oauth_applications_client_secret_hash_column
  327. 1428 BCrypt::Password.new(oauth_application[oauth_applications_client_secret_hash_column]) == secret
  328. else
  329. oauth_application[oauth_applications_client_secret_column] == secret
  330. end
  331. end
  332. 12 def set_client_secret(params, secret)
  333. 600 if oauth_applications_client_secret_hash_column
  334. 600 params[oauth_applications_client_secret_hash_column] = secret_hash(secret)
  335. else
  336. params[oauth_applications_client_secret_column] = secret
  337. end
  338. end
  339. 12 def secret_hash(secret)
  340. 1428 password_hash(secret)
  341. end
  342. 12 def oauth_unique_id_generator
  343. 3852 SecureRandom.urlsafe_base64(32)
  344. end
  345. 12 def generate_token_hash(token)
  346. 300 Base64.urlsafe_encode64(Digest::SHA256.digest(token))
  347. end
  348. 12 def grant_from_application?(oauth_grant, oauth_application)
  349. 204 oauth_grant[oauth_grants_oauth_application_id_column] == oauth_application[oauth_applications_id_column]
  350. end
  351. 12 def password_hash(password)
  352. 1428 return super if features.include?(:login_password_requirements_base)
  353. BCrypt::Password.create(password, cost: BCrypt::Engine::DEFAULT_COST)
  354. end
  355. 12 def generate_token(grant_params = {}, should_generate_refresh_token = true)
  356. 1020 if grant_params[oauth_grants_id_column] && (oauth_reuse_access_token &&
  357. (
  358. 192 if oauth_grants_token_hash_column
  359. 96 grant_params[oauth_grants_token_hash_column]
  360. else
  361. 96 grant_params[oauth_grants_token_column]
  362. end
  363. ))
  364. 96 return grant_params
  365. end
  366. 308 update_params = {
  367. 616 oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_access_token_expires_in),
  368. oauth_grants_code_column => nil
  369. }
  370. 924 rescue_from_uniqueness_error do
  371. 924 access_token = _generate_access_token(update_params)
  372. 924 refresh_token = _generate_refresh_token(update_params) if should_generate_refresh_token
  373. 924 oauth_grant = store_token(grant_params, update_params)
  374. 924 return unless oauth_grant
  375. 924 oauth_grant[oauth_grants_token_column] = access_token
  376. 924 oauth_grant[oauth_grants_refresh_token_column] = refresh_token if refresh_token
  377. 924 oauth_grant
  378. end
  379. end
  380. 12 def _generate_access_token(params = {})
  381. 636 token = oauth_unique_id_generator
  382. 636 if oauth_grants_token_hash_column
  383. 108 params[oauth_grants_token_hash_column] = generate_token_hash(token)
  384. else
  385. 528 params[oauth_grants_token_column] = token
  386. end
  387. 636 token
  388. end
  389. 12 def _generate_refresh_token(params)
  390. 636 token = oauth_unique_id_generator
  391. 636 if oauth_grants_refresh_token_hash_column
  392. 108 params[oauth_grants_refresh_token_hash_column] = generate_token_hash(token)
  393. else
  394. 528 params[oauth_grants_refresh_token_column] = token
  395. end
  396. 636 token
  397. end
  398. 12 def _grant_with_access_token?(oauth_grant)
  399. if oauth_grants_token_hash_column
  400. oauth_grant[oauth_grants_token_hash_column]
  401. else
  402. oauth_grant[oauth_grants_token_column]
  403. end
  404. end
  405. 12 def store_token(grant_params, update_params = {})
  406. 924 ds = db[oauth_grants_table]
  407. 924 if __one_oauth_token_per_account
  408. 154 to_update_if_null = [
  409. 308 oauth_grants_token_column,
  410. oauth_grants_token_hash_column,
  411. oauth_grants_refresh_token_column,
  412. oauth_grants_refresh_token_hash_column
  413. ].compact.map do |attribute|
  414. 344 [
  415. 688 attribute,
  416. (
  417. 1032 if ds.respond_to?(:supports_insert_conflict?) && ds.supports_insert_conflict?
  418. 516 Sequel.function(:coalesce, Sequel[oauth_grants_table][attribute], Sequel[:excluded][attribute])
  419. else
  420. 516 Sequel.function(:coalesce, Sequel[oauth_grants_table][attribute], update_params[attribute])
  421. end
  422. )
  423. ]
  424. end
  425. 462 token = __insert_or_update_and_return__(
  426. ds,
  427. oauth_grants_id_column,
  428. oauth_grants_unique_columns,
  429. grant_params.merge(update_params),
  430. Sequel.expr(Sequel[oauth_grants_table][oauth_grants_expires_in_column]) > Sequel::CURRENT_TIMESTAMP,
  431. Hash[to_update_if_null]
  432. )
  433. # if the previous operation didn't return a row, it means that the conditions
  434. # invalidated the update, and the existing token is still valid.
  435. 462 token || ds.where(
  436. oauth_grants_account_id_column => update_params[oauth_grants_account_id_column],
  437. oauth_grants_oauth_application_id_column => update_params[oauth_grants_oauth_application_id_column]
  438. ).first
  439. else
  440. 462 if oauth_reuse_access_token
  441. 192 unique_conds = Hash[oauth_grants_unique_columns.map { |column| [column, update_params[column]] }]
  442. 48 valid_token_ds = valid_oauth_grant_ds(unique_conds)
  443. 48 if oauth_grants_token_hash_column
  444. 24 valid_token_ds.exclude(oauth_grants_token_hash_column => nil)
  445. else
  446. 24 valid_token_ds.exclude(oauth_grants_token_column => nil)
  447. end
  448. 48 valid_token = valid_token_ds.first
  449. 48 return valid_token if valid_token
  450. end
  451. 462 if grant_params[oauth_grants_id_column]
  452. 384 __update_and_return__(ds.where(oauth_grants_id_column => grant_params[oauth_grants_id_column]), update_params)
  453. else
  454. 78 __insert_and_return__(ds, oauth_grants_id_column, grant_params.merge(update_params))
  455. end
  456. end
  457. end
  458. 12 def valid_locked_oauth_grant(grant_params = nil)
  459. 1092 oauth_grant = valid_oauth_grant_ds(grant_params).for_update.first
  460. 1092 redirect_response_error("invalid_grant") unless oauth_grant
  461. 900 oauth_grant
  462. end
  463. 12 def valid_oauth_grant_ds(grant_params = nil)
  464. 1992 ds = db[oauth_grants_table]
  465. .where(Sequel[oauth_grants_table][oauth_grants_revoked_at_column] => nil)
  466. .where(Sequel.expr(Sequel[oauth_grants_table][oauth_grants_expires_in_column]) >= Sequel::CURRENT_TIMESTAMP)
  467. 1992 ds = ds.where(grant_params) if grant_params
  468. 1992 ds
  469. end
  470. 12 def oauth_grant_by_token_ds(token)
  471. 444 ds = valid_oauth_grant_ds
  472. 444 if oauth_grants_token_hash_column
  473. 48 ds.where(Sequel[oauth_grants_table][oauth_grants_token_hash_column] => generate_token_hash(token))
  474. else
  475. 396 ds.where(Sequel[oauth_grants_table][oauth_grants_token_column] => token)
  476. end
  477. end
  478. 12 def oauth_grant_by_token(token)
  479. 372 oauth_grant_by_token_ds(token).first
  480. end
  481. 12 def oauth_grant_by_refresh_token_ds(token, revoked: false)
  482. 396 ds = db[oauth_grants_table].where(oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column])
  483. #
  484. # filter expired refresh tokens out.
  485. # an expired refresh token is a token whose access token expired for a period longer than the
  486. # refresh token expiration period.
  487. #
  488. 396 ds = ds.where(Sequel.date_add(oauth_grants_expires_in_column,
  489. 396 seconds: (oauth_refresh_token_expires_in - oauth_access_token_expires_in)) >= Sequel::CURRENT_TIMESTAMP)
  490. 396 ds = if oauth_grants_refresh_token_hash_column
  491. 36 ds.where(oauth_grants_refresh_token_hash_column => generate_token_hash(token))
  492. else
  493. 360 ds.where(oauth_grants_refresh_token_column => token)
  494. end
  495. 396 ds = ds.where(oauth_grants_revoked_at_column => nil) unless revoked
  496. 396 ds
  497. end
  498. 12 def oauth_grant_by_refresh_token(token, **kwargs)
  499. 96 oauth_grant_by_refresh_token_ds(token, **kwargs).first
  500. end
  501. 12 def json_access_token_payload(oauth_grant)
  502. 396 payload = {
  503. 792 "access_token" => oauth_grant[oauth_grants_token_column],
  504. "token_type" => oauth_token_type,
  505. "expires_in" => oauth_access_token_expires_in
  506. }
  507. 1188 payload["refresh_token"] = oauth_grant[oauth_grants_refresh_token_column] if oauth_grant[oauth_grants_refresh_token_column]
  508. 1188 payload
  509. end
  510. # Access Tokens
  511. 12 def validate_token_params
  512. 1764 unless (grant_type = param_or_nil("grant_type"))
  513. 60 redirect_response_error("invalid_request")
  514. end
  515. 1704 redirect_response_error("invalid_request") if grant_type == "refresh_token" && !param_or_nil("refresh_token")
  516. end
  517. 12 def create_token(grant_type)
  518. 432 redirect_response_error("invalid_request") unless supported_grant_type?(grant_type, "refresh_token")
  519. 300 refresh_token = param("refresh_token")
  520. # fetch potentially revoked oauth token
  521. 300 oauth_grant = oauth_grant_by_refresh_token_ds(refresh_token, revoked: true).for_update.first
  522. 300 update_params = { oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP,
  523. seconds: oauth_access_token_expires_in) }
  524. 300 if !oauth_grant || oauth_grant[oauth_grants_revoked_at_column]
  525. 144 redirect_response_error("invalid_grant")
  526. 156 elsif oauth_refresh_token_protection_policy == "rotation"
  527. # https://tools.ietf.org/html/draft-ietf-oauth-v2-1-00#section-6.1
  528. #
  529. # If a refresh token is compromised and subsequently used by both the attacker and the legitimate
  530. # client, one of them will present an invalidated refresh token, which will inform the authorization
  531. # server of the breach. The authorization server cannot determine which party submitted the invalid
  532. # refresh token, but it will revoke the active refresh token. This stops the attack at the cost of
  533. # forcing the legitimate client to obtain a fresh authorization grant.
  534. 72 refresh_token = _generate_refresh_token(update_params)
  535. end
  536. 156 update_params[oauth_grants_oauth_application_id_column] = oauth_grant[oauth_grants_oauth_application_id_column]
  537. 156 oauth_grant = create_token_from_token(oauth_grant, update_params)
  538. 144 oauth_grant[oauth_grants_refresh_token_column] = refresh_token
  539. 144 oauth_grant
  540. end
  541. 12 def create_token_from_token(oauth_grant, update_params)
  542. 156 redirect_response_error("invalid_grant") unless grant_from_application?(oauth_grant, oauth_application)
  543. 156 rescue_from_uniqueness_error do
  544. 192 oauth_grants_ds = db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
  545. 192 access_token = _generate_access_token(update_params)
  546. 192 oauth_grant = __update_and_return__(oauth_grants_ds, update_params)
  547. 144 oauth_grant[oauth_grants_token_column] = access_token
  548. 144 oauth_grant
  549. end
  550. end
  551. 12 def supported_grant_type?(grant_type, expected_grant_type = grant_type)
  552. 2100 return false unless grant_type == expected_grant_type
  553. 1668 grant_types_supported = if oauth_application[oauth_applications_grant_types_column]
  554. 48 oauth_application[oauth_applications_grant_types_column].split(/ +/)
  555. else
  556. 1620 oauth_grant_types_supported
  557. end
  558. 1668 grant_types_supported.include?(grant_type)
  559. end
  560. 12 def supported_response_type?(response_type, expected_response_type = response_type)
  561. 756 return false unless response_type == expected_response_type
  562. 756 response_types_supported = if oauth_application[oauth_applications_grant_types_column]
  563. 12 oauth_application[oauth_applications_response_types_column].split(/ +/)
  564. else
  565. 744 oauth_response_types_supported
  566. end
  567. 756 response_types = response_type.split(/ +/)
  568. 756 (response_types - response_types_supported).empty?
  569. end
  570. 12 def supported_response_mode?(response_mode, expected_response_mode = response_mode)
  571. 744 return false unless response_mode == expected_response_mode
  572. 744 response_modes_supported = if oauth_application[oauth_applications_response_modes_column]
  573. oauth_application[oauth_applications_response_modes_column].split(/ +/)
  574. else
  575. 744 oauth_response_modes_supported
  576. end
  577. 744 response_modes_supported.include?(response_mode)
  578. end
  579. 12 def oauth_server_metadata_body(path = nil)
  580. 264 issuer = base_url
  581. 264 issuer += "/#{path}" if path
  582. 88 {
  583. 176 issuer: issuer,
  584. token_endpoint: token_url,
  585. scopes_supported: oauth_application_scopes,
  586. response_types_supported: oauth_response_types_supported,
  587. response_modes_supported: oauth_response_modes_supported,
  588. grant_types_supported: oauth_grant_types_supported,
  589. token_endpoint_auth_methods_supported: oauth_token_endpoint_auth_methods_supported,
  590. service_documentation: oauth_metadata_service_documentation,
  591. ui_locales_supported: oauth_metadata_ui_locales_supported,
  592. op_policy_uri: oauth_metadata_op_policy_uri,
  593. op_tos_uri: oauth_metadata_op_tos_uri
  594. }
  595. end
  596. 12 def redirect_response_error(error_code, redirect_url = redirect_uri || request.referer || default_redirect)
  597. 1224 if accepts_json?
  598. 780 status_code = if respond_to?(:"oauth_#{error_code}_response_status")
  599. 12 send(:"oauth_#{error_code}_response_status")
  600. else
  601. 768 oauth_invalid_response_status
  602. end
  603. 780 throw_json_response_error(status_code, error_code)
  604. else
  605. 444 redirect_url = URI.parse(redirect_url)
  606. 444 params = []
  607. 444 params << if respond_to?(:"oauth_#{error_code}_error_code")
  608. 24 ["error", send(:"oauth_#{error_code}_error_code")]
  609. else
  610. 420 ["error", error_code]
  611. end
  612. 444 if respond_to?(:"oauth_#{error_code}_message")
  613. 180 message = send(:"oauth_#{error_code}_message")
  614. 180 params << ["error_description", CGI.escape(message)]
  615. end
  616. 444 state = param_or_nil("state")
  617. 444 params << ["state", state] if state
  618. 444 _redirect_response_error(redirect_url, params)
  619. end
  620. end
  621. 12 def _redirect_response_error(redirect_url, params)
  622. 804 params = params.map { |k, v| "#{k}=#{v}" }
  623. 312 params << redirect_url.query if redirect_url.query
  624. 312 redirect_url.query = params.join("&")
  625. 312 redirect(redirect_url.to_s)
  626. end
  627. 12 def json_response_success(body, cache = false)
  628. 1788 response.status = 200
  629. 1788 response["Content-Type"] ||= json_response_content_type
  630. 1788 if cache
  631. # defaulting to 1-day for everyone, for now at least
  632. 300 max_age = 60 * 60 * 24
  633. 300 response["Cache-Control"] = "private, max-age=#{max_age}"
  634. else
  635. 1488 response["Cache-Control"] = "no-store"
  636. 1488 response["Pragma"] = "no-cache"
  637. end
  638. 1788 json_payload = _json_response_body(body)
  639. 1788 return_response(json_payload)
  640. end
  641. 12 def throw_json_response_error(status, error_code, message = nil)
  642. 2076 set_response_error_status(status)
  643. 2076 code = if respond_to?(:"oauth_#{error_code}_error_code")
  644. 36 send(:"oauth_#{error_code}_error_code")
  645. else
  646. 2040 error_code
  647. end
  648. 2076 payload = { "error" => code }
  649. 2076 payload["error_description"] = message || (send(:"oauth_#{error_code}_message") if respond_to?(:"oauth_#{error_code}_message"))
  650. 2076 json_payload = _json_response_body(payload)
  651. 2076 response["Content-Type"] ||= json_response_content_type
  652. 2076 response["WWW-Authenticate"] = oauth_token_type.upcase if status == 401
  653. 2076 return_response(json_payload)
  654. end
  655. 12 def _json_response_body(hash)
  656. 4464 return super if features.include?(:json)
  657. 4464 if request.respond_to?(:convert_to_json)
  658. request.send(:convert_to_json, hash)
  659. else
  660. 4464 JSON.dump(hash)
  661. end
  662. end
  663. 12 if Gem::Version.new(Rodauth.version) < Gem::Version.new("2.23")
  664. def return_response(body = nil)
  665. response.write(body) if body
  666. request.halt
  667. end
  668. end
  669. 12 def authorization_required
  670. 192 throw_json_response_error(oauth_authorization_required_error_status, "invalid_client")
  671. end
  672. 12 def check_valid_scopes?(scp = scopes)
  673. 1752 return false unless scp
  674. 1752 (scp - oauth_application[oauth_applications_scopes_column].split(oauth_scope_separator)).empty?
  675. end
  676. 12 def check_valid_uri?(uri)
  677. 7824 URI::DEFAULT_PARSER.make_regexp(oauth_valid_uri_schemes).match?(uri)
  678. end
  679. 12 def check_valid_no_fragment_uri?(uri)
  680. 2364 check_valid_uri?(uri) && URI.parse(uri).fragment.nil?
  681. end
  682. # Resource server mode
  683. 12 def authorization_server_metadata
  684. 48 auth_url = URI(authorization_server_url).dup
  685. 48 auth_url.path = "/.well-known/oauth-authorization-server"
  686. 48 http_request_with_cache(auth_url)
  687. end
  688. end
  689. end

lib/rodauth/features/oauth_client_credentials_grant.rb

100.0% lines covered

17 relevant lines. 17 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 12 require "rodauth/oauth"
  3. 12 module Rodauth
  4. 12 Feature.define(:oauth_client_credentials_grant, :OauthClientCredentialsGrant) do
  5. 12 depends :oauth_base
  6. 12 def oauth_grant_types_supported
  7. 96 super | %w[client_credentials]
  8. end
  9. 12 private
  10. 12 def create_token(grant_type)
  11. 72 return super unless supported_grant_type?(grant_type, "client_credentials")
  12. 60 grant_scopes = scopes
  13. 60 grant_scopes = if grant_scopes
  14. 12 redirect_response_error("invalid_scope") unless check_valid_scopes?
  15. 12 grant_scopes.join(oauth_scope_separator)
  16. else
  17. 48 oauth_application[oauth_applications_scopes_column]
  18. end
  19. 20 grant_params = {
  20. 40 oauth_grants_type_column => "client_credentials",
  21. oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
  22. oauth_grants_scopes_column => grant_scopes
  23. }
  24. 60 generate_token(grant_params, false)
  25. end
  26. end
  27. end

lib/rodauth/features/oauth_device_code_grant.rb

95.33% lines covered

107 relevant lines. 102 lines covered and 5 lines missed.
    
  1. # frozen_string_literal: true
  2. 12 require "rodauth/oauth"
  3. 12 module Rodauth
  4. 12 Feature.define(:oauth_device_code_grant, :OauthDeviceCodeGrant) do
  5. 12 depends :oauth_authorize_base
  6. 12 before "device_authorization"
  7. 12 before "device_verification"
  8. 12 notice_flash "The device is verified", "device_verification"
  9. 12 error_flash "No device to authorize with the given user code", "user_code_not_found"
  10. 12 view "device_verification", "Device Verification", "device_verification"
  11. 12 view "device_search", "Device Search", "device_search"
  12. 12 button "Verify", "oauth_device_verification"
  13. 12 button "Search", "oauth_device_search"
  14. 12 auth_value_method :oauth_grants_user_code_column, :user_code
  15. 12 auth_value_method :oauth_grants_last_polled_at_column, :last_polled_at
  16. 12 translatable_method :oauth_device_search_page_lead, "Insert the user code from the device you'd like to authorize."
  17. 12 translatable_method :oauth_device_verification_page_lead, "The device with user code %<user_code>s would like to access your data."
  18. 12 translatable_method :oauth_expired_token_message, "the device code has expired"
  19. 12 translatable_method :oauth_access_denied_message, "the authorization request has been denied"
  20. 12 translatable_method :oauth_authorization_pending_message, "the authorization request is still pending"
  21. 12 translatable_method :oauth_slow_down_message, "authorization request is still pending but poll interval should be increased"
  22. 12 auth_value_method :oauth_device_code_grant_polling_interval, 5 # seconds
  23. 12 auth_value_method :oauth_device_code_grant_user_code_size, 8 # characters
  24. 12 %w[user_code].each do |param|
  25. 12 auth_value_method :"oauth_grant_#{param}_param", param
  26. end
  27. 12 translatable_method :oauth_grant_user_code_label, "User code"
  28. 12 auth_value_methods(
  29. :generate_user_code
  30. )
  31. # /device-authorization
  32. 12 auth_server_route(:device_authorization) do |r|
  33. 24 require_oauth_application
  34. 24 before_device_authorization_route
  35. 24 r.post do
  36. 24 user_code = generate_user_code
  37. 24 device_code = transaction do
  38. 24 before_device_authorization
  39. 24 create_oauth_grant(
  40. oauth_grants_type_column => "device_code",
  41. oauth_grants_user_code_column => user_code
  42. )
  43. end
  44. 24 json_response_success \
  45. "device_code" => device_code,
  46. "user_code" => user_code,
  47. "verification_uri" => device_url,
  48. "verification_uri_complete" => device_url(user_code: user_code),
  49. "expires_in" => oauth_grant_expires_in,
  50. "interval" => oauth_device_code_grant_polling_interval
  51. end
  52. end
  53. # /device
  54. 12 auth_server_route(:device) do |r|
  55. 252 require_authorizable_account
  56. 240 before_device_route
  57. 240 r.get do
  58. 204 if (user_code = param_or_nil("user_code"))
  59. 72 oauth_grant = valid_oauth_grant_ds(oauth_grants_user_code_column => user_code).first
  60. 72 unless oauth_grant
  61. 36 set_redirect_error_flash user_code_not_found_error_flash
  62. 36 redirect device_path
  63. end
  64. 36 scope.instance_variable_set(:@oauth_grant, oauth_grant)
  65. 36 device_verification_view
  66. else
  67. 132 device_search_view
  68. end
  69. end
  70. 36 r.post do
  71. 36 catch_error do
  72. 36 unless (user_code = param_or_nil("user_code")) && !user_code.empty?
  73. 12 set_redirect_error_flash oauth_invalid_grant_message
  74. 12 redirect device_path
  75. end
  76. 24 transaction do
  77. 24 before_device_verification
  78. 24 create_token("device_code")
  79. end
  80. end
  81. 24 set_notice_flash device_verification_notice_flash
  82. 24 redirect device_path
  83. end
  84. end
  85. 12 def check_csrf?
  86. 564 case request.path
  87. when device_authorization_path
  88. 24 false
  89. else
  90. 540 super
  91. end
  92. end
  93. 12 def oauth_grant_types_supported
  94. 132 super | %w[urn:ietf:params:oauth:grant-type:device_code]
  95. end
  96. 12 private
  97. 12 def generate_user_code
  98. 24 user_code_size = oauth_device_code_grant_user_code_size
  99. 16 SecureRandom.random_number(36**user_code_size)
  100. .to_s(36) # 0 to 9, a to z
  101. .upcase
  102. 8 .rjust(user_code_size, "0")
  103. end
  104. # TODO: think about removing this and recommend PKCE
  105. 12 def supports_auth_method?(oauth_application, auth_method)
  106. 156 return super unless auth_method == "none"
  107. 132 request.path == device_authorization_path || request.params.key?("device_code") || super
  108. end
  109. 12 def create_token(grant_type)
  110. 144 if supported_grant_type?(grant_type, "urn:ietf:params:oauth:grant-type:device_code")
  111. 120 oauth_grant = db[oauth_grants_table].where(
  112. oauth_grants_type_column => "device_code",
  113. oauth_grants_code_column => param("device_code"),
  114. oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column]
  115. ).for_update.first
  116. 120 throw_json_response_error(oauth_invalid_response_status, "invalid_grant") unless oauth_grant
  117. 108 now = Time.now
  118. 108 if oauth_grant[oauth_grants_user_code_column].nil?
  119. 16 return create_token_from_authorization_code(
  120. { oauth_grants_id_column => oauth_grant[oauth_grants_id_column] },
  121. oauth_grant: oauth_grant
  122. 8 )
  123. end
  124. 84 if oauth_grant[oauth_grants_revoked_at_column]
  125. 24 throw_json_response_error(oauth_invalid_response_status, "access_denied")
  126. 60 elsif oauth_grant[oauth_grants_expires_in_column] < now
  127. 12 throw_json_response_error(oauth_invalid_response_status, "expired_token")
  128. else
  129. 48 last_polled_at = oauth_grant[oauth_grants_last_polled_at_column]
  130. 48 if last_polled_at && convert_timestamp(last_polled_at) + oauth_device_code_grant_polling_interval > now
  131. 12 throw_json_response_error(oauth_invalid_response_status, "slow_down")
  132. else
  133. 36 db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
  134. 12 .update(oauth_grants_last_polled_at_column => Sequel::CURRENT_TIMESTAMP)
  135. 36 throw_json_response_error(oauth_invalid_response_status, "authorization_pending")
  136. end
  137. end
  138. 24 elsif grant_type == "device_code"
  139. # fetch oauth grant
  140. 24 rs = valid_oauth_grant_ds(
  141. oauth_grants_user_code_column => param("user_code")
  142. ).update(oauth_grants_user_code_column => nil, oauth_grants_type_column => "device_code")
  143. 24 return unless rs.positive?
  144. else
  145. super
  146. end
  147. end
  148. 12 def validate_token_params
  149. 132 grant_type = param_or_nil("grant_type")
  150. 132 if grant_type == "urn:ietf:params:oauth:grant-type:device_code" && !param_or_nil("device_code")
  151. 12 redirect_response_error("invalid_request")
  152. end
  153. 120 super
  154. end
  155. 12 def store_token(grant_params, update_params = {})
  156. 24 return super unless grant_params[oauth_grants_user_code_column]
  157. # do not clean up device code just yet
  158. update_params.delete(oauth_grants_code_column)
  159. update_params[oauth_grants_user_code_column] = nil
  160. update_params.merge!(resource_params)
  161. super(grant_params, update_params)
  162. end
  163. 12 def oauth_server_metadata_body(*)
  164. 12 super.tap do |data|
  165. 12 data[:device_authorization_endpoint] = device_authorization_url
  166. end
  167. end
  168. end
  169. end

lib/rodauth/features/oauth_dynamic_client_registration.rb

96.36% lines covered

220 relevant lines. 212 lines covered and 8 lines missed.
    
  1. # frozen_string_literal: true
  2. 12 require "rodauth/oauth"
  3. 12 module Rodauth
  4. 12 Feature.define(:oauth_dynamic_client_registration, :OauthDynamicClientRegistration) do
  5. 12 depends :oauth_base
  6. 12 before "register"
  7. 12 auth_value_method :oauth_client_registration_required_params, %w[redirect_uris client_name]
  8. 12 auth_value_method :oauth_applications_registration_access_token_column, :registration_access_token
  9. 12 auth_value_method :registration_client_uri_route, "register"
  10. 12 PROTECTED_APPLICATION_ATTRIBUTES = %w[account_id client_id].freeze
  11. 12 def load_registration_client_uri_routes
  12. 48 request.on(registration_client_uri_route) do
  13. # CLIENT REGISTRATION URI
  14. 48 request.on(String) do |client_id|
  15. 48 (token = ((v = request.env["HTTP_AUTHORIZATION"]) && v[/\A *Bearer (.*)\Z/, 1]))
  16. 48 next unless token
  17. 48 oauth_application = db[oauth_applications_table]
  18. .where(oauth_applications_client_id_column => client_id)
  19. .first
  20. 48 next unless oauth_application
  21. 48 authorization_required unless password_hash_match?(oauth_application[oauth_applications_registration_access_token_column], token)
  22. 48 request.is do
  23. 48 request.get do
  24. 12 json_response_oauth_application(oauth_application)
  25. end
  26. 36 request.on method: :put do
  27. 16 %w[client_id registration_access_token registration_client_uri client_secret_expires_at
  28. 8 client_id_issued_at].each do |prohibited_param|
  29. 72 if request.params.key?(prohibited_param)
  30. 12 register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(prohibited_param))
  31. end
  32. end
  33. 12 validate_client_registration_params
  34. # if the client includes the "client_secret" field in the request, the value of this field MUST match the currently
  35. # issued client secret for that client. The client MUST NOT be allowed to overwrite its existing client secret with
  36. # its own chosen value.
  37. 12 authorization_required if request.params.key?("client_secret") && secret_matches?(oauth_application,
  38. request.params["client_secret"])
  39. 12 oauth_application = transaction do
  40. 12 applications_ds = db[oauth_applications_table]
  41. 12 __update_and_return__(applications_ds, @oauth_application_params)
  42. end
  43. 12 json_response_oauth_application(oauth_application)
  44. end
  45. 12 request.on method: :delete do
  46. 12 applications_ds = db[oauth_applications_table]
  47. 12 applications_ds.where(oauth_applications_client_id_column => client_id).delete
  48. 12 response.status = 204
  49. 12 response["Cache-Control"] = "no-store"
  50. 12 response["Pragma"] = "no-cache"
  51. 12 response.finish
  52. end
  53. end
  54. end
  55. end
  56. end
  57. # /register
  58. 12 auth_server_route(:register) do |r|
  59. 1200 before_register_route
  60. 1200 r.post do
  61. 1200 oauth_client_registration_required_params.each do |required_param|
  62. 2352 unless request.params.key?(required_param)
  63. 48 register_throw_json_response_error("invalid_client_metadata", register_required_param_message(required_param))
  64. end
  65. end
  66. 1152 validate_client_registration_params
  67. 576 response_params = transaction do
  68. 576 before_register
  69. 576 do_register
  70. end
  71. 576 response.status = 201
  72. 576 response["Content-Type"] = json_response_content_type
  73. 576 response["Cache-Control"] = "no-store"
  74. 576 response["Pragma"] = "no-cache"
  75. 576 response.write(_json_response_body(response_params))
  76. end
  77. end
  78. 12 def check_csrf?
  79. 1248 case request.path
  80. when register_path
  81. 1200 false
  82. else
  83. 48 super
  84. end
  85. end
  86. 12 private
  87. 12 def _before_register
  88. raise %{dynamic client registration requires authentication.
  89. Override ´before_register` to perform it.
  90. example:
  91. before_register do
  92. account = _account_from_login(request.env["HTTP_X_USER_EMAIL"])
  93. authorization_required unless account
  94. @oauth_application_params[:account_id] = account[:id]
  95. end
  96. }
  97. end
  98. 12 def validate_client_registration_params(request_params = request.params)
  99. 1188 @oauth_application_params = request_params.each_with_object({}) do |(key, value), params|
  100. 14268 case key
  101. when "redirect_uris"
  102. 1152 if value.is_a?(Array)
  103. 1140 value = value.each do |uri|
  104. 2160 unless check_valid_no_fragment_uri?(uri)
  105. 24 register_throw_json_response_error("invalid_redirect_uri",
  106. register_invalid_uri_message(uri))
  107. end
  108. end.join(" ")
  109. else
  110. 12 register_throw_json_response_error("invalid_redirect_uri", register_invalid_uri_message(value))
  111. end
  112. 1116 key = oauth_applications_redirect_uri_column
  113. when "token_endpoint_auth_method"
  114. 540 unless oauth_token_endpoint_auth_methods_supported.include?(value)
  115. 12 register_throw_json_response_error("invalid_client_metadata", register_invalid_client_metadata_message(key, value))
  116. end
  117. # verify if in range
  118. 528 key = oauth_applications_token_endpoint_auth_method_column
  119. when "grant_types"
  120. 600 if value.is_a?(Array)
  121. 588 value = value.each do |grant_type|
  122. 1068 unless oauth_grant_types_supported.include?(grant_type)
  123. 24 register_throw_json_response_error("invalid_client_metadata", register_invalid_client_metadata_message(grant_type, value))
  124. end
  125. end.join(" ")
  126. else
  127. 12 register_throw_json_response_error("invalid_client_metadata", register_invalid_client_metadata_message(key, value))
  128. end
  129. 564 key = oauth_applications_grant_types_column
  130. when "response_types"
  131. 624 if value.is_a?(Array)
  132. 612 grant_types = request_params["grant_types"] || %w[authorization_code]
  133. 612 value = value.each do |response_type|
  134. 624 unless oauth_response_types_supported.include?(response_type)
  135. 12 register_throw_json_response_error("invalid_client_metadata",
  136. register_invalid_response_type_message(response_type))
  137. end
  138. 612 validate_client_registration_response_type(response_type, grant_types)
  139. end.join(" ")
  140. else
  141. 12 register_throw_json_response_error("invalid_client_metadata", register_invalid_client_metadata_message(key, value))
  142. end
  143. 552 key = oauth_applications_response_types_column
  144. # verify if in range and match grant type
  145. when "client_uri", "logo_uri", "tos_uri", "policy_uri", "jwks_uri"
  146. 5268 register_throw_json_response_error("invalid_client_metadata", register_invalid_uri_message(value)) unless check_valid_uri?(value)
  147. 5208 case key
  148. when "client_uri"
  149. 1092 key = oauth_applications_homepage_url_column
  150. when "jwks_uri"
  151. 984 if request_params.key?("jwks")
  152. 12 register_throw_json_response_error("invalid_client_metadata",
  153. register_invalid_jwks_param_message(key, "jwks"))
  154. end
  155. end
  156. 5196 key = __send__(:"oauth_applications_#{key}_column")
  157. when "jwks"
  158. 24 register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(value)) unless value.is_a?(Hash)
  159. 12 if request_params.key?("jwks_uri")
  160. register_throw_json_response_error("invalid_client_metadata",
  161. register_invalid_jwks_param_message(key, "jwks_uri"))
  162. end
  163. 12 key = oauth_applications_jwks_column
  164. 12 value = JSON.dump(value)
  165. when "scope"
  166. 1104 register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(value)) unless value.is_a?(String)
  167. 1104 scopes = value.split(" ") - oauth_application_scopes
  168. 1104 register_throw_json_response_error("invalid_client_metadata", register_invalid_scopes_message(value)) unless scopes.empty?
  169. 1080 key = oauth_applications_scopes_column
  170. # verify if in range
  171. when "contacts"
  172. 1056 register_throw_json_response_error("invalid_client_metadata", register_invalid_contacts_message(value)) unless value.is_a?(Array)
  173. 1044 value = value.join(" ")
  174. 1044 key = oauth_applications_contacts_column
  175. when "client_name"
  176. 1104 register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(value)) unless value.is_a?(String)
  177. 1104 key = oauth_applications_name_column
  178. when "require_pushed_authorization_requests"
  179. 36 unless respond_to?(:oauth_applications_require_pushed_authorization_requests_column)
  180. register_throw_json_response_error("invalid_client_metadata",
  181. register_invalid_param_message(key))
  182. end
  183. 36 request_params[key] = value = convert_to_boolean(key, value)
  184. 24 key = oauth_applications_require_pushed_authorization_requests_column
  185. when "tls_client_certificate_bound_access_tokens"
  186. 12 property = :oauth_applications_tls_client_certificate_bound_access_tokens_column
  187. 12 register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key)) unless respond_to?(property)
  188. 12 request_params[key] = value = convert_to_boolean(key, value)
  189. 12 key = oauth_applications_tls_client_certificate_bound_access_tokens_column
  190. when /\Atls_client_auth_/
  191. 84 unless respond_to?(:"oauth_applications_#{key}_column")
  192. register_throw_json_response_error("invalid_client_metadata",
  193. register_invalid_param_message(key))
  194. end
  195. # client using the tls_client_auth authentication method MUST use exactly one of the below metadata
  196. # parameters to indicate the certificate subject value that the authorization server is to expect when
  197. # authenticating the respective client.
  198. 1020 if params.any? { |k, _| k.to_s.start_with?("tls_client_auth_") }
  199. 12 register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key))
  200. end
  201. 72 key = __send__(:"oauth_applications_#{key}_column")
  202. else
  203. 2664 if respond_to?(:"oauth_applications_#{key}_column")
  204. 2604 if PROTECTED_APPLICATION_ATTRIBUTES.include?(key)
  205. 12 register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key))
  206. end
  207. 2592 property = :"oauth_applications_#{key}_column"
  208. 2592 key = __send__(property)
  209. 60 elsif !db[oauth_applications_table].columns.include?(key.to_sym)
  210. 36 register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key))
  211. end
  212. end
  213. 13920 params[key] = value
  214. end
  215. end
  216. 12 def validate_client_registration_response_type(response_type, grant_types)
  217. 564 case response_type
  218. when "code"
  219. 492 unless grant_types.include?("authorization_code")
  220. register_throw_json_response_error("invalid_client_metadata",
  221. register_invalid_response_type_for_grant_type_message(response_type,
  222. "authorization_code"))
  223. end
  224. when "token"
  225. 60 unless grant_types.include?("implicit")
  226. 24 register_throw_json_response_error("invalid_client_metadata",
  227. register_invalid_response_type_for_grant_type_message(response_type, "implicit"))
  228. end
  229. when "none"
  230. 12 if grant_types.include?("implicit") || grant_types.include?("authorization_code")
  231. 12 register_throw_json_response_error("invalid_client_metadata", register_invalid_response_type_message(response_type))
  232. end
  233. end
  234. end
  235. 12 def do_register(return_params = request.params.dup)
  236. 576 applications_ds = db[oauth_applications_table]
  237. 576 application_columns = applications_ds.columns
  238. # set defaults
  239. 576 create_params = @oauth_application_params
  240. # If omitted, an authorization server MAY register a client with a default set of scopes
  241. 576 create_params[oauth_applications_scopes_column] ||= return_params["scopes"] = oauth_application_scopes.join(" ")
  242. # https://datatracker.ietf.org/doc/html/rfc7591#section-2
  243. 576 if create_params[oauth_applications_grant_types_column] ||= begin
  244. # If omitted, the default behavior is that the client will use only the "authorization_code" Grant Type.
  245. 300 return_params["grant_types"] = %w[authorization_code] # rubocop:disable Lint/AssignmentInCondition
  246. 300 "authorization_code"
  247. end
  248. 576 create_params[oauth_applications_token_endpoint_auth_method_column] ||= begin
  249. # If unspecified or omitted, the default is "client_secret_basic", denoting the HTTP Basic
  250. # authentication scheme as specified in Section 2.3.1 of OAuth 2.0.
  251. 312 return_params["token_endpoint_auth_method"] = "client_secret_basic"
  252. 312 "client_secret_basic"
  253. end
  254. end
  255. 576 create_params[oauth_applications_response_types_column] ||= begin
  256. # If omitted, the default is that the client will use only the "code" response type.
  257. 300 return_params["response_types"] = %w[code]
  258. 300 "code"
  259. end
  260. 576 rescue_from_uniqueness_error do
  261. 576 initialize_register_params(create_params, return_params)
  262. 11220 create_params.delete_if { |k, _| !application_columns.include?(k) }
  263. 576 applications_ds.insert(create_params)
  264. end
  265. 576 return_params
  266. end
  267. 12 def initialize_register_params(create_params, return_params)
  268. 576 client_id = oauth_unique_id_generator
  269. 576 create_params[oauth_applications_client_id_column] = client_id
  270. 576 return_params["client_id"] = client_id
  271. 576 return_params["client_id_issued_at"] = Time.now.utc.iso8601
  272. 576 registration_access_token = oauth_unique_id_generator
  273. 576 create_params[oauth_applications_registration_access_token_column] = secret_hash(registration_access_token)
  274. 576 return_params["registration_access_token"] = registration_access_token
  275. 576 return_params["registration_client_uri"] = "#{base_url}/#{registration_client_uri_route}/#{return_params['client_id']}"
  276. 576 if create_params.key?(oauth_applications_client_secret_column)
  277. 12 set_client_secret(create_params, create_params[oauth_applications_client_secret_column])
  278. 12 return_params.delete("client_secret")
  279. else
  280. 564 client_secret = oauth_unique_id_generator
  281. 564 set_client_secret(create_params, client_secret)
  282. 564 return_params["client_secret"] = client_secret
  283. 564 return_params["client_secret_expires_at"] = 0
  284. end
  285. end
  286. 12 def register_throw_json_response_error(code, message)
  287. 636 throw_json_response_error(oauth_invalid_response_status, code, message)
  288. end
  289. 12 def register_required_param_message(key)
  290. 60 "The param '#{key}' is required by this server."
  291. end
  292. 12 def register_invalid_param_message(key)
  293. 108 "The param '#{key}' is not supported by this server."
  294. end
  295. 12 def register_invalid_client_metadata_message(key, value)
  296. 192 "The value '#{value}' is not supported by this server for param '#{key}'."
  297. end
  298. 12 def register_invalid_contacts_message(contacts)
  299. 12 "The contacts '#{contacts}' are not allowed by this server."
  300. end
  301. 12 def register_invalid_uri_message(uri)
  302. 168 "The '#{uri}' URL is not allowed by this server."
  303. end
  304. 12 def register_invalid_jwks_param_message(key1, key2)
  305. 12 "The param '#{key1}' cannot be accepted together with param '#{key2}'."
  306. end
  307. 12 def register_invalid_scopes_message(scopes)
  308. 24 "The given scopes (#{scopes}) are not allowed by this server."
  309. end
  310. 12 def register_oauth_invalid_grant_type_message(grant_type)
  311. "The grant type #{grant_type} is not allowed by this server."
  312. end
  313. 12 def register_invalid_response_type_message(response_type)
  314. 24 "The response type #{response_type} is not allowed by this server."
  315. end
  316. 12 def register_invalid_response_type_for_grant_type_message(response_type, grant_type)
  317. 36 "The grant type '#{grant_type}' must be registered for the response " \
  318. "type '#{response_type}' to be allowed."
  319. end
  320. 12 def convert_to_boolean(key, value)
  321. 48 case value
  322. 24 when "true" then true
  323. 12 when "false" then false
  324. else
  325. 12 register_throw_json_response_error(
  326. "invalid_client_metadata",
  327. register_invalid_param_message(key)
  328. )
  329. end
  330. end
  331. 12 def json_response_oauth_application(oauth_application)
  332. 10192 params = methods.map { |k| k.to_s[/\Aoauth_applications_(\w+)_column\z/, 1] }.compact
  333. 24 body = params.each_with_object({}) do |k, hash|
  334. 552 next if %w[id account_id client_id client_secret cliennt_secret_hash].include?(k)
  335. 456 value = oauth_application[__send__(:"oauth_applications_#{k}_column")]
  336. 456 next unless value
  337. 168 case k
  338. when "redirect_uri"
  339. 24 hash["redirect_uris"] = value.split(" ")
  340. when "token_endpoint_auth_method", "grant_types", "response_types", "request_uris", "post_logout_redirect_uris"
  341. hash[k] = value.split(" ")
  342. when "scopes"
  343. 24 hash["scope"] = value
  344. when "jwks"
  345. hash[k] = value.is_a?(String) ? JSON.parse(value) : value
  346. when "homepage_url"
  347. 24 hash["client_uri"] = value
  348. when "name"
  349. 24 hash["client_name"] = value
  350. else
  351. 72 hash[k] = value
  352. end
  353. end
  354. 24 response.status = 200
  355. 24 response["Content-Type"] ||= json_response_content_type
  356. 24 response["Cache-Control"] = "no-store"
  357. 24 response["Pragma"] = "no-cache"
  358. 24 json_payload = _json_response_body(body)
  359. 24 return_response(json_payload)
  360. end
  361. 12 def oauth_server_metadata_body(*)
  362. 24 super.tap do |data|
  363. 24 data[:registration_endpoint] = register_url
  364. end
  365. end
  366. end
  367. end

lib/rodauth/features/oauth_grant_management.rb

100.0% lines covered

33 relevant lines. 33 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 12 require "rodauth/oauth"
  3. 12 module Rodauth
  4. 12 Feature.define(:oauth_grant_management, :OauthTokenManagement) do
  5. 12 depends :oauth_management_base, :oauth_token_revocation
  6. 12 view "oauth_grants", "My Oauth Grants", "oauth_grants"
  7. 12 button "Revoke", "oauth_grant_revoke"
  8. 12 auth_value_method :oauth_grants_path, "oauth-grants"
  9. 12 %w[type token refresh_token expires_in revoked_at].each do |param|
  10. 60 translatable_method :"oauth_grants_#{param}_label", param.gsub("_", " ").capitalize
  11. end
  12. 12 translatable_method :oauth_no_grants_text, "No oauth grants yet!"
  13. 12 auth_value_method :oauth_grants_route, "oauth-grants"
  14. 12 auth_value_method :oauth_grants_id_pattern, Integer
  15. 12 auth_value_method :oauth_grants_per_page, 20
  16. 12 auth_value_methods(
  17. :oauth_grant_path
  18. )
  19. 12 def oauth_grants_path(opts = {})
  20. 660 route_path(oauth_grants_route, opts)
  21. end
  22. 12 def oauth_grant_path(id)
  23. 252 "#{oauth_grants_path}/#{id}"
  24. end
  25. 12 def load_oauth_grant_management_routes
  26. 96 request.on(oauth_grants_route) do
  27. 96 check_csrf if check_csrf?
  28. 96 require_account
  29. 96 request.post(oauth_grants_id_pattern) do |id|
  30. 8 db[oauth_grants_table]
  31. .where(oauth_grants_id_column => id)
  32. .where(oauth_grants_account_id_column => account_id)
  33. 4 .update(oauth_grants_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
  34. 12 set_notice_flash revoke_oauth_grant_notice_flash
  35. 12 redirect oauth_grants_path || "/"
  36. end
  37. 84 request.is do
  38. 84 request.get do
  39. 84 page = Integer(param_or_nil("page") || 1)
  40. 84 per_page = per_page_param(oauth_grants_per_page)
  41. 84 scope.instance_variable_set(:@oauth_grants, db[oauth_grants_table]
  42. .select(Sequel[oauth_grants_table].*, Sequel[oauth_applications_table][oauth_applications_name_column])
  43. .join(oauth_applications_table, Sequel[oauth_grants_table][oauth_grants_oauth_application_id_column] =>
  44. Sequel[oauth_applications_table][oauth_applications_id_column])
  45. .where(Sequel[oauth_grants_table][oauth_grants_account_id_column] => account_id)
  46. .where(oauth_grants_revoked_at_column => nil)
  47. .order(Sequel.desc(oauth_grants_id_column))
  48. .paginate(page, per_page))
  49. 84 oauth_grants_view
  50. end
  51. end
  52. end
  53. end
  54. end
  55. end

lib/rodauth/features/oauth_implicit_grant.rb

100.0% lines covered

49 relevant lines. 49 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 12 require "rodauth/oauth"
  3. 12 module Rodauth
  4. 12 Feature.define(:oauth_implicit_grant, :OauthImplicitGrant) do
  5. 12 depends :oauth_authorize_base
  6. 12 def oauth_grant_types_supported
  7. 2208 super | %w[implicit]
  8. end
  9. 12 def oauth_response_types_supported
  10. 1104 super | %w[token]
  11. end
  12. 12 def oauth_response_modes_supported
  13. 1236 super | %w[fragment]
  14. end
  15. 12 private
  16. 12 def validate_authorize_params
  17. 1176 super
  18. 1092 response_mode = param_or_nil("response_mode")
  19. 1092 return unless response_mode
  20. 348 response_type = param_or_nil("response_type")
  21. 348 return unless response_type == "token"
  22. 72 redirect_response_error("invalid_request") unless oauth_response_modes_for_token_supported.include?(response_mode)
  23. end
  24. 12 def oauth_response_modes_for_token_supported
  25. 72 %w[fragment]
  26. end
  27. 12 def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
  28. 444 response_type = param("response_type")
  29. 444 return super unless response_type == "token" && supported_response_type?(response_type)
  30. 48 response_mode ||= "fragment"
  31. 48 redirect_response_error("invalid_request") unless supported_response_mode?(response_mode)
  32. 48 oauth_grant = _do_authorize_token
  33. 48 response_params.replace(json_access_token_payload(oauth_grant))
  34. 48 response_params["state"] = param("state") if param_or_nil("state")
  35. 48 [response_params, response_mode]
  36. end
  37. 12 def _do_authorize_token(grant_params = {})
  38. 20 grant_params = {
  39. 40 oauth_grants_type_column => "implicit",
  40. oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
  41. oauth_grants_scopes_column => scopes,
  42. **resource_owner_params
  43. }.merge(grant_params)
  44. 60 generate_token(grant_params, false)
  45. end
  46. 12 def _redirect_response_error(redirect_url, params)
  47. 180 response_types = param("response_type").split(/ +/)
  48. 180 return super if response_types.empty? || response_types == %w[code]
  49. 192 params = params.map { |k, v| "#{k}=#{v}" }
  50. 84 redirect_url.fragment = params.join("&")
  51. 84 redirect(redirect_url.to_s)
  52. end
  53. 12 def authorize_response(params, mode)
  54. 348 return super unless mode == "fragment"
  55. 228 redirect_url = URI.parse(redirect_uri)
  56. 228 params = [URI.encode_www_form(params)]
  57. 228 params << redirect_url.query if redirect_url.query
  58. 228 redirect_url.fragment = params.join("&")
  59. 228 redirect(redirect_url.to_s)
  60. end
  61. 12 def check_valid_response_type?
  62. 612 return true if param_or_nil("response_type") == "token"
  63. 480 super
  64. end
  65. end
  66. end

lib/rodauth/features/oauth_jwt.rb

100.0% lines covered

57 relevant lines. 57 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 12 require "rodauth/oauth"
  3. 12 require "rodauth/oauth/http_extensions"
  4. 12 module Rodauth
  5. 12 Feature.define(:oauth_jwt, :OauthJwt) do
  6. 12 depends :oauth_jwt_base, :oauth_jwt_jwks
  7. 12 auth_value_method :oauth_jwt_access_tokens, true
  8. 12 auth_value_methods(:jwt_claims)
  9. 12 def require_oauth_authorization(*scopes)
  10. 228 return super unless oauth_jwt_access_tokens
  11. 228 authorization_required unless authorization_token
  12. 216 token_scopes = authorization_token["scope"].split(" ")
  13. 432 authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
  14. end
  15. 12 def oauth_token_subject
  16. 324 return super unless oauth_jwt_access_tokens
  17. 324 return unless authorization_token
  18. 324 authorization_token["sub"]
  19. end
  20. 12 def current_oauth_account
  21. 156 subject = oauth_token_subject
  22. 156 return if subject == authorization_token["client_id"]
  23. 144 oauth_account_ds(subject).first
  24. end
  25. 12 def current_oauth_application
  26. 128 db[oauth_applications_table].where(
  27. oauth_applications_client_id_column => authorization_token["client_id"]
  28. 64 ).first
  29. end
  30. 12 private
  31. 12 def authorization_token
  32. 1572 return super unless oauth_jwt_access_tokens
  33. 1572 return @authorization_token if defined?(@authorization_token)
  34. 116 @authorization_token = begin
  35. 348 access_token = fetch_access_token
  36. 348 return unless access_token
  37. 336 jwt_claims = jwt_decode(access_token)
  38. 336 return unless jwt_claims
  39. 336 return unless jwt_claims["sub"]
  40. 336 return unless jwt_claims["aud"]
  41. 336 jwt_claims
  42. end
  43. end
  44. # /token
  45. 12 def create_token_from_token(_grant, update_params)
  46. 96 oauth_grant = super
  47. 96 if oauth_jwt_access_tokens
  48. 96 access_token = _generate_jwt_access_token(oauth_grant)
  49. 96 oauth_grant[oauth_grants_token_column] = access_token
  50. end
  51. 96 oauth_grant
  52. end
  53. 12 def generate_token(_grant_params = {}, should_generate_refresh_token = true)
  54. 396 oauth_grant = super
  55. 396 if oauth_jwt_access_tokens
  56. 384 access_token = _generate_jwt_access_token(oauth_grant)
  57. 384 oauth_grant[oauth_grants_token_column] = access_token
  58. end
  59. 396 oauth_grant
  60. end
  61. 12 def _generate_jwt_access_token(oauth_grant)
  62. 504 claims = jwt_claims(oauth_grant)
  63. # one of the points of using jwt is avoiding database lookups, so we put here all relevant
  64. # token data.
  65. 504 claims[:scope] = oauth_grant[oauth_grants_scopes_column]
  66. 504 jwt_encode(claims)
  67. end
  68. 12 def _generate_access_token(*)
  69. 492 return super unless oauth_jwt_access_tokens
  70. end
  71. 12 def jwt_claims(oauth_grant)
  72. 888 issued_at = Time.now.to_i
  73. 296 {
  74. 592 iss: oauth_jwt_issuer, # issuer
  75. iat: issued_at, # issued at
  76. #
  77. # sub REQUIRED - as defined in section 4.1.2 of [RFC7519]. In case of
  78. # access tokens obtained through grants where a resource owner is
  79. # involved, such as the authorization code grant, the value of "sub"
  80. # SHOULD correspond to the subject identifier of the resource owner.
  81. # In case of access tokens obtained through grants where no resource
  82. # owner is involved, such as the client credentials grant, the value
  83. # of "sub" SHOULD correspond to an identifier the authorization
  84. # server uses to indicate the client application.
  85. sub: jwt_subject(oauth_grant),
  86. client_id: oauth_application[oauth_applications_client_id_column],
  87. exp: issued_at + oauth_access_token_expires_in,
  88. aud: oauth_jwt_audience
  89. }
  90. end
  91. end
  92. end

lib/rodauth/features/oauth_jwt_base.rb

93.46% lines covered

214 relevant lines. 200 lines covered and 14 lines missed.
    
  1. # frozen_string_literal: true
  2. 12 require "rodauth/oauth"
  3. 12 require "rodauth/oauth/http_extensions"
  4. 12 module Rodauth
  5. 12 Feature.define(:oauth_jwt_base, :OauthJwtBase) do
  6. 12 depends :oauth_base
  7. 12 auth_value_method :oauth_application_jwt_public_key_param, "jwt_public_key"
  8. 12 auth_value_method :oauth_application_jwks_param, "jwks"
  9. 12 auth_value_method :oauth_jwt_keys, {}
  10. 12 auth_value_method :oauth_jwt_public_keys, {}
  11. 12 auth_value_method :oauth_jwt_jwe_keys, {}
  12. 12 auth_value_method :oauth_jwt_jwe_public_keys, {}
  13. 12 auth_value_method :oauth_jwt_jwe_copyright, nil
  14. 12 auth_value_methods(
  15. :jwt_encode,
  16. :jwt_decode,
  17. :jwt_decode_no_key,
  18. :generate_jti,
  19. :oauth_jwt_issuer,
  20. :oauth_jwt_audience,
  21. :resource_owner_params_from_jwt_claims
  22. )
  23. 12 private
  24. 12 def oauth_jwt_issuer
  25. # The JWT MUST contain an "iss" (issuer) claim that contains a
  26. # unique identifier for the entity that issued the JWT.
  27. 2022 @oauth_jwt_issuer ||= authorization_server_url
  28. end
  29. 12 def oauth_jwt_audience
  30. # The JWT MUST contain an "aud" (audience) claim containing a
  31. # value that identifies the authorization server as an intended
  32. # audience. The token endpoint URL of the authorization server
  33. # MAY be used as a value for an "aud" element to identify the
  34. # authorization server as an intended audience of the JWT.
  35. 888 @oauth_jwt_audience ||= if is_authorization_server?
  36. 672 oauth_application[oauth_applications_client_id_column]
  37. else
  38. metadata = authorization_server_metadata
  39. return unless metadata
  40. metadata[:token_endpoint]
  41. end
  42. end
  43. 12 def grant_from_application?(grant_or_claims, oauth_application)
  44. 108 return super if grant_or_claims[oauth_grants_id_column]
  45. if grant_or_claims["client_id"]
  46. grant_or_claims["client_id"] == oauth_application[oauth_applications_client_id_column]
  47. else
  48. Array(grant_or_claims["aud"]).include?(oauth_application[oauth_applications_client_id_column])
  49. end
  50. end
  51. 12 def jwt_subject(oauth_grant, client_application = oauth_application)
  52. 900 account_id = oauth_grant[oauth_grants_account_id_column]
  53. 900 return account_id.to_s if account_id
  54. 36 client_application[oauth_applications_client_id_column]
  55. end
  56. 12 def resource_owner_params_from_jwt_claims(claims)
  57. 108 { oauth_grants_account_id_column => claims["sub"] }
  58. end
  59. 12 def oauth_server_metadata_body(path = nil)
  60. 144 metadata = super
  61. 144 metadata.merge! \
  62. token_endpoint_auth_signing_alg_values_supported: oauth_jwt_keys.keys.uniq
  63. 144 metadata
  64. end
  65. 12 def _jwt_key
  66. 255 @_jwt_key ||= (oauth_application_jwks(oauth_application) if oauth_application)
  67. end
  68. # Resource Server only!
  69. #
  70. # returns the jwks set from the authorization server.
  71. 12 def auth_server_jwks_set
  72. 48 metadata = authorization_server_metadata
  73. 48 return unless metadata && (jwks_uri = metadata[:jwks_uri])
  74. 48 jwks_uri = URI(jwks_uri)
  75. 48 http_request_with_cache(jwks_uri)
  76. end
  77. 12 def generate_jti(payload)
  78. # Use the key and iat to create a unique key per request to prevent replay attacks
  79. 437 jti_raw = [
  80. 874 payload[:aud] || payload["aud"],
  81. payload[:iat] || payload["iat"]
  82. ].join(":").to_s
  83. 1311 Digest::SHA256.hexdigest(jti_raw)
  84. end
  85. 12 def verify_jti(jti, claims)
  86. 279 generate_jti(claims) == jti
  87. end
  88. 12 def verify_aud(expected_aud, aud)
  89. 606 expected_aud == aud
  90. end
  91. 12 def oauth_application_jwks(oauth_application)
  92. 1095 jwks = oauth_application[oauth_applications_jwks_column]
  93. 1095 if jwks
  94. 627 jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
  95. 627 return jwks
  96. end
  97. 468 jwks_uri = oauth_application[oauth_applications_jwks_uri_column]
  98. 468 return unless jwks_uri
  99. 24 jwks_uri = URI(jwks_uri)
  100. 24 http_request_with_cache(jwks_uri)
  101. end
  102. 12 if defined?(JSON::JWT)
  103. # json-jwt
  104. 3 auth_value_method :oauth_jwt_jws_algorithms_supported, %w[
  105. HS256 HS384 HS512
  106. RS256 RS384 RS512
  107. PS256 PS384 PS512
  108. ES256 ES384 ES512 ES256K
  109. ]
  110. 3 auth_value_method :oauth_jwt_jwe_algorithms_supported, %w[
  111. RSA1_5 RSA-OAEP dir A128KW A256KW
  112. ]
  113. 3 auth_value_method :oauth_jwt_jwe_encryption_methods_supported, %w[
  114. A128GCM A256GCM A128CBC-HS256 A256CBC-HS512
  115. ]
  116. 3 def key_to_jwk(key)
  117. 15 JSON::JWK.new(key)
  118. end
  119. 3 def jwk_export(key)
  120. 12 key_to_jwk(key)
  121. end
  122. 3 def jwk_import(jwk)
  123. 6 JSON::JWK.new(jwk)
  124. end
  125. 3 def jwk_key(jwk)
  126. jwk = jwk_import(jwk) unless jwk.is_a?(JSON::JWK)
  127. jwk.to_key
  128. end
  129. 3 def jwk_thumbprint(jwk)
  130. 6 jwk = jwk_import(jwk) if jwk.is_a?(Hash)
  131. 6 jwk.thumbprint
  132. end
  133. 3 def jwt_encode(payload,
  134. jwks: nil,
  135. encryption_algorithm: oauth_jwt_jwe_keys.keys.dig(0, 0),
  136. encryption_method: oauth_jwt_jwe_keys.keys.dig(0, 1),
  137. jwe_key: oauth_jwt_jwe_keys[[encryption_algorithm,
  138. encryption_method]],
  139. signing_algorithm: oauth_jwt_keys.keys.first)
  140. 258 payload[:jti] = generate_jti(payload)
  141. 258 jwt = JSON::JWT.new(payload)
  142. 258 key = oauth_jwt_keys[signing_algorithm] || _jwt_key
  143. 258 key = key.first if key.is_a?(Array)
  144. 258 jwk = JSON::JWK.new(key || "")
  145. 258 jwt = jwt.sign(jwk, signing_algorithm)
  146. 258 jwt.kid = jwk.thumbprint
  147. 318 if jwks && (jwk = jwks.find { |k| k[:use] == "enc" && k[:alg] == encryption_algorithm && k[:enc] == encryption_method })
  148. 18 jwk = JSON::JWK.new(jwk)
  149. 18 jwe = jwt.encrypt(jwk, encryption_algorithm.to_sym, encryption_method.to_sym)
  150. 18 jwe.to_s
  151. 240 elsif jwe_key
  152. 3 jwe_key = jwe_key.first if jwe_key.is_a?(Array)
  153. 3 algorithm = encryption_algorithm.to_sym if encryption_algorithm
  154. 3 meth = encryption_method.to_sym if encryption_method
  155. 3 jwt.encrypt(jwe_key, algorithm, meth)
  156. else
  157. 237 jwt.to_s
  158. end
  159. end
  160. 3 def jwt_decode(
  161. token,
  162. jwks: nil,
  163. jws_algorithm: oauth_jwt_public_keys.keys.first || oauth_jwt_keys.keys.first,
  164. jws_key: oauth_jwt_keys[jws_algorithm] || _jwt_key,
  165. jws_encryption_algorithm: oauth_jwt_jwe_keys.keys.dig(0, 0),
  166. jws_encryption_method: oauth_jwt_jwe_keys.keys.dig(0, 1),
  167. jwe_key: oauth_jwt_jwe_keys[[jws_encryption_algorithm, jws_encryption_method]] || oauth_jwt_jwe_keys.values.first,
  168. verify_claims: true,
  169. verify_jti: true,
  170. verify_iss: true,
  171. verify_aud: true,
  172. **
  173. )
  174. 201 jws_key = jws_key.first if jws_key.is_a?(Array)
  175. 201 if jwe_key
  176. 9 jwe_key = jwe_key.first if jwe_key.is_a?(Array)
  177. 9 token = JSON::JWT.decode(token, jwe_key).plain_text
  178. end
  179. 201 claims = if is_authorization_server?
  180. 189 if jwks
  181. 72 jwks = jwks[:keys] if jwks.is_a?(Hash)
  182. 72 enc_algs = [jws_encryption_algorithm].compact
  183. 72 enc_meths = [jws_encryption_method].compact
  184. 150 sig_algs = jws_algorithm ? [jws_algorithm] : jwks.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
  185. 72 sig_algs = sig_algs.compact.map(&:to_sym)
  186. # JWKs may be set up without a KID, when there's a single one
  187. 72 if jwks.size == 1 && !jwks[0][:kid]
  188. 3 key = jwks[0]
  189. 3 jwk_key = JSON::JWK.new(key)
  190. 3 jws = JSON::JWT.decode(token, jwk_key)
  191. else
  192. 69 jws = JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks }), enc_algs + sig_algs, enc_meths)
  193. 63 jws = JSON::JWT.decode(jws.plain_text, JSON::JWK::Set.new({ keys: jwks }), sig_algs) if jws.is_a?(JSON::JWE)
  194. end
  195. 66 jws
  196. 117 elsif jws_key
  197. 117 JSON::JWT.decode(token, jws_key)
  198. end
  199. 12 elsif (jwks = auth_server_jwks_set)
  200. 12 JSON::JWT.decode(token, JSON::JWK::Set.new(jwks))
  201. end
  202. 195 now = Time.now
  203. 195 if verify_claims && (
  204. (!claims[:exp] || Time.at(claims[:exp]) < now) &&
  205. (claims[:nbf] && Time.at(claims[:nbf]) < now) &&
  206. (claims[:iat] && Time.at(claims[:iat]) < now) &&
  207. (verify_iss && claims[:iss] != oauth_jwt_issuer) &&
  208. (verify_aud && !verify_aud(claims[:aud], claims[:client_id])) &&
  209. (verify_jti && !verify_jti(claims[:jti], claims))
  210. )
  211. return
  212. end
  213. 195 claims
  214. rescue JSON::JWT::Exception
  215. 6 nil
  216. end
  217. 3 def jwt_decode_no_key(token)
  218. 24 jws = JSON::JWT.decode(token, :skip_verification)
  219. 24 [jws.to_h, jws.header]
  220. end
  221. 9 elsif defined?(JWT)
  222. # ruby-jwt
  223. 9 require "rodauth/oauth/jwe_extensions" if defined?(JWE)
  224. 9 auth_value_method :oauth_jwt_jws_algorithms_supported, %w[
  225. HS256 HS384 HS512 HS512256
  226. RS256 RS384 RS512
  227. ED25519
  228. ES256 ES384 ES512
  229. PS256 PS384 PS512
  230. ]
  231. 9 if defined?(JWE)
  232. 9 auth_value_methods(
  233. :oauth_jwt_jwe_algorithms_supported,
  234. :oauth_jwt_jwe_encryption_methods_supported
  235. )
  236. 9 def oauth_jwt_jwe_algorithms_supported
  237. 189 JWE::VALID_ALG
  238. end
  239. 9 def oauth_jwt_jwe_encryption_methods_supported
  240. 180 JWE::VALID_ENC
  241. end
  242. else
  243. auth_value_method :oauth_jwt_jwe_algorithms_supported, []
  244. auth_value_method :oauth_jwt_jwe_encryption_methods_supported, []
  245. end
  246. 9 def key_to_jwk(key)
  247. 45 JWT::JWK.new(key)
  248. end
  249. 9 def jwk_export(key)
  250. 36 key_to_jwk(key).export
  251. end
  252. 9 def jwk_import(jwk)
  253. 9 JWT::JWK.import(jwk)
  254. end
  255. 9 def jwk_key(jwk)
  256. jwk = jwk_import(jwk) unless jwk.is_a?(JWT::JWK)
  257. jwk.keypair
  258. end
  259. 9 def jwk_thumbprint(jwk)
  260. 18 jwk = jwk_import(jwk) if jwk.is_a?(Hash)
  261. 18 JWT::JWK::Thumbprint.new(jwk).generate
  262. end
  263. 9 def jwt_encode(payload,
  264. signing_algorithm: oauth_jwt_keys.keys.first, **)
  265. 774 headers = {}
  266. 774 key = oauth_jwt_keys[signing_algorithm] || _jwt_key
  267. 774 key = key.first if key.is_a?(Array)
  268. 774 case key
  269. when OpenSSL::PKey::PKey
  270. 558 jwk = JWT::JWK.new(key)
  271. 558 headers[:kid] = jwk.kid
  272. 558 key = jwk.keypair
  273. end
  274. # @see JWT reserved claims - https://tools.ietf.org/html/draft-jones-json-web-token-07#page-7
  275. 774 payload[:jti] = generate_jti(payload)
  276. 774 JWT.encode(payload, key, signing_algorithm, headers)
  277. end
  278. 9 if defined?(JWE)
  279. 9 def jwt_encode_with_jwe(
  280. payload,
  281. jwks: nil,
  282. encryption_algorithm: oauth_jwt_jwe_keys.keys.dig(0, 0),
  283. encryption_method: oauth_jwt_jwe_keys.keys.dig(0, 1),
  284. jwe_key: oauth_jwt_jwe_keys[[encryption_algorithm, encryption_method]],
  285. **args
  286. )
  287. 774 token = jwt_encode_without_jwe(payload, **args)
  288. 774 return token unless encryption_algorithm && encryption_method
  289. 153 if jwks && jwks.any? { |k| k[:use] == "enc" }
  290. 54 JWE.__rodauth_oauth_encrypt_from_jwks(token, jwks, alg: encryption_algorithm, enc: encryption_method)
  291. 9 elsif jwe_key
  292. 9 jwe_key = jwe_key.first if jwe_key.is_a?(Array)
  293. 3 params = {
  294. 6 zip: "DEF",
  295. copyright: oauth_jwt_jwe_copyright
  296. }
  297. 9 params[:enc] = encryption_method if encryption_method
  298. 9 params[:alg] = encryption_algorithm if encryption_algorithm
  299. 9 JWE.encrypt(token, jwe_key, **params)
  300. else
  301. token
  302. end
  303. end
  304. 9 alias_method :jwt_encode_without_jwe, :jwt_encode
  305. 9 alias_method :jwt_encode, :jwt_encode_with_jwe
  306. end
  307. 9 def jwt_decode(
  308. token,
  309. jwks: nil,
  310. jws_algorithm: oauth_jwt_public_keys.keys.first || oauth_jwt_keys.keys.first,
  311. jws_key: oauth_jwt_keys[jws_algorithm] || _jwt_key,
  312. verify_claims: true,
  313. verify_jti: true,
  314. verify_iss: true,
  315. verify_aud: true
  316. )
  317. 594 jws_key = jws_key.first if jws_key.is_a?(Array)
  318. # verifying the JWT implies verifying:
  319. #
  320. # issuer: check that server generated the token
  321. # aud: check the audience field (client is who he says he is)
  322. # iat: check that the token didn't expire
  323. #
  324. # subject can't be verified automatically without having access to the account id,
  325. # which we don't because that's the whole point.
  326. #
  327. 594 verify_claims_params = if verify_claims
  328. 186 {
  329. 372 verify_iss: verify_iss,
  330. iss: oauth_jwt_issuer,
  331. # can't use stock aud verification, as it's dependent on the client application id
  332. verify_aud: false,
  333. 558 verify_jti: (verify_jti ? method(:verify_jti) : false),
  334. verify_iat: true
  335. }
  336. else
  337. 36 {}
  338. end
  339. # decode jwt
  340. 594 claims = if is_authorization_server?
  341. 558 if jwks
  342. 207 jwks = jwks[:keys] if jwks.is_a?(Hash)
  343. # JWKs may be set up without a KID, when there's a single one
  344. 207 if jwks.size == 1 && !jwks[0][:kid]
  345. 9 key = jwks[0]
  346. 9 algo = key[:alg]
  347. 9 key = JWT::JWK.import(key).keypair
  348. 9 JWT.decode(token, key, true, algorithms: [algo], **verify_claims_params).first
  349. else
  350. 432 algorithms = jws_algorithm ? [jws_algorithm] : jwks.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
  351. 198 JWT.decode(token, nil, true, algorithms: algorithms, jwks: { keys: jwks }, **verify_claims_params).first
  352. end
  353. 351 elsif jws_key
  354. 351 JWT.decode(token, jws_key, true, algorithms: [jws_algorithm], **verify_claims_params).first
  355. end
  356. 36 elsif (jwks = auth_server_jwks_set)
  357. 108 algorithms = jwks[:keys].select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
  358. 36 JWT.decode(token, nil, true, jwks: jwks, algorithms: algorithms, **verify_claims_params).first
  359. end
  360. 585 return if verify_claims && verify_aud && !verify_aud(claims["aud"], claims["client_id"])
  361. 585 claims
  362. rescue JWT::DecodeError, JWT::JWKError
  363. 9 nil
  364. end
  365. 9 if defined?(JWE)
  366. 9 def jwt_decode_with_jwe(
  367. token,
  368. jwks: nil,
  369. jws_encryption_algorithm: oauth_jwt_jwe_keys.keys.dig(0, 0),
  370. jws_encryption_method: oauth_jwt_jwe_keys.keys.dig(0, 1),
  371. jwe_key: oauth_jwt_jwe_keys[[jws_encryption_algorithm, jws_encryption_method]] || oauth_jwt_jwe_keys.values.first,
  372. **args
  373. )
  374. 864 token = if jwks && jwks.any? { |k| k[:use] == "enc" }
  375. 27 JWE.__rodauth_oauth_decrypt_from_jwks(token, jwks, alg: jws_encryption_algorithm, enc: jws_encryption_method)
  376. 576 elsif jwe_key
  377. 27 jwe_key = jwe_key.first if jwe_key.is_a?(Array)
  378. 27 JWE.decrypt(token, jwe_key)
  379. else
  380. 549 token
  381. end
  382. 585 jwt_decode_without_jwe(token, jwks: jwks, **args)
  383. rescue JWE::DecodeError => e
  384. 18 jwt_decode_without_jwe(token, jwks: jwks, **args) if e.message.include?("Not enough or too many segments")
  385. end
  386. 9 alias_method :jwt_decode_without_jwe, :jwt_decode
  387. 9 alias_method :jwt_decode, :jwt_decode_with_jwe
  388. end
  389. 9 def jwt_decode_no_key(token)
  390. 72 JWT.decode(token, nil, false)
  391. end
  392. else
  393. skipped # :nocov:
  394. skipped def jwk_export(_key)
  395. skipped raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
  396. skipped end
  397. skipped
  398. skipped def jwk_import(_jwk)
  399. skipped raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
  400. skipped end
  401. skipped
  402. skipped def jwk_thumbprint(_jwk)
  403. skipped raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
  404. skipped end
  405. skipped
  406. skipped def jwt_encode(_token)
  407. skipped raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
  408. skipped end
  409. skipped
  410. skipped def jwt_decode(_token, **)
  411. skipped raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
  412. skipped end
  413. skipped # :nocov:
  414. end
  415. end
  416. end

lib/rodauth/features/oauth_jwt_bearer_grant.rb

100.0% lines covered

47 relevant lines. 47 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 12 require "rodauth/oauth"
  3. 12 module Rodauth
  4. 12 Feature.define(:oauth_jwt_bearer_grant, :OauthJwtBearerGrant) do
  5. 12 depends :oauth_assertion_base, :oauth_jwt
  6. 12 auth_value_method :max_param_bytesize, nil if Rodauth::VERSION >= "2.26.0"
  7. 12 auth_value_methods(
  8. :require_oauth_application_from_jwt_bearer_assertion_issuer,
  9. :require_oauth_application_from_jwt_bearer_assertion_subject,
  10. :account_from_jwt_bearer_assertion
  11. )
  12. 12 def oauth_token_endpoint_auth_methods_supported
  13. 36 if oauth_applications_client_secret_hash_column.nil?
  14. 12 super | %w[client_secret_jwt private_key_jwt urn:ietf:params:oauth:client-assertion-type:jwt-bearer]
  15. else
  16. 24 super | %w[private_key_jwt]
  17. end
  18. end
  19. 12 def oauth_grant_types_supported
  20. 96 super | %w[urn:ietf:params:oauth:grant-type:jwt-bearer]
  21. end
  22. 12 private
  23. 12 def require_oauth_application_from_jwt_bearer_assertion_issuer(assertion)
  24. 36 claims = jwt_assertion(assertion)
  25. 36 return unless claims
  26. 24 db[oauth_applications_table].where(
  27. oauth_applications_client_id_column => claims["iss"]
  28. 12 ).first
  29. end
  30. 12 def require_oauth_application_from_jwt_bearer_assertion_subject(assertion)
  31. 96 claims, header = jwt_decode_no_key(assertion)
  32. 96 client_id = claims["sub"]
  33. 96 case header["alg"]
  34. when "none"
  35. # do not accept jwts with no alg set
  36. 12 authorization_required
  37. when /\AHS/
  38. 36 require_oauth_application_from_client_secret_jwt(client_id, assertion, header["alg"])
  39. else
  40. 48 require_oauth_application_from_private_key_jwt(client_id, assertion)
  41. end
  42. end
  43. 12 def require_oauth_application_from_client_secret_jwt(client_id, assertion, alg)
  44. 36 oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
  45. 36 authorization_required unless oauth_application && supports_auth_method?(oauth_application, "client_secret_jwt")
  46. 24 client_secret = oauth_application[oauth_applications_client_secret_column]
  47. 24 claims = jwt_assertion(assertion, jws_key: client_secret, jws_algorithm: alg)
  48. 24 authorization_required unless claims && claims["iss"] == client_id
  49. 24 oauth_application
  50. end
  51. 12 def require_oauth_application_from_private_key_jwt(client_id, assertion)
  52. 48 oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
  53. 48 authorization_required unless oauth_application && supports_auth_method?(oauth_application, "private_key_jwt")
  54. 36 jwks = oauth_application_jwks(oauth_application)
  55. 36 claims = jwt_assertion(assertion, jwks: jwks)
  56. 36 authorization_required unless claims
  57. 36 oauth_application
  58. end
  59. 12 def account_from_jwt_bearer_assertion(assertion)
  60. 36 claims = jwt_assertion(assertion)
  61. 36 return unless claims
  62. 36 account_from_bearer_assertion_subject(claims["sub"])
  63. end
  64. 12 def jwt_assertion(assertion, **kwargs)
  65. 132 claims = jwt_decode(assertion, verify_iss: false, verify_aud: false, verify_jti: false, **kwargs)
  66. 132 return unless claims && verify_aud(request.url, claims["aud"])
  67. 132 claims
  68. end
  69. end
  70. end

lib/rodauth/features/oauth_jwt_jwks.rb

100.0% lines covered

23 relevant lines. 23 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 12 require "rodauth/oauth"
  3. 12 require "rodauth/oauth/http_extensions"
  4. 12 module Rodauth
  5. 12 Feature.define(:oauth_jwt_jwks, :OauthJwtJwks) do
  6. 12 depends :oauth_jwt_base
  7. 12 auth_value_methods(:jwks_set)
  8. 12 auth_server_route(:jwks) do |r|
  9. 36 before_jwks_route
  10. 36 r.get do
  11. 36 json_response_success({ keys: jwks_set }, true)
  12. end
  13. end
  14. 12 private
  15. 12 def oauth_server_metadata_body(path = nil)
  16. 132 metadata = super
  17. 132 metadata.merge!(jwks_uri: jwks_url)
  18. 132 metadata
  19. end
  20. 12 def jwks_set
  21. 36 @jwks_set ||= [
  22. *(
  23. 36 unless oauth_jwt_public_keys.empty?
  24. 72 oauth_jwt_public_keys.flat_map { |algo, pkeys| Array(pkeys).map { |pkey| jwk_export(pkey).merge(use: "sig", alg: algo) } }
  25. end
  26. ),
  27. *(
  28. 36 unless oauth_jwt_jwe_public_keys.empty?
  29. 12 oauth_jwt_jwe_public_keys.flat_map do |(algo, _enc), pkeys|
  30. 12 Array(pkeys).map do |pkey|
  31. 12 jwk_export(pkey).merge(use: "enc", alg: algo)
  32. end
  33. end
  34. end
  35. )
  36. ].compact
  37. end
  38. end
  39. end

lib/rodauth/features/oauth_jwt_secured_authorization_request.rb

98.28% lines covered

58 relevant lines. 57 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 12 require "rodauth/oauth"
  3. 12 module Rodauth
  4. 12 Feature.define(:oauth_jwt_secured_authorization_request, :OauthJwtSecuredAuthorizationRequest) do
  5. 12 ALLOWED_REQUEST_URI_CONTENT_TYPES = %w[application/jose application/oauth-authz-req+jwt].freeze
  6. 12 depends :oauth_authorize_base, :oauth_jwt_base
  7. 12 auth_value_method :oauth_require_request_uri_registration, false
  8. 12 auth_value_method :oauth_request_object_signing_alg_allow_none, false
  9. 12 auth_value_method :oauth_applications_request_uris_column, :request_uris
  10. 12 auth_value_method :oauth_applications_request_object_signing_alg_column, :request_object_signing_alg
  11. 12 auth_value_method :oauth_applications_request_object_encryption_alg_column, :request_object_encryption_alg
  12. 12 auth_value_method :oauth_applications_request_object_encryption_enc_column, :request_object_encryption_enc
  13. 12 translatable_method :oauth_invalid_request_object_message, "request object is invalid"
  14. 12 auth_value_method :max_param_bytesize, nil if Rodauth::VERSION >= "2.26.0"
  15. 12 private
  16. # /authorize
  17. 12 def validate_authorize_params
  18. 456 request_object = param_or_nil("request")
  19. 456 request_uri = param_or_nil("request_uri")
  20. 456 return super unless (request_object || request_uri) && oauth_application
  21. 372 if request_uri
  22. 108 request_uri = CGI.unescape(request_uri)
  23. 108 redirect_response_error("invalid_request_uri") unless supported_request_uri?(request_uri, oauth_application)
  24. 60 response = http_request(request_uri)
  25. 60 unless response.code.to_i == 200 && ALLOWED_REQUEST_URI_CONTENT_TYPES.include?(response["content-type"])
  26. 12 redirect_response_error("invalid_request_uri")
  27. end
  28. 48 request_object = response.body
  29. end
  30. 312 claims = decode_request_object(request_object)
  31. 216 redirect_response_error("invalid_request_object") unless claims
  32. 216 if (iss = claims["iss"]) && (iss != oauth_application[oauth_applications_client_id_column])
  33. 12 redirect_response_error("invalid_request_object")
  34. end
  35. 204 if (aud = claims["aud"]) && !verify_aud(aud, oauth_jwt_issuer)
  36. 12 redirect_response_error("invalid_request_object")
  37. end
  38. # If signed, the Authorization Request
  39. # Object SHOULD contain the Claims "iss" (issuer) and "aud" (audience)
  40. # as members, with their semantics being the same as defined in the JWT
  41. # [RFC7519] specification. The value of "aud" should be the value of
  42. # the Authorization Server (AS) "issuer" as defined in RFC8414
  43. # [RFC8414].
  44. 192 claims.delete("iss")
  45. 192 audience = claims.delete("aud")
  46. 192 redirect_response_error("invalid_request_object") if audience && audience != oauth_jwt_issuer
  47. 192 claims.each do |k, v|
  48. 1188 request.params[k.to_s] = v
  49. end
  50. 192 super
  51. end
  52. 12 def supported_request_uri?(request_uri, oauth_application)
  53. 108 return false unless check_valid_uri?(request_uri)
  54. 84 request_uris = oauth_application[oauth_applications_request_uris_column]
  55. 144 request_uris.nil? || request_uris.split(oauth_scope_separator).one? { |uri| request_uri.start_with?(uri) }
  56. end
  57. 12 def decode_request_object(request_object)
  58. 108 request_sig_enc_opts = {
  59. 216 jws_algorithm: oauth_application[oauth_applications_request_object_signing_alg_column],
  60. jws_encryption_algorithm: oauth_application[oauth_applications_request_object_encryption_alg_column],
  61. jws_encryption_method: oauth_application[oauth_applications_request_object_encryption_enc_column]
  62. }.compact
  63. 324 request_sig_enc_opts[:jws_algorithm] ||= "none" if oauth_request_object_signing_alg_allow_none
  64. 324 if request_sig_enc_opts[:jws_algorithm] == "none"
  65. jwks = nil
  66. 324 elsif (jwks = oauth_application_jwks(oauth_application))
  67. 252 jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
  68. else
  69. 72 redirect_response_error("invalid_request_object")
  70. end
  71. 252 claims = jwt_decode(request_object,
  72. jwks: jwks,
  73. verify_jti: false,
  74. verify_iss: false,
  75. verify_aud: false,
  76. **request_sig_enc_opts)
  77. 252 redirect_response_error("invalid_request_object") unless claims
  78. 228 claims
  79. end
  80. 12 def oauth_server_metadata_body(*)
  81. 24 super.tap do |data|
  82. 24 data[:request_parameter_supported] = true
  83. 24 data[:request_uri_parameter_supported] = true
  84. 24 data[:require_request_uri_registration] = oauth_require_request_uri_registration
  85. end
  86. end
  87. end
  88. end

lib/rodauth/features/oauth_jwt_secured_authorization_response_mode.rb

98.53% lines covered

68 relevant lines. 67 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 12 require "rodauth/oauth"
  3. 12 module Rodauth
  4. 12 Feature.define(:oauth_jwt_secured_authorization_response_mode, :OauthJwtSecuredAuthorizationResponseMode) do
  5. 12 depends :oauth_authorize_base, :oauth_jwt_base
  6. 12 auth_value_method :oauth_authorization_response_mode_expires_in, 60 * 5 # 5 minutes
  7. 12 auth_value_method :oauth_applications_authorization_signed_response_alg_column, :authorization_signed_response_alg
  8. 12 auth_value_method :oauth_applications_authorization_encrypted_response_alg_column, :authorization_encrypted_response_alg
  9. 12 auth_value_method :oauth_applications_authorization_encrypted_response_enc_column, :authorization_encrypted_response_enc
  10. 12 auth_value_methods(
  11. :authorization_signing_alg_values_supported,
  12. :authorization_encryption_alg_values_supported,
  13. :authorization_encryption_enc_values_supported
  14. )
  15. 12 def oauth_response_modes_supported
  16. 516 jwt_response_modes = %w[jwt]
  17. 516 jwt_response_modes.push("query.jwt", "form_post.jwt") if features.include?(:oauth_authorization_code_grant)
  18. 516 jwt_response_modes << "fragment.jwt" if features.include?(:oauth_implicit_grant)
  19. 516 super | jwt_response_modes
  20. end
  21. 12 def authorization_signing_alg_values_supported
  22. 12 oauth_jwt_jws_algorithms_supported
  23. end
  24. 12 def authorization_encryption_alg_values_supported
  25. 24 oauth_jwt_jwe_algorithms_supported
  26. end
  27. 12 def authorization_encryption_enc_values_supported
  28. 24 oauth_jwt_jwe_encryption_methods_supported
  29. end
  30. 12 private
  31. 12 def oauth_response_modes_for_code_supported
  32. 144 return [] unless features.include?(:oauth_authorization_code_grant)
  33. 144 super | %w[query.jwt form_post.jwt jwt]
  34. end
  35. 12 def oauth_response_modes_for_token_supported
  36. 60 return [] unless features.include?(:oauth_implicit_grant)
  37. 60 super | %w[fragment.jwt jwt]
  38. end
  39. 12 def authorize_response(params, mode)
  40. 120 return super unless mode.end_with?("jwt")
  41. 120 response_type = param_or_nil("response_type")
  42. 120 redirect_url = URI.parse(redirect_uri)
  43. 120 jwt = jwt_encode_authorization_response_mode(params)
  44. 120 if mode == "query.jwt" || (mode == "jwt" && response_type == "code")
  45. 60 return super unless features.include?(:oauth_authorization_code_grant)
  46. 60 params = ["response=#{CGI.escape(jwt)}"]
  47. 60 params << redirect_url.query if redirect_url.query
  48. 60 redirect_url.query = params.join("&")
  49. 60 redirect(redirect_url.to_s)
  50. 60 elsif mode == "form_post.jwt"
  51. 12 return super unless features.include?(:oauth_authorization_code_grant)
  52. 12 response["Content-Type"] = "text/html"
  53. 12 body = form_post_response_html(redirect_url) do
  54. 12 "<input type=\"hidden\" name=\"response\" value=\"#{scope.h(jwt)}\" />"
  55. end
  56. 12 response.write(body)
  57. 12 request.halt
  58. 48 elsif mode == "fragment.jwt" || (mode == "jwt" && response_type == "token")
  59. 48 return super unless features.include?(:oauth_implicit_grant)
  60. 48 params = ["response=#{CGI.escape(jwt)}"]
  61. 48 params << redirect_url.query if redirect_url.query
  62. 48 redirect_url.fragment = params.join("&")
  63. 48 redirect(redirect_url.to_s)
  64. else
  65. super
  66. end
  67. end
  68. 12 def _redirect_response_error(redirect_url, params)
  69. 36 response_mode = param_or_nil("response_mode")
  70. 36 return super unless response_mode.end_with?("jwt")
  71. 36 authorize_response(Hash[params], response_mode)
  72. end
  73. 12 def jwt_encode_authorization_response_mode(params)
  74. 120 now = Time.now.to_i
  75. 40 claims = {
  76. 80 iss: oauth_jwt_issuer,
  77. aud: oauth_application[oauth_applications_client_id_column],
  78. exp: now + oauth_authorization_response_mode_expires_in,
  79. iat: now
  80. }.merge(params)
  81. 40 encode_params = {
  82. 80 jwks: oauth_application_jwks(oauth_application),
  83. signing_algorithm: oauth_application[oauth_applications_authorization_signed_response_alg_column],
  84. encryption_algorithm: oauth_application[oauth_applications_authorization_encrypted_response_alg_column],
  85. encryption_method: oauth_application[oauth_applications_authorization_encrypted_response_enc_column]
  86. }.compact
  87. 120 jwt_encode(claims, **encode_params)
  88. end
  89. 12 def oauth_server_metadata_body(*)
  90. 24 super.tap do |data|
  91. 24 data[:authorization_signing_alg_values_supported] = authorization_signing_alg_values_supported
  92. 24 data[:authorization_encryption_alg_values_supported] = authorization_encryption_alg_values_supported
  93. 24 data[:authorization_encryption_enc_values_supported] = authorization_encryption_enc_values_supported
  94. end
  95. end
  96. end
  97. end

lib/rodauth/features/oauth_management_base.rb

100.0% lines covered

33 relevant lines. 33 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 12 require "rodauth/oauth"
  3. 12 module Rodauth
  4. 12 Feature.define(:oauth_management_base, :OauthManagementBase) do
  5. 12 depends :oauth_authorize_base
  6. 12 button "Previous", "oauth_management_pagination_previous"
  7. 12 button "Next", "oauth_management_pagination_next"
  8. 12 def oauth_management_pagination_links(paginated_ds)
  9. 168 html = +'<nav aria-label="Pagination"><ul class="pagination">'
  10. 168 html << oauth_management_pagination_link(paginated_ds.prev_page, label: oauth_management_pagination_previous_button)
  11. 168 html << oauth_management_pagination_link(paginated_ds.current_page - 1) unless paginated_ds.first_page?
  12. 168 html << oauth_management_pagination_link(paginated_ds.current_page, label: paginated_ds.current_page, current: true)
  13. 168 html << oauth_management_pagination_link(paginated_ds.current_page + 1) unless paginated_ds.last_page?
  14. 168 html << oauth_management_pagination_link(paginated_ds.next_page, label: oauth_management_pagination_next_button)
  15. 168 html << "</ul></nav>"
  16. end
  17. 12 def oauth_management_pagination_link(page, label: page, current: false, classes: "")
  18. 558 classes += " disabled" if current || !page
  19. 558 classes += " active" if current
  20. 558 if page
  21. 276 params = URI.encode_www_form(request.GET.merge("page" => page))
  22. 276 href = "#{request.path}?#{params}"
  23. 276 <<-HTML
  24. <li class="page-item #{classes}" #{'aria-current="page"' if current}>
  25. <a class="page-link" href="#{href}" tabindex="-1" aria-disabled="#{current || !page}">
  26. #{label}
  27. </a>
  28. </li>
  29. HTML
  30. else
  31. 282 <<-HTML
  32. <li class="page-item #{classes}">
  33. <span class="page-link">
  34. #{label}
  35. #{'<span class="sr-only">(current)</span>' if current}
  36. </span>
  37. </li>
  38. HTML
  39. end
  40. end
  41. 12 def post_configure
  42. 78 super
  43. # TODO: remove this in v1, when resource-server mode does not load all of the provider features.
  44. 78 return unless db
  45. 78 db.extension :pagination
  46. end
  47. 12 private
  48. 12 def per_page_param(default_per_page)
  49. 216 per_page = param_or_nil("per_page")
  50. 216 return default_per_page unless per_page
  51. 54 per_page = per_page.to_i
  52. 54 return default_per_page if per_page <= 0
  53. 54 [per_page, default_per_page].min
  54. end
  55. end
  56. end

lib/rodauth/features/oauth_pkce.rb

100.0% lines covered

49 relevant lines. 49 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 12 require "rodauth/oauth"
  3. 12 module Rodauth
  4. 12 Feature.define(:oauth_pkce, :OauthPkce) do
  5. 12 depends :oauth_authorization_code_grant
  6. 12 auth_value_method :oauth_require_pkce, true
  7. 12 auth_value_method :oauth_pkce_challenge_method, "S256"
  8. 12 auth_value_method :oauth_grants_code_challenge_column, :code_challenge
  9. 12 auth_value_method :oauth_grants_code_challenge_method_column, :code_challenge_method
  10. 12 auth_value_method :oauth_code_challenge_required_error_code, "invalid_request"
  11. 12 translatable_method :oauth_code_challenge_required_message, "code challenge required"
  12. 12 auth_value_method :oauth_unsupported_transform_algorithm_error_code, "invalid_request"
  13. 12 translatable_method :oauth_unsupported_transform_algorithm_message, "transform algorithm not supported"
  14. 12 private
  15. 12 def supports_auth_method?(oauth_application, auth_method)
  16. 72 return super unless auth_method == "none"
  17. 48 request.params.key?("code_verifier") || super
  18. end
  19. 12 def validate_authorize_params
  20. 48 validate_pkce_challenge_params
  21. 36 super
  22. end
  23. 12 def create_oauth_grant(create_params = {})
  24. # PKCE flow
  25. 12 if (code_challenge = param_or_nil("code_challenge"))
  26. 12 code_challenge_method = param_or_nil("code_challenge_method") || oauth_pkce_challenge_method
  27. 12 create_params[oauth_grants_code_challenge_column] = code_challenge
  28. 12 create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
  29. end
  30. 12 super
  31. end
  32. 12 def create_token_from_authorization_code(grant_params, *args, oauth_grant: nil)
  33. 72 oauth_grant ||= valid_locked_oauth_grant(grant_params)
  34. 72 if oauth_grant[oauth_grants_code_challenge_column]
  35. 60 code_verifier = param_or_nil("code_verifier")
  36. 60 redirect_response_error("invalid_request") unless code_verifier && check_valid_grant_challenge?(oauth_grant, code_verifier)
  37. 12 elsif oauth_require_pkce
  38. 12 redirect_response_error("code_challenge_required")
  39. end
  40. 24 super({ oauth_grants_id_column => oauth_grant[oauth_grants_id_column] }, *args, oauth_grant: oauth_grant)
  41. end
  42. 12 def validate_pkce_challenge_params
  43. 48 if param_or_nil("code_challenge")
  44. 24 challenge_method = param_or_nil("code_challenge_method")
  45. 24 redirect_response_error("code_challenge_required") unless oauth_pkce_challenge_method == challenge_method
  46. else
  47. 24 return unless oauth_require_pkce
  48. 12 redirect_response_error("code_challenge_required")
  49. end
  50. end
  51. 12 def check_valid_grant_challenge?(grant, verifier)
  52. 48 challenge = grant[oauth_grants_code_challenge_column]
  53. 48 case grant[oauth_grants_code_challenge_method_column]
  54. when "plain"
  55. 12 challenge == verifier
  56. when "S256"
  57. 24 generated_challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(verifier), padding: false)
  58. 24 challenge == generated_challenge
  59. else
  60. 12 redirect_response_error("unsupported_transform_algorithm")
  61. end
  62. end
  63. 12 def oauth_server_metadata_body(*)
  64. 12 super.tap do |data|
  65. 12 data[:code_challenge_methods_supported] = oauth_pkce_challenge_method
  66. end
  67. end
  68. end
  69. end

lib/rodauth/features/oauth_pushed_authorization_request.rb

95.16% lines covered

62 relevant lines. 59 lines covered and 3 lines missed.
    
  1. # frozen_string_literal: true
  2. 12 require "rodauth/oauth"
  3. 12 module Rodauth
  4. 12 Feature.define(:oauth_pushed_authorization_request, :OauthJwtPushedAuthorizationRequest) do
  5. 12 depends :oauth_authorize_base
  6. 12 auth_value_method :oauth_require_pushed_authorization_requests, false
  7. 12 auth_value_method :oauth_applications_require_pushed_authorization_requests_column, :require_pushed_authorization_requests
  8. 12 auth_value_method :oauth_pushed_authorization_request_expires_in, 90 # 90 seconds
  9. 12 auth_value_method :oauth_require_pushed_authorization_request_iss_request_object, true
  10. 12 auth_value_method :oauth_pushed_authorization_requests_table, :oauth_pushed_requests
  11. 8 %i[
  12. oauth_application_id params code expires_in
  13. 4 ].each do |column|
  14. 48 auth_value_method :"oauth_pushed_authorization_requests_#{column}_column", column
  15. end
  16. # /par
  17. 12 auth_server_route(:par) do |r|
  18. 48 require_oauth_application
  19. 36 before_par_route
  20. 36 r.post do
  21. 36 validate_par_params
  22. 24 ds = db[oauth_pushed_authorization_requests_table]
  23. 24 code = oauth_unique_id_generator
  24. 8 push_request_params = {
  25. 16 oauth_pushed_authorization_requests_oauth_application_id_column => oauth_application[oauth_applications_id_column],
  26. oauth_pushed_authorization_requests_code_column => code,
  27. oauth_pushed_authorization_requests_params_column => URI.encode_www_form(request.params),
  28. oauth_pushed_authorization_requests_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP,
  29. seconds: oauth_pushed_authorization_request_expires_in)
  30. }
  31. 24 rescue_from_uniqueness_error do
  32. 24 ds.insert(push_request_params)
  33. end
  34. 24 json_response_success(
  35. "request_uri" => "urn:ietf:params:oauth:request_uri:#{code}",
  36. "expires_in" => oauth_pushed_authorization_request_expires_in
  37. )
  38. end
  39. end
  40. 12 def check_csrf?
  41. 336 case request.path
  42. when par_path
  43. 48 false
  44. else
  45. 288 super
  46. end
  47. end
  48. 12 private
  49. 12 def validate_par_params
  50. # https://datatracker.ietf.org/doc/html/rfc9126#section-2.1
  51. # The request_uri authorization request parameter is one exception, and it MUST NOT be provided.
  52. 36 redirect_response_error("invalid_request") if param_or_nil("request_uri")
  53. 24 if (request_object = param_or_nil("request")) && features.include?(:oauth_jwt_secured_authorization_request)
  54. 12 claims = decode_request_object(request_object)
  55. # https://datatracker.ietf.org/doc/html/rfc9126#section-3-5.3
  56. # reject the request if the authenticated client_id does not match the client_id claim in the Request Object
  57. 12 if (client_id = claims["client_id"]) && (client_id != oauth_application[oauth_applications_client_id_column])
  58. redirect_response_error("invalid_request_object")
  59. end
  60. # requiring the iss claim to match the client_id is at the discretion of the authorization server
  61. 12 if oauth_require_pushed_authorization_request_iss_request_object &&
  62. 12 (iss = claims.delete("iss")) &&
  63. iss != oauth_application[oauth_applications_client_id_column]
  64. redirect_response_error("invalid_request_object")
  65. end
  66. 12 if (aud = claims.delete("aud")) && !verify_aud(aud, oauth_jwt_issuer)
  67. redirect_response_error("invalid_request_object")
  68. end
  69. 12 claims.delete("exp")
  70. 12 request.params.delete("request")
  71. 12 claims.each do |k, v|
  72. 72 request.params[k.to_s] = v
  73. end
  74. end
  75. 24 validate_authorize_params
  76. end
  77. 12 def validate_authorize_params
  78. 108 return super unless request.get? && request.path == authorize_path
  79. 72 if (request_uri = param_or_nil("request_uri"))
  80. 24 code = request_uri.delete_prefix("urn:ietf:params:oauth:request_uri:")
  81. 24 table = oauth_pushed_authorization_requests_table
  82. 24 ds = db[table]
  83. 24 pushed_request = ds.where(
  84. oauth_pushed_authorization_requests_oauth_application_id_column => oauth_application[oauth_applications_id_column],
  85. oauth_pushed_authorization_requests_code_column => code
  86. ).where(
  87. Sequel.expr(Sequel[table][oauth_pushed_authorization_requests_expires_in_column]) >= Sequel::CURRENT_TIMESTAMP
  88. ).first
  89. 24 redirect_response_error("invalid_request") unless pushed_request
  90. 12 URI.decode_www_form(pushed_request[oauth_pushed_authorization_requests_params_column]).each do |k, v|
  91. 36 request.params[k.to_s] = v
  92. end
  93. 48 elsif oauth_require_pushed_authorization_requests ||
  94. (oauth_application && oauth_application[oauth_applications_require_pushed_authorization_requests_column])
  95. 24 redirect_authorize_error("request_uri")
  96. end
  97. 36 super
  98. end
  99. 12 def oauth_server_metadata_body(*)
  100. 12 super.tap do |data|
  101. 12 data[:require_pushed_authorization_requests] = oauth_require_pushed_authorization_requests
  102. 12 data[:pushed_authorization_request_endpoint] = par_url
  103. end
  104. end
  105. end
  106. end

lib/rodauth/features/oauth_resource_indicators.rb

93.9% lines covered

82 relevant lines. 77 lines covered and 5 lines missed.
    
  1. # frozen_string_literal: true
  2. 12 require "rodauth/oauth"
  3. 12 module Rodauth
  4. 12 Feature.define(:oauth_resource_indicators, :OauthResourceIndicators) do
  5. 12 depends :oauth_authorize_base
  6. 12 auth_value_method :oauth_grants_resource_column, :resource
  7. 12 def resource_indicators
  8. 480 return @resource_indicators if defined?(@resource_indicators)
  9. 120 resources = param_or_nil("resource")
  10. 120 return unless resources
  11. 120 if json_request? || param_or_nil("request") # signed request
  12. 24 resources = Array(resources)
  13. else
  14. 96 query = if request.form_data?
  15. 60 request.body.rewind
  16. 60 request.body.read
  17. else
  18. 36 request.query_string
  19. end
  20. # resource query param does not conform to rack parsing rules
  21. 96 resources = URI.decode_www_form(query).each_with_object([]) do |(k, v), memo|
  22. 504 memo << v if k == "resource"
  23. end
  24. end
  25. 120 @resource_indicators = resources
  26. end
  27. 12 def require_oauth_authorization(*)
  28. 84 super
  29. # done so to support token-in-grant-db, jwt, and resource-server mode
  30. 72 token_indicators = authorization_token[oauth_grants_resource_column] || authorization_token["resource"]
  31. 72 return unless token_indicators
  32. 60 token_indicators = token_indicators.split(" ") if token_indicators.is_a?(String)
  33. 120 authorization_required unless token_indicators.any? { |resource| base_url.start_with?(resource) }
  34. end
  35. 12 private
  36. 12 def validate_token_params
  37. 48 super
  38. 48 return unless resource_indicators
  39. 48 resource_indicators.each do |resource|
  40. 48 redirect_response_error("invalid_target") unless check_valid_no_fragment_uri?(resource)
  41. end
  42. end
  43. 12 def create_token_from_token(oauth_grant, update_params)
  44. return super unless resource_indicators
  45. grant_indicators = oauth_grant[oauth_grants_resource_column]
  46. grant_indicators = grant_indicators.split(" ") if grant_indicators.is_a?(String)
  47. redirect_response_error("invalid_target") unless (grant_indicators - resource_indicators) != grant_indicators
  48. super(oauth_grant, update_params.merge(oauth_grants_resource_column => resource_indicators))
  49. end
  50. 12 module IndicatorAuthorizationCodeGrant
  51. 12 private
  52. 12 def validate_authorize_params
  53. 72 super
  54. 72 return unless resource_indicators
  55. 72 resource_indicators.each do |resource|
  56. 72 redirect_response_error("invalid_target") unless check_valid_no_fragment_uri?(resource)
  57. end
  58. end
  59. 12 def create_token_from_authorization_code(grant_params, *args, oauth_grant: nil)
  60. 48 return super unless resource_indicators
  61. 48 oauth_grant ||= valid_locked_oauth_grant(grant_params)
  62. 48 redirect_response_error("invalid_target") unless oauth_grant[oauth_grants_resource_column]
  63. 48 grant_indicators = oauth_grant[oauth_grants_resource_column]
  64. 48 grant_indicators = grant_indicators.split(" ") if grant_indicators.is_a?(String)
  65. 48 redirect_response_error("invalid_target") unless (grant_indicators - resource_indicators) != grant_indicators
  66. # update ownership
  67. 36 if grant_indicators != resource_indicators
  68. 12 oauth_grant = __update_and_return__(
  69. db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column]),
  70. oauth_grants_resource_column => resource_indicators
  71. )
  72. end
  73. 36 super({ oauth_grants_id_column => oauth_grant[oauth_grants_id_column] }, *args, oauth_grant: oauth_grant)
  74. end
  75. 12 def create_oauth_grant(create_params = {})
  76. 12 create_params[oauth_grants_resource_column] = resource_indicators.join(" ") if resource_indicators
  77. 12 super
  78. end
  79. end
  80. 12 module IndicatorIntrospection
  81. 12 def json_token_introspect_payload(grant)
  82. 12 return super unless grant[oauth_grants_id_column]
  83. 12 payload = super
  84. 12 token_indicators = grant[oauth_grants_resource_column]
  85. 12 token_indicators = token_indicators.split(" ") if token_indicators.is_a?(String)
  86. 12 payload[:aud] = token_indicators
  87. 12 payload
  88. end
  89. 12 def introspection_request(*)
  90. 36 payload = super
  91. 36 payload[oauth_grants_resource_column] = payload["aud"] if payload["aud"]
  92. 36 payload
  93. end
  94. end
  95. 12 module IndicatorJwt
  96. 12 def jwt_claims(*)
  97. 12 return super unless resource_indicators
  98. 12 super.merge(aud: resource_indicators)
  99. end
  100. 12 def jwt_decode(token, verify_aud: true, **args)
  101. 36 claims = super(token, verify_aud: false, **args)
  102. 36 return claims unless verify_aud
  103. 24 return unless claims["aud"] && claims["aud"].one? { |aud| request.url.starts_with?(aud) }
  104. 12 claims
  105. end
  106. end
  107. 12 def self.included(rodauth)
  108. 180 super
  109. 180 rodauth.send(:include, IndicatorAuthorizationCodeGrant) if rodauth.features.include?(:oauth_authorization_code_grant)
  110. 180 rodauth.send(:include, IndicatorIntrospection) if rodauth.features.include?(:oauth_token_introspection)
  111. 180 rodauth.send(:include, IndicatorJwt) if rodauth.features.include?(:oauth_jwt)
  112. end
  113. end
  114. end

lib/rodauth/features/oauth_resource_server.rb

96.3% lines covered

27 relevant lines. 26 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 12 require "rodauth/oauth"
  3. 12 module Rodauth
  4. 12 Feature.define(:oauth_resource_server, :OauthResourceServer) do
  5. 12 depends :oauth_token_introspection
  6. 12 auth_value_method :is_authorization_server?, false
  7. 12 auth_value_methods(
  8. :before_introspection_request
  9. )
  10. 12 def authorization_token
  11. 216 return @authorization_token if defined?(@authorization_token)
  12. # check if there is a token
  13. 108 access_token = fetch_access_token
  14. 108 return unless access_token
  15. # where in resource server, NOT the authorization server.
  16. 84 payload = introspection_request("access_token", access_token)
  17. 84 return unless payload["active"]
  18. 72 @authorization_token = payload
  19. end
  20. 12 def require_oauth_authorization(*scopes)
  21. 108 authorization_required unless authorization_token
  22. 72 aux_scopes = authorization_token["scope"]
  23. 72 token_scopes = if aux_scopes
  24. 72 aux_scopes.split(oauth_scope_separator)
  25. else
  26. []
  27. end
  28. 144 authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
  29. end
  30. 12 private
  31. 12 def introspection_request(token_type_hint, token)
  32. 84 introspect_url = URI("#{authorization_server_url}#{introspect_path}")
  33. 84 response = http_request(introspect_url, { "token_type_hint" => token_type_hint, "token" => token }) do |request|
  34. 84 before_introspection_request(request)
  35. end
  36. 84 JSON.parse(response.body)
  37. end
  38. 12 def before_introspection_request(request); end
  39. end
  40. end

lib/rodauth/features/oauth_saml_bearer_grant.rb

96.0% lines covered

50 relevant lines. 48 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 12 require "onelogin/ruby-saml"
  3. 12 require "rodauth/oauth"
  4. 12 module Rodauth
  5. 12 Feature.define(:oauth_saml_bearer_grant, :OauthSamlBearerGrant) do
  6. 12 depends :oauth_assertion_base
  7. 12 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"
  8. 12 auth_value_method :oauth_saml_cert, nil
  9. 12 auth_value_method :oauth_saml_cert_fingerprint_algorithm, nil
  10. 12 auth_value_method :oauth_saml_name_identifier_format, "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
  11. 12 auth_value_method :oauth_saml_security_authn_requests_signed, true
  12. 12 auth_value_method :oauth_saml_security_metadata_signed, true
  13. 12 auth_value_method :oauth_saml_security_digest_method, XMLSecurity::Document::SHA1
  14. 12 auth_value_method :oauth_saml_security_signature_method, XMLSecurity::Document::RSA_SHA1
  15. 12 auth_value_method :max_param_bytesize, nil if Rodauth::VERSION >= "2.26.0"
  16. 12 auth_value_methods(
  17. :require_oauth_application_from_saml2_bearer_assertion_issuer,
  18. :require_oauth_application_from_saml2_bearer_assertion_subject,
  19. :account_from_saml2_bearer_assertion
  20. )
  21. 12 def oauth_grant_types_supported
  22. 24 super | %w[urn:ietf:params:oauth:grant-type:saml2-bearer]
  23. end
  24. 12 private
  25. 12 def require_oauth_application_from_saml2_bearer_assertion_issuer(assertion)
  26. 12 saml = saml_assertion(assertion)
  27. 12 return unless saml
  28. 8 db[oauth_applications_table].where(
  29. oauth_applications_homepage_url_column => saml.issuers
  30. 4 ).first
  31. end
  32. 12 def require_oauth_application_from_saml2_bearer_assertion_subject(assertion)
  33. 12 saml = saml_assertion(assertion)
  34. 12 return unless saml
  35. 8 db[oauth_applications_table].where(
  36. oauth_applications_client_id_column => saml.nameid
  37. 4 ).first
  38. end
  39. 12 def account_from_saml2_bearer_assertion(assertion)
  40. 12 saml = saml_assertion(assertion)
  41. 12 return unless saml
  42. 12 account_from_bearer_assertion_subject(saml.nameid)
  43. end
  44. 12 def saml_assertion(assertion)
  45. 36 settings = OneLogin::RubySaml::Settings.new
  46. 36 settings.idp_cert = oauth_saml_cert
  47. 36 settings.idp_cert_fingerprint = oauth_saml_cert_fingerprint
  48. 36 settings.idp_cert_fingerprint_algorithm = oauth_saml_cert_fingerprint_algorithm
  49. 36 settings.name_identifier_format = oauth_saml_name_identifier_format
  50. 36 settings.security[:authn_requests_signed] = oauth_saml_security_authn_requests_signed
  51. 36 settings.security[:metadata_signed] = oauth_saml_security_metadata_signed
  52. 36 settings.security[:digest_method] = oauth_saml_security_digest_method
  53. 36 settings.security[:signature_method] = oauth_saml_security_signature_method
  54. 36 response = OneLogin::RubySaml::Response.new(assertion, settings: settings, skip_recipient_check: true)
  55. # 3. he Assertion MUST have an expiry that limits the time window ...
  56. # 4. The Assertion MUST have an expiry that limits the time window ...
  57. # 5. The <Subject> element MUST contain at least one ...
  58. # 6. The authorization server MUST reject the entire Assertion if the ...
  59. # 7. If the Assertion issuer directly authenticated the subject, ...
  60. 36 redirect_response_error("invalid_grant") unless response.is_valid?
  61. # In order to issue an access token response as described in OAuth 2.0
  62. # [RFC6749] or to rely on an Assertion for client authentication, the
  63. # authorization server MUST validate the Assertion according to the
  64. # criteria below.
  65. # 1. The Assertion's <Issuer> element MUST contain a unique identifier
  66. # for the entity that issued the Assertion.
  67. 36 redirect_response_error("invalid_grant") unless response.issuers.size == 1
  68. # 2. in addition to the URI references
  69. # discussed there, the token endpoint URL of the authorization
  70. # server MAY be used as a URI that identifies the authorization
  71. # server as an intended audience. The authorization server MUST
  72. # reject any Assertion that does not contain its own identity as
  73. # the intended audience.
  74. 36 redirect_response_error("invalid_grant") if response.audiences && !response.audiences.include?(token_url)
  75. 36 response
  76. end
  77. 12 def oauth_server_metadata_body(*)
  78. super.tap do |data|
  79. data[:token_endpoint_auth_methods_supported] << "urn:ietf:params:oauth:client-assertion-type:saml2-bearer"
  80. end
  81. end
  82. end
  83. end

lib/rodauth/features/oauth_tls_client_auth.rb

89.16% lines covered

83 relevant lines. 74 lines covered and 9 lines missed.
    
  1. # frozen_string_literal: true
  2. 12 require "openssl"
  3. 12 require "ipaddr"
  4. 12 require "uri"
  5. 12 require "rodauth/oauth"
  6. 12 module Rodauth
  7. 12 Feature.define(:oauth_tls_client_auth, :OauthTlsClientAuth) do
  8. 12 depends :oauth_jwt_base
  9. 12 auth_value_method :oauth_tls_client_certificate_bound_access_tokens, false
  10. 8 %i[
  11. tls_client_auth_subject_dn t