workos 7.1.0 → 7.1.1
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/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +7 -0
- data/Gemfile.lock +2 -2
- data/lib/workos/session.rb +16 -5
- data/lib/workos/version.rb +1 -1
- data/test/workos/test_session.rb +125 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 784d66731f3736d0a82674d067f89b258f5979c497ae57bbf0d413af3fa3cd61
|
|
4
|
+
data.tar.gz: 7fed939e2d52cf44085df0a6aeb3d7cad07778eefff07c1380b023ee7810624c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0e75fe9404664c71cf3dc2fa392c0871406cbcc9aca8b0e2b87f09c88e894b40a8485a79d45e1feb2df068ff2a93d204a83ff331ae0c5a6c4d1eccb601021b98
|
|
7
|
+
data.tar.gz: 5504f24a552331b35908cf14dc1cdd54023ef646fad049ccb7ebe1e95a1f71c24c4f5f8ef0cda714ecdd795c34dbbda2fafcb898cbc93099ce61e6aff215712b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [7.1.1](https://github.com/workos/workos-ruby/compare/v7.1.0...v7.1.1) (2026-04-29)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* seal session client-side in Session#refresh ([#470](https://github.com/workos/workos-ruby/issues/470)) ([32662ab](https://github.com/workos/workos-ruby/commit/32662ab3d67ffdcc895141aa8fd5efb22ba79fdb))
|
|
9
|
+
|
|
3
10
|
## [7.1.0](https://github.com/workos/workos-ruby/compare/v7.0.0...v7.1.0) (2026-04-27)
|
|
4
11
|
|
|
5
12
|
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
workos (7.1.
|
|
4
|
+
workos (7.1.1)
|
|
5
5
|
jwt (~> 3.1)
|
|
6
6
|
logger (~> 1.7)
|
|
7
7
|
zeitwerk (~> 2.6)
|
|
@@ -124,7 +124,7 @@ CHECKSUMS
|
|
|
124
124
|
unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
|
|
125
125
|
unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
|
|
126
126
|
webmock (3.26.2) sha256=774556f2ea6371846cca68c01769b2eac0d134492d21f6d0ab5dd643965a4c90
|
|
127
|
-
workos (7.1.
|
|
127
|
+
workos (7.1.1)
|
|
128
128
|
zeitwerk (2.7.5) sha256=d8da92128c09ea6ec62c949011b00ed4a20242b255293dd66bf41545398f73dd
|
|
129
129
|
|
|
130
130
|
BUNDLED WITH
|
data/lib/workos/session.rb
CHANGED
|
@@ -90,18 +90,27 @@ module WorkOS
|
|
|
90
90
|
body = {
|
|
91
91
|
"grant_type" => "refresh_token",
|
|
92
92
|
"client_id" => @client.client_id,
|
|
93
|
-
"refresh_token" => session["refresh_token"]
|
|
94
|
-
"session" => {"seal_session" => true, "cookie_password" => effective_password}
|
|
93
|
+
"refresh_token" => session["refresh_token"]
|
|
95
94
|
}
|
|
96
95
|
body["organization_id"] = organization_id if organization_id
|
|
97
96
|
|
|
98
97
|
response = @client.request(method: :post, path: "/user_management/authenticate", auth: true, body: body)
|
|
99
98
|
auth_response = JSON.parse(response.body)
|
|
100
|
-
sealed = auth_response["sealed_session"].to_s
|
|
101
|
-
@seal_data = sealed
|
|
102
|
-
@cookie_password = effective_password
|
|
103
99
|
|
|
100
|
+
sealed = @manager.seal_session_from_auth_response(
|
|
101
|
+
access_token: auth_response["access_token"],
|
|
102
|
+
refresh_token: auth_response["refresh_token"],
|
|
103
|
+
cookie_password: effective_password,
|
|
104
|
+
user: auth_response["user"],
|
|
105
|
+
impersonator: auth_response["impersonator"]
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Decode before mutating session state so a malformed access_token
|
|
109
|
+
# doesn't leave the Session half-updated.
|
|
104
110
|
decoded = @manager.decode_jwt(auth_response["access_token"])
|
|
111
|
+
|
|
112
|
+
@seal_data = sealed
|
|
113
|
+
@cookie_password = effective_password
|
|
105
114
|
SessionManager::RefreshSuccess.new(
|
|
106
115
|
authenticated: true,
|
|
107
116
|
sealed_session: sealed,
|
|
@@ -117,6 +126,8 @@ module WorkOS
|
|
|
117
126
|
)
|
|
118
127
|
rescue WorkOS::AuthenticationError, WorkOS::InvalidRequestError => e
|
|
119
128
|
SessionManager::RefreshError.new(authenticated: false, reason: e.message)
|
|
129
|
+
rescue JWT::DecodeError => e
|
|
130
|
+
SessionManager::RefreshError.new(authenticated: false, reason: e.message)
|
|
120
131
|
end
|
|
121
132
|
|
|
122
133
|
# Build the WorkOS session-logout URL for the currently authenticated session.
|
data/lib/workos/version.rb
CHANGED
data/test/workos/test_session.rb
CHANGED
|
@@ -206,6 +206,131 @@ class SessionTest < Minitest::Test
|
|
|
206
206
|
assert_equal "https://app/cb", params["return_to"]
|
|
207
207
|
end
|
|
208
208
|
|
|
209
|
+
# --- Session#refresh -------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
def test_refresh_seals_session_client_side_and_returns_refresh_success
|
|
212
|
+
rsa, pub = signing_key_pair
|
|
213
|
+
old_access = make_jwt({"sid" => "session_old", "exp" => Time.now.to_i - 60}, rsa)
|
|
214
|
+
sealed = @sm.seal_data({"access_token" => old_access, "refresh_token" => "rt_old", "user" => {"id" => "u_1"}}, PASSWORD)
|
|
215
|
+
|
|
216
|
+
new_access = make_jwt({"sid" => "session_new", "org_id" => "org_1", "role" => "admin", "exp" => Time.now.to_i + 300}, rsa)
|
|
217
|
+
api_response = {
|
|
218
|
+
"access_token" => new_access,
|
|
219
|
+
"refresh_token" => "rt_new",
|
|
220
|
+
"user" => {"id" => "u_1", "email" => "a@b.com"},
|
|
221
|
+
"impersonator" => nil
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
stub_request(:post, "https://api.workos.com/user_management/authenticate")
|
|
225
|
+
.with(body: hash_including("grant_type" => "refresh_token", "refresh_token" => "rt_old"))
|
|
226
|
+
.to_return(status: 200, body: api_response.to_json)
|
|
227
|
+
stub_request(:get, "https://api.workos.com/sso/jwks/client_001")
|
|
228
|
+
.to_return(status: 200, body: jwks_payload(pub).to_json)
|
|
229
|
+
|
|
230
|
+
session = @sm.load(seal_data: sealed, cookie_password: PASSWORD)
|
|
231
|
+
result = session.refresh
|
|
232
|
+
|
|
233
|
+
assert_kind_of WorkOS::SessionManager::RefreshSuccess, result
|
|
234
|
+
assert result.authenticated
|
|
235
|
+
assert_equal "session_new", result.session_id
|
|
236
|
+
assert_equal "org_1", result.organization_id
|
|
237
|
+
assert_equal "admin", result.role
|
|
238
|
+
assert_equal "u_1", result.user["id"]
|
|
239
|
+
|
|
240
|
+
# sealed_session should be a non-empty string that round-trips
|
|
241
|
+
refute_empty result.sealed_session
|
|
242
|
+
unsealed = @sm.unseal_data(result.sealed_session, PASSWORD)
|
|
243
|
+
assert_equal new_access, unsealed["access_token"]
|
|
244
|
+
assert_equal "rt_new", unsealed["refresh_token"]
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def test_refresh_updates_internal_seal_data_for_subsequent_authenticate
|
|
248
|
+
rsa, pub = signing_key_pair
|
|
249
|
+
old_access = make_jwt({"sid" => "session_old", "exp" => Time.now.to_i - 60}, rsa)
|
|
250
|
+
sealed = @sm.seal_data({"access_token" => old_access, "refresh_token" => "rt_old", "user" => {"id" => "u_1"}}, PASSWORD)
|
|
251
|
+
|
|
252
|
+
new_access = make_jwt({"sid" => "session_refreshed", "org_id" => "org_2", "exp" => Time.now.to_i + 300}, rsa)
|
|
253
|
+
api_response = {
|
|
254
|
+
"access_token" => new_access,
|
|
255
|
+
"refresh_token" => "rt_new",
|
|
256
|
+
"user" => {"id" => "u_1"}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
stub_request(:post, "https://api.workos.com/user_management/authenticate")
|
|
260
|
+
.to_return(status: 200, body: api_response.to_json)
|
|
261
|
+
stub_request(:get, "https://api.workos.com/sso/jwks/client_001")
|
|
262
|
+
.to_return(status: 200, body: jwks_payload(pub).to_json)
|
|
263
|
+
|
|
264
|
+
session = @sm.load(seal_data: sealed, cookie_password: PASSWORD)
|
|
265
|
+
session.refresh
|
|
266
|
+
|
|
267
|
+
# A subsequent authenticate should use the refreshed token
|
|
268
|
+
auth = session.authenticate
|
|
269
|
+
assert_kind_of WorkOS::SessionManager::AuthSuccess, auth
|
|
270
|
+
assert auth.authenticated
|
|
271
|
+
assert_equal "session_refreshed", auth.session_id
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def test_refresh_returns_error_on_invalid_cookie
|
|
275
|
+
result = @sm.refresh(seal_data: "garbage", cookie_password: PASSWORD)
|
|
276
|
+
assert_kind_of WorkOS::SessionManager::RefreshError, result
|
|
277
|
+
refute result.authenticated
|
|
278
|
+
assert_equal WorkOS::SessionManager::INVALID_SESSION_COOKIE, result.reason
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def test_refresh_returns_error_when_no_refresh_token
|
|
282
|
+
sealed = @sm.seal_data({"access_token" => "at_only"}, PASSWORD)
|
|
283
|
+
result = @sm.refresh(seal_data: sealed, cookie_password: PASSWORD)
|
|
284
|
+
assert_kind_of WorkOS::SessionManager::RefreshError, result
|
|
285
|
+
assert_equal WorkOS::SessionManager::INVALID_SESSION_COOKIE, result.reason
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def test_refresh_does_not_send_session_param_to_api
|
|
289
|
+
rsa, pub = signing_key_pair
|
|
290
|
+
old_access = make_jwt({"sid" => "s", "exp" => Time.now.to_i - 60}, rsa)
|
|
291
|
+
sealed = @sm.seal_data({"access_token" => old_access, "refresh_token" => "rt_x", "user" => {"id" => "u"}}, PASSWORD)
|
|
292
|
+
|
|
293
|
+
new_access = make_jwt({"sid" => "s2", "exp" => Time.now.to_i + 300}, rsa)
|
|
294
|
+
api_response = {"access_token" => new_access, "refresh_token" => "rt_y", "user" => {"id" => "u"}}
|
|
295
|
+
|
|
296
|
+
stub = stub_request(:post, "https://api.workos.com/user_management/authenticate")
|
|
297
|
+
.with { |req| !req.body.include?("seal_session") }
|
|
298
|
+
.to_return(status: 200, body: api_response.to_json)
|
|
299
|
+
stub_request(:get, "https://api.workos.com/sso/jwks/client_001")
|
|
300
|
+
.to_return(status: 200, body: jwks_payload(pub).to_json)
|
|
301
|
+
|
|
302
|
+
session = @sm.load(seal_data: sealed, cookie_password: PASSWORD)
|
|
303
|
+
session.refresh
|
|
304
|
+
|
|
305
|
+
assert_requested(stub)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def test_refresh_returns_error_on_malformed_access_token_without_mutating_state
|
|
309
|
+
rsa, pub = signing_key_pair
|
|
310
|
+
old_access = make_jwt({"sid" => "session_old", "exp" => Time.now.to_i - 60}, rsa)
|
|
311
|
+
sealed = @sm.seal_data({"access_token" => old_access, "refresh_token" => "rt_old", "user" => {"id" => "u_1"}}, PASSWORD)
|
|
312
|
+
|
|
313
|
+
api_response = {
|
|
314
|
+
"access_token" => "not-a-valid-jwt",
|
|
315
|
+
"refresh_token" => "rt_new",
|
|
316
|
+
"user" => {"id" => "u_1"}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
stub_request(:post, "https://api.workos.com/user_management/authenticate")
|
|
320
|
+
.to_return(status: 200, body: api_response.to_json)
|
|
321
|
+
stub_request(:get, "https://api.workos.com/sso/jwks/client_001")
|
|
322
|
+
.to_return(status: 200, body: jwks_payload(pub).to_json)
|
|
323
|
+
|
|
324
|
+
session = @sm.load(seal_data: sealed, cookie_password: PASSWORD)
|
|
325
|
+
result = session.refresh
|
|
326
|
+
|
|
327
|
+
assert_kind_of WorkOS::SessionManager::RefreshError, result
|
|
328
|
+
refute result.authenticated
|
|
329
|
+
|
|
330
|
+
# Session state should not have been mutated
|
|
331
|
+
assert_equal sealed, session.seal_data
|
|
332
|
+
end
|
|
333
|
+
|
|
209
334
|
# --- Session constructor validation ---------------------------------------
|
|
210
335
|
|
|
211
336
|
def test_session_load_requires_cookie_password
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: workos
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 7.1.
|
|
4
|
+
version: 7.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- WorkOS
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: jwt
|