rodauth-oauth 0.0.4 → 0.0.5
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 +37 -0
- data/lib/rodauth/features/oauth.rb +205 -95
- data/lib/rodauth/features/oauth_jwt.rb +91 -64
- data/lib/rodauth/oauth/ttl_store.rb +59 -0
- data/lib/rodauth/oauth/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a809caa12783f20af9ba7b6d4b8710f2b783d11c5c74c0be6cbb129841ebce19
|
4
|
+
data.tar.gz: 928fd524dd2f9ec502deccc96b17eb232119543b2150363a398d04a5008b4d22
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 15b98394108c73ba4888bd92835d264750c46194b77322b0b159b45134031f0ac9c264a2a8d0a9991fb0d36b05b85b0d127d94468d77d3f7fb45777ec5bb6002
|
7
|
+
data.tar.gz: 282f53b5fd4eff08fdce56a42a7c42e3ef00cf888ced3acb2a692fd416b8dbb7250c982a53fc3c4a61a83884d8eb1068981580390e3bea2ba35d741165aa2c6a
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,43 @@
|
|
2
2
|
|
3
3
|
## master
|
4
4
|
|
5
|
+
### 0.0.5 (26/6/2020)
|
6
|
+
|
7
|
+
#### Features
|
8
|
+
|
9
|
+
* new option: `oauth_scope_separator` (default: `" "`), to define how scopes are stored;
|
10
|
+
|
11
|
+
##### Resource Server mode
|
12
|
+
|
13
|
+
`rodauth-oauth` can now be used in a resource server, i.e. only for authorizing access to resources:
|
14
|
+
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
plugin :rodauth do
|
18
|
+
enable :oauth
|
19
|
+
|
20
|
+
is_authorization_server? false
|
21
|
+
authorization_server_url "https://auth-server"
|
22
|
+
end
|
23
|
+
```
|
24
|
+
|
25
|
+
It **requires** the authorization to implement the server metadata endpoint (`/.well-known/oauth-authorization-server`), and if using JWS, the JWKs URI endpoint (unless `oauth_jwt_public_key` is defined).
|
26
|
+
|
27
|
+
#### Improvements
|
28
|
+
|
29
|
+
* Multiple Redirect URIs are now allowed for client applications out-of-the-box. In order to use it in API mode, you can pass the `redirect_uri` with an array of strings (the URLs) as values; in the new client application form, you can add several input fields with name field as `redirect_uri[]`. **ATTENTION!!** When using multiple redirect URIs, passing the desired redirect URI to the authorize form becomes mandatory.
|
30
|
+
* store scopes with whitespace instead of comma; set separator as `oauth_scope_separator` option, to keep backwards-compatibility;
|
31
|
+
* client application can now store multiple redirect uris; the POST API parameters can accept the redirect_uri param value both as a string or an array of string; internally, they'll be stored in a whitespace-separated string;
|
32
|
+
|
33
|
+
#### Bugfixes
|
34
|
+
|
35
|
+
* Fixed `RETURNING` support in the databases supporting it (such as postgres).
|
36
|
+
|
37
|
+
#### Chore
|
38
|
+
|
39
|
+
* option `scopes_param` renamed to `scope_param`;
|
40
|
+
*
|
41
|
+
|
5
42
|
## 0.0.4 (13/6/2020)
|
6
43
|
|
7
44
|
### Features
|
@@ -1,10 +1,15 @@
|
|
1
1
|
# frozen-string-literal: true
|
2
2
|
|
3
3
|
require "base64"
|
4
|
+
require "securerandom"
|
5
|
+
require "net/http"
|
6
|
+
|
7
|
+
require "rodauth/oauth/ttl_store"
|
4
8
|
|
5
9
|
module Rodauth
|
6
10
|
Feature.define(:oauth) do
|
7
11
|
# RUBY EXTENSIONS
|
12
|
+
# :nocov:
|
8
13
|
unless Regexp.method_defined?(:match?)
|
9
14
|
module RegexpExtensions
|
10
15
|
refine(Regexp) do
|
@@ -32,6 +37,7 @@ module Rodauth
|
|
32
37
|
end
|
33
38
|
using(SuffixExtensions)
|
34
39
|
end
|
40
|
+
# :nocov:
|
35
41
|
|
36
42
|
SCOPES = %w[profile.read].freeze
|
37
43
|
|
@@ -65,7 +71,7 @@ module Rodauth
|
|
65
71
|
|
66
72
|
auth_value_method :json_response_content_type, "application/json"
|
67
73
|
|
68
|
-
auth_value_method :oauth_grant_expires_in, 60 * 5 #
|
74
|
+
auth_value_method :oauth_grant_expires_in, 60 * 5 # 5 minutes
|
69
75
|
auth_value_method :oauth_token_expires_in, 60 * 60 # 60 minutes
|
70
76
|
auth_value_method :use_oauth_implicit_grant_type?, false
|
71
77
|
auth_value_method :use_oauth_pkce?, true
|
@@ -76,17 +82,7 @@ module Rodauth
|
|
76
82
|
|
77
83
|
auth_value_method :oauth_valid_uri_schemes, %w[http https]
|
78
84
|
|
79
|
-
|
80
|
-
|
81
|
-
# Authorize / token
|
82
|
-
%w[
|
83
|
-
grant_type code refresh_token client_id client_secret scope
|
84
|
-
state redirect_uri scopes token_type_hint token
|
85
|
-
access_type approval_prompt response_type
|
86
|
-
code_challenge code_challenge_method code_verifier
|
87
|
-
].each do |param|
|
88
|
-
auth_value_method :"#{param}_param", param
|
89
|
-
end
|
85
|
+
auth_value_method :oauth_scope_separator, " "
|
90
86
|
|
91
87
|
# Application
|
92
88
|
APPLICATION_REQUIRED_PARAMS = %w[name description scopes homepage_url redirect_uri client_secret].freeze
|
@@ -94,7 +90,11 @@ module Rodauth
|
|
94
90
|
|
95
91
|
(APPLICATION_REQUIRED_PARAMS + %w[client_id]).each do |param|
|
96
92
|
auth_value_method :"oauth_application_#{param}_param", param
|
93
|
+
translatable_method :"#{param}_label", param.gsub("_", " ").capitalize
|
97
94
|
end
|
95
|
+
button "Register", "oauth_application"
|
96
|
+
button "Authorize", "oauth_authorize"
|
97
|
+
button "Revoke", "oauth_token_revoke"
|
98
98
|
|
99
99
|
# OAuth Token
|
100
100
|
auth_value_method :oauth_tokens_path, "oauth-tokens"
|
@@ -173,11 +173,18 @@ module Rodauth
|
|
173
173
|
auth_value_method :oauth_metadata_op_policy_uri, nil
|
174
174
|
auth_value_method :oauth_metadata_op_tos_uri, nil
|
175
175
|
|
176
|
+
# Resource Server params
|
177
|
+
# Only required to use if the plugin is to be used in a resource server
|
178
|
+
auth_value_method :is_authorization_server?, true
|
179
|
+
|
176
180
|
auth_value_methods(
|
177
181
|
:fetch_access_token,
|
178
182
|
:oauth_unique_id_generator,
|
179
183
|
:secret_matches?,
|
180
|
-
:secret_hash
|
184
|
+
:secret_hash,
|
185
|
+
:generate_token_hash,
|
186
|
+
:authorization_server_url,
|
187
|
+
:before_introspection_request
|
181
188
|
)
|
182
189
|
|
183
190
|
auth_value_methods(:only_json?)
|
@@ -198,6 +205,8 @@ module Rodauth
|
|
198
205
|
|
199
206
|
auth_value_method :json_request_regexp, %r{\bapplication/(?:vnd\.api\+)?json\b}i
|
200
207
|
|
208
|
+
SERVER_METADATA = OAuth::TtlStore.new
|
209
|
+
|
201
210
|
def check_csrf?
|
202
211
|
case request.path
|
203
212
|
when oauth_token_path, oauth_introspect_path
|
@@ -223,12 +232,14 @@ module Rodauth
|
|
223
232
|
end
|
224
233
|
|
225
234
|
unless method_defined?(:json_request?)
|
235
|
+
# :nocov:
|
226
236
|
# copied from the jwt feature
|
227
237
|
def json_request?
|
228
238
|
return @json_request if defined?(@json_request)
|
229
239
|
|
230
240
|
@json_request = request.content_type =~ json_request_regexp
|
231
241
|
end
|
242
|
+
# :nocov:
|
232
243
|
end
|
233
244
|
|
234
245
|
def initialize(scope)
|
@@ -236,38 +247,39 @@ module Rodauth
|
|
236
247
|
end
|
237
248
|
|
238
249
|
def state
|
239
|
-
param_or_nil(
|
250
|
+
param_or_nil("state")
|
240
251
|
end
|
241
252
|
|
242
253
|
def scopes
|
243
|
-
(param_or_nil(
|
254
|
+
(param_or_nil("scope") || oauth_application_default_scope).split(" ")
|
244
255
|
end
|
245
256
|
|
246
257
|
def client_id
|
247
|
-
param_or_nil(
|
248
|
-
end
|
249
|
-
|
250
|
-
def client_secret
|
251
|
-
param_or_nil(client_secret_param)
|
258
|
+
param_or_nil("client_id")
|
252
259
|
end
|
253
260
|
|
254
261
|
def redirect_uri
|
255
|
-
param_or_nil(
|
262
|
+
param_or_nil("redirect_uri") || begin
|
263
|
+
return unless oauth_application
|
264
|
+
|
265
|
+
redirect_uris = oauth_application[oauth_applications_redirect_uri_column].split(" ")
|
266
|
+
redirect_uris.size == 1 ? redirect_uris.first : nil
|
267
|
+
end
|
256
268
|
end
|
257
269
|
|
258
270
|
def token_type_hint
|
259
|
-
param_or_nil(
|
271
|
+
param_or_nil("token_type_hint") || "access_token"
|
260
272
|
end
|
261
273
|
|
262
274
|
def token
|
263
|
-
param_or_nil(
|
275
|
+
param_or_nil("token")
|
264
276
|
end
|
265
277
|
|
266
278
|
def oauth_application
|
267
279
|
return @oauth_application if defined?(@oauth_application)
|
268
280
|
|
269
281
|
@oauth_application = begin
|
270
|
-
client_id =
|
282
|
+
client_id = param_or_nil("client_id")
|
271
283
|
|
272
284
|
return unless client_id
|
273
285
|
|
@@ -291,17 +303,36 @@ module Rodauth
|
|
291
303
|
return @authorization_token if defined?(@authorization_token)
|
292
304
|
|
293
305
|
# check if there is a token
|
306
|
+
bearer_token = fetch_access_token
|
307
|
+
|
308
|
+
return unless bearer_token
|
309
|
+
|
294
310
|
# check if token has not expired
|
295
311
|
# check if token has been revoked
|
296
|
-
@authorization_token = oauth_token_by_token(
|
312
|
+
@authorization_token = oauth_token_by_token(bearer_token)
|
297
313
|
end
|
298
314
|
|
299
315
|
def require_oauth_authorization(*scopes)
|
300
|
-
|
316
|
+
token_scopes = if is_authorization_server?
|
317
|
+
authorization_required unless authorization_token
|
318
|
+
|
319
|
+
scopes << oauth_application_default_scope if scopes.empty?
|
320
|
+
|
321
|
+
authorization_token[oauth_tokens_scopes_column].split(oauth_scope_separator)
|
322
|
+
else
|
323
|
+
bearer_token = fetch_access_token
|
324
|
+
|
325
|
+
authorization_required unless bearer_token
|
326
|
+
|
327
|
+
scopes << oauth_application_default_scope if scopes.empty?
|
301
328
|
|
302
|
-
|
329
|
+
# where in resource server, NOT the authorization server.
|
330
|
+
payload = introspection_request("access_token", bearer_token)
|
303
331
|
|
304
|
-
|
332
|
+
authorization_required unless payload["active"]
|
333
|
+
|
334
|
+
payload["scope"].split(oauth_scope_separator)
|
335
|
+
end
|
305
336
|
|
306
337
|
authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
|
307
338
|
end
|
@@ -314,6 +345,7 @@ module Rodauth
|
|
314
345
|
request.get "new" do
|
315
346
|
new_oauth_application_view
|
316
347
|
end
|
348
|
+
|
317
349
|
request.on(oauth_applications_id_pattern) do |id|
|
318
350
|
oauth_application = db[oauth_applications_table].where(oauth_applications_id_column => id).first
|
319
351
|
scope.instance_variable_set(:@oauth_application, oauth_application)
|
@@ -366,6 +398,64 @@ module Rodauth
|
|
366
398
|
|
367
399
|
private
|
368
400
|
|
401
|
+
def authorization_server_url
|
402
|
+
base_url
|
403
|
+
end
|
404
|
+
|
405
|
+
def authorization_server_metadata
|
406
|
+
auth_url = URI(authorization_server_url)
|
407
|
+
|
408
|
+
server_metadata = SERVER_METADATA[auth_url]
|
409
|
+
|
410
|
+
return server_metadata if server_metadata
|
411
|
+
|
412
|
+
SERVER_METADATA.set(auth_url) do
|
413
|
+
http = Net::HTTP.new(auth_url.host, auth_url.port)
|
414
|
+
http.use_ssl = auth_url.scheme == "https"
|
415
|
+
|
416
|
+
request = Net::HTTP::Get.new("/.well-known/oauth-authorization-server")
|
417
|
+
request["accept"] = json_response_content_type
|
418
|
+
response = http.request(request)
|
419
|
+
authorization_required unless response.code.to_i == 200
|
420
|
+
|
421
|
+
# time-to-live
|
422
|
+
ttl = if response.key?("cache-control")
|
423
|
+
cache_control = response["cache_control"]
|
424
|
+
cache_control[/max-age=(\d+)/, 1]
|
425
|
+
elsif response.key?("expires")
|
426
|
+
Time.httpdate(response["expires"]).utc.to_i - Time.now.utc.to_i
|
427
|
+
end
|
428
|
+
|
429
|
+
[JSON.parse(response.body, symbolize_names: true), ttl]
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
def introspection_request(token_type_hint, token)
|
434
|
+
auth_url = URI(authorization_server_url)
|
435
|
+
http = Net::HTTP.new(auth_url.host, auth_url.port)
|
436
|
+
http.use_ssl = auth_url.scheme == "https"
|
437
|
+
|
438
|
+
request = Net::HTTP::Post.new(oauth_introspect_path)
|
439
|
+
request["content-type"] = json_response_content_type
|
440
|
+
request["accept"] = json_response_content_type
|
441
|
+
request.body = JSON.dump({ "token_type_hint" => token_type_hint, "token" => token })
|
442
|
+
|
443
|
+
before_introspection_request(request)
|
444
|
+
response = http.request(request)
|
445
|
+
authorization_required unless response.code.to_i == 200
|
446
|
+
|
447
|
+
JSON.parse(response.body)
|
448
|
+
end
|
449
|
+
|
450
|
+
def before_introspection_request(request); end
|
451
|
+
|
452
|
+
def template_path(page)
|
453
|
+
path = File.join(File.dirname(__FILE__), "../../../templates", "#{page}.str")
|
454
|
+
return super unless File.exist?(path)
|
455
|
+
|
456
|
+
path
|
457
|
+
end
|
458
|
+
|
369
459
|
# to be used internally. Same semantics as require account, must:
|
370
460
|
# fetch an authorization basic header
|
371
461
|
# parse client id and secret
|
@@ -378,8 +468,8 @@ module Rodauth
|
|
378
468
|
if (token = ((v = request.env["HTTP_AUTHORIZATION"]) && v[/\A *Basic (.*)\Z/, 1]))
|
379
469
|
client_id, client_secret = Base64.decode64(token).split(/:/, 2)
|
380
470
|
else
|
381
|
-
client_id = param_or_nil(
|
382
|
-
client_secret = param_or_nil(
|
471
|
+
client_id = param_or_nil("client_id")
|
472
|
+
client_secret = param_or_nil("client_secret")
|
383
473
|
end
|
384
474
|
|
385
475
|
authorization_required unless client_id
|
@@ -387,7 +477,7 @@ module Rodauth
|
|
387
477
|
@oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => client_id).first
|
388
478
|
|
389
479
|
# skip if using pkce
|
390
|
-
return if @oauth_application && use_oauth_pkce? && param_or_nil(
|
480
|
+
return if @oauth_application && use_oauth_pkce? && param_or_nil("code_verifier")
|
391
481
|
|
392
482
|
authorization_required unless @oauth_application && secret_matches?(@oauth_application, client_secret)
|
393
483
|
end
|
@@ -413,22 +503,22 @@ module Rodauth
|
|
413
503
|
end
|
414
504
|
|
415
505
|
unless method_defined?(:password_hash)
|
506
|
+
# :nocov:
|
416
507
|
# From login_requirements_base feature
|
417
508
|
if ENV["RACK_ENV"] == "test"
|
418
509
|
def password_hash_cost
|
419
510
|
BCrypt::Engine::MIN_COST
|
420
511
|
end
|
421
512
|
else
|
422
|
-
# :nocov:
|
423
513
|
def password_hash_cost
|
424
514
|
BCrypt::Engine::DEFAULT_COST
|
425
515
|
end
|
426
|
-
# :nocov:
|
427
516
|
end
|
428
517
|
|
429
518
|
def password_hash(password)
|
430
519
|
BCrypt::Password.create(password, cost: password_hash_cost)
|
431
520
|
end
|
521
|
+
# :nocov:
|
432
522
|
end
|
433
523
|
|
434
524
|
def generate_oauth_token(params = {}, should_generate_refresh_token = true)
|
@@ -466,7 +556,7 @@ module Rodauth
|
|
466
556
|
|
467
557
|
begin
|
468
558
|
if ds.supports_returning?(:insert)
|
469
|
-
ds.returning.insert(params)
|
559
|
+
ds.returning.insert(params).first
|
470
560
|
else
|
471
561
|
id = ds.insert(params)
|
472
562
|
ds.where(oauth_tokens_id_column => id).first
|
@@ -523,11 +613,21 @@ module Rodauth
|
|
523
613
|
|
524
614
|
def validate_oauth_application_params
|
525
615
|
oauth_application_params.each do |key, value|
|
526
|
-
if key == oauth_application_homepage_url_param
|
527
|
-
|
616
|
+
if key == oauth_application_homepage_url_param
|
617
|
+
|
618
|
+
set_field_error(key, invalid_url_message) unless check_valid_uri?(value)
|
528
619
|
|
529
|
-
|
620
|
+
elsif key == oauth_application_redirect_uri_param
|
530
621
|
|
622
|
+
if value.respond_to?(:each)
|
623
|
+
value.each do |uri|
|
624
|
+
next if uri.empty?
|
625
|
+
|
626
|
+
set_field_error(key, invalid_url_message) unless check_valid_uri?(uri)
|
627
|
+
end
|
628
|
+
else
|
629
|
+
set_field_error(key, invalid_url_message) unless check_valid_uri?(value)
|
630
|
+
end
|
531
631
|
elsif key == oauth_application_scopes_param
|
532
632
|
|
533
633
|
value.each do |scope|
|
@@ -545,10 +645,12 @@ module Rodauth
|
|
545
645
|
oauth_applications_name_column => oauth_application_params[oauth_application_name_param],
|
546
646
|
oauth_applications_description_column => oauth_application_params[oauth_application_description_param],
|
547
647
|
oauth_applications_scopes_column => oauth_application_params[oauth_application_scopes_param],
|
548
|
-
oauth_applications_homepage_url_column => oauth_application_params[oauth_application_homepage_url_param]
|
549
|
-
oauth_applications_redirect_uri_column => oauth_application_params[oauth_application_redirect_uri_param]
|
648
|
+
oauth_applications_homepage_url_column => oauth_application_params[oauth_application_homepage_url_param]
|
550
649
|
}
|
551
650
|
|
651
|
+
redirect_uris = oauth_application_params[oauth_application_redirect_uri_param]
|
652
|
+
redirect_uris = redirect_uris.to_a.reject(&:empty?).join(" ") if redirect_uris.respond_to?(:each)
|
653
|
+
create_params[oauth_applications_redirect_uri_column] = redirect_uris unless redirect_uris.empty?
|
552
654
|
# set client ID/secret pairs
|
553
655
|
|
554
656
|
create_params.merge! \
|
@@ -557,25 +659,18 @@ module Rodauth
|
|
557
659
|
secret_hash(oauth_application_params[oauth_application_client_secret_param])
|
558
660
|
|
559
661
|
create_params[oauth_applications_scopes_column] = if create_params[oauth_applications_scopes_column]
|
560
|
-
create_params[oauth_applications_scopes_column].join(
|
662
|
+
create_params[oauth_applications_scopes_column].join(oauth_scope_separator)
|
561
663
|
else
|
562
664
|
oauth_application_default_scope
|
563
665
|
end
|
564
666
|
|
565
|
-
ds = db[oauth_applications_table]
|
566
|
-
|
567
667
|
id = nil
|
568
668
|
raised = begin
|
569
|
-
|
570
|
-
|
571
|
-
else
|
572
|
-
id = db[oauth_applications_table].insert(create_params)
|
573
|
-
db[oauth_applications_table].where(oauth_applications_id_column => id).get(oauth_applications_id_column)
|
574
|
-
end
|
575
|
-
false
|
669
|
+
id = db[oauth_applications_table].insert(create_params)
|
670
|
+
false
|
576
671
|
rescue Sequel::ConstraintViolation => e
|
577
672
|
e
|
578
|
-
|
673
|
+
end
|
579
674
|
|
580
675
|
if raised
|
581
676
|
field = raised.message[/\.(.*)$/, 1]
|
@@ -596,6 +691,8 @@ module Rodauth
|
|
596
691
|
end
|
597
692
|
|
598
693
|
def validate_oauth_grant_params
|
694
|
+
redirect_response_error("invalid_request", request.referer || default_redirect) unless oauth_application && check_valid_redirect_uri?
|
695
|
+
|
599
696
|
unless oauth_application && check_valid_redirect_uri? && check_valid_access_type? &&
|
600
697
|
check_valid_approval_prompt? && check_valid_response_type?
|
601
698
|
redirect_response_error("invalid_request")
|
@@ -606,7 +703,7 @@ module Rodauth
|
|
606
703
|
end
|
607
704
|
|
608
705
|
def try_approval_prompt
|
609
|
-
approval_prompt = param_or_nil(
|
706
|
+
approval_prompt = param_or_nil("approval_prompt")
|
610
707
|
|
611
708
|
return unless approval_prompt && approval_prompt == "auto"
|
612
709
|
|
@@ -614,7 +711,7 @@ module Rodauth
|
|
614
711
|
oauth_grants_account_id_column => account_id,
|
615
712
|
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
616
713
|
oauth_grants_redirect_uri_column => redirect_uri,
|
617
|
-
oauth_grants_scopes_column => scopes.join(
|
714
|
+
oauth_grants_scopes_column => scopes.join(oauth_scope_separator),
|
618
715
|
oauth_grants_access_type_column => "online"
|
619
716
|
).count.zero?
|
620
717
|
|
@@ -628,14 +725,13 @@ module Rodauth
|
|
628
725
|
oauth_grants_account_id_column => account_id,
|
629
726
|
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
630
727
|
oauth_grants_redirect_uri_column => redirect_uri,
|
631
|
-
oauth_grants_code_column => oauth_unique_id_generator,
|
632
728
|
oauth_grants_expires_in_column => Time.now + oauth_grant_expires_in,
|
633
|
-
oauth_grants_scopes_column => scopes.join(
|
729
|
+
oauth_grants_scopes_column => scopes.join(oauth_scope_separator)
|
634
730
|
}
|
635
731
|
|
636
732
|
# Access Type flow
|
637
733
|
if use_oauth_access_type?
|
638
|
-
if (access_type = param_or_nil(
|
734
|
+
if (access_type = param_or_nil("access_type"))
|
639
735
|
create_params[oauth_grants_access_type_column] = access_type
|
640
736
|
end
|
641
737
|
end
|
@@ -643,8 +739,8 @@ module Rodauth
|
|
643
739
|
# PKCE flow
|
644
740
|
if use_oauth_pkce?
|
645
741
|
|
646
|
-
if (code_challenge = param_or_nil(
|
647
|
-
code_challenge_method = param_or_nil(
|
742
|
+
if (code_challenge = param_or_nil("code_challenge"))
|
743
|
+
code_challenge_method = param_or_nil("code_challenge_method")
|
648
744
|
|
649
745
|
create_params[oauth_grants_code_challenge_column] = code_challenge
|
650
746
|
create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
|
@@ -656,12 +752,10 @@ module Rodauth
|
|
656
752
|
ds = db[oauth_grants_table]
|
657
753
|
|
658
754
|
begin
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
ds.where(oauth_grants_id_column => id).get(oauth_grants_code_column)
|
664
|
-
end
|
755
|
+
authorization_code = oauth_unique_id_generator
|
756
|
+
create_params[oauth_grants_code_column] = authorization_code
|
757
|
+
ds.insert(create_params)
|
758
|
+
authorization_code
|
665
759
|
rescue Sequel::UniqueConstraintViolation
|
666
760
|
retry
|
667
761
|
end
|
@@ -674,23 +768,23 @@ module Rodauth
|
|
674
768
|
end
|
675
769
|
|
676
770
|
def validate_oauth_token_params
|
677
|
-
unless (grant_type = param_or_nil(
|
771
|
+
unless (grant_type = param_or_nil("grant_type"))
|
678
772
|
redirect_response_error("invalid_request")
|
679
773
|
end
|
680
774
|
|
681
775
|
case grant_type
|
682
776
|
when "authorization_code"
|
683
|
-
redirect_response_error("invalid_request") unless param_or_nil(
|
777
|
+
redirect_response_error("invalid_request") unless param_or_nil("code")
|
684
778
|
|
685
779
|
when "refresh_token"
|
686
|
-
redirect_response_error("invalid_request") unless param_or_nil(
|
780
|
+
redirect_response_error("invalid_request") unless param_or_nil("refresh_token")
|
687
781
|
else
|
688
782
|
redirect_response_error("invalid_request")
|
689
783
|
end
|
690
784
|
end
|
691
785
|
|
692
786
|
def create_oauth_token
|
693
|
-
case param(
|
787
|
+
case param("grant_type")
|
694
788
|
when "authorization_code"
|
695
789
|
create_oauth_token_from_authorization_code(oauth_application)
|
696
790
|
when "refresh_token"
|
@@ -703,8 +797,8 @@ module Rodauth
|
|
703
797
|
def create_oauth_token_from_authorization_code(oauth_application)
|
704
798
|
# fetch oauth grant
|
705
799
|
oauth_grant = db[oauth_grants_table].where(
|
706
|
-
oauth_grants_code_column => param(
|
707
|
-
oauth_grants_redirect_uri_column => param(
|
800
|
+
oauth_grants_code_column => param("code"),
|
801
|
+
oauth_grants_redirect_uri_column => param("redirect_uri"),
|
708
802
|
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
709
803
|
oauth_grants_revoked_at_column => nil
|
710
804
|
).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
|
@@ -716,7 +810,7 @@ module Rodauth
|
|
716
810
|
# PKCE
|
717
811
|
if use_oauth_pkce?
|
718
812
|
if oauth_grant[oauth_grants_code_challenge_column]
|
719
|
-
code_verifier = param_or_nil(
|
813
|
+
code_verifier = param_or_nil("code_verifier")
|
720
814
|
|
721
815
|
redirect_response_error("invalid_request") unless code_verifier && check_valid_grant_challenge?(oauth_grant, code_verifier)
|
722
816
|
elsif oauth_require_pkce
|
@@ -743,7 +837,7 @@ module Rodauth
|
|
743
837
|
|
744
838
|
def create_oauth_token_from_token(oauth_application)
|
745
839
|
# fetch oauth token
|
746
|
-
oauth_token = oauth_token_by_refresh_token(param(
|
840
|
+
oauth_token = oauth_token_by_refresh_token(param("refresh_token"))
|
747
841
|
|
748
842
|
redirect_response_error("invalid_grant") unless oauth_token && token_from_application?(oauth_token, oauth_application)
|
749
843
|
|
@@ -764,7 +858,7 @@ module Rodauth
|
|
764
858
|
|
765
859
|
oauth_token = begin
|
766
860
|
if ds.supports_returning?(:update)
|
767
|
-
ds.returning.update(update_params)
|
861
|
+
ds.returning.update(update_params).first
|
768
862
|
else
|
769
863
|
ds.update(update_params)
|
770
864
|
ds.first
|
@@ -787,7 +881,7 @@ module Rodauth
|
|
787
881
|
redirect_response_error("unsupported_token_type") unless TOKEN_HINT_TYPES.include?(token_type_hint)
|
788
882
|
end
|
789
883
|
|
790
|
-
redirect_response_error("invalid_request") unless param_or_nil(
|
884
|
+
redirect_response_error("invalid_request") unless param_or_nil("token")
|
791
885
|
end
|
792
886
|
|
793
887
|
def json_token_introspect_payload(token)
|
@@ -795,16 +889,14 @@ module Rodauth
|
|
795
889
|
|
796
890
|
{
|
797
891
|
active: true,
|
798
|
-
scope: token[oauth_tokens_scopes_column]
|
892
|
+
scope: token[oauth_tokens_scopes_column],
|
799
893
|
client_id: oauth_application[oauth_applications_client_id_column],
|
800
894
|
# username
|
801
895
|
token_type: oauth_token_type
|
802
896
|
}
|
803
897
|
end
|
804
898
|
|
805
|
-
def before_introspect
|
806
|
-
require_oauth_application
|
807
|
-
end
|
899
|
+
def before_introspect; end
|
808
900
|
|
809
901
|
# Token revocation
|
810
902
|
|
@@ -816,7 +908,7 @@ module Rodauth
|
|
816
908
|
# check if valid token hint type
|
817
909
|
redirect_response_error("unsupported_token_type") unless TOKEN_HINT_TYPES.include?(token_type_hint)
|
818
910
|
|
819
|
-
redirect_response_error("invalid_request") unless param_or_nil(
|
911
|
+
redirect_response_error("invalid_request") unless param_or_nil("token")
|
820
912
|
end
|
821
913
|
|
822
914
|
def revoke_oauth_token
|
@@ -827,14 +919,21 @@ module Rodauth
|
|
827
919
|
oauth_token_by_refresh_token(token)
|
828
920
|
end
|
829
921
|
|
830
|
-
redirect_response_error("invalid_request") unless oauth_token
|
922
|
+
redirect_response_error("invalid_request") unless oauth_token
|
923
|
+
|
924
|
+
if oauth_application
|
925
|
+
redirect_response_error("invalid_request") unless token_from_application?(oauth_token, oauth_application)
|
926
|
+
else
|
927
|
+
@oauth_application = db[oauth_applications_table].where(oauth_applications_id_column =>
|
928
|
+
oauth_token[oauth_tokens_oauth_application_id_column]).first
|
929
|
+
end
|
831
930
|
|
832
931
|
update_params = { oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP }
|
833
932
|
|
834
933
|
ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
|
835
934
|
|
836
935
|
oauth_token = if ds.supports_returning?(:update)
|
837
|
-
ds.returning.update(update_params)
|
936
|
+
ds.returning.update(update_params).first
|
838
937
|
else
|
839
938
|
ds.update(update_params)
|
840
939
|
ds.first
|
@@ -854,7 +953,7 @@ module Rodauth
|
|
854
953
|
|
855
954
|
# Response helpers
|
856
955
|
|
857
|
-
def redirect_response_error(error_code, redirect_url = request.referer || default_redirect)
|
956
|
+
def redirect_response_error(error_code, redirect_url = redirect_uri || request.referer || default_redirect)
|
858
957
|
if accepts_json?
|
859
958
|
throw_json_response_error(invalid_oauth_response_status, error_code)
|
860
959
|
else
|
@@ -903,6 +1002,7 @@ module Rodauth
|
|
903
1002
|
end
|
904
1003
|
|
905
1004
|
unless method_defined?(:_json_response_body)
|
1005
|
+
# :nocov:
|
906
1006
|
def _json_response_body(hash)
|
907
1007
|
if request.respond_to?(:convert_to_json)
|
908
1008
|
request.send(:convert_to_json, hash)
|
@@ -910,6 +1010,7 @@ module Rodauth
|
|
910
1010
|
JSON.dump(hash)
|
911
1011
|
end
|
912
1012
|
end
|
1013
|
+
# :nocov:
|
913
1014
|
end
|
914
1015
|
|
915
1016
|
def authorization_required
|
@@ -921,14 +1022,18 @@ module Rodauth
|
|
921
1022
|
end
|
922
1023
|
end
|
923
1024
|
|
1025
|
+
def check_valid_uri?(uri)
|
1026
|
+
URI::DEFAULT_PARSER.make_regexp(oauth_valid_uri_schemes).match?(uri)
|
1027
|
+
end
|
1028
|
+
|
924
1029
|
def check_valid_scopes?
|
925
1030
|
return false unless scopes
|
926
1031
|
|
927
|
-
(scopes - oauth_application[oauth_applications_scopes_column].split(
|
1032
|
+
(scopes - oauth_application[oauth_applications_scopes_column].split(oauth_scope_separator)).empty?
|
928
1033
|
end
|
929
1034
|
|
930
1035
|
def check_valid_redirect_uri?
|
931
|
-
|
1036
|
+
oauth_application[oauth_applications_redirect_uri_column].split(" ").include?(redirect_uri)
|
932
1037
|
end
|
933
1038
|
|
934
1039
|
ACCESS_TYPES = %w[offline online].freeze
|
@@ -936,7 +1041,7 @@ module Rodauth
|
|
936
1041
|
def check_valid_access_type?
|
937
1042
|
return true unless use_oauth_access_type?
|
938
1043
|
|
939
|
-
access_type = param_or_nil(
|
1044
|
+
access_type = param_or_nil("access_type")
|
940
1045
|
!access_type || ACCESS_TYPES.include?(access_type)
|
941
1046
|
end
|
942
1047
|
|
@@ -945,12 +1050,12 @@ module Rodauth
|
|
945
1050
|
def check_valid_approval_prompt?
|
946
1051
|
return true unless use_oauth_access_type?
|
947
1052
|
|
948
|
-
approval_prompt = param_or_nil(
|
1053
|
+
approval_prompt = param_or_nil("approval_prompt")
|
949
1054
|
!approval_prompt || APPROVAL_PROMPTS.include?(approval_prompt)
|
950
1055
|
end
|
951
1056
|
|
952
1057
|
def check_valid_response_type?
|
953
|
-
response_type = param_or_nil(
|
1058
|
+
response_type = param_or_nil("response_type")
|
954
1059
|
|
955
1060
|
return true if response_type.nil? || response_type == "code"
|
956
1061
|
|
@@ -962,9 +1067,9 @@ module Rodauth
|
|
962
1067
|
# PKCE
|
963
1068
|
|
964
1069
|
def validate_pkce_challenge_params
|
965
|
-
if param_or_nil(
|
1070
|
+
if param_or_nil("code_challenge")
|
966
1071
|
|
967
|
-
challenge_method = param_or_nil(
|
1072
|
+
challenge_method = param_or_nil("code_challenge_method")
|
968
1073
|
redirect_response_error("code_challenge_required") unless oauth_pkce_challenge_method == challenge_method
|
969
1074
|
else
|
970
1075
|
return unless oauth_require_pkce
|
@@ -1054,16 +1159,21 @@ module Rodauth
|
|
1054
1159
|
catch_error do
|
1055
1160
|
validate_oauth_introspect_params
|
1056
1161
|
|
1057
|
-
oauth_token = case param(
|
1162
|
+
oauth_token = case param("token_type_hint")
|
1058
1163
|
when "access_token"
|
1059
|
-
oauth_token_by_token(param(
|
1164
|
+
oauth_token_by_token(param("token"))
|
1060
1165
|
when "refresh_token"
|
1061
|
-
oauth_token_by_refresh_token(param(
|
1166
|
+
oauth_token_by_refresh_token(param("token"))
|
1062
1167
|
else
|
1063
|
-
oauth_token_by_token(param(
|
1168
|
+
oauth_token_by_token(param("token")) || oauth_token_by_refresh_token(param("token"))
|
1064
1169
|
end
|
1065
1170
|
|
1066
|
-
|
1171
|
+
if oauth_application
|
1172
|
+
redirect_response_error("invalid_request") if oauth_token && !token_from_application?(oauth_token, oauth_application)
|
1173
|
+
elsif oauth_token
|
1174
|
+
@oauth_application = db[oauth_applications_table].where(oauth_applications_id_column =>
|
1175
|
+
oauth_token[oauth_tokens_oauth_application_id_column]).first
|
1176
|
+
end
|
1067
1177
|
|
1068
1178
|
json_response_success(json_token_introspect_payload(oauth_token))
|
1069
1179
|
end
|
@@ -1120,9 +1230,9 @@ module Rodauth
|
|
1120
1230
|
fragment_params = []
|
1121
1231
|
|
1122
1232
|
transaction do
|
1123
|
-
case param(
|
1233
|
+
case param("response_type")
|
1124
1234
|
when "token"
|
1125
|
-
redirect_response_error("invalid_request"
|
1235
|
+
redirect_response_error("invalid_request") unless use_oauth_implicit_grant_type?
|
1126
1236
|
|
1127
1237
|
create_params = {
|
1128
1238
|
oauth_tokens_account_id_column => account_id,
|
@@ -1,22 +1,17 @@
|
|
1
1
|
# frozen-string-literal: true
|
2
2
|
|
3
|
+
require "rodauth/oauth/ttl_store"
|
4
|
+
|
3
5
|
module Rodauth
|
4
6
|
Feature.define(:oauth_jwt) do
|
5
7
|
depends :oauth
|
6
8
|
|
7
|
-
auth_value_method :grant_type_param, "grant_type"
|
8
|
-
auth_value_method :assertion_param, "assertion"
|
9
|
-
|
10
9
|
auth_value_method :oauth_jwt_token_issuer, "Example"
|
11
10
|
|
12
11
|
auth_value_method :oauth_jwt_key, nil
|
13
12
|
auth_value_method :oauth_jwt_public_key, nil
|
14
13
|
auth_value_method :oauth_jwt_algorithm, "HS256"
|
15
14
|
|
16
|
-
auth_value_method :oauth_jwt_jwk_key, nil
|
17
|
-
auth_value_method :oauth_jwt_jwk_public_key, nil
|
18
|
-
auth_value_method :oauth_jwt_jwk_algorithm, "RS256"
|
19
|
-
|
20
15
|
auth_value_method :oauth_jwt_jwe_key, nil
|
21
16
|
auth_value_method :oauth_jwt_jwe_public_key, nil
|
22
17
|
auth_value_method :oauth_jwt_jwe_algorithm, nil
|
@@ -31,6 +26,8 @@ module Rodauth
|
|
31
26
|
:jwks_set
|
32
27
|
)
|
33
28
|
|
29
|
+
JWKS = OAuth::TtlStore.new
|
30
|
+
|
34
31
|
def require_oauth_authorization(*scopes)
|
35
32
|
authorization_required unless authorization_token
|
36
33
|
|
@@ -46,28 +43,42 @@ module Rodauth
|
|
46
43
|
def authorization_token
|
47
44
|
return @authorization_token if defined?(@authorization_token)
|
48
45
|
|
49
|
-
@authorization_token =
|
46
|
+
@authorization_token = begin
|
47
|
+
bearer_token = fetch_access_token
|
48
|
+
|
49
|
+
return unless bearer_token
|
50
|
+
|
51
|
+
jwt_token = jwt_decode(bearer_token)
|
52
|
+
|
53
|
+
return unless jwt_token
|
54
|
+
|
55
|
+
return if jwt_token["iss"] != oauth_jwt_token_issuer ||
|
56
|
+
jwt_token["aud"] != oauth_jwt_audience ||
|
57
|
+
!jwt_token["sub"]
|
58
|
+
|
59
|
+
jwt_token
|
60
|
+
end
|
50
61
|
end
|
51
62
|
|
52
63
|
# /token
|
53
64
|
|
54
65
|
def before_token
|
55
66
|
# requset authentication optional for assertions
|
56
|
-
return if param(
|
67
|
+
return if param("grant_type") == "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
57
68
|
|
58
69
|
super
|
59
70
|
end
|
60
71
|
|
61
72
|
def validate_oauth_token_params
|
62
|
-
if param(
|
63
|
-
redirect_response_error("invalid_client") unless param_or_nil(
|
73
|
+
if param("grant_type") == "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
74
|
+
redirect_response_error("invalid_client") unless param_or_nil("assertion")
|
64
75
|
else
|
65
76
|
super
|
66
77
|
end
|
67
78
|
end
|
68
79
|
|
69
80
|
def create_oauth_token
|
70
|
-
if param(
|
81
|
+
if param("grant_type") == "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
71
82
|
create_oauth_token_from_assertion
|
72
83
|
else
|
73
84
|
super
|
@@ -75,7 +86,7 @@ module Rodauth
|
|
75
86
|
end
|
76
87
|
|
77
88
|
def create_oauth_token_from_assertion
|
78
|
-
claims = jwt_decode(param(
|
89
|
+
claims = jwt_decode(param("assertion"))
|
79
90
|
|
80
91
|
redirect_response_error("invalid_grant") unless claims
|
81
92
|
|
@@ -111,7 +122,7 @@ module Rodauth
|
|
111
122
|
|
112
123
|
oauth_token = _generate_oauth_token(create_params)
|
113
124
|
|
114
|
-
issued_at = Time.
|
125
|
+
issued_at = Time.now.utc.to_i
|
115
126
|
|
116
127
|
payload = {
|
117
128
|
sub: oauth_token[oauth_tokens_account_id_column],
|
@@ -133,7 +144,7 @@ module Rodauth
|
|
133
144
|
|
134
145
|
# one of the points of using jwt is avoiding database lookups, so we put here all relevant
|
135
146
|
# token data.
|
136
|
-
scope: oauth_token[oauth_tokens_scopes_column]
|
147
|
+
scope: oauth_token[oauth_tokens_scopes_column]
|
137
148
|
}
|
138
149
|
|
139
150
|
token = jwt_encode(payload)
|
@@ -182,7 +193,42 @@ module Rodauth
|
|
182
193
|
end
|
183
194
|
|
184
195
|
def _jwt_key
|
185
|
-
@_jwt_key ||= oauth_jwt_key || oauth_application[oauth_applications_client_secret_column]
|
196
|
+
@_jwt_key ||= oauth_jwt_key || (oauth_application[oauth_applications_client_secret_column] if oauth_application)
|
197
|
+
end
|
198
|
+
|
199
|
+
# Resource Server only!
|
200
|
+
#
|
201
|
+
# returns the jwks set from the authorization server.
|
202
|
+
def auth_server_jwks_set
|
203
|
+
metadata = authorization_server_metadata
|
204
|
+
|
205
|
+
return unless metadata && (jwks_uri = metadata[:jwks_uri])
|
206
|
+
|
207
|
+
jwks_uri = URI(jwks_uri)
|
208
|
+
|
209
|
+
jwks = JWKS[jwks_uri]
|
210
|
+
|
211
|
+
return jwks if jwks
|
212
|
+
|
213
|
+
JWKS.set(jwks_uri) do
|
214
|
+
http = Net::HTTP.new(jwks_uri.host, jwks_uri.port)
|
215
|
+
http.use_ssl = jwks_uri.scheme == "https"
|
216
|
+
|
217
|
+
request = Net::HTTP::Get.new(jwks_uri.request_uri)
|
218
|
+
request["accept"] = json_response_content_type
|
219
|
+
response = http.request(request)
|
220
|
+
authorization_required unless response.code.to_i == 200
|
221
|
+
|
222
|
+
# time-to-live
|
223
|
+
ttl = if response.key?("cache-control")
|
224
|
+
cache_control = response["cache_control"]
|
225
|
+
cache_control[/max-age=(\d+)/, 1]
|
226
|
+
elsif response.key?("expires")
|
227
|
+
Time.httpdate(response["expires"]).utc.to_i - Time.now.utc.to_i
|
228
|
+
end
|
229
|
+
|
230
|
+
[JSON.parse(response.body, symbolize_names: true), ttl]
|
231
|
+
end
|
186
232
|
end
|
187
233
|
|
188
234
|
if defined?(JSON::JWT)
|
@@ -191,14 +237,11 @@ module Rodauth
|
|
191
237
|
# json-jwt
|
192
238
|
def jwt_encode(payload)
|
193
239
|
jwt = JSON::JWT.new(payload)
|
240
|
+
jwk = JSON::JWK.new(_jwt_key)
|
241
|
+
|
242
|
+
jwt = jwt.sign(jwk, oauth_jwt_algorithm)
|
243
|
+
jwt.kid = jwk.thumbprint
|
194
244
|
|
195
|
-
jwt = if oauth_jwt_jwk_key
|
196
|
-
jwk = JSON::JWK.new(oauth_jwt_jwk_key)
|
197
|
-
jwt.kid = jwk.thumbprint
|
198
|
-
jwt.sign(oauth_jwt_jwk_key, oauth_jwt_jwk_algorithm)
|
199
|
-
else
|
200
|
-
jwt.sign(_jwt_key, oauth_jwt_algorithm)
|
201
|
-
end
|
202
245
|
if oauth_jwt_jwe_key
|
203
246
|
algorithm = oauth_jwt_jwe_algorithm.to_sym if oauth_jwt_jwe_algorithm
|
204
247
|
jwt = jwt.encrypt(oauth_jwt_jwe_public_key || oauth_jwt_jwe_key,
|
@@ -213,11 +256,12 @@ module Rodauth
|
|
213
256
|
|
214
257
|
token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if oauth_jwt_jwe_key
|
215
258
|
|
216
|
-
|
217
|
-
|
259
|
+
jwk = oauth_jwt_public_key || _jwt_key
|
260
|
+
|
261
|
+
@jwt_token = if jwk
|
218
262
|
JSON::JWT.decode(token, jwk)
|
219
|
-
|
220
|
-
JSON::JWT.decode(token,
|
263
|
+
elsif !is_authorization_server? && auth_server_jwks_set
|
264
|
+
JSON::JWT.decode(token, JSON::JWK::Set.new(auth_server_jwks_set))
|
221
265
|
end
|
222
266
|
rescue JSON::JWT::Exception
|
223
267
|
nil
|
@@ -225,10 +269,11 @@ module Rodauth
|
|
225
269
|
|
226
270
|
def jwks_set
|
227
271
|
[
|
228
|
-
(JSON::JWK.new(
|
272
|
+
(JSON::JWK.new(oauth_jwt_public_key).merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
|
229
273
|
(JSON::JWK.new(oauth_jwt_jwe_public_key).merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key)
|
230
274
|
].compact
|
231
275
|
end
|
276
|
+
|
232
277
|
# :nocov:
|
233
278
|
elsif defined?(JWT)
|
234
279
|
|
@@ -237,18 +282,14 @@ module Rodauth
|
|
237
282
|
def jwt_encode(payload)
|
238
283
|
headers = {}
|
239
284
|
|
240
|
-
key
|
241
|
-
jwk_key = JWT::JWK.new(oauth_jwt_jwk_key)
|
242
|
-
# JWK
|
243
|
-
# Currently only supports RSA public keys.
|
244
|
-
headers[:kid] = jwk_key.kid
|
285
|
+
key = _jwt_key
|
245
286
|
|
246
|
-
|
247
|
-
|
248
|
-
|
287
|
+
if key.is_a?(OpenSSL::PKey::RSA)
|
288
|
+
jwk = JWT::JWK.new(_jwt_key)
|
289
|
+
headers[:kid] = jwk.kid
|
249
290
|
|
250
|
-
|
251
|
-
|
291
|
+
key = jwk.keypair
|
292
|
+
end
|
252
293
|
|
253
294
|
# Use the key and iat to create a unique key per request to prevent replay attacks
|
254
295
|
jti_raw = [key, payload[:iat]].join(":").to_s
|
@@ -256,7 +297,7 @@ module Rodauth
|
|
256
297
|
|
257
298
|
# @see JWT reserved claims - https://tools.ietf.org/html/draft-jones-json-web-token-07#page-7
|
258
299
|
payload[:jti] = jti
|
259
|
-
token = JWT.encode(payload, key,
|
300
|
+
token = JWT.encode(payload, key, oauth_jwt_algorithm, headers)
|
260
301
|
|
261
302
|
if oauth_jwt_jwe_key
|
262
303
|
params = {
|
@@ -278,35 +319,21 @@ module Rodauth
|
|
278
319
|
token = JWE.decrypt(token, oauth_jwt_jwe_key) if oauth_jwt_jwe_key
|
279
320
|
|
280
321
|
# decode jwt
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
end
|
291
|
-
|
292
|
-
headers[:algorithms] = [oauth_jwt_jwk_algorithm]
|
293
|
-
headers[:jwks] = jwk_loader
|
294
|
-
|
295
|
-
nil
|
296
|
-
else
|
297
|
-
# JWS
|
298
|
-
# worst case scenario, the key is the application key
|
299
|
-
oauth_jwt_public_key || _jwt_key
|
300
|
-
end
|
301
|
-
@jwt_token, = JWT.decode(token, key, true, headers)
|
302
|
-
@jwt_token
|
303
|
-
rescue JWT::DecodeError
|
322
|
+
key = oauth_jwt_public_key || _jwt_key
|
323
|
+
|
324
|
+
@jwt_token = if key
|
325
|
+
JWT.decode(token, key, true, algorithms: [oauth_jwt_algorithm]).first
|
326
|
+
elsif !is_authorization_server? && auth_server_jwks_set
|
327
|
+
algorithms = auth_server_jwks_set[:keys].select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
|
328
|
+
JWT.decode(token, nil, true, jwks: auth_server_jwks_set, algorithms: algorithms).first
|
329
|
+
end
|
330
|
+
rescue JWT::DecodeError, JWT::JWKError
|
304
331
|
nil
|
305
332
|
end
|
306
333
|
|
307
334
|
def jwks_set
|
308
335
|
[
|
309
|
-
(JWT::JWK.new(
|
336
|
+
(JWT::JWK.new(oauth_jwt_public_key).export.merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
|
310
337
|
(JWT::JWK.new(oauth_jwt_jwe_public_key).export.merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key)
|
311
338
|
].compact
|
312
339
|
end
|
@@ -328,7 +355,7 @@ module Rodauth
|
|
328
355
|
|
329
356
|
route(:oauth_jwks) do |r|
|
330
357
|
r.get do
|
331
|
-
json_response_success(jwks_set)
|
358
|
+
json_response_success({ keys: jwks_set })
|
332
359
|
end
|
333
360
|
end
|
334
361
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# The TTL store is a data structure which keeps data by a key, and with a time-to-live.
|
5
|
+
# It is specifically designed for data which is static, i.e. for a certain key in a
|
6
|
+
# sufficiently large span, the value will be the same.
|
7
|
+
#
|
8
|
+
# Because of that, synchronizations around reads do not exist, while write synchronizations
|
9
|
+
# will be short-circuited by a read.
|
10
|
+
#
|
11
|
+
class Rodauth::OAuth::TtlStore
|
12
|
+
DEFAULT_TTL = 60 * 60 * 24 # default TTL is one day
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@store_mutex = Mutex.new
|
16
|
+
@store = Hash.new {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def [](key)
|
20
|
+
lookup(key, now)
|
21
|
+
end
|
22
|
+
|
23
|
+
def set(key, &block)
|
24
|
+
@store_mutex.synchronize do
|
25
|
+
# short circuit
|
26
|
+
return @store[key][:payload] if @store[key] && @store[key][:ttl] < now
|
27
|
+
|
28
|
+
payload, ttl = block.call
|
29
|
+
@store[key] = { payload: payload, ttl: (ttl || (now + DEFAULT_TTL)) }
|
30
|
+
|
31
|
+
@store[key][:payload]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def uncache(key)
|
36
|
+
@store_mutex.synchronize do
|
37
|
+
@store.delete(key)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def now
|
44
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
45
|
+
end
|
46
|
+
|
47
|
+
# do not use directly!
|
48
|
+
def lookup(key, ttl)
|
49
|
+
return unless @store.key?(key)
|
50
|
+
|
51
|
+
value = @store[key]
|
52
|
+
|
53
|
+
return if value.empty?
|
54
|
+
|
55
|
+
return unless value[:ttl] > ttl
|
56
|
+
|
57
|
+
value[:payload]
|
58
|
+
end
|
59
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rodauth-oauth
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tiago Cardoso
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-06-
|
11
|
+
date: 2020-06-28 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Implementation of the OAuth 2.0 protocol on top of rodauth.
|
14
14
|
email:
|
@@ -34,6 +34,7 @@ files:
|
|
34
34
|
- lib/rodauth/features/oauth_jwt.rb
|
35
35
|
- lib/rodauth/oauth.rb
|
36
36
|
- lib/rodauth/oauth/railtie.rb
|
37
|
+
- lib/rodauth/oauth/ttl_store.rb
|
37
38
|
- lib/rodauth/oauth/version.rb
|
38
39
|
homepage: https://gitlab.com/honeyryderchuck/roda-oauth
|
39
40
|
licenses: []
|