rodauth-oauth 0.10.4 → 1.0.0.pre.beta2

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