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
@@ -3,18 +3,20 @@
3
3
  require "time"
4
4
  require "base64"
5
5
  require "securerandom"
6
- require "net/http"
6
+ require "cgi"
7
7
  require "rodauth/version"
8
- require "rodauth/oauth/version"
9
- require "rodauth/oauth/ttl_store"
8
+ require "rodauth/oauth"
10
9
  require "rodauth/oauth/database_extensions"
11
- require "rodauth/oauth/refinements"
10
+ require "rodauth/oauth/http_extensions"
12
11
 
13
12
  module Rodauth
14
13
  Feature.define(:oauth_base, :OauthBase) do
15
- using RegexpExtensions
14
+ include OAuth::HTTPExtensions
16
15
 
17
- SCOPES = %w[profile.read].freeze
16
+ EMPTY_HASH = {}.freeze
17
+
18
+ auth_value_methods(:http_request)
19
+ auth_value_methods(:http_request_cache)
18
20
 
19
21
  before "token"
20
22
 
@@ -26,41 +28,33 @@ module Rodauth
26
28
  auth_value_method :json_response_content_type, "application/json"
27
29
 
28
30
  auth_value_method :oauth_grant_expires_in, 60 * 5 # 5 minutes
29
- auth_value_method :oauth_token_expires_in, 60 * 60 # 60 minutes
31
+ auth_value_method :oauth_access_token_expires_in, 60 * 60 # 60 minutes
30
32
  auth_value_method :oauth_refresh_token_expires_in, 60 * 60 * 24 * 360 # 1 year
31
33
  auth_value_method :oauth_unique_id_generation_retries, 3
32
34
 
33
- auth_value_method :oauth_response_mode, "query"
34
- auth_value_method :oauth_auth_methods_supported, %w[client_secret_basic client_secret_post]
35
+ auth_value_method :oauth_token_endpoint_auth_methods_supported, %w[client_secret_basic client_secret_post]
36
+ auth_value_method :oauth_grant_types_supported, %w[refresh_token]
37
+ auth_value_method :oauth_response_types_supported, []
38
+ auth_value_method :oauth_response_modes_supported, []
35
39
 
36
40
  auth_value_method :oauth_valid_uri_schemes, %w[https]
37
41
  auth_value_method :oauth_scope_separator, " "
38
42
 
39
- auth_value_method :oauth_tokens_table, :oauth_tokens
40
- auth_value_method :oauth_tokens_id_column, :id
41
-
42
- %i[
43
- oauth_application_id oauth_token_id oauth_grant_id account_id
44
- token refresh_token scopes
45
- expires_in revoked_at
46
- ].each do |column|
47
- auth_value_method :"oauth_tokens_#{column}_column", column
48
- end
49
-
50
43
  # OAuth Grants
51
44
  auth_value_method :oauth_grants_table, :oauth_grants
52
45
  auth_value_method :oauth_grants_id_column, :id
53
46
  %i[
54
- account_id oauth_application_id
55
- redirect_uri code scopes access_type
47
+ account_id oauth_application_id type
48
+ redirect_uri code scopes
56
49
  expires_in revoked_at
50
+ token refresh_token
57
51
  ].each do |column|
58
52
  auth_value_method :"oauth_grants_#{column}_column", column
59
53
  end
60
54
 
61
- # Oauth Token Hash
62
- auth_value_method :oauth_tokens_token_hash_column, nil
63
- auth_value_method :oauth_tokens_refresh_token_hash_column, nil
55
+ # Enables Token Hash
56
+ auth_value_method :oauth_grants_token_hash_column, :token
57
+ auth_value_method :oauth_grants_refresh_token_hash_column, :refresh_token
64
58
 
65
59
  # Access Token reuse
66
60
  auth_value_method :oauth_reuse_access_token, false
@@ -73,36 +67,34 @@ module Rodauth
73
67
  name description scopes
74
68
  client_id client_secret
75
69
  homepage_url redirect_uri
76
- token_endpoint_auth_method grant_types response_types
70
+ token_endpoint_auth_method grant_types response_types response_modes
77
71
  logo_uri tos_uri policy_uri jwks jwks_uri
78
72
  contacts software_id software_version
79
73
  ].each do |column|
80
74
  auth_value_method :"oauth_applications_#{column}_column", column
81
75
  end
76
+ # Enables client secret Hash
77
+ auth_value_method :oauth_applications_client_secret_hash_column, :client_secret
82
78
 
83
- auth_value_method :authorization_required_error_status, 401
84
- auth_value_method :invalid_oauth_response_status, 400
85
- auth_value_method :already_in_use_response_status, 409
79
+ auth_value_method :oauth_authorization_required_error_status, 401
80
+ auth_value_method :oauth_invalid_response_status, 400
81
+ auth_value_method :oauth_already_in_use_response_status, 409
86
82
 
87
83
  # Feature options
88
- auth_value_method :oauth_application_default_scope, SCOPES.first
89
- auth_value_method :oauth_application_scopes, SCOPES
84
+ auth_value_method :oauth_application_scopes, []
90
85
  auth_value_method :oauth_token_type, "bearer"
91
- auth_value_method :oauth_refresh_token_protection_policy, "none" # can be: none, sender_constrained, rotation
86
+ auth_value_method :oauth_refresh_token_protection_policy, "rotation" # can be: none, sender_constrained, rotation
92
87
 
93
- translatable_method :invalid_client_message, "Invalid client"
94
- translatable_method :invalid_grant_type_message, "Invalid grant type"
95
- translatable_method :invalid_grant_message, "Invalid grant"
96
- translatable_method :invalid_scope_message, "Invalid scope"
97
- translatable_method :unsupported_token_type_message, "Invalid token type hint"
88
+ translatable_method :oauth_invalid_client_message, "Invalid client"
89
+ translatable_method :oauth_invalid_grant_type_message, "Invalid grant type"
90
+ translatable_method :oauth_invalid_grant_message, "Invalid grant"
91
+ translatable_method :oauth_invalid_scope_message, "Invalid scope"
92
+ translatable_method :oauth_unsupported_token_type_message, "Invalid token type hint"
98
93
 
