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