rodauth-oauth 0.0.1 → 0.0.6

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