99
- translatable_method :unique_error_message, "is already in use"
100
- translatable_method :already_in_use_message, "error generating unique token"
101
- auth_value_method :already_in_use_error_code, "invalid_request"
102
- auth_value_method :invalid_grant_type_error_code, "unsupported_grant_type"
94
+ translatable_method :oauth_already_in_use_message, "error generating unique token"
95
+ auth_value_method :oauth_already_in_use_error_code, "invalid_request"
96
+ auth_value_method :oauth_invalid_grant_type_error_code, "unsupported_grant_type"
103
97
 
104
- # Resource Server params
105
- # Only required to use if the plugin is to be used in a resource server
106
98
  auth_value_method :is_authorization_server?, true
107
99
 
108
100
  auth_value_methods(:only_json?)
@@ -122,41 +114,39 @@ module Rodauth
122
114
  :secret_matches?,
123
115
  :authorization_server_url,
124
116
  :oauth_unique_id_generator,
125
- :oauth_tokens_unique_columns,
126
- :require_authorizable_account
117
+ :oauth_grants_unique_columns,
118
+ :require_authorizable_account,
119
+ :oauth_account_ds,
120
+ :oauth_application_ds
127
121
  )
128
122
 
129
123
  # /token
130
- route(:token) do |r|
131
- next unless is_authorization_server?
132
-
133
- before_token_route
124
+ auth_server_route(:token) do |r|
134
125
  require_oauth_application
126
+ before_token_route
135
127
 
136
128
  r.post do
137
129
  catch_error do
138
- validate_oauth_token_params
130
+ validate_token_params
139
131
 
140
- oauth_token = nil
132
+ oauth_grant = nil
141
133
 
142
134
  transaction do
143
135
  before_token
144
- oauth_token = create_oauth_token(param("grant_type"))
136
+ oauth_grant = create_token(param("grant_type"))
145
137
  end
146
138
 
147
- json_response_success(json_access_token_payload(oauth_token))
139
+ json_response_success(json_access_token_payload(oauth_grant))
148
140
  end
149
141
 
150
- throw_json_response_error(invalid_oauth_response_status, "invalid_request")
142
+ throw_json_response_error(oauth_invalid_response_status, "invalid_request")
151
143
  end
152
144
  end
153
145
 
154
- def oauth_server_metadata(issuer = nil)
146
+ def load_oauth_server_metadata_route(issuer = nil)
155
147
  request.on(".well-known") do
156
- request.on("oauth-authorization-server") do
157
- request.get do
158
- json_response_success(oauth_server_metadata_body(issuer), true)
159
- end
148
+ request.get("oauth-authorization-server") do
149
+ json_response_success(oauth_server_metadata_body(issuer), true)
160
150
  end
161
151
  end
162
152
  end
@@ -170,18 +160,25 @@ module Rodauth
170
160
  end
171
161
  end
172
162
 
173
- # Overrides session_value, so that a valid authorization token also authenticates a request
174
- # TODO: deprecate
175
- def session_value
176
- super || oauth_token_subject
177
- end
178
-
179
163
  def oauth_token_subject
180
164
  return unless authorization_token
181
165
 
182
- # TODO: fix this once tokens know which type they were generated with
183
- authorization_token[oauth_tokens_account_id_column] ||
184
- authorization_token[oauth_tokens_oauth_application_id_column]
166
+ authorization_token[oauth_grants_account_id_column] ||
167
+ db[oauth_applications_table].where(
168
+ oauth_applications_id_column => authorization_token[oauth_grants_oauth_application_id_column]
169
+ ).select_map(oauth_applications_client_id_column).first
170
+ end
171
+
172
+ def current_oauth_account
173
+ account_id = authorization_token[oauth_grants_account_id_column]
174
+
175
+ return unless account_id
176
+
177
+ oauth_account_ds(account_id).first
178
+ end
179
+
180
+ def current_oauth_application
181
+ oauth_application_ds(authorization_token[oauth_grants_oauth_application_id_column]).first
185
182
  end
186
183
 
187
184
  def accepts_json?
@@ -190,13 +187,12 @@ module Rodauth
190
187
  (accept = request.env["HTTP_ACCEPT"]) && accept =~ json_request_regexp
191
188
  end
192
189
 
193
- unless method_defined?(:json_request?)
194
- # copied from the jwt feature
195
- def json_request?
196
- return @json_request if defined?(@json_request)
190
+ # copied from the jwt feature
191
+ def json_request?
192
+ return super if features.include?(:jsonn)
193
+ return @json_request if defined?(@json_request)
197
194
 
198
- @json_request = request.content_type =~ json_request_regexp
199
- end
195
+ @json_request = request.content_type =~ json_request_regexp
200
196
  end
201
197
 
202
198
  def scopes
@@ -206,8 +202,6 @@ module Rodauth
206
202
  scope
207
203
  when String
208
204
  scope.split(" ")
209
- when nil
210
- Array(oauth_application_default_scope)
211
205
  end
212
206
  end
213
207
 
@@ -233,13 +227,20 @@ module Rodauth
233
227
  end
234
228
 
235
229
  def fetch_access_token
236
- value = request.env["HTTP_AUTHORIZATION"]
230
+ if (token = request.params["access_token"])
231
+ if request.post? && !(request.content_type.start_with?("application/x-www-form-urlencoded") &&
232
+ request.params.size == 1)
233
+ return
234
+ end
235
+ else
236
+ value = request.env["HTTP_AUTHORIZATION"]
237
237
 
238
- return unless value && !value.empty?
238
+ return unless value && !value.empty?
239
239
 
240
- scheme, token = value.split(" ", 2)
240
+ scheme, token = value.split(" ", 2)
241
241
 
242
- return unless scheme.downcase == oauth_token_type
242
+ return unless scheme.downcase == oauth_token_type
243
+ end
243
244
 
244
245
  return if token.nil? || token.empty?
