rodauth-oauth 0.10.4 → 1.0.0.pre.beta2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/MIGRATION-GUIDE-v1.md +286 -0
  3. data/README.md +28 -35
  4. data/doc/release_notes/1_0_0_beta1.md +38 -0
  5. data/doc/release_notes/1_0_0_beta2.md +34 -0
  6. data/lib/generators/rodauth/oauth/install_generator.rb +0 -1
  7. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +21 -11
  8. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_search.html.erb +1 -1
  9. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_verification.html.erb +2 -2
  10. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb +1 -6
  11. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +0 -2
  12. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_grants.html.erb +41 -0
  13. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +2 -2
  14. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_grants.html.erb +37 -0
  15. data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +57 -57
  16. data/lib/rodauth/features/oauth_application_management.rb +61 -74
  17. data/lib/rodauth/features/oauth_assertion_base.rb +19 -23
  18. data/lib/rodauth/features/oauth_authorization_code_grant.rb +62 -90
  19. data/lib/rodauth/features/oauth_authorize_base.rb +115 -22
  20. data/lib/rodauth/features/oauth_base.rb +397 -315
  21. data/lib/rodauth/features/oauth_client_credentials_grant.rb +20 -18
  22. data/lib/rodauth/features/{oauth_device_grant.rb → oauth_device_code_grant.rb} +62 -73
  23. data/lib/rodauth/features/oauth_dynamic_client_registration.rb +52 -31
  24. data/lib/rodauth/features/oauth_grant_management.rb +70 -0
  25. data/lib/rodauth/features/oauth_implicit_grant.rb +29 -27
  26. data/lib/rodauth/features/oauth_jwt.rb +53 -689
  27. data/lib/rodauth/features/oauth_jwt_base.rb +458 -0
  28. data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +48 -17
  29. data/lib/rodauth/features/oauth_jwt_jwks.rb +47 -0
  30. data/lib/rodauth/features/oauth_jwt_secured_authorization_request.rb +116 -0
  31. data/lib/rodauth/features/oauth_management_base.rb +2 -0
  32. data/lib/rodauth/features/oauth_pkce.rb +22 -26
  33. data/lib/rodauth/features/oauth_resource_indicators.rb +33 -25
  34. data/lib/rodauth/features/oauth_resource_server.rb +59 -0
  35. data/lib/rodauth/features/oauth_saml_bearer_grant.rb +7 -1
  36. data/lib/rodauth/features/oauth_token_introspection.rb +76 -46
  37. data/lib/rodauth/features/oauth_token_revocation.rb +46 -33
  38. data/lib/rodauth/features/oidc.rb +382 -241
  39. data/lib/rodauth/features/oidc_dynamic_client_registration.rb +127 -51
  40. data/lib/rodauth/features/oidc_rp_initiated_logout.rb +115 -0
  41. data/lib/rodauth/oauth/database_extensions.rb +8 -6
  42. data/lib/rodauth/oauth/http_extensions.rb +74 -0
  43. data/lib/rodauth/oauth/railtie.rb +20 -0
  44. data/lib/rodauth/oauth/ttl_store.rb +2 -0
  45. data/lib/rodauth/oauth/version.rb +1 -1
  46. data/lib/rodauth/oauth.rb +29 -1
  47. data/locales/en.yml +34 -22
  48. data/locales/pt.yml +34 -22
  49. data/templates/authorize.str +19 -17
  50. data/templates/device_search.str +1 -1
  51. data/templates/device_verification.str +2 -2
  52. data/templates/jwks_field.str +1 -0
  53. data/templates/new_oauth_application.str +1 -2
  54. data/templates/oauth_application.str +2 -2
  55. data/templates/oauth_application_oauth_grants.str +54 -0
  56. data/templates/oauth_applications.str +2 -2
  57. data/templates/oauth_grants.str +52 -0
  58. metadata +23 -16
  59. data/lib/generators/rodauth/oauth/templates/app/models/oauth_token.rb +0 -4
  60. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +0 -39
  61. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +0 -35
  62. data/lib/rodauth/features/oauth.rb +0 -9
  63. data/lib/rodauth/features/oauth_http_mac.rb +0 -86
  64. data/lib/rodauth/features/oauth_token_management.rb +0 -81
  65. data/lib/rodauth/oauth/refinements.rb +0 -48
  66. data/templates/jwt_public_key_field.str +0 -4
  67. data/templates/oauth_application_oauth_tokens.str +0 -52
  68. data/templates/oauth_tokens.str +0 -50
