rodauth-oauth 0.0.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,7 +29,8 @@ class CreateRodauthOAuth < ActiveRecord::Migration<%= migration_version %>
29
29
  # uncomment to enable PKCE
30
30
  # t.string :code_challenge
31
31
  # t.string :code_challenge_method
32
-
32
+ # uncomment to use OIDC nonce
33
+ # t.string :nonce
33
34
  t.index(%i[oauth_application_id code], unique: true)
34
35
  end
35
36
 
@@ -42,18 +43,20 @@ class CreateRodauthOAuth < ActiveRecord::Migration<%= migration_version %>
42
43
  t.foreign_key :oauth_tokens, column: :oauth_token_id
43
44
  t.integer :oauth_application_id
44
45
  t.foreign_key :oauth_applications, column: :oauth_application_id
45
- t.string :token, null: false, token: true
46
+ t.string :token, null: false, token: true, unique: true
46
47
  # uncomment if setting oauth_tokens_token_hash_column
47
48
  # and delete the token column
48
- # t.string :token_hash, token: true
49
- t.string :refresh_token
49
+ # t.string :token_hash, token: true, unique: true
50
+ t.string :refresh_token, unique: true
50
51
  # uncomment if setting oauth_tokens_refresh_token_hash_column
51
52
  # and delete the refresh_token column
52
- # t.string :refresh_token_hash, token: true
53
+ # t.string :refresh_token_hash, token: true, unique: true
53
54
  t.datetime :expires_in, null: false
54
55
  t.datetime :revoked_at
55
56
  t.string :scopes, null: false
56
57
  t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
58
+ # uncomment to use OIDC nonce
59
+ # t.string :nonce
57
60
  end
58
61
  end
59
62
  end
@@ -1,9 +1,21 @@
1
1
  # frozen-string-literal: true
2
2
 
3
+ require "time"
4
+ require "base64"
5
+ require "securerandom"
6
+ require "net/http"
7
+
8
+ require "rodauth/oauth/ttl_store"
9
+ require "rodauth/oauth/database_extensions"
10
+
3
11
  module Rodauth
4
12
  Feature.define(:oauth) do
5
13
  # RUBY EXTENSIONS
6
14
  unless Regexp.method_defined?(:match?)
15
+ # If you wonder why this is there: the oauth feature uses a refinement to enhance the
16
+ # Regexp class locally with #match? , but this is never tested, because ActiveSupport
17
+ # monkey-patches the same method... Please ActiveSupport, stop being so intrusive!
18
+ # :nocov:
7
19
  module RegexpExtensions
8
20
  refine(Regexp) do
9
21
  def match?(*args)
@@ -12,6 +24,7 @@ module Rodauth
12
24
  end
13
25
  end
14
26
  using(RegexpExtensions)
27
+ # :nocov:
15
28
  end
16
29
 
17
30
  unless String.method_defined?(:delete_suffix!)
@@ -35,26 +48,24 @@ module Rodauth
35
48
 
36
49
  before "authorize"
37
50
  after "authorize"
38
- after "authorize_failure"
39
51
 
40
52
  before "token"
41
- after "token"
42
53
 
43
54
  before "revoke"
44
55
  after "revoke"
45
56
 
57
+ before "introspect"
58
+
46
59
  before "create_oauth_application"
47
60
  after "create_oauth_application"
48
61
 
49
- error_flash "OAuth Authorization invalid parameters", "oauth_grant_valid_parameters"
50
-
51
62
  error_flash "Please authorize to continue", "require_authorization"
52
63
  error_flash "There was an error registering your oauth application", "create_oauth_application"
53
64
  notice_flash "Your oauth application has been registered", "create_oauth_application"
54
65
 
55
66
  notice_flash "The oauth token has been revoked", "revoke_oauth_token"
56
67
 
57
- view "oauth_authorize", "Authorize", "authorize"
68
+ view "authorize", "Authorize", "authorize"
58
69
  view "oauth_applications", "Oauth Applications", "oauth_applications"
59
70
  view "oauth_application", "Oauth Application", "oauth_application"
60
71
  view "new_oauth_application", "New Oauth Application", "new_oauth_application"
@@ -62,7 +73,7 @@ module Rodauth
62
73
 
63
74
  auth_value_method :json_response_content_type, "application/json"
64
75
 
65
- auth_value_method :oauth_grant_expires_in, 60 * 5 # 60 minutes
76
+ auth_value_method :oauth_grant_expires_in, 60 * 5 # 5 minutes
66
77
  auth_value_method :oauth_token_expires_in, 60 * 60 # 60 minutes
67
78
  auth_value_method :use_oauth_implicit_grant_type?, false
68
79
  auth_value_method :use_oauth_pkce?, true
@@ -71,19 +82,9 @@ module Rodauth
71
82
  auth_value_method :oauth_require_pkce, false
72
83
  auth_value_method :oauth_pkce_challenge_method, "S256"
73
84
 
74
- auth_value_method :oauth_valid_uri_schemes, %w[http https]
85
+ auth_value_method :oauth_valid_uri_schemes, %w[https]
75
86
 
76
- # URL PARAMS
77
-
78
- # Authorize / token
79
- %w[
80
- grant_type code refresh_token client_id client_secret scope
81
- state redirect_uri scopes token_type_hint token
82
- access_type approval_prompt response_type
83
- code_challenge code_challenge_method code_verifier
84
- ].each do |param|
85
- auth_value_method :"#{param}_param", param
86
- end
87
+ auth_value_method :oauth_scope_separator, " "
87
88
 
88
89
  # Application
89
90
  APPLICATION_REQUIRED_PARAMS = %w[name description scopes homepage_url redirect_uri client_secret].freeze
@@ -91,7 +92,11 @@ module Rodauth
91
92
 
92
93
  (APPLICATION_REQUIRED_PARAMS + %w[client_id]).each do |param|
93
94
  auth_value_method :"oauth_application_#{param}_param", param
95
+ translatable_method :"#{param}_label", param.gsub("_", " ").capitalize
94
96
  end
97
+ button "Register", "oauth_application"
98
+ button "Authorize", "oauth_authorize"
99
+ button "Revoke", "oauth_token_revoke"
95
100
 
96
101
  # OAuth Token
97
102
  auth_value_method :oauth_tokens_path, "oauth-tokens"
@@ -110,6 +115,8 @@ module Rodauth
110
115
  auth_value_method :oauth_tokens_token_hash_column, nil
111
116
  auth_value_method :oauth_tokens_refresh_token_hash_column, nil
112
117
 
118
+ # Access Token reuse
119
+ auth_value_method :oauth_reuse_access_token, false
113
120
  # OAuth Grants
114
121
  auth_value_method :oauth_grants_table, :oauth_grants
115
122
  auth_value_method :oauth_grants_id_column, :id
@@ -124,6 +131,7 @@ module Rodauth
124
131
 
125
132
  auth_value_method :authorization_required_error_status, 401
126
133
  auth_value_method :invalid_oauth_response_status, 400
134
+ auth_value_method :already_in_use_response_status, 409
127
135
 
128
136
  # OAuth Applications
129
137
  auth_value_method :oauth_applications_path, "oauth-applications"
@@ -143,11 +151,9 @@ module Rodauth
143
151
 
144
152
  auth_value_method :oauth_application_default_scope, SCOPES.first
145
153
  auth_value_method :oauth_application_scopes, SCOPES
146
- auth_value_method :oauth_token_type, "Bearer"
154
+ auth_value_method :oauth_token_type, "bearer"
147
155
 