245
246
 
@@ -250,39 +251,17 @@ module Rodauth
250
251
  return @authorization_token if defined?(@authorization_token)
251
252
 
252
253
  # check if there is a token
253
- bearer_token = fetch_access_token
254
-
255
- return unless bearer_token
256
-
257
- @authorization_token = if is_authorization_server?
258
- # check if token has not expired
259
- # check if token has been revoked
260
- oauth_token_by_token(bearer_token)
261
- else
262
- # where in resource server, NOT the authorization server.
263
- payload = introspection_request("access_token", bearer_token)
254
+ access_token = fetch_access_token
264
255
 
265
- return unless payload["active"]
256
+ return unless access_token
266
257
 
267
- payload
268
- end
258
+ @authorization_token = oauth_grant_by_token(access_token)
269
259
  end
270
260
 
271
261
  def require_oauth_authorization(*scopes)
272
262
  authorization_required unless authorization_token
273
263
 
274
- scopes << oauth_application_default_scope if scopes.empty?
275
-
276
- token_scopes = if is_authorization_server?
277
- authorization_token[oauth_tokens_scopes_column].split(oauth_scope_separator)
278
- else
279
- aux_scopes = authorization_token["scope"]
280
- if aux_scopes
281
- aux_scopes.split(oauth_scope_separator)
282
- else
283
- []
284
- end
285
- end
264
+ token_scopes = authorization_token[oauth_grants_scopes_column].split(oauth_scope_separator)
286
265
 
287
266
  authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
288
267
  end
@@ -291,9 +270,20 @@ module Rodauth
291
270
  true
292
271
  end
293
272
 
273
+ # override
274
+ def translate(key, default, args = EMPTY_HASH)
275
+ return i18n_translate(key, default, **args) if features.include?(:i18n)
276
+ # do not attempt to translate by default
277
+ return default if args.nil?
278
+
279
+ default % args
280
+ end
281
+
294
282
  def post_configure
295
283
  super
296
284
 
285
+ i18n_register(File.expand_path(File.join(__dir__, "..", "..", "..", "locales"))) if features.include?(:i18n)
286
+
297
287
  # all of the extensions below involve DB changes. Resource server mode doesn't use
298
288
  # database functions for OAuth though.
299
289
  return unless is_authorization_server?
@@ -301,18 +291,24 @@ module Rodauth
301
291
  self.class.__send__(:include, Rodauth::OAuth::ExtendDatabase(db))
302
292
 
303
293
  # Check whether we can reutilize db entries for the same account / application pair
304
- one_oauth_token_per_account = db.indexes(oauth_tokens_table).values.any? do |definition|
294
+ one_oauth_token_per_account = db.indexes(oauth_grants_table).values.any? do |definition|
305
295
  definition[:unique] &&
306
- definition[:columns] == oauth_tokens_unique_columns
296
+ definition[:columns] == oauth_grants_unique_columns
307
297
  end
308
298
 
309
299
  self.class.send(:define_method, :__one_oauth_token_per_account) { one_oauth_token_per_account }
310
-
311
- i18n_register(File.expand_path(File.join(__dir__, "..", "..", "..", "locales"))) if features.include?(:i18n)
312
300
  end
313
301
 
314
302
  private
315
303
 
304
+ def oauth_account_ds(account_id)
305
+ account_ds(account_id)
306
+ end
307
+
308
+ def oauth_application_ds(oauth_application_id)
309
+ db[oauth_applications_table].where(oauth_applications_id_column => oauth_application_id)
310
+ end
311
+
316
312
  def require_authorizable_account
317
313
  require_account
318
314
  end
@@ -329,11 +325,11 @@ module Rodauth
329
325
  end
330
326
 
331
327
  # OAuth Token Unique/Reuse
332
- def oauth_tokens_unique_columns
328
+ def oauth_grants_unique_columns
333
329
  [
334
- oauth_tokens_oauth_application_id_column,
335
- oauth_tokens_account_id_column,
336
- oauth_tokens_scopes_column
330
+ oauth_grants_oauth_application_id_column,
331
+ oauth_grants_account_id_column,
332
+ oauth_grants_scopes_column
337
333
  ]
338
334
  end
339
335
 
@@ -353,53 +349,59 @@ module Rodauth
353
349
  # parse client id and secret
354
350
  #
355
351
  def require_oauth_application
356
- # get client credentials
357
- auth_method = nil
358
- client_id = client_secret = nil
359
-
360
- if (token = ((v = request.env["HTTP_AUTHORIZATION"]) && v[/\A *Basic (.*)\Z/, 1]))
361
- # client_secret_basic
362
- client_id, client_secret = Base64.decode64(token).split(/:/, 2)
363
- auth_method = "client_secret_basic"
364
- else
365
- # client_secret_post
366
- client_id = param_or_nil("client_id")
367
- client_secret = param_or_nil("client_secret")
368
- auth_method = "client_secret_post" if client_secret
369
- end
352
+ @oauth_application = if (token = ((v = request.env["HTTP_AUTHORIZATION"]) && v[/\A *Basic (.*)\Z/, 1]))
353
+ # client_secret_basic
354
+ require_oauth_application_from_client_secret_basic(token)
355
+ elsif (client_id = param_or_nil("client_id"))
356
+ if (client_secret = param_or_nil("client_secret"))
357
+ # client_secret_post
358
+ require_oauth_application_from_client_secret_post(client_id, client_secret)
359
+ else
360
+ # none
361
+ require_oauth_application_from_none(client_id)
362
+ end
363
+ else
364
+ authorization_required
365
+ end
366
+ end
370
367
 
368
+ def require_oauth_application_from_client_secret_basic(token)
369
+ client_id, client_secret = Base64.decode64(token).split(/:/, 2)
371
370
  authorization_required unless client_id
371
+ oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
372
+ authorization_required unless supports_auth_method?(oauth_application,
373
+ "client_secret_basic") && secret_matches?(oauth_application, client_secret)
374
+ oauth_application
375
+ end
372
376
 