@@ -1,67 +1,52 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rodauth/oauth"
4
+
3
5
  module Rodauth
4
6
  Feature.define(:oauth_authorization_code_grant, :OauthAuthorizationCodeGrant) do
5
7
  depends :oauth_authorize_base
6
8
 
7
- auth_value_method :use_oauth_access_type?, true
9
+ auth_value_method :oauth_response_mode, "form_post"
10
+
11
+ def oauth_grant_types_supported
12
+ super | %w[authorization_code]
13
+ end
14
+
15
+ def oauth_response_types_supported
16
+ super | %w[code]
17
+ end
18
+
19
+ def oauth_response_modes_supported
20
+ super | %w[query form_post]
21
+ end
8
22
 
9
23
  private
10
24
 
11
25
  def validate_authorize_params
12
26
  super
13
27
 
14
- redirect_response_error("invalid_request") unless check_valid_access_type? && check_valid_approval_prompt?
28
+ response_mode = param_or_nil("response_mode")
15
29
 
16
- redirect_response_error("invalid_request") if (response_mode = param_or_nil("response_mode")) && response_mode != "form_post"
17
-
18
- try_approval_prompt if use_oauth_access_type? && request.get?
30
+ redirect_response_error("invalid_request") if response_mode && !oauth_response_modes_supported.include?(response_mode)
19
31
  end
20
32
 
21
- def validate_oauth_token_params
33
+ def validate_token_params
22
34
  redirect_response_error("invalid_request") if param_or_nil("grant_type") == "authorization_code" && !param_or_nil("code")
23
35
  super
24
36
  end
25
37
 
26
- def try_approval_prompt
27
- approval_prompt = param_or_nil("approval_prompt")
28
-
29
- return unless approval_prompt && approval_prompt == "auto"
30
-
31
- return if db[oauth_grants_table].where(
32
- oauth_grants_account_id_column => account_id,
33
- oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
34
- oauth_grants_redirect_uri_column => redirect_uri,
35
- oauth_grants_scopes_column => scopes.join(oauth_scope_separator),
36
- oauth_grants_access_type_column => "online"
37
- ).count.zero?
38
-
39
- # if there's a previous oauth grant for the params combo, it means that this user has approved before.
40
- request.env["REQUEST_METHOD"] = "POST"
41
- end
38
+ def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
39
+ response_mode ||= oauth_response_mode
42
40
 
43
- def create_oauth_grant(create_params = {})
44
- # Access Type flow
45
- if use_oauth_access_type? && (access_type = param_or_nil("access_type"))
46
- create_params[oauth_grants_access_type_column] = access_type
47
- end
41
+ redirect_response_error("invalid_request") unless response_mode.nil? || supported_response_mode?(response_mode)
48
42
 
49
- super
50
- end
43
+ response_type = param_or_nil("response_type")
51
44
 
52
- def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
53
- case param("response_type")
45
+ redirect_response_error("invalid_request") unless response_type.nil? || supported_response_type?(response_type)
54
46
 
55
- when "code"
56
- response_mode ||= "query"
47
+ case response_type
48
+ when "code", nil
57
49
  response_params.replace(_do_authorize_code)
58
- when "none"
59
- response_mode ||= "none"
60
- when "", nil
61
- response_mode ||= oauth_response_mode
62
- response_params.replace(_do_authorize_code)
63
- else
64
- return super if response_params.empty?
65
50
  end
66
51
 
67
52
  response_params["state"] = param("state") if param_or_nil("state")
@@ -70,11 +55,11 @@ module Rodauth
70
55
  end
71
56
 
72
57
  def _do_authorize_code
73
- create_params = { oauth_grants_account_id_column => account_id }
74
- # Access Type flow
75
- if use_oauth_access_type? && (access_type = param_or_nil("access_type"))
76
- create_params[oauth_grants_access_type_column] = access_type
77
- end
58
+ create_params = {
59
+ oauth_grants_type_column => "authorization_code",
60
+ oauth_grants_account_id_column => account_id
61
+ }
62
+
78
63
  { "code" => create_oauth_grant(create_params) }
79
64
  end
80
65
 
@@ -82,7 +67,7 @@ module Rodauth
82
67
  redirect_url = URI.parse(redirect_uri)
83
68
  case mode
84
69
  when "query"
