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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 22808f6f421ce935e5f69301b1f0edc37a286baedf0c4a4039d29b3ff678bf40
4
- data.tar.gz: 64305ba438e3035309ca428d4820f00f7b169ca219882575262ee8242a800f50
3
+ metadata.gz: a809caa12783f20af9ba7b6d4b8710f2b783d11c5c74c0be6cbb129841ebce19
4
+ data.tar.gz: 928fd524dd2f9ec502deccc96b17eb232119543b2150363a398d04a5008b4d22
5
5
  SHA512:
6
- metadata.gz: c6302e1e592bf760b9a1a1ac4adc6e9189545f58818553ec85fda0f86299531dcea1e1b59cba65aaa62d607e2cf541fb18ee2a9c2161ce96678b5b2812326a0b
7
- data.tar.gz: 689a8e88d89c8fe74b665c65138ff8372cee05d0783e473e8e124b9a9f21d9806bb397d185edf9d0c87ef21cfcd2d81bfa16ab0aaa3da64b9b28d6e5e2e24ab3
6
+ metadata.gz: 15b98394108c73ba4888bd92835d264750c46194b77322b0b159b45134031f0ac9c264a2a8d0a9991fb0d36b05b85b0d127d94468d77d3f7fb45777ec5bb6002
7
+ data.tar.gz: 282f53b5fd4eff08fdce56a42a7c42e3ef00cf888ced3acb2a692fd416b8dbb7250c982a53fc3c4a61a83884d8eb1068981580390e3bea2ba35d741165aa2c6a
@@ -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 # 60 minutes
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
- # URL PARAMS
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(state_param)
250
+ param_or_nil("state")
240
251
  end
241
252
 
242
253
  def scopes
243
- (param_or_nil(scopes_param) || oauth_application_default_scope).split(" ")
254
+ (param_or_nil("scope") || oauth_application_default_scope).split(" ")
244
255
  end
245
256
 
246
257
  def client_id
247
- param_or_nil(client_id_param)
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(redirect_uri_param) || oauth_application[oauth_applications_redirect_uri_column]
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(token_type_hint_param) || "access_token"
271
+ param_or_nil("token_type_hint") || "access_token"
260
272
  end
261
273
 
262
274
  def token
263
- param_or_nil(token_param)
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 = param(client_id_param)
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(fetch_access_token)
312
+ @authorization_token = oauth_token_by_token(bearer_token)
297
313
  end
298
314
 
299
315
  def require_oauth_authorization(*scopes)
300
- authorization_required unless authorization_token
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
- scopes << oauth_application_default_scope if scopes.empty?
329
+ # where in resource server, NOT the authorization server.
330
+ payload = introspection_request("access_token", bearer_token)
303
331
 
304
- token_scopes = authorization_token[oauth_tokens_scopes_column].split(",")
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(client_id_param)
382
- client_secret = param_or_nil(client_secret_param)
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(code_verifier_param)
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
- key == oauth_application_redirect_uri_param
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
- set_field_error(key, invalid_url_message) unless URI::DEFAULT_PARSER.make_regexp(oauth_valid_uri_schemes).match?(value)
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
- id = if ds.supports_returning?(:insert)
570
- ds.returning(oauth_applications_id_column).insert(create_params)
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
- end
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(approval_prompt_param)
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(access_type_param))
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(code_challenge_param))
647
- code_challenge_method = param_or_nil(code_challenge_method_param)
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
- if ds.supports_returning?(:insert)
660
- ds.returning(authorize_code_column).insert(create_params)
661
- else
662
- id = ds.insert(create_params)
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(grant_type_param))
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(code_param)
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(refresh_token_param)
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(grant_type_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(code_param),
707
- oauth_grants_redirect_uri_column => param(redirect_uri_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(code_verifier_param)
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(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(token_param)
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].gsub(",", " "),
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(token_param)
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 && token_from_application?(oauth_token, oauth_application)
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(",")).empty?
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
- redirect_uri == oauth_application[oauth_applications_redirect_uri_column]
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(access_type_param)
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(approval_prompt_param)
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(response_type_param)
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(code_challenge_param)
1070
+ if param_or_nil("code_challenge")
966
1071
 
967
- challenge_method = param_or_nil(code_challenge_method_param)
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(token_type_hint_param)
1162
+ oauth_token = case param("token_type_hint")
1058
1163
  when "access_token"
1059
- oauth_token_by_token(param(token_param))
1164
+ oauth_token_by_token(param("token"))
1060
1165
  when "refresh_token"
1061
- oauth_token_by_refresh_token(param(token_param))
1166
+ oauth_token_by_refresh_token(param("token"))
1062
1167
  else
1063
- oauth_token_by_token(param(token_param)) || oauth_token_by_refresh_token(param(token_param))
1168
+ oauth_token_by_token(param("token")) || oauth_token_by_refresh_token(param("token"))
1064
1169
  end
1065
1170
 
1066
- redirect_response_error("invalid_request") if oauth_token && !token_from_application?(oauth_token, oauth_application)
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(response_type_param)
1233
+ case param("response_type")
1124
1234
  when "token"
1125
- redirect_response_error("invalid_request", redirect_uri) unless use_oauth_implicit_grant_type?
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 = jwt_decode(fetch_access_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(grant_type_param) == "urn:ietf:params:oauth:grant-type:jwt-bearer"
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(grant_type_param) == "urn:ietf:params:oauth:grant-type:jwt-bearer"
63
- redirect_response_error("invalid_client") unless param_or_nil(assertion_param)
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(grant_type_param) == "urn:ietf:params:oauth:grant-type:jwt-bearer"
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(assertion_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.current.utc.to_i
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].gsub(",", " ")
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
- @jwt_token = if oauth_jwt_jwk_key
217
- jwk = JSON::JWK.new(oauth_jwt_jwk_public_key || oauth_jwt_jwk_key)
259
+ jwk = oauth_jwt_public_key || _jwt_key
260
+
261
+ @jwt_token = if jwk
218
262
  JSON::JWT.decode(token, jwk)
219
- else
220
- JSON::JWT.decode(token, oauth_jwt_public_key || _jwt_key)
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(oauth_jwt_jwk_public_key).merge(use: "sig", alg: oauth_jwt_jwk_algorithm) if oauth_jwt_jwk_public_key),
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, algorithm = if oauth_jwt_jwk_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
- [jwk_key.keypair, oauth_jwt_jwk_algorithm]
247
- else
248
- # JWS
287
+ if key.is_a?(OpenSSL::PKey::RSA)
288
+ jwk = JWT::JWK.new(_jwt_key)
289
+ headers[:kid] = jwk.kid
249
290
 
250
- [_jwt_key, oauth_jwt_algorithm]
251
- end
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, algorithm, headers)
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
- headers = { algorithms: [oauth_jwt_algorithm] }
282
-
283
- key = if oauth_jwt_jwk_key
284
- jwk_key = JWT::JWK.new(oauth_jwt_jwk_public_key || oauth_jwt_jwk_key)
285
- # JWK
286
- # The jwk loader would fetch the set of JWKs from a trusted source
287
- jwk_loader = lambda do |options|
288
- @cached_keys = nil if options[:invalidate] # need to reload the keys
289
- @cached_keys ||= { keys: [jwk_key.export] }
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(oauth_jwt_jwk_public_key).export.merge(use: "sig", alg: oauth_jwt_jwk_algorithm) if oauth_jwt_jwk_public_key),
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rodauth
4
4
  module OAuth
5
- VERSION = "0.0.4"
5
+ VERSION = "0.0.5"
6
6
  end
7
7
  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
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-13 00:00:00.000000000 Z
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: []