373
- @oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
374
-
375
- authorization_required unless @oauth_application
377
+ def require_oauth_application_from_client_secret_post(client_id, client_secret)
378
+ oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
379
+ authorization_required unless supports_auth_method?(oauth_application,
380
+ "client_secret_post") && secret_matches?(oauth_application, client_secret)
381
+ oauth_application
382
+ end
376
383
 
377
- authorization_required unless authorized_oauth_application?(@oauth_application, client_secret, auth_method)
384
+ def require_oauth_application_from_none(client_id)
385
+ oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
386
+ authorization_required unless supports_auth_method?(oauth_application, "none")
387
+ oauth_application
378
388
  end
379
389
 
380
- def authorized_oauth_application?(oauth_application, client_secret, auth_method)
390
+ def supports_auth_method?(oauth_application, auth_method)
381
391
  supported_auth_methods = if oauth_application[oauth_applications_token_endpoint_auth_method_column]
382
392
  oauth_application[oauth_applications_token_endpoint_auth_method_column].split(/ +/)
383
393
  else
384
- oauth_auth_methods_supported
394
+ oauth_token_endpoint_auth_methods_supported
385
395
  end
386
396
 
387
- if auth_method
388
- supported_auth_methods.include?(auth_method) && secret_matches?(oauth_application, client_secret)
389
- else
390
- supported_auth_methods.include?("none")
391
- end
392
- end
393
-
394
- def no_auth_oauth_application?(_oauth_application)
395
- supported_auth_methods.include?("none")
397
+ supported_auth_methods.include?(auth_method)
396
398
  end
397
399
 
398
400
  def require_oauth_application_from_account
399
401
  ds = db[oauth_applications_table]
400
- .join(oauth_tokens_table, Sequel[oauth_tokens_table][oauth_tokens_oauth_application_id_column] =>
402
+ .join(oauth_grants_table, Sequel[oauth_grants_table][oauth_grants_oauth_application_id_column] =>
401
403
  Sequel[oauth_applications_table][oauth_applications_id_column])
402
- .where(oauth_token_by_token_ds(param("token")).opts.fetch(:where, true))
404
+ .where(oauth_grant_by_token_ds(param("token")).opts.fetch(:where, true))
403
405
  .where(Sequel[oauth_applications_table][oauth_applications_account_id_column] => account_id)
404
406
 
405
407
  @oauth_application = ds.qualify.first
@@ -410,7 +412,19 @@ module Rodauth
410
412
  end
411
413
 
412
414
  def secret_matches?(oauth_application, secret)
413
- BCrypt::Password.new(oauth_application[oauth_applications_client_secret_column]) == secret
415
+ if oauth_applications_client_secret_hash_column
416
+ BCrypt::Password.new(oauth_application[oauth_applications_client_secret_hash_column]) == secret
417
+ else
418
+ oauth_application[oauth_applications_client_secret_column] == secret
419
+ end
420
+ end
421
+
422
+ def set_client_secret(params, secret)
423
+ if oauth_applications_client_secret_hash_column
424
+ params[oauth_applications_client_secret_hash_column] = secret_hash(secret)
425
+ else
426
+ params[oauth_applications_client_secret_column] = secret
427
+ end
414
428
  end
415
429
 
416
430
  def secret_hash(secret)
@@ -425,45 +439,53 @@ module Rodauth
425
439
  Base64.urlsafe_encode64(Digest::SHA256.digest(token))
426
440
  end
427
441
 
428
- def token_from_application?(oauth_token, oauth_application)
429
- oauth_token[oauth_tokens_oauth_application_id_column] == oauth_application[oauth_applications_id_column]
442
+ def grant_from_application?(oauth_grant, oauth_application)
443
+ oauth_grant[oauth_grants_oauth_application_id_column] == oauth_application[oauth_applications_id_column]
430
444
  end
431
445
 
432
- unless method_defined?(:password_hash)
433
- # From login_requirements_base feature
446
+ def password_hash(password)
447
+ return super if features.include?(:login_password_requirements_base)
434
448
 
435
- def password_hash(password)
436
- BCrypt::Password.create(password, cost: BCrypt::Engine::DEFAULT_COST)
437
- end
449
+ BCrypt::Password.create(password, cost: BCrypt::Engine::DEFAULT_COST)
438
450
  end
439
451
 
440
- def generate_oauth_token(params = {}, should_generate_refresh_token = true)
441
- create_params = {
442
- oauth_tokens_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
443
- }.merge(params)
444
-
445
- if create_params[oauth_tokens_scopes_column].is_a?(Array)
446
- create_params[oauth_tokens_scopes_column] =
447
- create_params[oauth_tokens_scopes_column].join(" ")
452
+ def generate_token(grant_params = {}, should_generate_refresh_token = true)
453
+ if grant_params[oauth_grants_id_column] && (oauth_reuse_access_token &&
454
+ (
455
+ if oauth_grants_token_hash_column
456
+ grant_params[oauth_grants_token_hash_column]
457
+ else
458
+ grant_params[oauth_grants_token_column]
459
+ end
460
+ ))
461
+ return grant_params
448
462
  end
449
463
 
464
+ update_params = {
465
+ oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_access_token_expires_in),
466
+ oauth_grants_code_column => nil
467
+ }
468
+
450
469
  rescue_from_uniqueness_error do
451
- access_token = _generate_access_token(create_params)
452
- refresh_token = _generate_refresh_token(create_params) if should_generate_refresh_token
453
- oauth_token = _store_oauth_token(create_params)
454
- oauth_token[oauth_tokens_token_column] = access_token
455
- oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
456
- oauth_token
470
+ access_token = _generate_access_token(update_params)
471
+ refresh_token = _generate_refresh_token(update_params) if should_generate_refresh_token
472
+ oauth_grant = store_token(grant_params, update_params)
473
+
474
+ return unless oauth_grant
475
+
476
+ oauth_grant[oauth_grants_token_column] = access_token
477
+ oauth_grant[oauth_grants_refresh_token_column] = refresh_token if refresh_token
478
+ oauth_grant
457
479
  end