85
- params = params.map { |k, v| "#{k}=#{v}" }
70
+ params = params.map { |k, v| "#{CGI.escape(k)}=#{CGI.escape(v)}" }
86
71
  params << redirect_url.query if redirect_url.query
87
72
  redirect_url.query = params.join("&")
88
73
  redirect(redirect_url.to_s)
@@ -94,7 +79,7 @@ module Rodauth
94
79
  <form method="post" action="#{redirect_uri}">
95
80
  #{
96
81
  params.map do |name, value|
97
- "<input type=\"hidden\" name=\"#{name}\" value=\"#{scope.h(value)}\" />"
82
+ "<input type=\"hidden\" name=\"#{scope.h(name)}\" value=\"#{scope.h(value)}\" />"
98
83
  end.join
99
84
  }
100
85
  <input type="submit" class="btn btn-outline-primary" value="#{scope.h(oauth_authorize_post_button)}"/>
@@ -102,70 +87,57 @@ module Rodauth
102
87
  </body>
103
88
  </html>
104
89
  FORM
105
- when "none"
106
- redirect(redirect_url.to_s)
90
+ end
91
+ end
92
+
93
+ def _redirect_response_error(redirect_url, query_params)
94
+ response_mode = param_or_nil("response_mode") || oauth_response_mode
95
+
96
+ case response_mode
97
+ when "form_post"
98
+ response["Content-Type"] = "text/html"
99
+ response.write <<-FORM
100
+ <html>
101
+ <head><title></title></head>
102
+ <body onload="javascript:document.forms[0].submit()">
103
+ <form method="post" action="#{redirect_uri}">
104
+ #{
105
+ query_params.map do |name, value|
106
+ "<input type=\"hidden\" name=\"#{name}\" value=\"#{scope.h(value)}\" />"
107
+ end.join
108
+ }
109
+ </form>
110
+ </body>
111
+ </html>
112
+ FORM
113
+ request.halt
107
114
  else
108
115
  super
109
116
  end
110
117
  end
111
118
 
112
- def create_oauth_token(grant_type)
119
+ def create_token(grant_type)
113
120
  return super unless supported_grant_type?(grant_type, "authorization_code")
114
121
 
115
- # fetch oauth grant
116
- oauth_grant = db[oauth_grants_table].where(
122
+ grant_params = {
123
+ oauth_grants_type_column => grant_type,
117
124
  oauth_grants_code_column => param("code"),
118
125
  oauth_grants_redirect_uri_column => param("redirect_uri"),
119
- oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
120
- oauth_grants_revoked_at_column => nil
121
- ).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
122
- .for_update
123
- .first
124
-
125
- redirect_response_error("invalid_grant") unless oauth_grant
126
-
127
- create_params = {
128
- oauth_tokens_account_id_column => oauth_grant[oauth_grants_account_id_column],
129
- oauth_tokens_oauth_application_id_column => oauth_grant[oauth_grants_oauth_application_id_column],
130
- oauth_tokens_oauth_grant_id_column => oauth_grant[oauth_grants_id_column],
131
- oauth_tokens_scopes_column => oauth_grant[oauth_grants_scopes_column]
126
+ oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column]
132
127
  }
133
- create_oauth_token_from_authorization_code(oauth_grant, create_params, !use_oauth_access_type?)
134
- end
135
-
136
- ACCESS_TYPES = %w[offline online].freeze
137
-
138
- def check_valid_access_type?
139
- return true unless use_oauth_access_type?
140
128
 
141
- access_type = param_or_nil("access_type")
142
- !access_type || ACCESS_TYPES.include?(access_type)
143
- end
144
-
145
- APPROVAL_PROMPTS = %w[force auto].freeze
146
-
147
- def check_valid_approval_prompt?
148
- return true unless use_oauth_access_type?
149
-
150
- approval_prompt = param_or_nil("approval_prompt")
151
- !approval_prompt || APPROVAL_PROMPTS.include?(approval_prompt)
129
+ create_token_from_authorization_code(grant_params)
152
130
  end
153
131
 
154
132
  def check_valid_response_type?
155
133
  response_type = param_or_nil("response_type")
156
134
 
157
- response_type.nil? || response_type == "code" || response_type == "none" || super
135
+ response_type == "code" || response_type == "none" || super
158
136
  end
159
137
 
160
138
  def oauth_server_metadata_body(*)
161
139
  super.tap do |data|
162
140
  data[:authorization_endpoint] = authorize_url
