loading
Generated 2023-01-10T23:20:48+00:00

All Files ( 96.39% covered at 380.62 hits/line )

30 files in total.
2714 relevant lines, 2616 lines covered and 98 lines missed. ( 96.39% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
lib/rodauth/features/oauth_application_management.rb 100.00 % 210 129 129 0 66.51
lib/rodauth/features/oauth_assertion_base.rb 100.00 % 92 47 47 0 59.17
lib/rodauth/features/oauth_authorization_code_grant.rb 100.00 % 143 66 66 0 452.14
lib/rodauth/features/oauth_authorize_base.rb 98.36 % 238 122 120 2 531.99
lib/rodauth/features/oauth_base.rb 96.47 % 867 453 437 16 1109.63
lib/rodauth/features/oauth_client_credentials_grant.rb 100.00 % 35 19 19 0 37.42
lib/rodauth/features/oauth_device_code_grant.rb 95.93 % 209 123 118 5 56.90
lib/rodauth/features/oauth_dynamic_client_registration.rb 91.95 % 273 149 137 12 758.36
lib/rodauth/features/oauth_grant_management.rb 100.00 % 70 40 40 0 75.75
lib/rodauth/features/oauth_implicit_grant.rb 100.00 % 77 44 44 0 135.61
lib/rodauth/features/oauth_jwt.rb 100.00 % 126 61 61 0 311.64
lib/rodauth/features/oauth_jwt_base.rb 93.78 % 458 209 196 13 216.34
lib/rodauth/features/oauth_jwt_bearer_grant.rb 100.00 % 91 48 48 0 46.06
lib/rodauth/features/oauth_jwt_jwks.rb 100.00 % 47 24 24 0 36.50
lib/rodauth/features/oauth_jwt_secured_authorization_request.rb 89.47 % 116 57 51 6 159.95
lib/rodauth/features/oauth_management_base.rb 100.00 % 74 38 38 0 185.13
lib/rodauth/features/oauth_pkce.rb 100.00 % 94 50 50 0 29.04
lib/rodauth/features/oauth_resource_indicators.rb 93.90 % 166 82 77 5 67.32
lib/rodauth/features/oauth_resource_server.rb 96.30 % 59 27 26 1 74.44
lib/rodauth/features/oauth_saml_bearer_grant.rb 96.00 % 108 50 48 2 22.62
lib/rodauth/features/oauth_token_introspection.rb 98.68 % 139 76 75 1 63.95
lib/rodauth/features/oauth_token_revocation.rb 100.00 % 124 63 63 0 96.57
lib/rodauth/features/oidc.rb 95.31 % 817 405 386 19 179.03
lib/rodauth/features/oidc_dynamic_client_registration.rb 91.82 % 223 110 101 9 103.96
lib/rodauth/features/oidc_rp_initiated_logout.rb 94.64 % 118 56 53 3 34.13
lib/rodauth/oauth.rb 100.00 % 35 18 18 0 2377.50
lib/rodauth/oauth/database_extensions.rb 100.00 % 88 42 42 0 1284.43
lib/rodauth/oauth/http_extensions.rb 95.56 % 74 45 43 2 137.87
lib/rodauth/oauth/jwe_extensions.rb 100.00 % 64 33 33 0 23.64
lib/rodauth/oauth/ttl_store.rb 92.86 % 67 28 26 2 15.54

lib/rodauth/features/oauth_application_management.rb

100.0% lines covered

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

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

66 relevant lines. 66 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 15 require "rodauth/oauth"
  3. 15 module Rodauth
  4. 15 Feature.define(:oauth_authorization_code_grant, :OauthAuthorizationCodeGrant) do
  5. 15 depends :oauth_authorize_base
  6. 15 auth_value_method :oauth_response_mode, "form_post"
  7. 15 def oauth_grant_types_supported
  8. 3579 super | %w[authorization_code]
  9. end
  10. 15 def oauth_response_types_supported
  11. 1497 super | %w[code]
  12. end
  13. 15 def oauth_response_modes_supported
  14. 1407 super | %w[query form_post]
  15. end
  16. 15 private
  17. 15 def validate_authorize_params
  18. 1914 super
  19. 1719 response_mode = param_or_nil("response_mode")
  20. 1719 redirect_response_error("invalid_request") if response_mode && !oauth_response_modes_supported.include?(response_mode)
  21. end
  22. 15 def validate_token_params
  23. 1242 redirect_response_error("invalid_request") if param_or_nil("grant_type") == "authorization_code" && !param_or_nil("code")
  24. 1242 super
  25. end
  26. 60 def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
  27. 627 response_mode ||= oauth_response_mode
  28. 627 redirect_response_error("invalid_request") unless response_mode.nil? || supported_response_mode?(response_mode)
  29. 627 response_type = param_or_nil("response_type")
  30. 627 redirect_response_error("invalid_request") unless response_type.nil? || supported_response_type?(response_type)
  31. 627 case response_type
  32. when "code", nil
  33. 300 response_params.replace(_do_authorize_code)
  34. end
  35. 612 response_params["state"] = param("state") if param_or_nil("state")
  36. 612 [response_params, response_mode]
  37. end
  38. 15 def _do_authorize_code
  39. 132 create_params = {
  40. 357 oauth_grants_type_column => "authorization_code",
  41. 93 oauth_grants_account_id_column => account_id
  42. }
  43. 492 { "code" => create_oauth_grant(create_params) }
  44. end
  45. 15 def authorize_response(params, mode)
  46. 360 redirect_url = URI.parse(redirect_uri)
  47. 360 case mode
  48. when "query"
  49. 765 params = params.map { |k, v| "#{CGI.escape(k)}=#{CGI.escape(v)}" }
  50. 330 params << redirect_url.query if redirect_url.query
  51. 330 redirect_url.query = params.join("&")
  52. 330 redirect(redirect_url.to_s)
  53. when "form_post"
  54. 36 scope.view layout: false, inline: <<-FORM
  55. <html>
  56. <head><title>Authorized</title></head>
  57. <body onload="javascript:document.forms[0].submit()">
  58. <form method="post" action="#{redirect_uri}">
  59. #{
  60. 6 params.map do |name, value|
  61. 30 "<input type=\"hidden\" name=\"#{scope.h(name)}\" value=\"#{scope.h(value)}\" />"
  62. end.join
  63. }
  64. 6 <input type="submit" class="btn btn-outline-primary" value="#{scope.h(oauth_authorize_post_button)}"/>
  65. </form>
  66. </body>
  67. </html>
  68. FORM
  69. end
  70. end
  71. 15 def _redirect_response_error(redirect_url, query_params)
  72. 390 response_mode = param_or_nil("response_mode") || oauth_response_mode
  73. 390 case response_mode
  74. when "form_post"
  75. 15 response["Content-Type"] = "text/html"
  76. 18 response.write <<-FORM
  77. <html>
  78. <head><title></title></head>
  79. <body onload="javascript:document.forms[0].submit()">
  80. <form method="post" action="#{redirect_uri}">
  81. #{
  82. 3 query_params.map do |name, value|
  83. 30 "<input type=\"hidden\" name=\"#{name}\" value=\"#{scope.h(value)}\" />"
  84. end.join
  85. }
  86. </form>
  87. </body>
  88. </html>
  89. FORM
  90. 15 request.halt
  91. else
  92. 375 super
  93. end
  94. end
  95. 15 def create_token(grant_type)
  96. 1032 return super unless supported_grant_type?(grant_type, "authorization_code")
  97. 196 grant_params = {
  98. 533 oauth_grants_code_column => param("code"),
  99. 141 oauth_grants_redirect_uri_column => param("redirect_uri"),
  100. 141 oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column]
  101. }
  102. 732 create_token_from_authorization_code(grant_params)
  103. end
  104. 15 def check_valid_response_type?
  105. 1215 response_type = param_or_nil("response_type")
  106. 1215 response_type == "code" || response_type == "none" || super
  107. end
  108. 15 def oauth_server_metadata_body(*)
  109. 240 super.tap do |data|
  110. 240 data[:authorization_endpoint] = authorize_url
  111. end
  112. end
  113. end
  114. end

