rodauth-oauth 0.0.1 → 0.0.6

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