rodauth-oauth 0.10.4 → 1.0.0.pre.beta1

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