lib/rodauth/features/oauth_authorize_base.rb

98.36% lines covered

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

lib/rodauth/features/oauth_base.rb

96.47% lines covered

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

lib/rodauth/features/oauth_client_credentials_grant.rb

100.0% lines covered

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

lib/rodauth/features/oauth_device_code_grant.rb

95.93% lines covered

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

lib/rodauth/features/oauth_dynamic_client_registration.rb

91.95% lines covered

149 relevant lines. 137 lines covered and 12 lines missed.
    
  1. # frozen_string_literal: true
  2. 15 require "rodauth/oauth"
  3. 15 module Rodauth
  4. 15 Feature.define(:oauth_dynamic_client_registration, :OauthDynamicClientRegistration) do
  5. 15 depends :oauth_base
  6. 15 before "register"
  7. 15 auth_value_method :oauth_client_registration_required_params, %w[redirect_uris client_name]
  8. 15 PROTECTED_APPLICATION_ATTRIBUTES = %w[account_id client_id].freeze
  9. # /register
  10. 15 auth_server_route(:register) do |r|
  11. 1185 before_register_route
  12. 1185 validate_client_registration_params
  13. 540 r.post do
  14. 540 response_params = transaction do
  15. 540 before_register
  16. 540 do_register
  17. end
  18. 540 response.status = 201
  19. 540 response["Content-Type"] = json_response_content_type
  20. 540 response["Cache-Control"] = "no-store"
  21. 540 response["Pragma"] = "no-cache"
  22. 540 response.write(_json_response_body(response_params))
  23. end
  24. end
  25. 15 def check_csrf?
  26. 1185 case request.path
  27. 234 when register_path
  28. 1185 false
  29. else
  30. super
  31. end
  32. end
  33. 15 private
  34. 15 def _before_register
  35. raise %{dynamic client registration requires authentication.
  36. Override ┬┤before_register` to perform it.
  37. example:
  38. before_register do
  39. account = _account_from_login(request.env["HTTP_X_USER_EMAIL"])
  40. authorization_required unless account
  41. @oauth_application_params[:account_id] = account[:id]
  42. end
  43. }
  44. end
  45. 15 def validate_client_registration_params
  46. 1185 oauth_client_registration_required_params.each do |required_param|
  47. 2310 unless request.params.key?(required_param)
  48. 60 register_throw_json_response_error("invalid_client_metadata", register_required_param_message(required_param))
  49. end
  50. end
  51. 1125 @oauth_application_params = request.params.each_with_object({}) do |(key, value), params|
  52. 13530 case key
  53. when "redirect_uris"
  54. 1125 if value.is_a?(Array)
  55. 1110 value = value.each do |uri|
  56. 2070 unless check_valid_no_fragment_uri?(uri)
  57. 30 register_throw_json_response_error("invalid_redirect_uri",
  58. 3 register_invalid_uri_message(uri))
  59. end
  60. 213 end.join(" ")
  61. else
  62. 15 register_throw_json_response_error("invalid_redirect_uri", register_invalid_uri_message(value))
  63. end
  64. 1080 key = oauth_applications_redirect_uri_column
  65. when "token_endpoint_auth_method"
  66. 585 unless oauth_token_endpoint_auth_methods_supported.include?(value)
  67. 15 register_throw_json_response_error("invalid_client_metadata", register_invalid_client_metadata_message(key, value))
  68. end
  69. # verify if in range
  70. 570 key = oauth_applications_token_endpoint_auth_method_column
  71. when "grant_types"
  72. 615 if value.is_a?(Array)
  73. 600 value = value.each do |grant_type|
  74. 1110 unless oauth_grant_types_supported.include?(grant_type)
  75. 30 register_throw_json_response_error("invalid_client_metadata", register_invalid_client_metadata_message(grant_type, value))
  76. end
  77. 111 end.join(" ")
  78. else
  79. 15 register_throw_json_response_error("invalid_client_metadata", register_invalid_client_metadata_message(key, value))
  80. end
  81. 570 key = oauth_applications_grant_types_column
  82. when "response_types"
  83. 615 if value.is_a?(Array)
  84. 600 grant_types = request.params["grant_types"] || oauth_grant_types_supported
  85. 600 value = value.each do |response_type|
  86. 615 unless oauth_response_types_supported.include?(response_type)
  87. 30 register_throw_json_response_error("invalid_client_metadata",
  88. 3 register_invalid_response_type_message(response_type))
  89. end
  90. 585 validate_client_registration_response_type(response_type, grant_types)
  91. 108 end.join(" ")
  92. else
  93. 15 register_throw_json_response_error("invalid_client_metadata", register_invalid_client_metadata_message(key, value))
  94. end
  95. 555 key = oauth_applications_response_types_column
  96. # verify if in range and match grant type
  97. when "client_uri", "logo_uri", "tos_uri", "policy_uri", "jwks_uri"
  98. 5040 register_throw_json_response_error("invalid_client_metadata", register_invalid_uri_message(value)) unless check_valid_uri?(value)
  99. 4965 case key
  100. when "client_uri"
  101. 1050 key = "homepage_url"
  102. when "jwks_uri"
  103. 945 if request.params.key?("jwks")
  104. 15 register_throw_json_response_error("invalid_client_metadata",
  105. register_invalid_jwks_param_message(key, "jwks"))
  106. end
  107. end
  108. 4950 key = __send__(:"oauth_applications_#{key}_column")
  109. when "jwks"
  110. register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(value)) unless value.is_a?(Hash)
  111. if request.params.key?("jwks_uri")
  112. register_throw_json_response_error("invalid_client_metadata",
  113. register_invalid_jwks_param_message(key, "jwks_uri"))
  114. end
  115. key = oauth_applications_jwks_column
  116. value = JSON.dump(value)
  117. when "scope"
  118. 1035 scopes = value.split(" ") - oauth_application_scopes
  119. 1035 register_throw_json_response_error("invalid_client_metadata", register_invalid_scopes_message(value)) unless scopes.empty?
  120. 1005 key = oauth_applications_scopes_column
  121. # verify if in range
  122. when "contacts"
  123. 1005 register_throw_json_response_error("invalid_client_metadata", register_invalid_contacts_message(value)) unless value.is_a?(Array)
  124. 990 value = value.join(" ")
  125. 990 key = oauth_applications_contacts_column
  126. when "client_name"
  127. 1065 key = oauth_applications_name_column
  128. else
  129. 2445 if respond_to?(:"oauth_applications_#{key}_column")
  130. 2430 if PROTECTED_APPLICATION_ATTRIBUTES.include?(key)
  131. 15 register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key))
  132. end
  133. 2415 property = :"oauth_applications_#{key}_column"
  134. 2415 key = __send__(property)
  135. 12 elsif !db[oauth_applications_table].columns.include?(key.to_sym)
  136. 15 register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key))
  137. end
  138. end
  139. 13200 params[key] = value
  140. end
  141. end
  142. 15 def validate_client_registration_response_type(response_type, grant_types)
  143. 555 case response_type
  144. when "code"
  145. 525 unless grant_types.include?("authorization_code")
  146. register_throw_json_response_error("invalid_client_metadata",
  147. register_invalid_response_type_for_grant_type_message(response_type,
  148. "authorization_code"))
  149. end
  150. when "token"
  151. 30 unless grant_types.include?("implicit")
  152. register_throw_json_response_error("invalid_client_metadata",
  153. register_invalid_response_type_for_grant_type_message(response_type, "implicit"))
  154. end
  155. when "none"
  156. if grant_types.include?("implicit") || grant_types.include?("authorization_code")
  157. register_throw_json_response_error("invalid_client_metadata", register_invalid_response_type_message(response_type))
  158. end
  159. end
  160. end
  161. 66 def do_register(return_params = request.params.dup)
  162. 540 applications_ds = db[oauth_applications_table]
  163. 540 application_columns = applications_ds.columns
  164. # set defaults
  165. 540 create_params = @oauth_application_params
  166. 540 create_params[oauth_applications_scopes_column] ||= return_params["scopes"] = oauth_application_scopes.join(" ")
  167. 540 if create_params[oauth_applications_grant_types_column] ||= begin
  168. 240 return_params["grant_types"] = %w[authorization_code] # rubocop:disable Lint/AssignmentInCondition
  169. 240 "authorization_code"
  170. end
  171. 540 create_params[oauth_applications_token_endpoint_auth_method_column] ||= begin
  172. 240 return_params["token_endpoint_auth_method"] = "client_secret_basic"
  173. 240 "client_secret_basic"
  174. end
  175. end
  176. 540 create_params[oauth_applications_response_types_column] ||= begin
  177. 240 return_params["response_types"] = %w[code]
  178. 240 "code"
  179. end
  180. 540 rescue_from_uniqueness_error do
  181. 540 client_id = oauth_unique_id_generator
  182. 540 create_params[oauth_applications_client_id_column] = client_id
  183. 540 return_params["client_id"] = client_id
  184. 540 return_params["client_id_issued_at"] = Time.now.utc.iso8601
  185. 540 if create_params.key?(oauth_applications_client_secret_column)
  186. 15 set_client_secret(create_params, create_params[oauth_applications_client_secret_column])
  187. 15 return_params.delete("client_secret")
  188. else
  189. 525 client_secret = oauth_unique_id_generator
  190. 525 set_client_secret(create_params, client_secret)
  191. 525 return_params["client_secret"] = client_secret
  192. 525 return_params["client_secret_expires_at"] = 0
  193. 9750 create_params.delete_if { |k, _| !application_columns.include?(k) }
  194. end
  195. 540 applications_ds.insert(create_params)
  196. end
  197. 540 return_params
  198. end
  199. 15 def register_throw_json_response_error(code, message)
  200. 645 throw_json_response_error(oauth_invalid_response_status, code, message)
  201. end
  202. 15 def register_required_param_message(key)
  203. 75 "The param '#{key}' is required by this server."
  204. end
  205. 15 def register_invalid_param_message(key)
  206. 45 "The param '#{key}' is not supported by this server."
  207. end
  208. 15 def register_invalid_client_metadata_message(key, value)
  209. 225 "The value '#{value}' is not supported by this server for param '#{key}'."
  210. end
  211. 15 def register_invalid_contacts_message(contacts)
  212. 15 "The contacts '#{contacts}' are not allowed by this server."
  213. end
  214. 15 def register_invalid_uri_message(uri)
  215. 195 "The '#{uri}' URL is not allowed by this server."
  216. end
  217. 15 def register_invalid_jwks_param_message(key1, key2)
  218. 15 "The param '#{key1}' cannot be accepted together with param '#{key2}'."
  219. end
  220. 15 def register_invalid_scopes_message(scopes)
  221. 30 "The given scopes (#{scopes}) are not allowed by this server."
  222. end
  223. 15 def register_oauth_invalid_grant_type_message(grant_type)
  224. "The grant type #{grant_type} is not allowed by this server."
  225. end
  226. 15 def register_invalid_response_type_message(response_type)
  227. 30 "The response type #{response_type} is not allowed by this server."
  228. end
  229. 15 def register_invalid_response_type_for_grant_type_message(response_type, grant_type)
  230. 15 "The grant type '#{grant_type}' must be registered for the response " \
  231. "type '#{response_type}' to be allowed."
  232. end
  233. 15 def oauth_server_metadata_body(*)
  234. 30 super.tap do |data|
  235. 30 data[:registration_endpoint] = register_url
  236. end
  237. end
  238. end
  239. end

lib/rodauth/features/oauth_grant_management.rb

100.0% lines covered

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

44 relevant lines. 44 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 15 require "rodauth/oauth"
  3. 15 module Rodauth
  4. 15 Feature.define(:oauth_implicit_grant, :OauthImplicitGrant) do
  5. 15 depends :oauth_authorize_base
  6. 15 def oauth_grant_types_supported
  7. 462 super | %w[implicit]
  8. end
  9. 15 def oauth_response_types_supported
  10. 402 super | %w[token]
  11. end
  12. 15 def oauth_response_modes_supported
  13. 327 super | %w[fragment]
  14. end
  15. 15 private
  16. 72 def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
  17. 297 response_type = param("response_type")
  18. 297 return super unless response_type == "token" && supported_response_type?(response_type)
  19. 45 response_mode ||= "fragment"
  20. 45 redirect_response_error("invalid_request") unless supported_response_mode?(response_mode)
  21. 30 oauth_grant = _do_authorize_token
  22. 30 response_params.replace(json_access_token_payload(oauth_grant))
  23. 30 response_params["state"] = param("state") if param_or_nil("state")
  24. 30 [response_params, response_mode]
  25. end
  26. 15 def _do_authorize_token(grant_params = {})
  27. 12 grant_params = {
  28. 30 oauth_grants_type_column => "implicit",
  29. 6 oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
  30. 6 oauth_grants_scopes_column => scopes,
  31. 6 oauth_grants_account_id_column => account_id
  32. 6 }.merge(grant_params)
  33. 45 generate_token(grant_params, false)
  34. end
  35. 15 def _redirect_response_error(redirect_url, query_params)
  36. 30 response_types = param("response_type").split(/ +/)
  37. 30 return super if response_types.empty? || response_types == %w[code]
  38. 75 query_params = query_params.map { |k, v| "#{k}=#{v}" }
  39. 30 redirect_url.fragment = query_params.join("&")
  40. 30 redirect(redirect_url.to_s)
  41. end
  42. 15 def authorize_response(params, mode)
  43. 282 return super unless mode == "fragment"
  44. 267 redirect_url = URI.parse(redirect_uri)
  45. 891 params = params.map { |k, v| "#{k}=#{v}" }
  46. 267 params << redirect_url.query if redirect_url.query
  47. 267 redirect_url.fragment = params.join("&")
  48. 267 redirect(redirect_url.to_s)
  49. end
  50. 15 def check_valid_response_type?
  51. 639 return true if param_or_nil("response_type") == "token"
  52. 534 super
  53. end
  54. end
  55. end

lib/rodauth/features/oauth_jwt.rb

100.0% lines covered

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

lib/rodauth/features/oauth_jwt_base.rb

93.78% lines covered

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

lib/rodauth/features/oauth_jwt_bearer_grant.rb

100.0% lines covered

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

lib/rodauth/features/oauth_jwt_jwks.rb

100.0% lines covered

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

lib/rodauth/features/oauth_jwt_secured_authorization_request.rb

89.47% lines covered

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

lib/rodauth/features/oauth_management_base.rb

100.0% lines covered

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

lib/rodauth/features/oauth_pkce.rb

100.0% lines covered

50 relevant lines. 50 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 15 require "rodauth/oauth"
  3. 15 module Rodauth
  4. 15 Feature.define(:oauth_pkce, :OauthPkce) do
  5. 15 depends :oauth_authorization_code_grant
  6. 15 auth_value_method :oauth_require_pkce, true
  7. 15 auth_value_method :oauth_pkce_challenge_method, "S256"
  8. 15 auth_value_method :oauth_grants_code_challenge_column, :code_challenge
  9. 15 auth_value_method :oauth_grants_code_challenge_method_column, :code_challenge_method
  10. 15 auth_value_method :oauth_code_challenge_required_error_code, "invalid_request"
  11. 15 translatable_method :oauth_code_challenge_required_message, "code challenge required"
  12. 15 auth_value_method :oauth_unsupported_transform_algorithm_error_code, "invalid_request"
  13. 15 translatable_method :oauth_unsupported_transform_algorithm_message, "transform algorithm not supported"
  14. 15 private
  15. 15 def supports_auth_method?(oauth_application, auth_method)
  16. 90 return super unless auth_method == "none"
  17. 60 request.params.key?("code_verifier") || super
  18. end
  19. 15 def validate_authorize_params
  20. 60 validate_pkce_challenge_params
  21. 45 super
  22. end
  23. 15 def create_oauth_grant(create_params = {})
  24. # PKCE flow
  25. 15 if (code_challenge = param_or_nil("code_challenge"))
  26. 15 code_challenge_method = param_or_nil("code_challenge_method") || oauth_pkce_challenge_method
  27. 15 create_params[oauth_grants_code_challenge_column] = code_challenge
  28. 15 create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
  29. end
  30. 15 super
  31. end
  32. 15 def create_token_from_authorization_code(grant_params, *args, oauth_grant: nil)
  33. 90 oauth_grant ||= valid_locked_oauth_grant(grant_params)
  34. 90 if oauth_grant[oauth_grants_code_challenge_column]
  35. 75 code_verifier = param_or_nil("code_verifier")
  36. 75 redirect_response_error("invalid_request") unless code_verifier && check_valid_grant_challenge?(oauth_grant, code_verifier)
  37. 12 elsif oauth_require_pkce
  38. 15 redirect_response_error("code_challenge_required")
  39. end
  40. 30 super({ oauth_grants_id_column => oauth_grant[oauth_grants_id_column] }, *args, oauth_grant: oauth_grant)
  41. end
  42. 15 def validate_pkce_challenge_params
  43. 60 if param_or_nil("code_challenge")
  44. 30 challenge_method = param_or_nil("code_challenge_method")
  45. 30 redirect_response_error("code_challenge_required") unless oauth_pkce_challenge_method == challenge_method
  46. else
  47. 30 return unless oauth_require_pkce
  48. 15 redirect_response_error("code_challenge_required")
  49. end
  50. end
  51. 15 def check_valid_grant_challenge?(grant, verifier)
  52. 60 challenge = grant[oauth_grants_code_challenge_column]
  53. 60 case grant[oauth_grants_code_challenge_method_column]
  54. when "plain"
  55. 15 challenge == verifier
  56. when "S256"
  57. 30 generated_challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(verifier))
  58. 30 generated_challenge.delete_suffix!("=") while generated_challenge.end_with?("=")
  59. 30 challenge == generated_challenge
  60. else
  61. 15 redirect_response_error("unsupported_transform_algorithm")
  62. end
  63. end
  64. 15 def oauth_server_metadata_body(*)
  65. 15 super.tap do |data|
  66. 15 data[:code_challenge_methods_supported] = oauth_pkce_challenge_method
  67. end
  68. end
  69. end
  70. 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. 15 require "rodauth/oauth"
  3. 15 module Rodauth
  4. 15 Feature.define(:oauth_resource_indicators, :OauthResourceIndicators) do
  5. 15 depends :oauth_authorize_base
  6. 15 auth_value_method :oauth_grants_resource_column, :resource
  7. 15 def resource_indicators
  8. 600 return @resource_indicators if defined?(@resource_indicators)
  9. 150 resources = param_or_nil("resource")
  10. 150 return unless resources
  11. 150 if json_request? || param_or_nil("request") # signed request
  12. 30 resources = Array(resources)
  13. else
  14. 120 query = if request.form_data?
  15. 75 request.body.rewind
  16. 75 request.body.read
  17. else
  18. 45 request.query_string
  19. end
  20. # resource query param does not conform to rack parsing rules
  21. 120 resources = URI.decode_www_form(query).each_with_object([]) do |(k, v), memo|
  22. 630 memo << v if k == "resource"
  23. end
  24. end
  25. 150 @resource_indicators = resources
  26. end
  27. 15 def require_oauth_authorization(*)
  28. 105 super
  29. # done so to support token-in-grant-db, jwt, and resource-server mode
  30. 90 token_indicators = authorization_token[oauth_grants_resource_column] || authorization_token["resource"]
  31. 90 return unless token_indicators
  32. 75 token_indicators = token_indicators.split(" ") if token_indicators.is_a?(String)
  33. 150 authorization_required unless token_indicators.any? { |resource| base_url.start_with?(resource) }
  34. end
  35. 15 private
  36. 15 def validate_token_params
  37. 60 super
  38. 60 return unless resource_indicators
  39. 60 resource_indicators.each do |resource|
  40. 60 redirect_response_error("invalid_target") unless check_valid_no_fragment_uri?(resource)
  41. end
  42. end
  43. 15 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. 15 module IndicatorAuthorizationCodeGrant
  51. 15 private
  52. 15 def validate_authorize_params
  53. 90 super
  54. 90 return unless resource_indicators
  55. 90 resource_indicators.each do |resource|
  56. 90 redirect_response_error("invalid_target") unless check_valid_no_fragment_uri?(resource)
  57. end
  58. end
  59. 15 def create_token_from_authorization_code(grant_params, *args, oauth_grant: nil)
  60. 60 return super unless resource_indicators
  61. 60 oauth_grant ||= valid_locked_oauth_grant(grant_params)
  62. 60 redirect_response_error("invalid_target") unless oauth_grant[oauth_grants_resource_column]
  63. 60 grant_indicators = oauth_grant[oauth_grants_resource_column]
  64. 60 grant_indicators = grant_indicators.split(" ") if grant_indicators.is_a?(String)
  65. 60 redirect_response_error("invalid_target") unless (grant_indicators - resource_indicators) != grant_indicators
  66. # update ownership
  67. 45 if grant_indicators != resource_indicators
  68. 15 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. 45 super({ oauth_grants_id_column => oauth_grant[oauth_grants_id_column] }, *args, oauth_grant: oauth_grant)
  74. end
  75. 15 def create_oauth_grant(create_params = {})
  76. 15 create_params[oauth_grants_resource_column] = resource_indicators.join(" ") if resource_indicators
  77. 15 super
  78. end
  79. end
  80. 15 module IndicatorIntrospection
  81. 15 def json_token_introspect_payload(grant)
  82. 15 return super unless grant[oauth_grants_id_column]
  83. 15 payload = super
  84. 15 token_indicators = grant[oauth_grants_resource_column]
  85. 15 token_indicators = token_indicators.split(" ") if token_indicators.is_a?(String)
  86. 15 payload[:aud] = token_indicators
  87. 15 payload
  88. end
  89. 15 def introspection_request(*)
  90. 45 payload = super
  91. 45 payload[oauth_grants_resource_column] = payload["aud"] if payload["aud"]
  92. 45 payload
  93. end
  94. end
  95. 15 module IndicatorJwt
  96. 15 def jwt_claims(*)
  97. 15 return super unless resource_indicators
  98. 15 super.merge(aud: resource_indicators)
  99. end
  100. 15 def jwt_decode(token, verify_aud: true, **args)
  101. 45 claims = super(token, verify_aud: false, **args)
  102. 45 return claims unless verify_aud
  103. 30 return unless claims["aud"] && claims["aud"].one? { |aud| request.url.starts_with?(aud) }
  104. 15 claims
  105. end
  106. end
  107. 15 def self.included(rodauth)
  108. 225 super
  109. 225 rodauth.send(:include, IndicatorAuthorizationCodeGrant) if rodauth.features.include?(:oauth_authorization_code_grant)
  110. 225 rodauth.send(:include, IndicatorIntrospection) if rodauth.features.include?(:oauth_token_introspection)
  111. 225 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. 15 require "rodauth/oauth"
  3. 15 module Rodauth
  4. 15 Feature.define(:oauth_resource_server, :OauthResourceServer) do
  5. 15 depends :oauth_token_introspection
  6. 15 auth_value_method :is_authorization_server?, false
  7. 15 auth_value_methods(
  8. :before_introspection_request
  9. )
  10. 15 def authorization_token
  11. 270 return @authorization_token if defined?(@authorization_token)
  12. # check if there is a token
  13. 135 access_token = fetch_access_token
  14. 135 return unless access_token
  15. # where in resource server, NOT the authorization server.
  16. 105 payload = introspection_request("access_token", access_token)
  17. 105 return unless payload["active"]
  18. 90 @authorization_token = payload
  19. end
  20. 15 def require_oauth_authorization(*scopes)
  21. 135 authorization_required unless authorization_token
  22. 90 aux_scopes = authorization_token["scope"]
  23. 90 token_scopes = if aux_scopes
  24. 90 aux_scopes.split(oauth_scope_separator)
  25. else
  26. []
  27. end
  28. 180 authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
  29. end
  30. 15 private
  31. 15 def introspection_request(token_type_hint, token)
  32. 105 introspect_url = URI("#{authorization_server_url}#{introspect_path}")
  33. 105 response = http_request(introspect_url, { "token_type_hint" => token_type_hint, "token" => token }) do |request|
  34. 105 before_introspection_request(request)
  35. end
  36. 105 JSON.parse(response.body)
  37. end
  38. 15 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. 15 require "onelogin/ruby-saml"
  3. 15 require "rodauth/oauth"
  4. 15 module Rodauth
  5. 15 Feature.define(:oauth_saml_bearer_grant, :OauthSamlBearerGrant) do
  6. 15 depends :oauth_assertion_base
  7. 15 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. 15 auth_value_method :oauth_saml_cert, nil
  9. 15 auth_value_method :oauth_saml_cert_fingerprint_algorithm, nil
  10. 15 auth_value_method :oauth_saml_name_identifier_format, "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
  11. 15 auth_value_method :oauth_saml_security_authn_requests_signed, true
  12. 15 auth_value_method :oauth_saml_security_metadata_signed, true
  13. 15 auth_value_method :oauth_saml_security_digest_method, XMLSecurity::Document::SHA1
  14. 15 auth_value_method :oauth_saml_security_signature_method, XMLSecurity::Document::RSA_SHA1
  15. 15 auth_value_method :max_param_bytesize, nil if Rodauth::VERSION >= "2.26.0"
  16. 15 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. 15 def oauth_grant_types_supported
  22. 30 super | %w[urn:ietf:params:oauth:grant-type:saml2-bearer]
  23. end
  24. 15 private
  25. 15 def require_oauth_application_from_saml2_bearer_assertion_issuer(assertion)
  26. 15 saml = saml_assertion(assertion)
  27. 15 return unless saml
  28. 14 db[oauth_applications_table].where(
  29. oauth_applications_homepage_url_column => saml.issuers
  30. 4 ).first
  31. end
  32. 15 def require_oauth_application_from_saml2_bearer_assertion_subject(assertion)
  33. 15 saml = saml_assertion(assertion)
  34. 15 return unless saml
  35. 14 db[oauth_applications_table].where(
  36. oauth_applications_client_id_column => saml.nameid
  37. 4 ).first
  38. end
  39. 15 def account_from_saml2_bearer_assertion(assertion)
  40. 15 saml = saml_assertion(assertion)
  41. 15 return unless saml
  42. 15 account_from_bearer_assertion_subject(saml.nameid)
  43. end
  44. 15 def saml_assertion(assertion)
  45. 45 settings = OneLogin::RubySaml::Settings.new
  46. 45 settings.idp_cert = oauth_saml_cert
  47. 45 settings.idp_cert_fingerprint = oauth_saml_cert_fingerprint
  48. 45 settings.idp_cert_fingerprint_algorithm = oauth_saml_cert_fingerprint_algorithm
  49. 45 settings.name_identifier_format = oauth_saml_name_identifier_format
  50. 45 settings.security[:authn_requests_signed] = oauth_saml_security_authn_requests_signed
  51. 45 settings.security[:metadata_signed] = oauth_saml_security_metadata_signed
  52. 45 settings.security[:digest_method] = oauth_saml_security_digest_method
  53. 45 settings.security[:signature_method] = oauth_saml_security_signature_method
  54. 45 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. 45 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. 45 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. 45 redirect_response_error("invalid_grant") if response.audiences && !response.audiences.include?(token_url)
  75. 45 response
  76. end
  77. 15 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_token_introspection.rb

98.68% lines covered

76 relevant lines. 75 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 15 require "rodauth/oauth"
  3. 15 require "rodauth/oauth/http_extensions"
  4. 15 module Rodauth
  5. 15 Feature.define(:oauth_token_introspection, :OauthTokenIntrospection) do
  6. 15 depends :oauth_base
  7. 15 before "introspect"
  8. 15 auth_value_methods(
  9. :resource_owner_identifier
  10. )
  11. # /introspect
  12. 15 auth_server_route(:introspect) do |r|
  13. 195 require_oauth_application_for_introspect
  14. 195 before_introspect_route
  15. 195 r.post do
  16. 195 catch_error do
  17. 195 validate_introspect_params
  18. 165 token_type_hint = param_or_nil("token_type_hint")
  19. 165 before_introspect
  20. 165 oauth_grant = case token_type_hint
  21. when "access_token", nil
  22. 150 if features.include?(:oauth_jwt) && oauth_jwt_access_tokens
  23. 45 jwt_decode(param("token"))
  24. else
  25. 105 oauth_grant_by_token(param("token"))
  26. end
  27. when "refresh_token"
  28. 15 oauth_grant_by_refresh_token(param("token"))
  29. end
  30. 165 oauth_grant ||= oauth_grant_by_refresh_token(param("token")) if token_type_hint.nil?
  31. 165 json_response_success(json_token_introspect_payload(oauth_grant))
  32. end
  33. throw_json_response_error(oauth_invalid_response_status, "invalid_request")
  34. end
  35. end
  36. # Token introspect
  37. 54 def validate_introspect_params(token_hint_types = %w[access_token refresh_token].freeze)
  38. # check if valid token hint type
  39. 195 if param_or_nil("token_type_hint") && !token_hint_types.include?(param("token_type_hint"))
  40. 15 redirect_response_error("unsupported_token_type")
  41. end
  42. 180 redirect_response_error("invalid_request") unless param_or_nil("token")
  43. end
  44. 15 def json_token_introspect_payload(grant_or_claims)
  45. 165 return { active: false } unless grant_or_claims
  46. 135 if grant_or_claims["sub"]
  47. # JWT
  48. 12 {
  49. 24 active: true,
  50. 6 scope: grant_or_claims["scope"],
  51. 6 client_id: grant_or_claims["client_id"],
  52. 6 username: resource_owner_identifier(grant_or_claims),
  53. token_type: "access_token",
  54. 6 exp: grant_or_claims["exp"],
  55. 6 iat: grant_or_claims["iat"],
  56. 6 nbf: grant_or_claims["nbf"],
  57. 6 sub: grant_or_claims["sub"],
  58. 6 aud: grant_or_claims["aud"],
  59. 6 iss: grant_or_claims["iss"],
  60. 6 jti: grant_or_claims["jti"]
  61. }
  62. else
  63. 24 {
  64. 48 active: true,
  65. 15 scope: grant_or_claims[oauth_grants_scopes_column],
  66. 15 client_id: oauth_application[oauth_applications_client_id_column],
  67. 15 username: resource_owner_identifier(grant_or_claims),
  68. 15 token_type: oauth_token_type,
  69. 15 exp: grant_or_claims[oauth_grants_expires_in_column].to_i
  70. }
  71. end
  72. end
  73. 15 def check_csrf?
  74. 255 case request.path
  75. 48 when introspect_path
  76. 195 false
  77. else
  78. 60 super
  79. end
  80. end
  81. 15 private
  82. 15 def require_oauth_application_for_introspect
  83. 195 (token = ((v = request.env["HTTP_AUTHORIZATION"]) && v[/\A *Bearer (.*)\Z/, 1]))
  84. 195 return require_oauth_application unless token
  85. 30 oauth_application = current_oauth_application
  86. 30 authorization_required unless oauth_application
  87. 30 @oauth_application = oauth_application
  88. end
  89. 15 def oauth_server_metadata_body(*)
  90. 30 super.tap do |data|
  91. 30 data[:introspection_endpoint] = introspect_url
  92. 30 data[:introspection_endpoint_auth_methods_supported] = %w[client_secret_basic]
  93. end
  94. end
  95. 15 def resource_owner_identifier(grant_or_claims)
  96. 135 if (account_id = grant_or_claims[oauth_grants_account_id_column])
  97. 75 account_ds(account_id).select(login_column).first[login_column]
  98. 60 elsif (app_id = grant_or_claims[oauth_grants_oauth_application_id_column])
  99. 11 db[oauth_applications_table].where(oauth_applications_id_column => app_id)
  100. .select(oauth_applications_name_column)
  101. 4 .first[oauth_applications_name_column]
  102. 45 elsif (subject = grant_or_claims["sub"])
  103. # JWT
  104. 45 if subject == grant_or_claims["client_id"]
  105. 11 db[oauth_applications_table].where(oauth_applications_client_id_column => subject)
  106. .select(oauth_applications_name_column)
  107. 4 .first[oauth_applications_name_column]
  108. else
  109. 30 account_ds(subject).select(login_column).first[login_column]
  110. end
  111. end
  112. end
  113. end
  114. end

lib/rodauth/features/oauth_token_revocation.rb

100.0% lines covered

63 relevant lines. 63 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 15 require "rodauth/oauth"
  3. 15 module Rodauth
  4. 15 Feature.define(:oauth_token_revocation, :OauthTokenRevocation) do
  5. 15 depends :oauth_base
  6. 15 before "revoke"
  7. 15 after "revoke"
  8. 15 notice_flash "The oauth grant has been revoked", "revoke_oauth_grant"
  9. # /revoke
  10. 15 auth_server_route(:revoke) do |r|
  11. 150 if logged_in?
  12. 15 require_account
  13. 15 require_oauth_application_from_account
  14. else
  15. 135 require_oauth_application
  16. end
  17. 150 before_revoke_route
  18. 150 r.post do
  19. 150 catch_error do
  20. 150 validate_revoke_params
  21. 105 oauth_grant = nil
  22. 105 transaction do
  23. 105 before_revoke
  24. 105 oauth_grant = revoke_oauth_grant
  25. 60 after_revoke
  26. end
  27. 60 if accepts_json?
  28. 12 json_payload = {
  29. 30 "revoked_at" => convert_timestamp(oauth_grant[oauth_grants_revoked_at_column])
  30. }
  31. 45 if param("token_type_hint") == "refresh_token"
  32. 30 json_payload["refresh_token"] = oauth_grant[oauth_grants_refresh_token_column]
  33. else
  34. 15 json_payload["token"] = oauth_grant[oauth_grants_token_column]
  35. end
  36. 45 json_response_success json_payload
  37. else
  38. 15 set_notice_flash revoke_oauth_grant_notice_flash
  39. 15 redirect request.referer || "/"
  40. end
  41. end
  42. 30 redirect_response_error("invalid_request", request.referer || "/")
  43. end
  44. end
  45. 45 def validate_revoke_params(token_hint_types = %w[access_token refresh_token].freeze)
  46. 150 token_hint = param_or_nil("token_type_hint")
  47. 150 if features.include?(:oauth_jwt) && oauth_jwt_access_tokens && (!token_hint || token_hint == "access_token")
  48. # JWT access tokens can't be revoked
  49. 30 throw(:rodauth_error)
  50. end
  51. # check if valid token hint type
  52. 120 redirect_response_error("unsupported_token_type") if token_hint && !token_hint_types.include?(token_hint)
  53. 105 redirect_response_error("invalid_request") unless param_or_nil("token")
  54. end
  55. 15 def check_csrf?
  56. 1179 case request.path
  57. 240 when revoke_path
  58. 150 !json_request?
  59. else
  60. 1029 super
  61. end
  62. end
  63. 15 private
  64. 15 def revoke_oauth_grant
  65. 105 token = param("token")
  66. 105 if param("token_type_hint") == "refresh_token"
  67. 30 oauth_grant = oauth_grant_by_refresh_token(token)
  68. 30 token_column = oauth_grants_refresh_token_column
  69. else
  70. 90 oauth_grant = oauth_grant_by_token_ds(token).where(
  71. 12 oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column]
  72. 12 ).first
  73. 75 token_column = oauth_grants_token_column
  74. end
  75. 105 redirect_response_error("invalid_request") unless oauth_grant
  76. 60 redirect_response_error("invalid_request") unless grant_from_application?(oauth_grant, oauth_application)
  77. 60 update_params = { oauth_grants_revoked_at_column => Sequel::CURRENT_TIMESTAMP }
  78. 60 ds = db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
  79. 60 oauth_grant = __update_and_return__(ds, update_params)
  80. 60 oauth_grant[token_column] = token
  81. 60 oauth_grant
  82. # If the particular
  83. # token is a refresh token and the authorization server supports the
  84. # revocation of access tokens, then the authorization server SHOULD
  85. # also invalidate all access tokens based on the same authorization
  86. # grant
  87. #
  88. # we don't need to do anything here, as we revalidate existing tokens
  89. end
  90. 15 def oauth_server_metadata_body(*)
  91. 30 super.tap do |data|
  92. 30 data[:revocation_endpoint] = revoke_url
  93. 30 data[:revocation_endpoint_auth_methods_supported] = nil # because it's client_secret_basic
  94. end
  95. end
  96. end
  97. end

lib/rodauth/features/oidc.rb

95.31% lines covered

405 relevant lines. 386 lines covered and 19 lines missed.
    
  1. # frozen_string_literal: true
  2. 15 require "rodauth/oauth"
  3. 15 module Rodauth
  4. 15 Feature.define(:oidc, :Oidc) do
  5. # https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
  6. 4 OIDC_SCOPES_MAP = {
  7. 8 "profile" => %i[name family_name given_name middle_name nickname preferred_username
  8. profile picture website gender birthdate zoneinfo locale updated_at].freeze,
  9. "email" => %i[email email_verified].freeze,
  10. "address" => %i[formatted street_address locality region postal_code country].freeze,
  11. "phone" => %i[phone_number phone_number_verified].freeze
  12. }.freeze
  13. 12 VALID_METADATA_KEYS = %i[
  14. issuer
  15. authorization_endpoint
  16. end_session_endpoint
  17. backchannel_logout_session_supported
  18. token_endpoint
  19. userinfo_endpoint
  20. jwks_uri
  21. registration_endpoint
  22. scopes_supported
  23. response_types_supported
  24. response_modes_supported
  25. grant_types_supported
  26. acr_values_supported
  27. subject_types_supported
  28. id_token_signing_alg_values_supported
  29. id_token_encryption_alg_values_supported
  30. id_token_encryption_enc_values_supported
  31. userinfo_signing_alg_values_supported
  32. userinfo_encryption_alg_values_supported
  33. userinfo_encryption_enc_values_supported
  34. request_object_signing_alg_values_supported
  35. request_object_encryption_alg_values_supported
  36. request_object_encryption_enc_values_supported
  37. token_endpoint_auth_methods_supported
  38. token_endpoint_auth_signing_alg_values_supported
  39. display_values_supported
  40. claim_types_supported
  41. claims_supported
  42. service_documentation
  43. claims_locales_supported
  44. ui_locales_supported
  45. claims_parameter_supported
  46. request_parameter_supported
  47. request_uri_parameter_supported
  48. require_request_uri_registration
  49. op_policy_uri
  50. op_tos_uri
  51. ].freeze
  52. 12 REQUIRED_METADATA_KEYS = %i[
  53. issuer
  54. authorization_endpoint
  55. token_endpoint
  56. jwks_uri
  57. response_types_supported
  58. subject_types_supported
  59. id_token_signing_alg_values_supported
  60. ].freeze
  61. 15 depends :account_expiration, :oauth_jwt, :oauth_jwt_jwks, :oauth_authorization_code_grant
  62. 15 auth_value_method :oauth_application_scopes, %w[openid]
  63. 11 %i[
  64. subject_type application_type sector_identifier_uri
  65. id_token_signed_response_alg id_token_encrypted_response_alg id_token_encrypted_response_enc
  66. userinfo_signed_response_alg userinfo_encrypted_response_alg userinfo_encrypted_response_enc
  67. 4 ].each do |column|
  68. 135 auth_value_method :"oauth_applications_#{column}_column", column
  69. end
  70. 15 %i[nonce acr claims_locales claims].each do |column|
  71. 60 auth_value_method :"oauth_grants_#{column}_column", column
  72. end
  73. 15 auth_value_method :oauth_jwt_subject_type, "public" # fallback subject type: public, pairwise
  74. 15 auth_value_method :oauth_jwt_subject_secret, nil # salt for pairwise generation
  75. 15 translatable_method :oauth_invalid_scope_message, "The Access Token expired"
  76. 15 auth_value_method :oauth_prompt_login_cookie_key, "_rodauth_oauth_prompt_login"
  77. 15 auth_value_method :oauth_prompt_login_cookie_options, {}.freeze
  78. 15 auth_value_method :oauth_prompt_login_interval, 5 * 60 * 60 # 5 minutes
  79. 15 auth_value_methods(
  80. :oauth_acr_values_supported,
  81. :get_oidc_account_last_login_at,
  82. :oidc_authorize_on_prompt_none?,
  83. :get_oidc_param,
  84. :get_additional_param,
  85. :require_acr_value_phr,
  86. :require_acr_value_phrh,
  87. :require_acr_value,
  88. :json_webfinger_payload
  89. )
  90. # /userinfo
  91. 15 auth_server_route(:userinfo) do |r|
  92. 135 r.on method: %i[get post] do
  93. 135 catch_error do
  94. 135 claims = authorization_token
  95. 135 throw_json_response_error(oauth_authorization_required_error_status, "invalid_token") unless claims
  96. 135 oauth_scopes = claims["scope"].split(" ")
  97. 135 throw_json_response_error(oauth_authorization_required_error_status, "invalid_token") unless oauth_scopes.include?("openid")
  98. 135 account = db[accounts_table].where(account_id_column => claims["sub"]).first
  99. 135 throw_json_response_error(oauth_authorization_required_error_status, "invalid_token") unless account
  100. 135 oauth_scopes.delete("openid")
  101. 135 oidc_claims = { "sub" => claims["sub"] }
  102. 135 @oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["client_id"]).first
  103. 135 throw_json_response_error(oauth_authorization_required_error_status, "invalid_token") unless @oauth_application
  104. 135 oauth_grant = valid_oauth_grant_ds(
  105. 24 oauth_grants_oauth_application_id_column => @oauth_application[oauth_applications_id_column],
  106. 24 oauth_grants_account_id_column => account[account_id_column]
  107. 24 ).first
  108. 135 claims_locales = oauth_grant[oauth_grants_claims_locales_column] if oauth_grant
  109. 135 if (claims = oauth_grant[oauth_grants_claims_column])
  110. 15 claims = JSON.parse(claims)
  111. 15 if (userinfo_essential_claims = claims["userinfo"])
  112. 15 oauth_scopes |= userinfo_essential_claims.to_a
  113. end
  114. end
  115. # 5.4 - The Claims requested by the profile, email, address, and phone scope values are returned from the UserInfo Endpoint
  116. 135 fill_with_account_claims(oidc_claims, account, oauth_scopes, claims_locales)
  117. 135 if (algo = @oauth_application[oauth_applications_userinfo_signed_response_alg_column])
  118. 8 params = {
  119. 19 jwks: oauth_application_jwks(@oauth_application),
  120. 3 encryption_algorithm: @oauth_application[oauth_applications_userinfo_encrypted_response_alg_column],
  121. 3 encryption_method: @oauth_application[oauth_applications_userinfo_encrypted_response_enc_column]
  122. 3 }.compact
  123. 30 jwt = jwt_encode(
  124. 3 oidc_claims.merge(
  125. # If signed, the UserInfo Response SHOULD contain the Claims iss (issuer) and aud (audience) as members. The iss value
  126. # SHOULD be the OP's Issuer Identifier URL. The aud value SHOULD be or include the RP's Client ID value.
  127. 3 iss: oauth_jwt_issuer,
  128. 3 aud: @oauth_application[oauth_applications_client_id_column]
  129. ),
  130. signing_algorithm: algo,
  131. **params
  132. )
  133. 30 jwt_response_success(jwt)
  134. else
  135. 105 json_response_success(oidc_claims)
  136. end
  137. end
  138. throw_json_response_error(oauth_authorization_required_error_status, "invalid_token")
  139. end
  140. end
  141. 15 def load_openid_configuration_route(alt_issuer = nil)
  142. 75 request.on(".well-known/openid-configuration") do
  143. 75 allow_cors(request)
  144. 60 request.is do
  145. 60 request.get do
  146. 60 json_response_success(openid_configuration_body(alt_issuer), cache: true)
  147. end
  148. end
  149. end
  150. end
  151. 15 def load_webfinger_route
  152. 30 request.on(".well-known/webfinger") do
  153. 30 request.get do
  154. 30 resource = param_or_nil("resource")
  155. 30 throw_json_response_error(400, "invalid_request") unless resource
  156. 15 response.status = 200
  157. 15 response["Content-Type"] ||= "application/jrd+json"
  158. 15 return_response(json_webfinger_payload)
  159. end
  160. end
  161. end
  162. 15 def check_csrf?
  163. 4059 case request.path
  164. 768 when userinfo_path
  165. 135 false
  166. else
  167. 3924 super
  168. end
  169. end
  170. 15 def oauth_response_types_supported
  171. 1032 grant_types = oauth_grant_types_supported
  172. 1032 oidc_response_types = %w[id_token none]
  173. 1032 oidc_response_types |= ["code id_token"] if grant_types.include?("authorization_code")
  174. 1032 oidc_response_types |= ["code token", "id_token token", "code id_token token"] if grant_types.include?("implicit")
  175. 1032 super | oidc_response_types
  176. end
  177. 15 def current_oauth_account
  178. 15 subject_type = current_oauth_application[oauth_applications_subject_type_column] || oauth_jwt_subject_type
  179. 15 return super unless subject_type == "pairwise"
  180. end
  181. 15 private
  182. 15 if defined?(::I18n)
  183. 15 def before_authorize_route
  184. 1089 if (ui_locales = param_or_nil("ui_locales"))
  185. 15 ui_locales = ui_locales.split(" ").map(&:to_sym)
  186. 15 ui_locales &= ::I18n.available_locales
  187. 15 ::I18n.locale = ui_locales.first unless ui_locales.empty?
  188. end
  189. 1089 super
  190. end
  191. end
  192. 15 def oauth_acr_values_supported
  193. 129 acr_values = []
  194. 129 acr_values << "phrh" if features.include?(:webauthn_login)
  195. 129 acr_values << "phr" if respond_to?(:require_two_factor_authenticated)
  196. 129 acr_values
  197. end
  198. 15 def oidc_authorize_on_prompt_none?(_account)
  199. 15 false
  200. end
  201. 15 def validate_authorize_params
  202. 1089 if (max_age = param_or_nil("max_age"))
  203. 30 max_age = Integer(max_age)
  204. 30 redirect_response_error("invalid_request") unless max_age.positive?
  205. 30 if Time.now - get_oidc_account_last_login_at(session_value) > max_age
  206. # force user to re-login
  207. 15 clear_session
  208. 15 set_session_value(login_redirect_session_key, request.fullpath)
  209. 15 redirect require_login_redirect
  210. end
  211. end
  212. 1074 if (claims = param_or_nil("claims"))
  213. # The value is a JSON object listing the requested Claims.
  214. 30 claims = JSON.parse(claims)
  215. 30 claims.each do |_, individual_claims|
  216. 60 redirect_response_error("invalid_request") unless individual_claims.is_a?(Hash)
  217. 60 individual_claims.each do |_, claim|
  218. 90 redirect_response_error("invalid_request") unless claim.nil? || individual_claims.is_a?(Hash)
  219. end
  220. end
  221. end
  222. 1074 sc = scopes
  223. 1074 if sc && sc.include?("offline_access")
  224. 45 sc.delete("offline_access")
  225. # MUST ensure that the prompt parameter contains consent
  226. # MUST ignore the offline_access request unless the Client
  227. # is using a response_type value that would result