|
6 | 6 | require "openssl" |
7 | 7 | require "jwt" |
8 | 8 | require "base64" |
| 9 | +require "securerandom" |
9 | 10 |
|
10 | 11 | class SessionTest < Minitest::Test |
11 | 12 | PASSWORD = "very-long-cookie-password-secret" |
@@ -84,6 +85,22 @@ def test_authenticate_returns_success_with_decoded_claims |
84 | 85 | assert_equal "u_1", result.user["id"] |
85 | 86 | end |
86 | 87 |
|
| 88 | + def test_authenticate_reads_legacy_v6_sealed_session |
| 89 | + rsa, pub = signing_key_pair |
| 90 | + access_token = make_jwt({"sid" => "session_v6", "org_id" => "org_legacy", "exp" => Time.now.to_i + 60}, rsa) |
| 91 | + sealed = legacy_v6_seal({"access_token" => access_token, "user" => {"id" => "u_legacy"}}, PASSWORD) |
| 92 | + |
| 93 | + stub_request(:get, "https://api.workos.com/sso/jwks/client_001") |
| 94 | + .to_return(status: 200, body: jwks_payload(pub).to_json) |
| 95 | + |
| 96 | + result = @sm.authenticate(seal_data: sealed, cookie_password: PASSWORD) |
| 97 | + assert_kind_of WorkOS::SessionManager::AuthSuccess, result |
| 98 | + assert result.authenticated |
| 99 | + assert_equal "session_v6", result.session_id |
| 100 | + assert_equal "org_legacy", result.organization_id |
| 101 | + assert_equal "u_legacy", result.user["id"] |
| 102 | + end |
| 103 | + |
87 | 104 | def test_authenticate_merges_custom_claims_from_block |
88 | 105 | rsa, pub = signing_key_pair |
89 | 106 | access_token = make_jwt( |
@@ -244,6 +261,45 @@ def test_refresh_seals_session_client_side_and_returns_refresh_success |
244 | 261 | assert_equal "rt_new", unsealed["refresh_token"] |
245 | 262 | end |
246 | 263 |
|
| 264 | + def test_refresh_reads_legacy_v6_sealed_session |
| 265 | + rsa, pub = signing_key_pair |
| 266 | + old_access = make_jwt({"sid" => "session_old_v6", "exp" => Time.now.to_i - 60}, rsa) |
| 267 | + sealed = legacy_v6_seal( |
| 268 | + {"access_token" => old_access, "refresh_token" => "rt_old_v6", "user" => {"id" => "u_v6"}}, |
| 269 | + PASSWORD |
| 270 | + ) |
| 271 | + |
| 272 | + new_access = make_jwt({"sid" => "session_new_v6", "org_id" => "org_v6", "role" => "member", "exp" => Time.now.to_i + 300}, rsa) |
| 273 | + api_response = { |
| 274 | + "access_token" => new_access, |
| 275 | + "refresh_token" => "rt_new_v6", |
| 276 | + "user" => {"id" => "u_v6", "email" => "legacy@example.com"}, |
| 277 | + "impersonator" => nil |
| 278 | + } |
| 279 | + |
| 280 | + stub_request(:post, "https://api.workos.com/user_management/authenticate") |
| 281 | + .with(body: hash_including("grant_type" => "refresh_token", "refresh_token" => "rt_old_v6")) |
| 282 | + .to_return(status: 200, body: api_response.to_json) |
| 283 | + stub_request(:get, "https://api.workos.com/sso/jwks/client_001") |
| 284 | + .to_return(status: 200, body: jwks_payload(pub).to_json) |
| 285 | + |
| 286 | + session = @sm.load(seal_data: sealed, cookie_password: PASSWORD) |
| 287 | + result = session.refresh |
| 288 | + |
| 289 | + assert_kind_of WorkOS::SessionManager::RefreshSuccess, result |
| 290 | + assert result.authenticated |
| 291 | + assert_equal "session_new_v6", result.session_id |
| 292 | + assert_equal "org_v6", result.organization_id |
| 293 | + assert_equal "member", result.role |
| 294 | + assert_equal "u_v6", result.user["id"] |
| 295 | + |
| 296 | + refute_empty result.sealed_session |
| 297 | + unsealed = @sm.unseal_data(result.sealed_session, PASSWORD) |
| 298 | + assert_equal new_access, unsealed["access_token"] |
| 299 | + assert_equal "rt_new_v6", unsealed["refresh_token"] |
| 300 | + assert_equal "u_v6", unsealed["user"]["id"] |
| 301 | + end |
| 302 | + |
247 | 303 | def test_refresh_updates_internal_seal_data_for_subsequent_authenticate |
248 | 304 | rsa, pub = signing_key_pair |
249 | 305 | old_access = make_jwt({"sid" => "session_old", "exp" => Time.now.to_i - 60}, rsa) |
@@ -383,4 +439,16 @@ def custom.unseal(sealed, _key) |
383 | 439 | assert_kind_of WorkOS::SessionManager::AuthSuccess, result |
384 | 440 | assert_equal "s_custom", result.session_id |
385 | 441 | end |
| 442 | + |
| 443 | + private |
| 444 | + |
| 445 | + def legacy_v6_seal(data, key) |
| 446 | + cipher = OpenSSL::Cipher.new("aes-256-gcm").encrypt |
| 447 | + iv = SecureRandom.random_bytes(12) |
| 448 | + cipher.key = key |
| 449 | + cipher.iv = iv |
| 450 | + ciphertext = cipher.update(JSON.generate(data)) + cipher.final |
| 451 | + |
| 452 | + Base64.encode64(iv + ciphertext + cipher.auth_tag) |
| 453 | + end |
386 | 454 | end |
0 commit comments