458
480
  end
459
481
 
460
482
  def _generate_access_token(params = {})
461
483
  token = oauth_unique_id_generator
462
484
 
463
- if oauth_tokens_token_hash_column
464
- params[oauth_tokens_token_hash_column] = generate_token_hash(token)
485
+ if oauth_grants_token_hash_column
486
+ params[oauth_grants_token_hash_column] = generate_token_hash(token)
465
487
  else
466
- params[oauth_tokens_token_column] = token
488
+ params[oauth_grants_token_column] = token
467
489
  end
468
490
 
469
491
  token
@@ -472,96 +494,154 @@ module Rodauth
472
494
  def _generate_refresh_token(params)
473
495
  token = oauth_unique_id_generator
474
496
 
475
- if oauth_tokens_refresh_token_hash_column
476
- params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(token)
497
+ if oauth_grants_refresh_token_hash_column
498
+ params[oauth_grants_refresh_token_hash_column] = generate_token_hash(token)
477
499
  else
478
- params[oauth_tokens_refresh_token_column] = token
500
+ params[oauth_grants_refresh_token_column] = token
479
501
  end
480
502
 
481
503
  token
482
504
  end
483
505
 
484
- def _store_oauth_token(params = {})
485
- ds = db[oauth_tokens_table]
506
+ def _grant_with_access_token?(oauth_grant)
507
+ if oauth_grants_token_hash_column
508
+ oauth_grant[oauth_grants_token_hash_column]
509
+ else
510
+ oauth_grant[oauth_grants_token_column]
511
+ end
512
+ end
513
+
514
+ def store_token(grant_params, update_params = {})
515
+ ds = db[oauth_grants_table]
486
516
 
487
517
  if __one_oauth_token_per_account
488
518
 
519
+ to_update_if_null = [
520
+ oauth_grants_token_column,
521
+ oauth_grants_token_hash_column,
522
+ oauth_grants_refresh_token_column,
523
+ oauth_grants_refresh_token_hash_column
524
+ ].compact.map do |attribute|
525
+ [
526
+ attribute,
527
+ (
528
+ if ds.respond_to?(:supports_insert_conflict?) && ds.supports_insert_conflict?
529
+ Sequel.function(:coalesce, Sequel[oauth_grants_table][attribute], Sequel[:excluded][attribute])
530
+ else
531
+ Sequel.function(:coalesce, Sequel[oauth_grants_table][attribute], update_params[attribute])
532
+ end
533
+ )
534
+ ]
535
+ end
536
+
489
537
  token = __insert_or_update_and_return__(
490
538
  ds,
491
- oauth_tokens_id_column,
492
- oauth_tokens_unique_columns,
493
- params,
494
- Sequel.expr(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column]) > Sequel::CURRENT_TIMESTAMP,
495
- ([oauth_tokens_token_column, oauth_tokens_refresh_token_column] if oauth_reuse_access_token)
539
+ oauth_grants_id_column,
540
+ oauth_grants_unique_columns,
541
+ grant_params.merge(update_params),
542
+ Sequel.expr(Sequel[oauth_grants_table][oauth_grants_expires_in_column]) > Sequel::CURRENT_TIMESTAMP,
543
+ Hash[to_update_if_null]
496
544
  )
497
545
 
498
546
  # if the previous operation didn't return a row, it means that the conditions
499
547
  # invalidated the update, and the existing token is still valid.
500
548
  token || ds.where(
501
- oauth_tokens_account_id_column => params[oauth_tokens_account_id_column],
502
- oauth_tokens_oauth_application_id_column => params[oauth_tokens_oauth_application_id_column]
549
+ oauth_grants_account_id_column => update_params[oauth_grants_account_id_column],
550
+ oauth_grants_oauth_application_id_column => update_params[oauth_grants_oauth_application_id_column]
503
551
  ).first
504
552
  else
553
+
505
554
  if oauth_reuse_access_token
506
- unique_conds = Hash[oauth_tokens_unique_columns.map { |column| [column, params[column]] }]
507
- valid_token = ds.where(Sequel.expr(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column]) > Sequel::CURRENT_TIMESTAMP)
508
- .where(unique_conds).first
555
+ unique_conds = Hash[oauth_grants_unique_columns.map { |column| [column, update_params[column]] }]
556
+ valid_token_ds = valid_oauth_grant_ds(unique_conds)
557
+ if oauth_grants_token_hash_column
558
+ valid_token_ds.exclude(oauth_grants_token_hash_column => nil)
559
+ else
560
+ valid_token_ds.exclude(oauth_grants_token_column => nil)
561
+ end
562
+
563
+ valid_token = valid_token_ds.first
564
+
509
565
  return valid_token if valid_token
510
566
  end
511
- __insert_and_return__(ds, oauth_tokens_id_column, params)
567
+
568
+ if grant_params[oauth_grants_id_column]
569
+ __update_and_return__(ds.where(oauth_grants_id_column => grant_params[oauth_grants_id_column]), update_params)
570
+ else
571
+ __insert_and_return__(ds, oauth_grants_id_column, grant_params.merge(update_params))
572
+ end
512
573
  end
513
574
  end
514
575
 
515
- def oauth_token_by_token_ds(token)
516
- ds = db[oauth_tokens_table]
576
+ def valid_locked_oauth_grant(grant_params = nil)
577
+ oauth_grant = valid_oauth_grant_ds(grant_params).for_update.first
517
578
 
518
- ds = if oauth_tokens_token_hash_column
519
- ds.where(Sequel[oauth_tokens_table][oauth_tokens_token_hash_column] => generate_token_hash(token))
520
- else
521
- ds.where(Sequel[oauth_tokens_table][oauth_tokens_token_column] => token)
522
- end
579
+ redirect_response_error("invalid_grant") unless oauth_grant
580
+
581
+ oauth_grant
582
+ end
583
+
584
+ def valid_oauth_grant_ds(grant_params = nil)
585
+ ds = db[oauth_grants_table]
586
+ .where(Sequel[oauth_grants_table][oauth_grants_revoked_at_column] => nil)
587
+ .where(Sequel.expr(Sequel[oauth_grants_table][oauth_grants_expires_in_column]) >= Sequel::CURRENT_TIMESTAMP)
588
+ ds = ds.where(grant_params) if grant_params
523
589
 
