rodauth-oauth 0.0.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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