163
- data[:response_types_supported] << "code"
164
-
165
- data[:response_modes_supported] << "query"
166
- data[:response_modes_supported] << "form_post"
167
-
168
- data[:grant_types_supported] << "authorization_code"
169
141
  end
170
142
  end
171
143
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rodauth/oauth"
4
+
3
5
  module Rodauth
4
6
  Feature.define(:oauth_authorize_base, :OauthAuthorizeBase) do
5
7
  depends :oauth_base
@@ -12,17 +14,22 @@ module Rodauth
12
14
  button "Authorize", "oauth_authorize"
13
15
  button "Back to Client Application", "oauth_authorize_post"
14
16
 
15
- translatable_method :oauth_tokens_scopes_label, "Scopes"
17
+ auth_value_method :use_oauth_access_type?, false
18
+
19
+ auth_value_method :oauth_grants_access_type_column, :access_type
20
+
21
+ translatable_method :authorize_page_lead, "The application %<name>s would like to access your data"
22
+ translatable_method :oauth_grants_scopes_label, "Scopes"
16
23
  translatable_method :oauth_applications_contacts_label, "Contacts"
17
24
  translatable_method :oauth_applications_tos_uri_label, "Terms of service URL"
18
25
  translatable_method :oauth_applications_policy_uri_label, "Policy URL"
26
+ translatable_method :oauth_unsupported_response_type_message, "Unsupported response type"
27
+ translatable_method :oauth_authorize_parameter_required, "'%<parameter>s' is a required parameter"
19
28
 
20
29
  # /authorize
21
- route(:authorize) do |r|
22
- next unless is_authorization_server?
23
-
24
- before_authorize_route
30
+ auth_server_route(:authorize) do |r|
25
31
  require_authorizable_account
32
+ before_authorize_route
26
33
 
27
34
  validate_authorize_params
28
35
 
@@ -49,14 +56,24 @@ module Rodauth
49
56
  end
50
57
  end
51
58
 
59
+ def authorize_scopes
60
+ scopes || begin
61
+ oauth_application[oauth_applications_scopes_column].split(oauth_scope_separator)
62
+ end
63
+ end
64
+
52
65
  private
53
66
 
54
67
  def validate_authorize_params
55
68
  redirect_response_error("invalid_request", request.referer || default_redirect) unless oauth_application && check_valid_redirect_uri?
56
69
 
57
- redirect_response_error("invalid_request") unless check_valid_response_type?
70
+ redirect_response_error("unsupported_response_type") unless check_valid_response_type?
71
+
72
+ redirect_response_error("invalid_request") unless check_valid_access_type? && check_valid_approval_prompt?
58
73
 
59
- redirect_response_error("invalid_scope") unless check_valid_scopes?
74
+ try_approval_prompt if use_oauth_access_type? && request.get?
75
+
76
+ redirect_response_error("invalid_scope") if (request.post? || param_or_nil("scope")) && !check_valid_scopes?
60
77
  end
61
78
 
62
79
  def check_valid_response_type?
@@ -64,12 +81,55 @@ module Rodauth
64
81
  end
65
82
 
66
83
  def check_valid_redirect_uri?
67
- oauth_application[oauth_applications_redirect_uri_column].split(" ").include?(redirect_uri)
84
+ application_redirect_uris = oauth_application[oauth_applications_redirect_uri_column].split(" ")
85
+
86
+ if (redirect_uri = param_or_nil("redirect_uri"))
87
+ application_redirect_uris.include?(redirect_uri)
88
+ else
89
+ set_error_flash(oauth_authorize_parameter_required(parameter: "redirect_uri")) if application_redirect_uris.size > 1
90
+
91
+ true
92
+ end
93
+ end
94
+
95
+ ACCESS_TYPES = %w[offline online].freeze
96
+
97
+ def check_valid_access_type?
98
+ return true unless use_oauth_access_type?
99
+
100
+ access_type = param_or_nil("access_type")
101
+ !access_type || ACCESS_TYPES.include?(access_type)
102
+ end
103
+
104
+ APPROVAL_PROMPTS = %w[force auto].freeze
105
+
106
+ def check_valid_approval_prompt?
107
+ return true unless use_oauth_access_type?
108
+
109
+ approval_prompt = param_or_nil("approval_prompt")
110
+ !approval_prompt || APPROVAL_PROMPTS.include?(approval_prompt)
111
+ end
112
+
113
+ def try_approval_prompt
114
+ approval_prompt = param_or_nil("approval_prompt")
115
+
116
+ return unless approval_prompt && approval_prompt == "auto"
117
+
118
+ return if db[oauth_grants_table].where(
119
+ oauth_grants_account_id_column => account_id,
120
+ oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
121
+ oauth_grants_redirect_uri_column => redirect_uri,
122
+ oauth_grants_scopes_column => scopes.join(oauth_scope_separator),
123
+ oauth_grants_access_type_column => "online"
124
+ ).count.zero?
125
+
126
+ # if there's a previous oauth grant for the params combo, it means that this user has approved before.
127
+ request.env["REQUEST_METHOD"] = "POST"
68
128
  end