524
- ds.where(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
525
- .where(Sequel[oauth_tokens_table][oauth_tokens_revoked_at_column] => nil)
590
+ ds
526
591
  end
527
592
 
528
- def oauth_token_by_token(token)
529
- oauth_token_by_token_ds(token).first
593
+ def oauth_grant_by_token_ds(token)
594
+ ds = valid_oauth_grant_ds
595
+
596
+ if oauth_grants_token_hash_column
597
+ ds.where(Sequel[oauth_grants_table][oauth_grants_token_hash_column] => generate_token_hash(token))
598
+ else
599
+ ds.where(Sequel[oauth_grants_table][oauth_grants_token_column] => token)
600
+ end
530
601
  end
531
602
 
532
- def oauth_token_by_refresh_token(token, revoked: false)
533
- ds = db[oauth_tokens_table].where(oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column])
603
+ def oauth_grant_by_token(token)
604
+ oauth_grant_by_token_ds(token).first
605
+ end
606
+
607
+ def oauth_grant_by_refresh_token_ds(token, revoked: false)
608
+ ds = db[oauth_grants_table].where(oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column])
534
609
  #
535
610
  # filter expired refresh tokens out.
536
611
  # an expired refresh token is a token whose access token expired for a period longer than the
537
612
  # refresh token expiration period.
538
613
  #
539
- ds = ds.where(Sequel.date_add(oauth_tokens_expires_in_column, seconds: oauth_refresh_token_expires_in) >= Sequel::CURRENT_TIMESTAMP)
614
+ ds = ds.where(Sequel.date_add(oauth_grants_expires_in_column,
615
+ seconds: (oauth_refresh_token_expires_in - oauth_access_token_expires_in)) >= Sequel::CURRENT_TIMESTAMP)
540
616
 
541
- ds = if oauth_tokens_refresh_token_hash_column
542
- ds.where(oauth_tokens_refresh_token_hash_column => generate_token_hash(token))
617
+ ds = if oauth_grants_refresh_token_hash_column
618
+ ds.where(oauth_grants_refresh_token_hash_column => generate_token_hash(token))
543
619
  else
544
- ds.where(oauth_tokens_refresh_token_column => token)
620
+ ds.where(oauth_grants_refresh_token_column => token)
545
621
  end
546
622
 
547
- ds = ds.where(oauth_tokens_revoked_at_column => nil) unless revoked
623
+ ds = ds.where(oauth_grants_revoked_at_column => nil) unless revoked
624
+
625
+ ds
626
+ end
548
627
 
549
- ds.first
628
+ def oauth_grant_by_refresh_token(token, **kwargs)
629
+ oauth_grant_by_refresh_token_ds(token, **kwargs).first
550
630
  end
551
631
 
552
- def json_access_token_payload(oauth_token)
632
+ def json_access_token_payload(oauth_grant)
553
633
  payload = {
554
- "access_token" => oauth_token[oauth_tokens_token_column],
634
+ "access_token" => oauth_grant[oauth_grants_token_column],
555
635
  "token_type" => oauth_token_type,
556
- "expires_in" => oauth_token_expires_in
636
+ "expires_in" => oauth_access_token_expires_in
557
637
  }
558
- payload["refresh_token"] = oauth_token[oauth_tokens_refresh_token_column] if oauth_token[oauth_tokens_refresh_token_column]
638
+ payload["refresh_token"] = oauth_grant[oauth_grants_refresh_token_column] if oauth_grant[oauth_grants_refresh_token_column]
559
639
  payload
560
640
  end
561
641
 
562
642
  # Access Tokens
563
643
 
564
- def validate_oauth_token_params
644
+ def validate_token_params
565
645
  unless (grant_type = param_or_nil("grant_type"))
566
646
  redirect_response_error("invalid_request")
567
647
  end
@@ -569,76 +649,88 @@ module Rodauth
569
649
  redirect_response_error("invalid_request") if grant_type == "refresh_token" && !param_or_nil("refresh_token")
570
650
  end
571
651
 
572
- def create_oauth_token(grant_type)
573
- if supported_grant_type?(grant_type, "refresh_token")
574
- # fetch potentially revoked oauth token
575
- oauth_token = oauth_token_by_refresh_token(param("refresh_token"), revoked: true)
576
-
577
- if !oauth_token
578
- redirect_response_error("invalid_grant")
579
- elsif oauth_token[oauth_tokens_revoked_at_column]
580
- if oauth_refresh_token_protection_policy == "rotation"
581
- # https://tools.ietf.org/html/draft-ietf-oauth-v2-1-00#section-6.1
582
- #
583
- # If a refresh token is compromised and subsequently used by both the attacker and the legitimate
584
- # client, one of them will present an invalidated refresh token, which will inform the authorization
585
- # server of the breach. The authorization server cannot determine which party submitted the invalid
586
- # refresh token, but it will revoke the active refresh token. This stops the attack at the cost of
587
- # forcing the legitimate client to obtain a fresh authorization grant.
588
-
589
- db[oauth_tokens_table].where(oauth_tokens_oauth_token_id_column => oauth_token[oauth_tokens_id_column])
590
- .update(oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
591
- end
592
- redirect_response_error("invalid_grant")
593
- end
652
+ def create_token(grant_type)
653
+ redirect_response_error("invalid_request") unless supported_grant_type?(grant_type, "refresh_token")
594
654
 
595
- update_params = {
596
- oauth_tokens_oauth_application_id_column => oauth_token[oauth_tokens_oauth_application_id_column],
597
- oauth_tokens_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
598
- }
599
- create_oauth_token_from_token(oauth_token, update_params)
600
- else
601
- redirect_response_error("invalid_request")
655
+ refresh_token = param("refresh_token")
656
+ # fetch potentially revoked oauth token
657
+ oauth_grant = oauth_grant_by_refresh_token_ds(refresh_token, revoked: true).for_update.first
658
+
659
+ update_params = { oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP,
660
+ seconds: oauth_access_token_expires_in) }
661
+
662
+ if !oauth_grant || oauth_grant[oauth_grants_revoked_at_column]
663
+ redirect_response_error("invalid_grant")
664
+ elsif oauth_refresh_token_protection_policy == "rotation"
665
+ # https://tools.ietf.org/html/draft-ietf-oauth-v2-1-00#section-6.1
666
+ #
667
+ # If a refresh token is compromised and subsequently used by both the attacker and the legitimate
668
+ # client, one of them will present an invalidated refresh token, which will inform the authorization
669
+ # server of the breach. The authorization server cannot determine which party submitted the invalid
670
+ # refresh token, but it will revoke the active refresh token. This stops the attack at the cost of
671
+ # forcing the legitimate client to obtain a fresh authorization grant.
672
+
673
+ refresh_token = _generate_refresh_token(update_params)
602
674
  end
