rodauth-oauth 0.10.3 → 1.0.0.pre.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) 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/0_10_3.md +1 -1
  5. data/doc/release_notes/0_10_4.md +11 -0
  6. data/doc/release_notes/1_0_0_beta1.md +38 -0
  7. data/lib/generators/rodauth/oauth/install_generator.rb +0 -1
  8. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +4 -6
  9. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_search.html.erb +1 -1
  10. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_verification.html.erb +2 -2
  11. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb +1 -6
  12. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +0 -2
  13. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_grants.html.erb +41 -0
  14. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +2 -2
  15. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_grants.html.erb +37 -0
  16. data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +18 -29
  17. data/lib/rodauth/features/oauth_application_management.rb +59 -72
  18. data/lib/rodauth/features/oauth_assertion_base.rb +19 -23
  19. data/lib/rodauth/features/oauth_authorization_code_grant.rb +35 -88
  20. data/lib/rodauth/features/oauth_authorize_base.rb +103 -20
  21. data/lib/rodauth/features/oauth_base.rb +365 -302
  22. data/lib/rodauth/features/oauth_client_credentials_grant.rb +20 -18
  23. data/lib/rodauth/features/{oauth_device_grant.rb → oauth_device_code_grant.rb} +62 -73
  24. data/lib/rodauth/features/oauth_dynamic_client_registration.rb +46 -28
  25. data/lib/rodauth/features/oauth_grant_management.rb +70 -0
  26. data/lib/rodauth/features/oauth_implicit_grant.rb +25 -24
  27. data/lib/rodauth/features/oauth_jwt.rb +52 -678
  28. data/lib/rodauth/features/oauth_jwt_base.rb +435 -0
  29. data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +45 -17
  30. data/lib/rodauth/features/oauth_jwt_jwks.rb +47 -0
  31. data/lib/rodauth/features/oauth_jwt_secured_authorization_request.rb +62 -0
  32. data/lib/rodauth/features/oauth_management_base.rb +2 -0
  33. data/lib/rodauth/features/oauth_pkce.rb +22 -26
  34. data/lib/rodauth/features/oauth_resource_indicators.rb +39 -22
  35. data/lib/rodauth/features/oauth_resource_server.rb +38 -0
  36. data/lib/rodauth/features/oauth_saml_bearer_grant.rb +5 -1
  37. data/lib/rodauth/features/oauth_token_introspection.rb +76 -45
  38. data/lib/rodauth/features/oauth_token_revocation.rb +46 -31
  39. data/lib/rodauth/features/oidc.rb +188 -95
  40. data/lib/rodauth/features/oidc_dynamic_client_registration.rb +89 -53
  41. data/lib/rodauth/oauth/database_extensions.rb +8 -6
  42. data/lib/rodauth/oauth/http_extensions.rb +61 -0
  43. data/lib/rodauth/oauth/railtie.rb +20 -0
  44. data/lib/rodauth/oauth/version.rb +1 -1
  45. data/lib/rodauth/oauth.rb +29 -1
  46. data/locales/en.yml +32 -22
  47. data/locales/pt.yml +32 -22
  48. data/templates/authorize.str +19 -24
  49. data/templates/device_search.str +1 -1
  50. data/templates/device_verification.str +2 -2
  51. data/templates/jwks_field.str +1 -0
  52. data/templates/new_oauth_application.str +1 -2
  53. data/templates/oauth_application.str +2 -2
  54. data/templates/oauth_application_oauth_grants.str +54 -0
  55. data/templates/oauth_applications.str +2 -2
  56. data/templates/oauth_grants.str +52 -0
  57. metadata +21 -17
  58. data/lib/generators/rodauth/oauth/templates/app/models/oauth_token.rb +0 -4
  59. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +0 -39
  60. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +0 -35
  61. data/lib/rodauth/features/oauth.rb +0 -9
  62. data/lib/rodauth/features/oauth_authorization_server.rb +0 -0
  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,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]
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