jwt_sessions 3.2.4 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3d616f460afd8b13982064b340c2ea3f3e326bb99dff16ec2e159599ffa4aebd
4
- data.tar.gz: baed64f2320056748d33c1f08c955e4b37cb958d6442f28ade5fa7c61656be56
3
+ metadata.gz: a1d64a93985f3ada50556e0d6b411b4452e8496363c99e72429ad44adf48bdf8
4
+ data.tar.gz: d8d8c55c021199059141fd392478c8c65896fec65df88ebbeab16d1d48035b9d
5
5
  SHA512:
6
- metadata.gz: d31922314b428ab7bb82d0817b5a357b8fec1f47361d11e194f9d8c8e9e3dc9cda791c095af43c12aa7f71b06c36e3d1912b83808644a9dcd55888d19679342b
7
- data.tar.gz: 3ffb8fa65a4c9d5599be7cc7d387773d2fee8a3f10c70b5c4ed1a7b30912d33400a678d69c07d469b4ce4113e021f3035b0d2e62949472cd32b707d3efded94b
6
+ metadata.gz: ccabd8a853a29a58b95c6ad66b8ed3d77437bc4c3ab4c107c935659639b0995e2f3463cbc1c66df1ad1f773d041148c66ff86cbc1276535998281d8e065783b4
7
+ data.tar.gz: 12d1e412ff49d36585c059447bffda9349ac7fdbb73bb5a6346207d5e75c3ec579830106a3d1255874b0debe3321d474359c41aa47fa5fac87fd6c34ec3babd2
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## 3.3.0 (October 13, 2026)
2
+
3
+ Fixes:
4
+
5
+ - do not restore a flushed refresh token during a refresh; a refresh racing a flush now raises `Unauthorized`
6
+
7
+ Support:
8
+
9
+ - `update_refresh` of a store adapter returns `false` when the refresh token no longer exists
10
+ - test against Ruby 4.0 and Rails 8.0 / 8.1
11
+
1
12
  ## 3.2.4 (Septemper 21, 2024)
2
13
 
3
14
  Support:
@@ -221,7 +221,12 @@ module JWTSessions
221
221
  end
222
222
 
223
223
  def update_refresh_token
224
- @_refresh.update(@_access.uid, @_access.expiration, @_csrf.encoded)
224
+ updated = @_refresh.update(@_access.uid, @_access.expiration, @_csrf.encoded)
225
+ if updated == false
226
+ AccessToken.destroy(@_access.uid, store)
227
+ raise Errors::Unauthorized, "Refresh token not found"
228
+ end
229
+
225
230
  @refresh_token = @_refresh.token
226
231
  link_access_to_refresh
227
232
  end
@@ -20,6 +20,8 @@ module JWTSessions
20
20
  raise NotImplementedError
21
21
  end
22
22
 
23
+ # Return false when the refresh token no longer exists. A refresh then
24
+ # fails instead of writing back a token that a flush has just removed.
23
25
  def update_refresh(_uid:, _access_expiration:, _access_uid:, _csrf:, _namespace:)
24
26
  raise NotImplementedError
25
27
  end
@@ -46,6 +46,8 @@ module JWTSessions
46
46
  end
47
47
 
48
48
  def update_refresh(uid:, access_expiration:, access_uid:, csrf:, namespace: "")
49
+ return false if value_if_not_expired(uid, "refresh", namespace.to_s).empty?
50
+
49
51
  update_refresh_fields(
50
52
  uid,
51
53
  namespace.to_s,
@@ -53,6 +55,7 @@ module JWTSessions
53
55
  access_expiration: access_expiration,
54
56
  access_uid: access_uid
55
57
  )
58
+ true
56
59
  end
57
60
 
58
61
  def all_refresh_tokens(namespace)
@@ -7,6 +7,15 @@ module JWTSessions
7
7
 
8
8
  REFRESH_KEYS = %i[csrf access_uid access_expiration expiration].freeze
9
9
  DEFAULT_POOL_SIZE = 5
10
+ # Updates a refresh token only while it exists, so that a refresh which
11
+ # races a flush cannot write the flushed token back.
12
+ UPDATE_REFRESH_SCRIPT = <<~LUA
13
+ if redis.call("EXISTS", KEYS[1]) == 0 then
14
+ return 0
15
+ end
16
+ redis.call("HMSET", KEYS[1], "csrf", ARGV[1], "access_expiration", ARGV[2], "access_uid", ARGV[3])
17
+ return 1
18
+ LUA
10
19
 
11
20
  def initialize(token_prefix: JWTSessions.token_prefix, redis_client: nil, **options)
