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