148
- auth_value_method :invalid_request, "Request is missing a required parameter"
149
- auth_value_method :invalid_client, "Invalid client"
150
- auth_value_method :unauthorized_client, "Unauthorized client"
156
+ auth_value_method :invalid_client_message, "Invalid client"
151
157
  auth_value_method :invalid_grant_type_message, "Invalid grant type"
152
158
  auth_value_method :invalid_grant_message, "Invalid grant"
153
159
  auth_value_method :invalid_scope_message, "Invalid scope"
@@ -157,6 +163,8 @@ module Rodauth
157
163
 
158
164
  auth_value_method :unique_error_message, "is already in use"
159
165
  auth_value_method :null_error_message, "is not filled"
166
+ auth_value_method :already_in_use_message, "error generating unique token"
167
+ auth_value_method :already_in_use_error_code, "invalid_request"
160
168
 
161
169
  # PKCE
162
170
  auth_value_method :code_challenge_required_error_code, "invalid_request"
@@ -164,37 +172,43 @@ module Rodauth
164
172
  auth_value_method :unsupported_transform_algorithm_error_code, "invalid_request"
165
173
  auth_value_method :unsupported_transform_algorithm_message, "transform algorithm not supported"
166
174
 
175
+ # METADATA
176
+ auth_value_method :oauth_metadata_service_documentation, nil
177
+ auth_value_method :oauth_metadata_ui_locales_supported, nil
178
+ auth_value_method :oauth_metadata_op_policy_uri, nil
179
+ auth_value_method :oauth_metadata_op_tos_uri, nil
180
+
181
+ # Resource Server params
182
+ # Only required to use if the plugin is to be used in a resource server
183
+ auth_value_method :is_authorization_server?, true
184
+
185
+ auth_value_method :oauth_unique_id_generation_retries, 3
186
+
167
187
  auth_value_methods(
188
+ :fetch_access_token,
168
189
  :oauth_unique_id_generator,
169
190
  :secret_matches?,
170
- :secret_hash
191
+ :secret_hash,
192
+ :generate_token_hash,
193
+ :authorization_server_url,
194
+ :before_introspection_request,
195
+ :require_authorizable_account,
196
+ :oauth_tokens_unique_columns
171
197
  )
172
198
 
173
199
  auth_value_methods(:only_json?)
174
200
 
175
- redirect(:oauth_application) do |id|
176
- "/#{oauth_applications_path}/#{id}"
177
- end
178
-
179
- redirect(:require_authorization) do
180
- if logged_in?
181
- oauth_authorize_path
182
- elsif respond_to?(:login_redirect)
183
- login_redirect
184
- else
185
- default_redirect
186
- end
187
- end
188
-
189
201
  auth_value_method :json_request_regexp, %r{\bapplication/(?:vnd\.api\+)?json\b}i
190
202
 
203
+ SERVER_METADATA = OAuth::TtlStore.new
204
+
191
205
  def check_csrf?
192
206
  case request.path
193
- when oauth_token_path
207
+ when token_path, introspect_path
194
208
  false
195
- when oauth_revoke_path
209
+ when revoke_path
196
210
  !json_request?