12
21
  @prefix = token_prefix
@@ -51,24 +60,27 @@ module JWTSessions
51
60
 
52
61
  def persist_refresh(uid:, access_expiration:, access_uid:, csrf:, expiration:, namespace: nil)
53
62
  key = full_refresh_key(uid, namespace)
54
- update_refresh(
55
- uid: uid,
56
- access_expiration: access_expiration,
57
- access_uid: access_uid,
58
- csrf: csrf,
59
- namespace: namespace
63
+ storage.call("HMSET",
64
+ key,
65
+ :csrf, csrf,
66
+ :access_expiration, access_expiration,
67
+ :access_uid, access_uid,
68
+ :expiration, expiration
60
69
  )
61
- storage.call("HSET", key, :expiration, expiration)
62
70
  storage.call("EXPIREAT", key, expiration)
63
71
  end
64
72
 
65
73
  def update_refresh(uid:, access_expiration:, access_uid:, csrf:, namespace: nil)
66
- storage.call("HMSET",
74
+ updated = storage.call(
75
+ "EVAL",
76
+ UPDATE_REFRESH_SCRIPT,
77
+ 1,
67
78
  full_refresh_key(uid, namespace),
68
- :csrf, csrf,
69
- :access_expiration, access_expiration,
70
- :access_uid, access_uid
79
+ csrf,
80
+ access_expiration,
81
+ access_uid
71
82
  )
83
+ updated == 1
72
84
  end
73
85
 
74
86
  def all_refresh_tokens(namespace)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JWTSessions
4
- VERSION = "3.2.4"
4
+ VERSION = "3.3.0"
5
5
  end
@@ -60,7 +60,7 @@ class TestMemoryStoreAdapter < Minitest::Test
60
60
  expiration: expiration,
61
61
  namespace: ""
62
62
  )
63
- store.update_refresh(
63
+ assert store.update_refresh(
64
64
  uid: "uid",
65
65
  access_expiration: expiration,
66
66
  access_uid: "access_uid",
@@ -71,6 +71,28 @@ class TestMemoryStoreAdapter < Minitest::Test
71
71
  assert_equal "csrf2", refresh[:csrf]
72
72
  end
73
73
 
74
+ def test_update_refresh_after_destroy
75
+ expiration = Time.now.to_i + 3600
76
+ store.persist_refresh(
77
+ uid: "uid",
78
+ access_expiration: expiration,
79
+ access_uid: "access_uid",
80
+ csrf: "csrf",
81
+ expiration: expiration,
82
+ namespace: ""
83
+ )
84
+ store.destroy_refresh("uid", "")
85
+
86
+ refute store.update_refresh(
87
+ uid: "uid",
88
+ access_expiration: expiration,
89
+ access_uid: "access_uid",
90
+ csrf: "csrf2",
91
+ namespace: ""
92
+ )
93
+ assert_equal({}, store.fetch_refresh("uid", ""))
94
+ end
95
+
74
96
  def test_all_refresh_tokens
75
97
  expiration = Time.now.to_i + 3600
76
98
  store.persist_refresh(
@@ -11,6 +11,55 @@ class TestRedisStoreAdapter < Minitest::Test
11
11
  JWTSessions.remove_instance_variable(:@redis_url) if JWTSessions.instance_variable_defined?(:@redis_url)
12
12
  end
13
13
 
14
+ def test_update_refresh
15
+ store = JWTSessions::StoreAdapters::RedisStoreAdapter.new
16
+ expiration = Time.now.to_i + 3600
17
+ store.persist_refresh(
18
+ uid: "uid",
19
+ access_expiration: expiration,
20
+ access_uid: "access_uid",
21
+ csrf: "csrf",
22
+ expiration: expiration,
23
+ namespace: "update_refresh"
24
+ )
25
+
26
+ assert store.update_refresh(
27
+ uid: "uid",
28
+ access_expiration: expiration,
29
+ access_uid: "access_uid",
30
+ csrf: "csrf2",
31
+ namespace: "update_refresh"
32
+ )
33
+ assert_equal "csrf2", store.fetch_refresh("uid", "update_refresh")[:csrf]
34
+ ensure
35
+ store.destroy_refresh("uid", "update_refresh")
36
+ end
37
+
38
+ def test_update_refresh_after_destroy
39
+ store = JWTSessions::StoreAdapters::RedisStoreAdapter.new
40
+ expiration = Time.now.to_i + 3600
41
+ store.persist_refresh(
42
+ uid: "uid",
43
+ access_expiration: expiration,
44
+ access_uid: "access_uid",
45
+ csrf: "csrf",
46
+ expiration: expiration,
47
+ namespace: "update_refresh_after_destroy"
48
+ )
49
+ store.destroy_refresh("uid", "update_refresh_after_destroy")
50
+
51
+ refute store.update_refresh(
52
+ uid: "uid",
53
+ access_expiration: expiration,
54
+ access_uid: "access_uid",
55
+ csrf: "csrf2",
56
+ namespace: "update_refresh_after_destroy"
57
+ )
58
+ assert_empty store.storage.call("KEYS", "#{store.prefix}_update_refresh_after_destroy_*")
59
+ ensure
60
+ store.destroy_refresh("uid", "update_refresh_after_destroy")
61
+ end
62
+
14
63
  def test_error_on_mixed_redis_options
15
64
  assert_raises ArgumentError do
16
65
  JWTSessions::StoreAdapters::RedisStoreAdapter.new(
@@ -20,6 +20,8 @@ class TestAuthorization < Minitest::Test
20
20
  def teardown
21
21
  JWTSessions.jwt_options[:verify_iss] = false
22
22
  JWTSessions.jwt_options[:verify_aud] = false
23
+
24
+ JWTSessions::Session.flush_all
23
25
  end
24
26
 
25
27
  def test_payload_when_token_is_nil
@@ -7,9 +7,9 @@ class TestRefreshToken < Minitest::Test
7
7
  attr_reader :csrf, :token, :access_uid
8
8
 
9
9
  def setup
10
+ JWTSessions.signing_key = "secure key"
10
11
  JWTSessions::Session.flush_all
11
12
 
12
- JWTSessions.signing_key = "secure key"
13
13
  @access_uid = SecureRandom.uuid
14
14
  @csrf = JWTSessions::CSRFToken.new
15
15
  @token = JWTSessions::RefreshToken.create(@csrf.encoded,
@@ -336,6 +336,21 @@ class TestSession < Minitest::Test
336
336
  assert_equal 0, session.flush_namespaced_access_tokens
337
337
  end
338
338
 
339
+ def test_refresh_after_concurrent_flush
340
+ refresh_token = tokens[:refresh]
341
+ racing_store = JWTSessions.token_store.dup
342
+ racing_store.define_singleton_method(:update_refresh) do |**fields|
343
+ JWTSessions::Session.new.flush_by_token(refresh_token)
344
+ super(**fields)
345
+ end
346
+ racing_session = JWTSessions::Session.new(payload: payload, store: racing_store)
347
+
348
+ assert_raises JWTSessions::Errors::Unauthorized do
349
+ racing_session.refresh(refresh_token)
350
+ end
351
+ assert_empty RedisClient.new.call("KEYS", "jwt_*")
352
+ end
353
+
339
354
  def test_flush_all
340
355
  refresh_token = @session.instance_variable_get(:"@_refresh")
341
356
  flushed_count = JWTSessions::Session.flush_all
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jwt_sessions
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.4
4
+ version: 3.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julija Alieckaja
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-09-21 00:00:00.000000000 Z
10
+ date: 2026-10-13 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: jwt
@@ -113,7 +112,6 @@ metadata:
113
112
  changelog_uri: https://github.com/tuwukee/jwt_sessions/blob/master/CHANGELOG.md
114
113
  source_code_uri: https://github.com/tuwukee/jwt_sessions
115
114
  bug_tracker_uri: https://github.com/tuwukee/jwt_sessions/issues
116
- post_install_message:
117
115
  rdoc_options: []
118
116
  require_paths:
119
117
  - lib
@@ -128,13 +126,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
128
126
  - !ruby/object:Gem::Version
129
127
  version: '0'
130
128
  requirements: []
131
- rubygems_version: 3.4.12
132
- signing_key:
129
+ rubygems_version: 4.0.3
133
130
  specification_version: 4
134
131
  summary: JWT Sessions
135
132
  test_files:
136
- - test/units/test_jwt_sessions.rb
137
- - test/units/test_token_store.rb
138
133
  - test/units/jwt_sessions/store_adapters/test_memory_store_adapter.rb
139
134
  - test/units/jwt_sessions/store_adapters/test_redis_store_adapter.rb
140
135
  - test/units/jwt_sessions/test_access_token.rb
@@ -143,3 +138,5 @@ test_files:
143
138
  - test/units/jwt_sessions/test_refresh_token.rb
144
139
  - test/units/jwt_sessions/test_session.rb
145
140
  - test/units/jwt_sessions/test_token.rb
141
+ - test/units/test_jwt_sessions.rb
142
+ - test/units/test_token_store.rb