diff --git a/lib/workos/session.rb b/lib/workos/session.rb index a70c3900..5165fb32 100644 --- a/lib/workos/session.rb +++ b/lib/workos/session.rb @@ -104,10 +104,13 @@ def refresh(organization_id: nil, cookie_password: nil) user: auth_response["user"], impersonator: auth_response["impersonator"] ) - @seal_data = sealed - @cookie_password = effective_password + # Decode before mutating session state so a malformed access_token + # doesn't leave the Session half-updated. decoded = @manager.decode_jwt(auth_response["access_token"]) + + @seal_data = sealed + @cookie_password = effective_password SessionManager::RefreshSuccess.new( authenticated: true, sealed_session: sealed, @@ -123,6 +126,8 @@ def refresh(organization_id: nil, cookie_password: nil) ) rescue WorkOS::AuthenticationError, WorkOS::InvalidRequestError => e SessionManager::RefreshError.new(authenticated: false, reason: e.message) + rescue JWT::DecodeError => e + SessionManager::RefreshError.new(authenticated: false, reason: e.message) end # Build the WorkOS session-logout URL for the currently authenticated session. diff --git a/test/workos/test_session.rb b/test/workos/test_session.rb index f63f2100..1fc6d116 100644 --- a/test/workos/test_session.rb +++ b/test/workos/test_session.rb @@ -305,6 +305,32 @@ def test_refresh_does_not_send_session_param_to_api assert_requested(stub) end + def test_refresh_returns_error_on_malformed_access_token_without_mutating_state + rsa, pub = signing_key_pair + old_access = make_jwt({"sid" => "session_old", "exp" => Time.now.to_i - 60}, rsa) + sealed = @sm.seal_data({"access_token" => old_access, "refresh_token" => "rt_old", "user" => {"id" => "u_1"}}, PASSWORD) + + api_response = { + "access_token" => "not-a-valid-jwt", + "refresh_token" => "rt_new", + "user" => {"id" => "u_1"} + } + + stub_request(:post, "https://api.workos.com/user_management/authenticate") + .to_return(status: 200, body: api_response.to_json) + stub_request(:get, "https://api.workos.com/sso/jwks/client_001") + .to_return(status: 200, body: jwks_payload(pub).to_json) + + session = @sm.load(seal_data: sealed, cookie_password: PASSWORD) + result = session.refresh + + assert_kind_of WorkOS::SessionManager::RefreshError, result + refute result.authenticated + + # Session state should not have been mutated + assert_equal sealed, session.seal_data + end + # --- Session constructor validation --------------------------------------- def test_session_load_requires_cookie_password