675
+
676
+ update_params[oauth_grants_oauth_application_id_column] = oauth_grant[oauth_grants_oauth_application_id_column]
677
+
678
+ oauth_grant = create_token_from_token(oauth_grant, update_params)
679
+ oauth_grant[oauth_grants_refresh_token_column] = refresh_token
680
+ oauth_grant
603
681
  end
604
682
 
605
- def create_oauth_token_from_token(oauth_token, update_params)
606
- redirect_response_error("invalid_grant") unless token_from_application?(oauth_token, oauth_application)
683
+ def create_token_from_token(oauth_grant, update_params)
684
+ redirect_response_error("invalid_grant") unless grant_from_application?(oauth_grant, oauth_application)
607
685
 
608
686
  rescue_from_uniqueness_error do
609
- oauth_tokens_ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
687
+ oauth_grants_ds = db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
610
688
  access_token = _generate_access_token(update_params)
689
+ oauth_grant = __update_and_return__(oauth_grants_ds, update_params)
611
690
 
612
- if oauth_refresh_token_protection_policy == "rotation"
613
- update_params = {
614
- **update_params,
615
- oauth_tokens_oauth_token_id_column => oauth_token[oauth_tokens_id_column],
616
- oauth_tokens_account_id_column => oauth_token[oauth_tokens_account_id_column],
617
- oauth_tokens_scopes_column => oauth_token[oauth_tokens_scopes_column]
618
- }
619
-
620
- refresh_token = _generate_refresh_token(update_params)
621
- else
622
- refresh_token = param("refresh_token")
623
- end
624
- oauth_token = __update_and_return__(oauth_tokens_ds, update_params)
625
-
626
- oauth_token[oauth_tokens_token_column] = access_token
627
- oauth_token[oauth_tokens_refresh_token_column] = refresh_token
628
- oauth_token
691
+ oauth_grant[oauth_grants_token_column] = access_token
692
+ oauth_grant
629
693
  end
630
694
  end
631
695
 
632
696
  def supported_grant_type?(grant_type, expected_grant_type = grant_type)
633
697
  return false unless grant_type == expected_grant_type
634
698
 
635
- return true unless (grant_types_supported = oauth_application[oauth_applications_grant_types_column])
636
-
637
- grant_types_supported = grant_types_supported.split(/ +/)
699
+ grant_types_supported = if oauth_application[oauth_applications_grant_types_column]
700
+ oauth_application[oauth_applications_grant_types_column].split(/ +/)
701
+ else
702
+ oauth_grant_types_supported
703
+ end
638
704
 
639
705
  grant_types_supported.include?(grant_type)
640
706
  end
641
707
 
708
+ def supported_response_type?(response_type, expected_response_type = response_type)
709
+ return false unless response_type == expected_response_type
710
+
711
+ response_types_supported = if oauth_application[oauth_applications_grant_types_column]
712
+ oauth_application[oauth_applications_response_types_column].split(/ +/)
713
+ else
714
+ oauth_response_types_supported
715
+ end
716
+
717
+ response_types = response_type.split(/ +/)
718
+
719
+ (response_types - response_types_supported).empty?
720
+ end
721
+
722
+ def supported_response_mode?(response_mode, expected_response_mode = response_mode)
723
+ return false unless response_mode == expected_response_mode
724
+
725
+ response_modes_supported = if oauth_application[oauth_applications_response_modes_column]
726
+ oauth_application[oauth_applications_response_modes_column].split(/ +/)
727
+ else
728
+ oauth_response_modes_supported
729
+ end
730
+
731
+ response_modes_supported.include?(response_mode)
732
+ end
733
+
642
734
  def oauth_server_metadata_body(path = nil)
643
735
  issuer = base_url
644
736
  issuer += "/#{path}" if path
@@ -647,10 +739,10 @@ module Rodauth
647
739
  issuer: issuer,
648
740
  token_endpoint: token_url,
649
741
  scopes_supported: oauth_application_scopes,
650
- response_types_supported: [],
651
- response_modes_supported: [],
652
- grant_types_supported: %w[refresh_token],
653
- token_endpoint_auth_methods_supported: oauth_auth_methods_supported,
742
+ response_types_supported: oauth_response_types_supported,
743
+ response_modes_supported: oauth_response_modes_supported,
744
+ grant_types_supported: oauth_grant_types_supported,
745
+ token_endpoint_auth_methods_supported: oauth_token_endpoint_auth_methods_supported,
654
746
  service_documentation: oauth_metadata_service_documentation,
655
747
  ui_locales_supported: oauth_metadata_ui_locales_supported,
656
748
  op_policy_uri: oauth_metadata_op_policy_uri,
@@ -660,10 +752,10 @@ module Rodauth
660
752
 
661
753
  def redirect_response_error(error_code, redirect_url = redirect_uri || request.referer || default_redirect)
662
754
  if accepts_json?
