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 +4 -4
- data/CHANGELOG.md +11 -0
- data/lib/jwt_sessions/session.rb +6 -1
- data/lib/jwt_sessions/store_adapters/abstract_store_adapter.rb +2 -0
- data/lib/jwt_sessions/store_adapters/memory_store_adapter.rb +3 -0
- data/lib/jwt_sessions/store_adapters/redis_store_adapter.rb +23 -11
- data/lib/jwt_sessions/version.rb +1 -1
- data/test/units/jwt_sessions/store_adapters/test_memory_store_adapter.rb +23 -1
- data/test/units/jwt_sessions/store_adapters/test_redis_store_adapter.rb +49 -0
- data/test/units/jwt_sessions/test_authorization.rb +2 -0
- data/test/units/jwt_sessions/test_refresh_token.rb +1 -1
- data/test/units/jwt_sessions/test_session.rb +15 -0
- metadata +5 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a1d64a93985f3ada50556e0d6b411b4452e8496363c99e72429ad44adf48bdf8
|
|
4
|
+
data.tar.gz: d8d8c55c021199059141fd392478c8c65896fec65df88ebbeab16d1d48035b9d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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:
|
data/lib/jwt_sessions/session.rb
CHANGED
|
@@ -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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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(
|
|
74
|
+
updated = storage.call(
|
|
75
|
+
"EVAL",
|
|
76
|
+
UPDATE_REFRESH_SCRIPT,
|
|
77
|
+
1,
|
|
67
78
|
full_refresh_key(uid, namespace),
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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)
|
data/lib/jwt_sessions/version.rb
CHANGED
|
@@ -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(
|
|
@@ -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.
|
|
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:
|
|
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:
|
|
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
|