rodauth-oauth 0.0.4 → 0.0.5

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