663
- status_code = if respond_to?(:"#{error_code}_response_status")
664
- send(:"#{error_code}_response_status")
755
+ status_code = if respond_to?(:"oauth_#{error_code}_response_status")
756
+ send(:"oauth_#{error_code}_response_status")
665
757
  else
666
- invalid_oauth_response_status
758
+ oauth_invalid_response_status
667
759
  end
668
760
 
669
761
  throw_json_response_error(status_code, error_code)
@@ -671,23 +763,32 @@ module Rodauth
671
763
  redirect_url = URI.parse(redirect_url)
672
764
  query_params = []
673
765
 
674
- query_params << if respond_to?(:"#{error_code}_error_code")
675
- "error=#{send(:"#{error_code}_error_code")}"
766
+ query_params << if respond_to?(:"oauth_#{error_code}_error_code")
767
+ ["error", send(:"oauth_#{error_code}_error_code")]
676
768
  else
677
- "error=#{error_code}"
769
+ ["error", error_code]
678
770
  end
679
771
 
680
- if respond_to?(:"#{error_code}_message")
681
- message = send(:"#{error_code}_message")
682
- query_params << ["error_description=#{CGI.escape(message)}"]
772
+ if respond_to?(:"oauth_#{error_code}_message")
773
+ message = send(:"oauth_#{error_code}_message")
774
+ query_params << ["error_description", CGI.escape(message)]
683
775
  end
684
776
 
685
- query_params << redirect_url.query if redirect_url.query
686
- redirect_url.query = query_params.join("&")
687
- redirect(redirect_url.to_s)
777
+ state = param_or_nil("state")
778
+
779
+ query_params << ["state", state] if state
780
+
781
+ _redirect_response_error(redirect_url, query_params)
688
782
  end
689
783
  end
690
784
 
785
+ def _redirect_response_error(redirect_url, query_params)
786
+ query_params = query_params.map { |k, v| "#{k}=#{v}" }
787
+ query_params << redirect_url.query if redirect_url.query
788
+ redirect_url.query = query_params.join("&")
789
+ redirect(redirect_url.to_s)
790
+ end
791
+
691
792
  def json_response_success(body, cache = false)
692
793
  response.status = 200
693
794
  response["Content-Type"] ||= json_response_content_type
@@ -705,26 +806,26 @@ module Rodauth
705
806
 
706
807
  def throw_json_response_error(status, error_code, message = nil)
707
808
  set_response_error_status(status)
708
- code = if respond_to?(:"#{error_code}_error_code")
709
- send(:"#{error_code}_error_code")
809
+ code = if respond_to?(:"oauth_#{error_code}_error_code")
810
+ send(:"oauth_#{error_code}_error_code")
710
811
  else
711
812
  error_code
712
813
  end
713
814
  payload = { "error" => code }
714
- payload["error_description"] = message || (send(:"#{error_code}_message") if respond_to?(:"#{error_code}_message"))
815
+ payload["error_description"] = message || (send(:"oauth_#{error_code}_message") if respond_to?(:"oauth_#{error_code}_message"))
715
816
  json_payload = _json_response_body(payload)
716
817
  response["Content-Type"] ||= json_response_content_type
717
818
  response["WWW-Authenticate"] = oauth_token_type.upcase if status == 401
718
819
  return_response(json_payload)
719
820
  end
720
821
 
721
- unless method_defined?(:_json_response_body)
722
- def _json_response_body(hash)
723
- if request.respond_to?(:convert_to_json)
724
- request.send(:convert_to_json, hash)
725
- else
726
- JSON.dump(hash)
727
- end
822
+ def _json_response_body(hash)
823
+ return super if features.include?(:json)
824
+
825
+ if request.respond_to?(:convert_to_json)
826
+ request.send(:convert_to_json, hash)
827
+ else
828
+ JSON.dump(hash)
728
829
  end
729
830
  end
730
831
 
@@ -736,7 +837,7 @@ module Rodauth
736
837
  end
737
838
 
738
839
  def authorization_required
739
- throw_json_response_error(authorization_required_error_status, "invalid_client")
840
+ throw_json_response_error(oauth_authorization_required_error_status, "invalid_client")
740
841
  end
741
842
 
742
843
  def check_valid_scopes?
@@ -749,36 +850,17 @@ module Rodauth
749
850
  URI::DEFAULT_PARSER.make_regexp(oauth_valid_uri_schemes).match?(uri)
750
851
  end
751
852
 
752
- # Resource server mode
853
+ def check_valid_no_fragment_uri?(uri)
854
+ check_valid_uri?(uri) && URI.parse(uri).fragment.nil?
855
+ end
753
856
 
754
- SERVER_METADATA = OAuth::TtlStore.new
857
+ # Resource server mode
755
858
 
756
859
  def authorization_server_metadata
757
- auth_url = URI(authorization_server_url)
758
-
759
- server_metadata = SERVER_METADATA[auth_url]
760
-
761
- return server_metadata if server_metadata
762
-
763
- SERVER_METADATA.set(auth_url) do
764
- http = Net::HTTP.new(auth_url.host, auth_url.port)
765
- http.use_ssl = auth_url.scheme == "https"
860
+ auth_url = URI(authorization_server_url).dup
861
+ auth_url.path = "/.well-known/oauth-authorization-server"
766
862
 
767
- request = Net::HTTP::Get.new("/.well-known/oauth-authorization-server")
768
- request["accept"] = json_response_content_type
769
- response = http.request(request)
770
- authorization_required unless response.code.to_i == 200
771
-
772
- # time-to-live
773
- ttl = if response.key?("cache-control")
774
- cache_control = response["cache-control"]
775
- cache_control[/max-age=(\d+)/, 1].to_i
776
- elsif response.key?("expires")
777
- Time.parse(response["expires"]).to_i - Time.now.to_i
778
- end
779
-
780
- [JSON.parse(response.body, symbolize_names: true), ttl]
781
- end
863
+ http_request_with_cache(auth_url)
782
864
  end
783
865
  end
784
866
  end