69
129
 
70
130
  def authorization_required
71
131
  if accepts_json?
72
- throw_json_response_error(authorization_required_error_status, "invalid_client")
132
+ throw_json_response_error(oauth_authorization_required_error_status, "invalid_client")
73
133
  else
74
134
  set_redirect_error_flash(require_authorization_error_flash)
75
135
  redirect(authorize_path)
@@ -80,29 +140,62 @@ module Rodauth
80
140
 
81
141
  def authorize_response(params, mode); end
82
142
 
83
- def create_oauth_token_from_authorization_code(oauth_grant, create_params, should_generate_refresh_token = false)
84
- # revoke oauth grant
85
- db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
86
- .update(oauth_grants_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
143
+ def create_token_from_authorization_code(grant_params, should_generate_refresh_token = !use_oauth_access_type?, oauth_grant: nil)
144
+ # fetch oauth grant
145
+ oauth_grant ||= valid_locked_oauth_grant(grant_params)
87
146
 
88
147
  should_generate_refresh_token ||= oauth_grant[oauth_grants_access_type_column] == "offline"
89
148
 
90
- generate_oauth_token(create_params, should_generate_refresh_token)
149
+ generate_token(oauth_grant, should_generate_refresh_token)
91
150
  end
92
151
 
93
152
  def create_oauth_grant(create_params = {})
94
- create_params.merge!(
95
- oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
96
- oauth_grants_redirect_uri_column => redirect_uri,
97
- oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_grant_expires_in),
98
- oauth_grants_scopes_column => scopes.join(oauth_scope_separator)
99
- )
153
+ create_params[oauth_grants_oauth_application_id_column] ||= oauth_application[oauth_applications_id_column]
154
+ create_params[oauth_grants_redirect_uri_column] ||= redirect_uri
155
+ create_params[oauth_grants_expires_in_column] ||= Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_grant_expires_in)
156
+ create_params[oauth_grants_scopes_column] ||= scopes.join(oauth_scope_separator)
157
+
158
+ if use_oauth_access_type? && (access_type = param_or_nil("access_type"))
159
+ create_params[oauth_grants_access_type_column] = access_type
160
+ end
100
161
 
101
162
  ds = db[oauth_grants_table]
102
163
 
164
+ create_params[oauth_grants_code_column] = oauth_unique_id_generator
165
+
166
+ if oauth_reuse_access_token
167
+ unique_conds = Hash[oauth_grants_unique_columns.map { |column| [column, create_params[column]] }]
168
+ valid_grant = valid_oauth_grant_ds(unique_conds).select(oauth_grants_id_column).first
169
+ if valid_grant
170
+ create_params[oauth_grants_id_column] = valid_grant[oauth_grants_id_column]
171
+ rescue_from_uniqueness_error do
172
+ __insert_or_update_and_return__(
173
+ ds,
174
+ oauth_grants_id_column,
175
+ [oauth_grants_id_column],
176
+ create_params
177
+ )
178
+ end
179
+ return create_params[oauth_grants_code_column]
180
+ end
181
+ end
182
+
103
183
  rescue_from_uniqueness_error do
104
- create_params[oauth_grants_code_column] = oauth_unique_id_generator
105
- __insert_and_return__(ds, oauth_grants_id_column, create_params)
184
+ if __one_oauth_token_per_account
185
+ __insert_or_update_and_return__(
186
+ ds,
187
+ oauth_grants_id_column,
188
+ oauth_grants_unique_columns,
189
+ create_params,
190
+ nil,
191
+ {
192
+ oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_grant_expires_in),
193
+ oauth_grants_revoked_at_column => nil
194
+ }
195
+ )
196
+ else
197
+ __insert_and_return__(ds, oauth_grants_id_column, create_params)
198
+ end
106
199
  end
107
200
  create_params[oauth_grants_code_column]
108
201
  end