From 1d2e0d2c4604ad69601cb7ed68b2205433fca4c1 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Wed, 15 Apr 2026 22:58:53 -0400 Subject: [PATCH 1/2] Add documentation on sealing sessions --- docs/V6_MIGRATION_GUIDE.md | 83 +++++++++++++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 6 deletions(-) diff --git a/docs/V6_MIGRATION_GUIDE.md b/docs/V6_MIGRATION_GUIDE.md index 6b24c2d9..36954296 100644 --- a/docs/V6_MIGRATION_GUIDE.md +++ b/docs/V6_MIGRATION_GUIDE.md @@ -11,7 +11,8 @@ v6 is still recognizably the WorkOS Python SDK, but it moves onto a generated cl 3. Rename `client.portal` to `client.admin_portal`. 4. Remove any `client.fga` usage before upgrading. 5. Replace Pydantic-specific model code such as `model_validate()` and `model_dump()`. -6. Review exception handling, pagination assumptions, and retry-sensitive call sites. +6. If you pass `session=` to `authenticate_with_code()` or `authenticate_with_refresh_token()`, switch to explicit sealing after authentication. +7. Review exception handling, pagination assumptions, and retry-sensitive call sites. ## HIGH Impact Changes @@ -181,6 +182,74 @@ v6 also exposes runtime and auth-flow specific errors such as: **Migration:** Rename caught exception classes, update imports, and review any code that depends on old exception names or attributes. +### `authenticate_with_code` and `authenticate_with_refresh_token` no longer accept `session` + +In v5, `session=` was supported on `authenticate_with_code` and `authenticate_with_refresh_token`. In v6, those public wrappers no longer accept `session=`. The SDK session APIs are unchanged, but wrapper-level sealing is now an explicit step, and the minimal helper does not preserve the full v5 sealed payload. + +**v5** + +```python +response = client.user_management.authenticate_with_code( + code=code, + session={"seal_session": True, "cookie_password": COOKIE_PASSWORD}, +) +sealed = response.sealed_session +``` + +**v6** + +```python +from workos.session import seal_session_from_auth_response + +response = client.user_management.authenticate_with_code(code=code) + +sealed = seal_session_from_auth_response( + access_token=response.access_token, + refresh_token=response.refresh_token, + user=response.user.to_dict(), + impersonator=( + response.impersonator.to_dict() + if response.impersonator + else None + ), + cookie_password=COOKIE_PASSWORD, +) +``` + +The same pattern applies to `authenticate_with_refresh_token(...)`. + +If you need the broader v5-style sealed payload, seal the serialized response directly instead of just the session runtime fields: + +```python +from workos.session import seal_data + +response = client.user_management.authenticate_with_code(code=code) + +sealed = seal_data(response.to_dict(), COOKIE_PASSWORD) +``` + +That preserves additional response properties such as `organization_id`, `authentication_method`, `authkit_authorization_code`, and `oauth_tokens`. This is closer to v5 behavior, which sealed the full auth response dict. + +If you were passing `session=None` (a no-op in v5), just drop the argument on either method: + +```python +# v5 +response = client.user_management.authenticate_with_code(code=code, session=None) + +# v6 +response = client.user_management.authenticate_with_code(code=code) +``` + +**What still works the same:** + +- `client.user_management.load_sealed_session(session_data=..., cookie_password=...)` +- `client.user_management.authenticate_with_session_cookie(session_data=..., cookie_password=...)` +- `Session.authenticate()`, `Session.refresh()`, and `Session.get_logout_url()` + +**Affected users:** Anyone passing `session=` to `authenticate_with_code()` or `authenticate_with_refresh_token()`. + +**Migration:** Call `seal_session_from_auth_response()` after `authenticate_with_code()` or `authenticate_with_refresh_token()` when you only need the sealed-session runtime fields, use `seal_data(response.to_dict(), ...)` when you need the broader v5-style payload, or remove the `session=None` no-op. + ## MEDIUM Impact Changes ### Paginated list responses now use `SyncPage` and `AsyncPage` @@ -323,15 +392,16 @@ from workos.types.organizations import Organization 4. Find and remove any `client.fga` usage. 5. Replace `model_validate()` and `model_dump()` with `from_dict()` and `to_dict()`. 6. Update exception imports and any code that catches or inspects SDK errors. -7. Audit pagination code that depends on the old list wrapper shape. -8. Review retry-sensitive call sites and set `max_retries=0` where required. -9. Migrate old model imports toward `workos..models` and `workos.common.models`. -10. Run sync and async integration tests and look for import errors, attribute errors, serialization mismatches, and changed retry behavior. +7. If you pass `session=` to `authenticate_with_code()` or `authenticate_with_refresh_token()`, switch to explicit sealing. +8. Audit pagination code that depends on the old list wrapper shape. +9. Review retry-sensitive call sites and set `max_retries=0` where required. +10. Migrate old model imports toward `workos..models` and `workos.common.models`. +11. Run sync and async integration tests and look for import errors, attribute errors, serialization mismatches, and changed retry behavior. ## Searches To Run ```sh -rg 'workos\.client|workos\.async_client|client\.portal|client\.fga|model_dump|model_validate|Exception|workos\.types' +rg 'workos\.client|workos\.async_client|client\.portal|client\.fga|model_dump|model_validate|Exception|workos\.types|session=|seal_session' ``` ## Migration Checklist @@ -342,6 +412,7 @@ rg 'workos\.client|workos\.async_client|client\.portal|client\.fga|model_dump|mo - `client.fga` usage is removed or isolated from the v6 upgrade. - Pydantic-only model helpers are gone. - Exception imports use v6 `*Error` names. +- `session=` usage in `authenticate_with_code()` and `authenticate_with_refresh_token()` has been replaced with explicit sealing. - Retry behavior has been reviewed explicitly. - Pagination code has been updated where needed. - Model imports are moving toward `workos..models` and `workos.common.models`. From 5319c37b8659f4e2e5004b962e494184dc2596f7 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Wed, 15 Apr 2026 22:59:27 -0400 Subject: [PATCH 2/2] `user` is not optional --- src/workos/session.py | 5 ++--- tests/test_session.py | 6 ++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/workos/session.py b/src/workos/session.py index 70c83395..fbee198e 100644 --- a/src/workos/session.py +++ b/src/workos/session.py @@ -142,7 +142,7 @@ def seal_session_from_auth_response( *, access_token: str, refresh_token: str, - user: Optional[Dict[str, Any]] = None, + user: Dict[str, Any], impersonator: Optional[Dict[str, Any]] = None, cookie_password: str, ) -> str: @@ -161,9 +161,8 @@ def seal_session_from_auth_response( session_data: Dict[str, Any] = { "access_token": access_token, "refresh_token": refresh_token, + "user": user, } - if user is not None: - session_data["user"] = user if impersonator is not None: session_data["impersonator"] = impersonator return seal_data(session_data, cookie_password) diff --git a/tests/test_session.py b/tests/test_session.py index 36d113c0..3355bfb2 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -82,19 +82,21 @@ def test_seal_and_unseal(self): assert data["access_token"] == "at_123" assert data["user"]["id"] == "user_01" - def test_seal_without_optional_fields(self): + def test_seal_without_impersonator(self): sealed = seal_session_from_auth_response( access_token="at_123", refresh_token="rt_456", + user={"id": "user_01", "email": "test@example.com"}, cookie_password=COOKIE_PASSWORD, ) data = unseal_data(sealed, COOKIE_PASSWORD) - assert "user" not in data + assert "impersonator" not in data def test_seal_with_impersonator(self): sealed = seal_session_from_auth_response( access_token="at_123", refresh_token="rt_456", + user={"id": "user_01", "email": "test@example.com"}, impersonator={"email": "admin@example.com"}, cookie_password=COOKIE_PASSWORD, )