@@ -206,6 +206,131 @@ def test_get_logout_url_includes_session_id_from_authenticate
206206 assert_equal "https://app/cb" , params [ "return_to" ]
207207 end
208208
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+
209334 # --- Session constructor validation ---------------------------------------
210335
211336 def test_session_load_requires_cookie_password
0 commit comments