197
- when oauth_authorize_path, %r{/#{oauth_applications_path}}
211
+ when authorize_path, %r{/#{oauth_applications_path}}
198
212
  only_json? ? false : super
199
213
  else
200
214
  super
@@ -221,45 +235,36 @@ module Rodauth
221
235
  end
222
236
  end
223
237
 
224
- attr_reader :oauth_application
225
-
226
238
  def initialize(scope)
227
239
  @scope = scope
228
240
  end
229
241
 
230
- def state
231
- param_or_nil(state_param)
232
- end
233
-
234
242
  def scopes
235
- (param_or_nil(scopes_param) || oauth_application_default_scope).split(" ")
236
- end
237
-
238
- def client_id
239
- param_or_nil(client_id_param)
240
- end
241
-
242
- def client_secret
243
- param_or_nil(client_secret_param)
243
+ scope = request.params["scope"]
244
+ case scope
245
+ when Array
246
+ scope
247
+ when String
248
+ scope.split(" ")
249
+ when nil
250
+ [oauth_application_default_scope]
251
+ end
244
252
  end
245
253
 
246
254
  def redirect_uri
247
- param_or_nil(redirect_uri_param) || oauth_application[oauth_applications_redirect_uri_column]
248
- end
249
-
250
- def token_type_hint
251
- param_or_nil(token_type_hint_param) || "access_token"
252
- end
255
+ param_or_nil("redirect_uri") || begin
256
+ return unless oauth_application
253
257
 
254
- def token
255
- param_or_nil(token_param)
258
+ redirect_uris = oauth_application[oauth_applications_redirect_uri_column].split(" ")
259
+ redirect_uris.size == 1 ? redirect_uris.first : nil
260
+ end
256
261
  end
257
262
 
258
263
  def oauth_application
259
264
  return @oauth_application if defined?(@oauth_application)
260
265
 
261
266
  @oauth_application = begin
262
- client_id = param(client_id_param)
267
+ client_id = param_or_nil("client_id")
263
268
 
264
269
  return unless client_id
265
270
 
@@ -267,31 +272,54 @@ module Rodauth
267
272
  end
268
273
  end
269
274
 
275
+ def fetch_access_token
276
+ value = request.env["HTTP_AUTHORIZATION"]
277
+
278
+ return unless value
279
+
280
+ scheme, token = value.split(" ", 2)
281
+
282
+ return unless scheme.downcase == oauth_token_type
283
+
284
+ return if token.empty?
285
+
286
+ token
287
+ end
288
+
270
289
  def authorization_token
271
290
  return @authorization_token if defined?(@authorization_token)
272
291
 
273
- @authorization_token = begin
274
- value = request.get_header("HTTP_AUTHORIZATION").to_s
275
-
276
- scheme, token = value.split(" ", 2)
292
+ # check if there is a token
293
+ bearer_token = fetch_access_token
277
294
 
278
- return unless scheme == "Bearer"
295
+ return unless bearer_token
279
296
 
280
- # check if there is a token
281
- # check if token has not expired
282
- # check if token has been revoked
283
- oauth_token_by_token(token).where(Sequel[oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
284
- .where(oauth_tokens_revoked_at_column => nil)
285
- .first
286
- end
297
+ # check if token has not expired
298
+ # check if token has been revoked
299
+ @authorization_token = oauth_token_by_token(bearer_token)
287
300
  end
288
301
 
289
302
  def require_oauth_authorization(*scopes)
290
- authorization_required unless authorization_token
303
+ token_scopes = if is_authorization_server?
304
+ authorization_required unless authorization_token
305
+
306
+ scopes << oauth_application_default_scope if scopes.empty?
307
+
308
+ authorization_token[oauth_tokens_scopes_column].split(oauth_scope_separator)
309
+ else
310
+ bearer_token = fetch_access_token
311
+
312
+ authorization_required unless bearer_token
291
313
 
292
- scopes << oauth_application_default_scope if scopes.empty?
314
+ scopes << oauth_application_default_scope if scopes.empty?
293
315
 
294
- token_scopes = authorization_token[oauth_tokens_scopes_column].split(",")
316
+ # where in resource server, NOT the authorization server.
317
+ payload = introspection_request("access_token", bearer_token)
318
+
319
+ authorization_required unless payload["active"]
320
+
321
+ payload["scope"].split(oauth_scope_separator)
322
+ end
295
323
 
296
324
  authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
297
325
  end
@@ -304,8 +332,11 @@ module Rodauth
304
332
  request.get "new" do
305
333
  new_oauth_application_view
306
334
  end
335
+
307
336
  request.on(oauth_applications_id_pattern) do |id|
308
337
  oauth_application = db[oauth_applications_table].where(oauth_applications_id_column => id).first
338
+ next unless oauth_application
339
+
309
340
  scope.instance_variable_set(:@oauth_application, oauth_application)
310
341
 
311
342
  request.is do
@@ -317,12 +348,14 @@ module Rodauth
317
348
  request.on(oauth_tokens_path) do
318
349
  oauth_tokens = db[oauth_tokens_table].where(oauth_tokens_oauth_application_id_column => id)
319
350
  scope.instance_variable_set(:@oauth_tokens, oauth_tokens)
320
- oauth_tokens_view
351
+ request.get do
352
+ oauth_tokens_view
353
+ end
321
354
  end
322
355
  end
323
356
 
324
357
  request.get do
325
- scope.instance_variable_set(:@oauth_applications, db[:oauth_applications])
358
+ scope.instance_variable_set(:@oauth_applications, db[oauth_applications_table])
326
359
  oauth_applications_view
327
360
  end
328
361
 
@@ -335,7 +368,7 @@ module Rodauth
335
368
  id = create_oauth_application
336
369
  after_create_oauth_application
337
370
  set_notice_flash create_oauth_application_notice_flash
338
- redirect oauth_application_redirect(id)
371
+ redirect "#{request.path}/#{id}"
339
372
  end
340
373
  end
341
374
  set_error_flash create_oauth_application_error_flash
@@ -344,8 +377,136 @@ module Rodauth
344
377
  end
345
378
  end
346
379
 
380
+ def oauth_server_metadata(issuer = nil)
381
+ request.on(".well-known") do
382
+ request.on("oauth-authorization-server") do
383
+ request.get do
384
+ json_response_success(oauth_server_metadata_body(issuer))
385
+ end
386
+ end
387
+ end
388
+ end
389
+
390
+ def post_configure
391
+ super
392
+ self.class.__send__(:include, Rodauth::OAuth::ExtendDatabase(db))
393
+
394
+ # Check whether we can reutilize db entries for the same account / application pair
395
+ one_oauth_token_per_account = begin
396
+ db.indexes(oauth_tokens_table).values.any? do |definition|
397
+ definition[:unique] &&
398
+ definition[:columns] == oauth_tokens_unique_columns
399
+ end
400
+ end
401
+ self.class.send(:define_method, :__one_oauth_token_per_account) { one_oauth_token_per_account }
402
+ end
403
+
347
404
  private
348
405
 
406
+ def rescue_from_uniqueness_error(&block)
407
+ retries = oauth_unique_id_generation_retries
408
+ begin
409
+ transaction(savepoint: :only, &block)
410
+ rescue Sequel::UniqueConstraintViolation
411
+ redirect_response_error("already_in_use") if retries.zero?
412
+ retries -= 1
413
+ retry
414
+ end
415
+ end
416
+
417
+ # OAuth Token Unique/Reuse
418
+ def oauth_tokens_unique_columns
419
+ [
420
+ oauth_tokens_oauth_application_id_column,
421
+ oauth_tokens_account_id_column,
422
+ oauth_tokens_scopes_column
423
+ ]
424
+ end
425
+
426
+ def authorization_server_url
427
+ base_url
428
+ end
429
+
430
+ def authorization_server_metadata
431
+ auth_url = URI(authorization_server_url)
432
+
433
+ server_metadata = SERVER_METADATA[auth_url]
434
+
435
+ return server_metadata if server_metadata
436
+
437
+ SERVER_METADATA.set(auth_url) do
438
+ http = Net::HTTP.new(auth_url.host, auth_url.port)
439
+ http.use_ssl = auth_url.scheme == "https"
440
+
441
+ request = Net::HTTP::Get.new("/.well-known/oauth-authorization-server")
442
+ request["accept"] = json_response_content_type
443
+ response = http.request(request)
444
+ authorization_required unless response.code.to_i == 200
445
+
446
+ # time-to-live
447
+ ttl = if response.key?("cache-control")
448
+ cache_control = response["cache-control"]
449
+ cache_control[/max-age=(\d+)/, 1]
450
+ elsif response.key?("expires")
451
+ DateTime.httpdate(response["expires"]).utc.to_i - Time.now.utc.to_i
452
+ end
453
+
454
+ [JSON.parse(response.body, symbolize_names: true), ttl]
455
+ end
456
+ end
457
+
458
+ def introspection_request(token_type_hint, token)
459
+ auth_url = URI(authorization_server_url)
460
+ http = Net::HTTP.new(auth_url.host, auth_url.port)
461
+ http.use_ssl = auth_url.scheme == "https"
462
+
463
+ request = Net::HTTP::Post.new(introspect_path)
464
+ request["content-type"] = json_response_content_type
465
+ request["accept"] = json_response_content_type
466
+ request.body = JSON.dump({ "token_type_hint" => token_type_hint, "token" => token })
467
+
468
+ before_introspection_request(request)
469
+ response = http.request(request)
470
+ authorization_required unless response.code.to_i == 200
471
+
472
+ JSON.parse(response.body)
473
+ end
474
+
475
+ def before_introspection_request(request); end
476
+
477
+ def template_path(page)
478
+ path = File.join(File.dirname(__FILE__), "../../../templates", "#{page}.str")
479
+ return super unless File.exist?(path)
480
+
481
+ path
482
+ end
483
+
484
+ # to be used internally. Same semantics as require account, must:
485
+ # fetch an authorization basic header
486
+ # parse client id and secret
487
+ #
488
+ def require_oauth_application
489
+ # get client credenntials
490
+ client_id = client_secret = nil
491
+
492
+ # client_secret_basic
493
+ if (token = ((v = request.env["HTTP_AUTHORIZATION"]) && v[/\A *Basic (.*)\Z/, 1]))
494
+ client_id, client_secret = Base64.decode64(token).split(/:/, 2)
495
+ else
496
+ client_id = param_or_nil("client_id")
497
+ client_secret = param_or_nil("client_secret")
498
+ end
499
+
500
+ authorization_required unless client_id
501
+
502
+ @oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
503
+
504
+ # skip if using pkce
505
+ return if @oauth_application && use_oauth_pkce? && param_or_nil("code_verifier")
506
+
507
+ authorization_required unless @oauth_application && secret_matches?(@oauth_application, client_secret)
508
+ end
509
+
349
510
  def secret_matches?(oauth_application, secret)
350
511
  BCrypt::Password.new(oauth_application[oauth_applications_client_secret_column]) == secret
351
512
  end
@@ -362,22 +523,15 @@ module Rodauth
362
523
  Base64.urlsafe_encode64(Digest::SHA256.digest(token))
363
524
  end
364
525
 
526
+ def token_from_application?(oauth_token, oauth_application)
527
+ oauth_token[oauth_tokens_oauth_application_id_column] == oauth_application[oauth_applications_id_column]
528
+ end
529
+
365
530
  unless method_defined?(:password_hash)
366
531
  # From login_requirements_base feature
367
- if ENV["RACK_ENV"] == "test"
368
- def password_hash_cost
369
- BCrypt::Engine::MIN_COST
370
- end
371
- else
372
- # :nocov:
373
- def password_hash_cost
374
- BCrypt::Engine::DEFAULT_COST
375
- end
376
- # :nocov:
377
- end
378
532
 
379
533
  def password_hash(password)
380
- BCrypt::Password.create(password, cost: password_hash_cost)
534
+ BCrypt::Password.create(password, cost: BCrypt::Engine::DEFAULT_COST)
381
535
  end
382
536
  end
383
537
 
@@ -386,71 +540,92 @@ module Rodauth
386
540
  oauth_grants_expires_in_column => Time.now + oauth_token_expires_in
387
541
  }.merge(params)
388
542
 
389
- token = oauth_unique_id_generator
390
- refresh_token = nil
543
+ rescue_from_uniqueness_error do
544
+ token = oauth_unique_id_generator
391
545
 
392
- if oauth_tokens_token_hash_column
393
- create_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
394
- else
395
- create_params[oauth_tokens_token_column] = token
396
- end
546
+ if oauth_tokens_token_hash_column
547
+ create_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
548
+ else
549
+ create_params[oauth_tokens_token_column] = token
550
+ end
397
551
 
398
- if should_generate_refresh_token
399
- refresh_token = oauth_unique_id_generator
552
+ refresh_token = nil
553
+ if should_generate_refresh_token
554
+ refresh_token = oauth_unique_id_generator
400
555
 
401
- if oauth_tokens_refresh_token_hash_column
402
- create_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
403
- else
404
- create_params[oauth_tokens_refresh_token_column] = refresh_token
556
+ if oauth_tokens_refresh_token_hash_column
557
+ create_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
558
+ else
559
+ create_params[oauth_tokens_refresh_token_column] = refresh_token
560
+ end
405
561
  end
562
+ oauth_token = _generate_oauth_token(create_params)
563
+ oauth_token[oauth_tokens_token_column] = token
564
+ oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
565
+ oauth_token
406
566
  end
407
- oauth_token = _generate_oauth_token(create_params)
408
-
409
- oauth_token[oauth_tokens_token_column] = token
410
- oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
411
- oauth_token
412
567
  end
413
568
 
414
569
  def _generate_oauth_token(params = {})
415
570
  ds = db[oauth_tokens_table]
416
571
 
417
- begin
418
- if ds.supports_returning?(:insert)
419
- ds.returning.insert(params)
420
- else
421
- id = ds.insert(params)
422
- ds.where(oauth_tokens_id_column => id).first
572
+ if __one_oauth_token_per_account
573
+
574
+ token = __insert_or_update_and_return__(
575
+ ds,
576
+ oauth_tokens_id_column,
577
+ oauth_tokens_unique_columns,
578
+ params,
579
+ Sequel.expr(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column]) > Sequel::CURRENT_TIMESTAMP,
580
+ ([oauth_tokens_token_column, oauth_tokens_refresh_token_column] if oauth_reuse_access_token)
581
+ )
582
+
583
+ # if the previous operation didn't return a row, it means that the conditions
584
+ # invalidated the update, and the existing token is still valid.
585
+ token || ds.where(
586
+ oauth_tokens_account_id_column => params[oauth_tokens_account_id_column],
587
+ oauth_tokens_oauth_application_id_column => params[oauth_tokens_oauth_application_id_column]
588
+ ).first
589
+ else
590
+ if oauth_reuse_access_token
591
+ unique_conds = Hash[oauth_tokens_unique_columns.map { |column| [column, params[column]] }]
592
+ valid_token = ds.where(Sequel.expr(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column]) > Sequel::CURRENT_TIMESTAMP)
593
+ .where(unique_conds).first
594
+ return valid_token if valid_token
423
595
  end
424
- rescue Sequel::UniqueConstraintViolation
425
- retry
596
+ __insert_and_return__(ds, oauth_tokens_id_column, params)
426
597
  end
427
598
  end
428
599
 
429
- def oauth_token_by_token(token)
430
- if oauth_tokens_token_hash_column
431
- db[oauth_tokens_table].where(oauth_tokens_token_hash_column => generate_token_hash(token))
432
- else
433
- db[oauth_tokens_table].where(oauth_tokens_token_column => token)
434
- end
600
+ def oauth_token_by_token(token, dataset = db[oauth_tokens_table])
601
+ ds = if oauth_tokens_token_hash_column
602
+ dataset.where(oauth_tokens_token_hash_column => generate_token_hash(token))
603
+ else
604
+ dataset.where(oauth_tokens_token_column => token)
605
+ end
606
+
607
+ ds.where(Sequel[oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
608
+ .where(oauth_tokens_revoked_at_column => nil).first
435
609
  end
436
610
 
437
- def oauth_token_by_refresh_token(token)
438
- if oauth_tokens_refresh_token_hash_column
439
- db[oauth_tokens_table].where(oauth_tokens_refresh_token_hash_column => generate_token_hash(token))
440
- else
441
- db[oauth_tokens_table].where(oauth_tokens_refresh_token_column => token)
442
- end
611
+ def oauth_token_by_refresh_token(token, dataset = db[oauth_tokens_table])
612
+ ds = if oauth_tokens_refresh_token_hash_column
613
+ dataset.where(oauth_tokens_refresh_token_hash_column => generate_token_hash(token))
614
+ else
615
+ dataset.where(oauth_tokens_refresh_token_column => token)
616
+ end
617
+
618
+ ds.where(Sequel[oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
619
+ .where(oauth_tokens_revoked_at_column => nil).first
443
620
  end
444
621
 
445
622
  def json_access_token_payload(oauth_token)
446
623
  payload = {
447
624
  "access_token" => oauth_token[oauth_tokens_token_column],
448
- "token_type" => oauth_token_type.downcase,
625
+ "token_type" => oauth_token_type,
449
626
  "expires_in" => oauth_token_expires_in
450
627
  }
451
- if oauth_token[oauth_tokens_refresh_token_column]
452
- payload["refresh_token"] = oauth_token[oauth_tokens_refresh_token_column]
453
- end
628
+ payload["refresh_token"] = oauth_token[oauth_tokens_refresh_token_column] if oauth_token[oauth_tokens_refresh_token_column]
454
629
  payload
455
630
  end
456
631
 
@@ -469,13 +644,21 @@ module Rodauth
469
644
 
470
645
  def validate_oauth_application_params
471
646
  oauth_application_params.each do |key, value|
472
- if key == oauth_application_homepage_url_param ||
473
- key == oauth_application_redirect_uri_param
647
+ if key == oauth_application_homepage_url_param
474
648
 
475
- unless URI::DEFAULT_PARSER.make_regexp(oauth_valid_uri_schemes).match?(value)
476
- set_field_error(key, invalid_url_message)
477
- end
649
+ set_field_error(key, invalid_url_message) unless check_valid_uri?(value)
478
650
 
651
+ elsif key == oauth_application_redirect_uri_param
652
+
653
+ if value.respond_to?(:each)
654
+ value.each do |uri|
655
+ next if uri.empty?
656
+
657
+ set_field_error(key, invalid_url_message) unless check_valid_uri?(uri)
658
+ end
659
+ else
660
+ set_field_error(key, invalid_url_message) unless check_valid_uri?(value)
661
+ end
479
662
  elsif key == oauth_application_scopes_param
480
663
 
481
664
  value.each do |scope|
@@ -493,57 +676,38 @@ module Rodauth
493
676
  oauth_applications_name_column => oauth_application_params[oauth_application_name_param],
494
677
  oauth_applications_description_column => oauth_application_params[oauth_application_description_param],
495
678
  oauth_applications_scopes_column => oauth_application_params[oauth_application_scopes_param],
496
- oauth_applications_homepage_url_column => oauth_application_params[oauth_application_homepage_url_param],
497
- oauth_applications_redirect_uri_column => oauth_application_params[oauth_application_redirect_uri_param]
679
+ oauth_applications_homepage_url_column => oauth_application_params[oauth_application_homepage_url_param]
498
680
  }
499
681
 
682
+ redirect_uris = oauth_application_params[oauth_application_redirect_uri_param]
683
+ redirect_uris = redirect_uris.to_a.reject(&:empty?).join(" ") if redirect_uris.respond_to?(:each)
684
+ create_params[oauth_applications_redirect_uri_column] = redirect_uris unless redirect_uris.empty?
500
685
  # set client ID/secret pairs
501
686
 
502
687
  create_params.merge! \
503
- oauth_applications_client_id_column => oauth_unique_id_generator,
504
688
  oauth_applications_client_secret_column => \
505
689
  secret_hash(oauth_application_params[oauth_application_client_secret_param])
506
690
 
507
691
  create_params[oauth_applications_scopes_column] = if create_params[oauth_applications_scopes_column]
508
- create_params[oauth_applications_scopes_column].join(",")
692
+ create_params[oauth_applications_scopes_column].join(oauth_scope_separator)
509
693
  else
510
694
  oauth_application_default_scope
511
695
  end
512
696
 
513
- ds = db[oauth_applications_table]
514
-
515
- id = nil
516
- raised = begin
517
- id = if ds.supports_returning?(:insert)
518
- ds.returning(oauth_applications_id_column).insert(create_params)
519
- else
520
- id = db[oauth_applications_table].insert(create_params)
521
- db[oauth_applications_table].where(oauth_applications_id_column => id).get(oauth_applications_id_column)
522
- end
523
- false
524
- rescue Sequel::ConstraintViolation => e
525
- e
526
- end
527
-
528
- if raised
529
- field = raised.message[/\.(.*)$/, 1]
530
- case raised
531
- when Sequel::UniqueConstraintViolation
532
- throw_error(field, unique_error_message)
533
- when Sequel::NotNullConstraintViolation
534
- throw_error(field, null_error_message)
535
- end
697
+ rescue_from_uniqueness_error do
698
+ create_params[oauth_applications_client_id_column] = oauth_unique_id_generator
699
+ db[oauth_applications_table].insert(create_params)
536
700
  end
537
-
538
- !raised && id
539
701
  end
540
702
 
541
703
  # Authorize
542
- def before_authorize
704
+ def require_authorizable_account
543
705
  require_account
544
706
  end
545
707
 
546
708
  def validate_oauth_grant_params
709
+ redirect_response_error("invalid_request", request.referer || default_redirect) unless oauth_application && check_valid_redirect_uri?
710
+
547
711
  unless oauth_application && check_valid_redirect_uri? && check_valid_access_type? &&
548
712
  check_valid_approval_prompt? && check_valid_response_type?
549
713
  redirect_response_error("invalid_request")
@@ -554,7 +718,7 @@ module Rodauth
554
718
  end
555
719
 
556
720
  def try_approval_prompt
557
- approval_prompt = param_or_nil(approval_prompt_param)
721
+ approval_prompt = param_or_nil("approval_prompt")
558
722
 
559
723
  return unless approval_prompt && approval_prompt == "auto"
560
724
 
@@ -562,7 +726,7 @@ module Rodauth
562
726
  oauth_grants_account_id_column => account_id,
563
727
  oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
564
728
  oauth_grants_redirect_uri_column => redirect_uri,
565
- oauth_grants_scopes_column => scopes.join(","),
729
+ oauth_grants_scopes_column => scopes.join(oauth_scope_separator),
566
730
  oauth_grants_access_type_column => "online"
567
731
  ).count.zero?
568
732
 
@@ -571,128 +735,142 @@ module Rodauth
571
735
  request.env["REQUEST_METHOD"] = "POST"
572
736
  end
573
737
 
574
- def create_oauth_grant
575
- create_params = {
738
+ def create_oauth_grant(create_params = {})
739
+ create_params.merge!(
576
740
  oauth_grants_account_id_column => account_id,
577
741
  oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
578
742
  oauth_grants_redirect_uri_column => redirect_uri,
579
- oauth_grants_code_column => oauth_unique_id_generator,
580
743
  oauth_grants_expires_in_column => Time.now + oauth_grant_expires_in,
581
- oauth_grants_scopes_column => scopes.join(",")
582
- }
744
+ oauth_grants_scopes_column => scopes.join(oauth_scope_separator)
745
+ )
583
746
 
584
747
  # Access Type flow
585
- if use_oauth_access_type?
586
- if (access_type = param_or_nil(access_type_param))
587
- create_params[oauth_grants_access_type_column] = access_type
588
- end
748
+ if use_oauth_access_type? && (access_type = param_or_nil("access_type"))
749
+ create_params[oauth_grants_access_type_column] = access_type
589
750
  end
590
751
 
591
752
  # PKCE flow
592
- if use_oauth_pkce?
593
-
594
- if (code_challenge = param_or_nil(code_challenge_param))
595
- code_challenge_method = param_or_nil(code_challenge_method_param)
753
+ if use_oauth_pkce? && (code_challenge = param_or_nil("code_challenge"))
754
+ code_challenge_method = param_or_nil("code_challenge_method")
596
755
 
597
- create_params[oauth_grants_code_challenge_column] = code_challenge
598
- create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
599
- elsif oauth_require_pkce
600
- redirect_response_error("code_challenge_required")
601
- end
756
+ create_params[oauth_grants_code_challenge_column] = code_challenge
757
+ create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
602
758
  end
603
759
 
604
760
  ds = db[oauth_grants_table]
605
761
 
606
- begin
607
- if ds.supports_returning?(:insert)
608
- ds.returning(authorize_code_column).insert(create_params)
762
+ rescue_from_uniqueness_error do
763
+ create_params[oauth_grants_code_column] = oauth_unique_id_generator
764
+ __insert_and_return__(ds, oauth_grants_id_column, create_params)
765
+ end
766
+ create_params[oauth_grants_code_column]
767
+ end
768
+
769
+ def do_authorize(redirect_url, query_params = [], fragment_params = [])
770
+ case param("response_type")
771
+ when "token"
772
+ redirect_response_error("invalid_request") unless use_oauth_implicit_grant_type?
773
+
774
+ fragment_params.replace(_do_authorize_token.map { |k, v| "#{k}=#{v}" })
775
+ when "code", "", nil
776
+ query_params.replace(_do_authorize_code.map { |k, v| "#{k}=#{v}" })
777
+ end
778
+
779
+ if param_or_nil("state")
780
+ if !fragment_params.empty?
781
+ fragment_params << "state=#{param('state')}"
609
782
  else
610
- id = ds.insert(create_params)
611
- ds.where(oauth_grants_id_column => id).get(oauth_grants_code_column)
783
+ query_params << "state=#{param('state')}"
612
784
  end
613
- rescue Sequel::UniqueConstraintViolation
614
- retry
615
785
  end
786
+
787
+ query_params << redirect_url.query if redirect_url.query
788
+
789
+ redirect_url.query = query_params.join("&") unless query_params.empty?
790
+ redirect_url.fragment = fragment_params.join("&") unless fragment_params.empty?
616
791
  end
617
792
 
618
- # Access Tokens
793
+ def _do_authorize_code
794
+ { "code" => create_oauth_grant }
795
+ end
619
796
 
620
- def validate_oauth_token_params
621
- redirect_response_error("invalid_request") unless param_or_nil(client_id_param)
797
+ def _do_authorize_token
798
+ create_params = {
799
+ oauth_tokens_account_id_column => account_id,
800
+ oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
801
+ oauth_tokens_scopes_column => scopes
802
+ }
803
+ oauth_token = generate_oauth_token(create_params, false)
622
804
 
623
- unless param_or_nil(client_secret_param)
624
- redirect_response_error("invalid_request") unless param_or_nil(code_verifier_param)
625
- end
805
+ json_access_token_payload(oauth_token)
806
+ end
807
+
808
+ # Access Tokens
626
809
 
627
- unless (grant_type = param_or_nil(grant_type_param))
810
+ def validate_oauth_token_params
811
+ unless (grant_type = param_or_nil("grant_type"))
628
812
  redirect_response_error("invalid_request")
629
813
  end
630
814
 
631
815
  case grant_type
632
816
  when "authorization_code"
633
- redirect_response_error("invalid_request") unless param_or_nil(code_param)
817
+ redirect_response_error("invalid_request") unless param_or_nil("code")
634
818
 
635
819
  when "refresh_token"
636
- redirect_response_error("invalid_request") unless param_or_nil(refresh_token_param)
820
+ redirect_response_error("invalid_request") unless param_or_nil("refresh_token")
637
821
  else
638
822
  redirect_response_error("invalid_request")
639
823
  end
640
824
  end
641
825
 
642
826
  def create_oauth_token
643
- oauth_application = db[oauth_applications_table].where(
644
- oauth_applications_client_id_column => param(client_id_param)
645
- ).first
646
-
647
- redirect_response_error("invalid_request") unless oauth_application
648
-
649
- if (client_secret = param_or_nil(client_secret_param))
650
- redirect_response_error("invalid_request") unless secret_matches?(oauth_application, client_secret)
651
- end
652
-
653
- case param(grant_type_param)
827
+ case param("grant_type")
654
828
  when "authorization_code"
655
- create_oauth_token_from_authorization_code(oauth_application)
829
+ # fetch oauth grant
830
+ oauth_grant = db[oauth_grants_table].where(
831
+ oauth_grants_code_column => param("code"),
832
+ oauth_grants_redirect_uri_column => param("redirect_uri"),
833
+ oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
834
+ oauth_grants_revoked_at_column => nil
835
+ ).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
836
+ .for_update
837
+ .first
838
+
839
+ redirect_response_error("invalid_grant") unless oauth_grant
840
+
841
+ create_params = {
842
+ oauth_tokens_account_id_column => oauth_grant[oauth_grants_account_id_column],
843
+ oauth_tokens_oauth_application_id_column => oauth_grant[oauth_grants_oauth_application_id_column],
844
+ oauth_tokens_oauth_grant_id_column => oauth_grant[oauth_grants_id_column],
845
+ oauth_tokens_scopes_column => oauth_grant[oauth_grants_scopes_column]
846
+ }
847
+ create_oauth_token_from_authorization_code(oauth_grant, create_params)
656
848
  when "refresh_token"
657
- create_oauth_token_from_token(oauth_application)
658
- else
659
- redirect_response_error("invalid_grant")
660
- end
661
- end
849
+ # fetch oauth token
850
+ oauth_token = oauth_token_by_refresh_token(param("refresh_token"))
662
851
 
663
- def create_oauth_token_from_authorization_code(oauth_application)
664
- # fetch oauth grant
665
- oauth_grant = db[oauth_grants_table].where(
666
- oauth_grants_code_column => param(code_param),
667
- oauth_grants_redirect_uri_column => param(redirect_uri_param),
668
- oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
669
- oauth_grants_revoked_at_column => nil
670
- ).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
671
- .for_update
672
- .first
852
+ redirect_response_error("invalid_grant") unless oauth_token
673
853
 
674
- redirect_response_error("invalid_grant") unless oauth_grant
854
+ update_params = {
855
+ oauth_tokens_oauth_application_id_column => oauth_token[oauth_grants_oauth_application_id_column],
856
+ oauth_tokens_expires_in_column => Time.now + oauth_token_expires_in
857
+ }
858
+ create_oauth_token_from_token(oauth_token, update_params)
859
+ end
860
+ end
675
861
 
862
+ def create_oauth_token_from_authorization_code(oauth_grant, create_params)
676
863
  # PKCE
677
864
  if use_oauth_pkce?
678
865
  if oauth_grant[oauth_grants_code_challenge_column]
679
- code_verifier = param_or_nil(code_verifier_param)
866
+ code_verifier = param_or_nil("code_verifier")
680
867
 
681
- unless code_verifier && check_valid_grant_challenge?(oauth_grant, code_verifier)
682
- redirect_response_error("invalid_request")
683
- end
868
+ redirect_response_error("invalid_request") unless code_verifier && check_valid_grant_challenge?(oauth_grant, code_verifier)
684
869
  elsif oauth_require_pkce
685
870
  redirect_response_error("code_challenge_required")
686
871
  end
687
872
  end
688
873
 
689
- create_params = {
690
- oauth_tokens_account_id_column => oauth_grant[oauth_grants_account_id_column],
691
- oauth_tokens_oauth_application_id_column => oauth_grant[oauth_grants_oauth_application_id_column],
692
- oauth_tokens_oauth_grant_id_column => oauth_grant[oauth_grants_id_column],
693
- oauth_tokens_scopes_column => oauth_grant[oauth_grants_scopes_column]
694
- }
695
-
696
874
  # revoke oauth grant
697
875
  db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
698
876
  .update(oauth_grants_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
@@ -703,91 +881,80 @@ module Rodauth
703
881
  generate_oauth_token(create_params, should_generate_refresh_token)
704
882
  end
705
883
 
706
- def create_oauth_token_from_token(oauth_application)
707
- # fetch oauth token
708
- oauth_token = oauth_token_by_refresh_token(param(refresh_token_param)).where(
709
- oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column]
710
- ).where(oauth_grants_revoked_at_column => nil).for_update.first
884
+ def create_oauth_token_from_token(oauth_token, update_params)
885
+ redirect_response_error("invalid_grant") unless token_from_application?(oauth_token, oauth_application)
711
886
 
712
- redirect_response_error("invalid_grant") unless oauth_token
887
+ rescue_from_uniqueness_error do
888
+ token = oauth_unique_id_generator
713
889
 
714
- token = oauth_unique_id_generator
890
+ if oauth_tokens_token_hash_column
891
+ update_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
892
+ else
893
+ update_params[oauth_tokens_token_column] = token
894
+ end
715
895
 
716
- update_params = {
717
- oauth_tokens_oauth_application_id_column => oauth_token[oauth_grants_oauth_application_id_column],
718
- oauth_tokens_expires_in_column => Time.now + oauth_token_expires_in
719
- }
896
+ ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
720
897
 
721
- if oauth_tokens_token_hash_column
722
- update_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
723
- else
724
- update_params[oauth_tokens_token_column] = token
898
+ oauth_token = __update_and_return__(ds, update_params)
899
+ oauth_token[oauth_tokens_token_column] = token
900
+ oauth_token
725
901
  end
902
+ end
726
903
 
727
- ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
904
+ TOKEN_HINT_TYPES = %w[access_token refresh_token].freeze
728
905
 
729
- oauth_token = begin
730
- if ds.supports_returning?(:update)
731
- ds.returning.update(update_params)
732
- else
733
- ds.update(update_params)
734
- ds.first
735
- end
736
- rescue Sequel::UniqueConstraintViolation
737
- retry
906
+ # Token introspect
907
+
908
+ def validate_oauth_introspect_params
909
+ # check if valid token hint type
910
+ if param_or_nil("token_type_hint") && !TOKEN_HINT_TYPES.include?(param("token_type_hint"))
911
+ redirect_response_error("unsupported_token_type")
738
912
  end
739
913
 
740
- oauth_token[oauth_tokens_token_column] = token
741
- oauth_token
914
+ redirect_response_error("invalid_request") unless param_or_nil("token")
742
915
  end
743
916
 
744
- # Token revocation
917
+ def json_token_introspect_payload(token)
918
+ return { active: false } unless token
745
919
 
746
- def before_revoke
747
- require_account
920
+ {
921
+ active: true,
922
+ scope: token[oauth_tokens_scopes_column],
923
+ client_id: oauth_application[oauth_applications_client_id_column],
924
+ # username
925
+ token_type: oauth_token_type
926
+ }
748
927
  end
749
928
 
750
- TOKEN_HINT_TYPES = %w[access_token refresh_token].freeze
929
+ # Token revocation
751
930
 
752
931
  def validate_oauth_revoke_params
753
932
  # check if valid token hint type
754
- redirect_response_error("unsupported_token_type") unless TOKEN_HINT_TYPES.include?(token_type_hint)
933
+ if param_or_nil("token_type_hint") && !TOKEN_HINT_TYPES.include?(param("token_type_hint"))
934
+ redirect_response_error("unsupported_token_type")
935
+ end
755
936
 
756
- redirect_response_error("invalid_request") unless param(token_param)
937
+ redirect_response_error("invalid_request") unless param_or_nil("token")
757
938
  end
758
939
 
759
940
  def revoke_oauth_token
760
- ds = case token_type_hint
761
- when "access_token"
762
- oauth_token_by_token(token)
763
- when "refresh_token"
764
- oauth_token_by_refresh_token(token)
765
- end
766
- # one can only revoke tokens which haven't been revoked before, and which are
767
- # either our tokens, or tokens from applications we own.
768
- oauth_token = ds.where(oauth_tokens_revoked_at_column => nil)
769
- .where(
770
- Sequel.or(
771
- oauth_tokens_account_id_column => account_id,
772
- oauth_tokens_oauth_application_id_column => db[oauth_applications_table].where(
773
- oauth_applications_client_id_column => param(client_id_param),
774
- oauth_applications_account_id_column => account_id
775
- ).select(oauth_applications_id_column)
776
- )
777
- ).for_update.first
941
+ token = param("token")
942
+
943
+ oauth_token = if param("token_type_hint") == "refresh_token"
944
+ oauth_token_by_refresh_token(token)
945
+ else
946
+ oauth_token_by_token(token)
947
+ end
778
948
 
779
949
  redirect_response_error("invalid_request") unless oauth_token
780
950
 
951
+ redirect_response_error("invalid_request") unless token_from_application?(oauth_token, oauth_application)
952
+
781
953
  update_params = { oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP }
782
954
 
783
955
  ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
784
956
 
785
- oauth_token = if ds.supports_returning?(:update)
786
- ds.returning.update(update_params)
787
- else
788
- ds.update(update_params)
789
- ds.first
790
- end
957
+ oauth_token = __update_and_return__(ds, update_params)
791
958
 
792
959
  oauth_token[oauth_tokens_token_column] = token
793
960
  oauth_token
@@ -803,9 +970,15 @@ module Rodauth
803
970
 
804
971
  # Response helpers
805
972
 
806
- def redirect_response_error(error_code, redirect_url = request.referer || default_redirect)
973
+ def redirect_response_error(error_code, redirect_url = redirect_uri || request.referer || default_redirect)
807
974
  if accepts_json?
808
- throw_json_response_error(invalid_oauth_response_status, error_code)
975
+ status_code = if respond_to?(:"#{error_code}_response_status")
976
+ send(:"#{error_code}_response_status")
977
+ else
978
+ invalid_oauth_response_status
979
+ end
980
+
981
+ throw_json_response_error(status_code, error_code)
809
982
  else
810
983
  redirect_url = URI.parse(redirect_url)
811
984
  query_params = []
@@ -827,6 +1000,14 @@ module Rodauth
827
1000
  end
828
1001
  end
829
1002
 
1003
+ def json_response_success(body)
1004
+ response.status = 200
1005
+ response["Content-Type"] ||= json_response_content_type
1006
+ json_payload = _json_response_body(body)
1007
+ response.write(json_payload)
1008
+ request.halt
1009
+ end
1010
+
830
1011
  def throw_json_response_error(status, error_code)
831
1012
  set_response_error_status(status)
832
1013
  code = if respond_to?(:"#{error_code}_error_code")
@@ -838,7 +1019,7 @@ module Rodauth
838
1019
  payload["error_description"] = send(:"#{error_code}_message") if respond_to?(:"#{error_code}_message")
839
1020
  json_payload = _json_response_body(payload)
840
1021
  response["Content-Type"] ||= json_response_content_type
841
- response["WWW-Authenticate"] = oauth_token_type if status == 401
1022
+ response["WWW-Authenticate"] = oauth_token_type.upcase if status == 401
842
1023
  response.write(json_payload)
843
1024
  request.halt
844
1025
  end
@@ -858,18 +1039,22 @@ module Rodauth
858
1039
  throw_json_response_error(authorization_required_error_status, "invalid_client")
859
1040
  else
860
1041
  set_redirect_error_flash(require_authorization_error_flash)
861
- redirect(require_authorization_redirect)
1042
+ redirect(authorize_path)
862
1043
  end
863
1044
  end
864
1045
 
1046
+ def check_valid_uri?(uri)
1047
+ URI::DEFAULT_PARSER.make_regexp(oauth_valid_uri_schemes).match?(uri)
1048
+ end
1049
+
865
1050
  def check_valid_scopes?
866
1051
  return false unless scopes
867
1052
 
868
- (scopes - oauth_application[oauth_applications_scopes_column].split(",")).empty?
1053
+ (scopes - oauth_application[oauth_applications_scopes_column].split(oauth_scope_separator)).empty?
869
1054
  end
870
1055
 
871
1056
  def check_valid_redirect_uri?
872
- redirect_uri == oauth_application[oauth_applications_redirect_uri_column]
1057
+ oauth_application[oauth_applications_redirect_uri_column].split(" ").include?(redirect_uri)
873
1058
  end
874
1059
 
875
1060
  ACCESS_TYPES = %w[offline online].freeze
@@ -877,7 +1062,7 @@ module Rodauth
877
1062
  def check_valid_access_type?
878
1063
  return true unless use_oauth_access_type?
879
1064
 
880
- access_type = param_or_nil(access_type_param)
1065
+ access_type = param_or_nil("access_type")
881
1066
  !access_type || ACCESS_TYPES.include?(access_type)
882
1067
  end
883
1068
 
@@ -886,12 +1071,12 @@ module Rodauth
886
1071
  def check_valid_approval_prompt?
887
1072
  return true unless use_oauth_access_type?
888
1073
 
889
- approval_prompt = param_or_nil(approval_prompt_param)
1074
+ approval_prompt = param_or_nil("approval_prompt")
890
1075
  !approval_prompt || APPROVAL_PROMPTS.include?(approval_prompt)
891
1076
  end
892
1077
 
893
1078
  def check_valid_response_type?
894
- response_type = param_or_nil(response_type_param)
1079
+ response_type = param_or_nil("response_type")
895
1080
 
896
1081
  return true if response_type.nil? || response_type == "code"
897
1082
 
@@ -903,9 +1088,9 @@ module Rodauth
903
1088
  # PKCE
904
1089
 
905
1090
  def validate_pkce_challenge_params
906
- if param_or_nil(code_challenge_param)
1091
+ if param_or_nil("code_challenge")
907
1092
 
908
- challenge_method = param_or_nil(code_challenge_method_param)
1093
+ challenge_method = param_or_nil("code_challenge_method")
909
1094
  redirect_response_error("code_challenge_required") unless oauth_pkce_challenge_method == challenge_method
910
1095
  else
911
1096
  return unless oauth_require_pkce
@@ -930,9 +1115,49 @@ module Rodauth
930
1115
  end
931
1116
  end
932
1117
 
933
- # /oauth-token
934
- route(:oauth_token) do |r|
935
- before_token
1118
+ # Server metadata
1119
+
1120
+ def oauth_server_metadata_body(path)
1121
+ issuer = base_url
1122
+ issuer += "/#{path}" if path
1123
+
1124
+ responses_supported = %w[code]
1125
+ response_modes_supported = %w[query]
1126
+ grant_types_supported = %w[authorization_code]
1127
+
1128
+ if use_oauth_implicit_grant_type?
1129
+ responses_supported << "token"
1130
+ response_modes_supported << "fragment"
1131
+ grant_types_supported << "implicit"
1132
+ end
1133
+ {
1134
+ issuer: issuer,
1135
+ authorization_endpoint: authorize_url,
1136
+ token_endpoint: token_url,
1137
+ registration_endpoint: "#{base_url}/#{oauth_applications_path}",
1138
+ scopes_supported: oauth_application_scopes,
1139
+ response_types_supported: responses_supported,
1140
+ response_modes_supported: response_modes_supported,
1141
+ grant_types_supported: grant_types_supported,
1142
+ token_endpoint_auth_methods_supported: %w[client_secret_basic client_secret_post],
1143
+ service_documentation: oauth_metadata_service_documentation,
1144
+ ui_locales_supported: oauth_metadata_ui_locales_supported,
1145
+ op_policy_uri: oauth_metadata_op_policy_uri,
1146
+ op_tos_uri: oauth_metadata_op_tos_uri,
1147
+ revocation_endpoint: revoke_url,
1148
+ revocation_endpoint_auth_methods_supported: nil, # because it's client_secret_basic
1149
+ introspection_endpoint: introspect_url,
1150
+ introspection_endpoint_auth_methods_supported: %w[client_secret_basic],
1151
+ code_challenge_methods_supported: (use_oauth_pkce? ? oauth_pkce_challenge_method : nil)
1152
+ }
1153
+ end
1154
+
1155
+ # /token
1156
+ route(:token) do |r|
1157
+ next unless is_authorization_server?
1158
+
1159
+ before_token_route
1160
+ require_oauth_application
936
1161
 
937
1162
  r.post do
938
1163
  catch_error do
@@ -940,104 +1165,105 @@ module Rodauth
940
1165
 
941
1166
  oauth_token = nil
942
1167
  transaction do
1168
+ before_token
943
1169
  oauth_token = create_oauth_token
944
- after_token
945
1170
  end
946
1171
 
947
- response.status = 200
948
- response["Content-Type"] ||= json_response_content_type
949
- json_payload = _json_response_body(json_access_token_payload(oauth_token))
950
- response.write(json_payload)
951
- request.halt
1172
+ json_response_success(json_access_token_payload(oauth_token))
952
1173
  end
953
1174
 
954
1175
  throw_json_response_error(invalid_oauth_response_status, "invalid_request")
955
1176
  end
956
1177
  end
957
1178
 
958
- # /oauth-revoke
959
- route(:oauth_revoke) do |r|
960
- require_account
961
- before_revoke
1179
+ # /introspect
1180
+ route(:introspect) do |r|
1181
+ next unless is_authorization_server?
1182
+
1183
+ before_introspect_route
1184
+
1185
+ r.post do
1186
+ catch_error do
1187
+ validate_oauth_introspect_params
1188
+
1189
+ before_introspect
1190
+ oauth_token = case param("token_type_hint")
1191
+ when "access_token"
1192
+ oauth_token_by_token(param("token"))
1193
+ when "refresh_token"
1194
+ oauth_token_by_refresh_token(param("token"))
1195
+ else
1196
+ oauth_token_by_token(param("token")) || oauth_token_by_refresh_token(param("token"))
1197
+ end
1198
+
1199
+ if oauth_application
1200
+ redirect_response_error("invalid_request") if oauth_token && !token_from_application?(oauth_token, oauth_application)
1201
+ elsif oauth_token
1202
+ @oauth_application = db[oauth_applications_table].where(oauth_applications_id_column =>
1203
+ oauth_token[oauth_tokens_oauth_application_id_column]).first
1204
+ end
1205
+
1206
+ json_response_success(json_token_introspect_payload(oauth_token))
1207
+ end
1208
+
1209
+ throw_json_response_error(invalid_oauth_response_status, "invalid_request")
1210
+ end
1211
+ end
1212
+
1213
+ # /revoke
1214
+ route(:revoke) do |r|
1215
+ next unless is_authorization_server?
1216
+
1217
+ before_revoke_route
1218
+ require_oauth_application
962
1219
 
963
- # access-token
964
1220
  r.post do
965
1221
  catch_error do
966
1222
  validate_oauth_revoke_params
967
1223
 
968
1224
  oauth_token = nil
969
1225
  transaction do
1226
+ before_revoke
970
1227
  oauth_token = revoke_oauth_token
971
1228
  after_revoke
972
1229
  end
973
1230
 
974
1231
  if accepts_json?
975
- response.status = 200
976
- response["Content-Type"] ||= json_response_content_type
977
- json_response = {
1232
+ json_response_success \
978
1233
  "token" => oauth_token[oauth_tokens_token_column],
979
1234
  "refresh_token" => oauth_token[oauth_tokens_refresh_token_column],
980
1235
  "revoked_at" => oauth_token[oauth_tokens_revoked_at_column]
981
- }
982
- json_payload = _json_response_body(json_response)
983
- response.write(json_payload)
984
- request.halt
985
1236
  else
986
1237
  set_notice_flash revoke_oauth_token_notice_flash
987
1238
  redirect request.referer || "/"
988
1239
  end
989
1240
  end
990
1241
 
991
- throw_json_response_error(invalid_oauth_response_status, "invalid_request")
1242
+ redirect_response_error("invalid_request", request.referer || "/")
992
1243
  end
993
1244
  end
994
1245
 
995
- # /oauth-authorize
996
- route(:oauth_authorize) do |r|
997
- require_account
1246
+ # /authorize
1247
+ route(:authorize) do |r|
1248
+ next unless is_authorization_server?
1249
+
1250
+ before_authorize_route
1251
+ require_authorizable_account
1252
+
998
1253
  validate_oauth_grant_params
999
1254
  try_approval_prompt if use_oauth_access_type? && request.get?
1000
1255
 
1001
- before_authorize
1002
-
1003
1256
  r.get do
1004
1257
  authorize_view
1005
1258
  end
1006
1259
 
1007
1260
  r.post do
1008
- code = nil
1009
- query_params = []
1010
- fragment_params = []
1261
+ redirect_url = URI.parse(redirect_uri)
1011
1262
 
1012
1263
  transaction do
1013
- case param(response_type_param)
1014
- when "token"
1015
- redirect_response_error("invalid_request", redirect_uri) unless use_oauth_implicit_grant_type?
1016
-
1017
- create_params = {
1018
- oauth_tokens_account_id_column => account_id,
1019
- oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
1020
- oauth_tokens_scopes_column => scopes
1021
- }
1022
- oauth_token = generate_oauth_token(create_params, false)
1023
-
1024
- token_payload = json_access_token_payload(oauth_token)
1025
- fragment_params.replace(token_payload.map { |k, v| "#{k}=#{v}" })
1026
- when "code", "", nil
1027
- code = create_oauth_grant
1028
- query_params << "code=#{code}"
1029
- else
1030
- redirect_response_error("invalid_request")
1031
- end
1032
- after_authorize
1264
+ before_authorize
1265
+ do_authorize(redirect_url)
1033
1266
  end
1034
-
1035
- redirect_url = URI.parse(redirect_uri)
1036
- query_params << "state=#{state}" if state
1037
- query_params << redirect_url.query if redirect_url.query
1038
- redirect_url.query = query_params.join("&") unless query_params.empty?
1039
- redirect_url.fragment = fragment_params.join("&") unless fragment_params.empty?
1040
-
1041
1267
  redirect(redirect_url.to_s)
1042
1268
  end
1043
1269
  end