Skip to content

Commit b9f53a3

Browse files
authored
Add list sessions and revoke session methods and tests (#483)
* Add list sessions and revoke session methods and tests * Refactored types and updated where session is exposed based on review feedback * Update tests * Update implementatino of DEFAULT_LIST_RESPONSE_LIMIT to align with methods like list_users, list_invitations, and list_auth_factors * Update list session arguments to align with type checks
1 parent f33b90c commit b9f53a3

File tree

7 files changed

+277
-0
lines changed

7 files changed

+277
-0
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from typing import Union
2+
3+
import pytest
4+
5+
from tests.utils.list_resource import list_response_of
6+
from tests.utils.syncify import syncify
7+
from tests.types.test_auto_pagination_function import TestAutoPaginationFunction
8+
from workos.user_management import AsyncUserManagement, UserManagement
9+
10+
11+
def _mock_session(id: str):
12+
now = "2025-07-23T14:00:00.000Z"
13+
return {
14+
"object": "session",
15+
"id": id,
16+
"user_id": "user_123",
17+
"organization_id": "org_123",
18+
"status": "active",
19+
"auth_method": "password",
20+
"impersonator": None,
21+
"ip_address": "192.168.1.1",
22+
"user_agent": "Mozilla/5.0",
23+
"expires_at": "2025-07-23T15:00:00.000Z",
24+
"ended_at": None,
25+
"created_at": now,
26+
"updated_at": now,
27+
}
28+
29+
30+
@pytest.mark.sync_and_async(UserManagement, AsyncUserManagement)
31+
class TestUserManagementListSessions:
32+
@pytest.fixture(autouse=True)
33+
def setup(self, module_instance: Union[UserManagement, AsyncUserManagement]):
34+
self.http_client = module_instance._http_client
35+
self.user_management = module_instance
36+
37+
def test_list_sessions_query_and_parsing(
38+
self, capture_and_mock_http_client_request
39+
):
40+
sessions = [_mock_session("session_1"), _mock_session("session_2")]
41+
response = list_response_of(data=sessions)
42+
request_kwargs = capture_and_mock_http_client_request(
43+
self.http_client, response, 200
44+
)
45+
46+
result = syncify(
47+
self.user_management.list_sessions(
48+
user_id="user_123", limit=10, before="before_id", order="desc"
49+
)
50+
)
51+
52+
assert request_kwargs["url"].endswith("user_management/users/user_123/sessions")
53+
assert request_kwargs["method"] == "get"
54+
assert request_kwargs["params"]["limit"] == 10
55+
assert request_kwargs["params"]["before"] == "before_id"
56+
assert request_kwargs["params"]["order"] == "desc"
57+
assert "after" not in request_kwargs["params"]
58+
assert len(result.data) == 2
59+
assert result.data[0].id == "session_1"
60+
assert result.list_metadata.before is None
61+
assert result.list_metadata.after is None
62+
63+
def test_list_sessions_auto_pagination(
64+
self, test_auto_pagination: TestAutoPaginationFunction
65+
):
66+
data = [_mock_session(str(i)) for i in range(40)]
67+
test_auto_pagination(
68+
http_client=self.http_client,
69+
list_function=self.user_management.list_sessions,
70+
list_function_params={"user_id": "user_123"},
71+
expected_all_page_data=data,
72+
url_path_keys=["user_id"],
73+
)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from typing import Union
2+
3+
import pytest
4+
5+
from tests.utils.syncify import syncify
6+
from workos.user_management import AsyncUserManagement, UserManagement
7+
8+
9+
def _mock_session(id: str):
10+
now = "2025-07-23T14:00:00.000Z"
11+
return {
12+
"object": "session",
13+
"id": id,
14+
"user_id": "user_123",
15+
"organization_id": "org_123",
16+
"status": "revoked",
17+
"auth_method": "password",
18+
"ip_address": "192.168.1.1",
19+
"user_agent": "Mozilla/5.0",
20+
"expires_at": "2025-07-23T15:00:00.000Z",
21+
"ended_at": now,
22+
"created_at": now,
23+
"updated_at": now,
24+
}
25+
26+
27+
@pytest.mark.sync_and_async(UserManagement, AsyncUserManagement)
28+
class TestUserManagementRevokeSession:
29+
@pytest.fixture(autouse=True)
30+
def setup(self, module_instance: Union[UserManagement, AsyncUserManagement]):
31+
self.http_client = module_instance._http_client
32+
self.user_management = module_instance
33+
34+
def test_revoke_session(self, capture_and_mock_http_client_request):
35+
mock = _mock_session("session_abc")
36+
request_kwargs = capture_and_mock_http_client_request(
37+
self.http_client, mock, 200
38+
)
39+
40+
response = syncify(
41+
self.user_management.revoke_session(session_id="session_abc")
42+
)
43+
44+
assert request_kwargs["url"].endswith("user_management/sessions/revoke")
45+
assert request_kwargs["method"] == "post"
46+
assert request_kwargs["json"] == {"session_id": "session_abc"}
47+
assert response.id == "session_abc"

workos/types/list_resource.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from workos.types.organizations import Organization
3535
from workos.types.sso import ConnectionWithDomains
3636
from workos.types.user_management import Invitation, OrganizationMembership, User
37+
from workos.types.user_management.session import Session as UserManagementSession
3738
from workos.types.vault import ObjectDigest
3839
from workos.types.workos_model import WorkOSModel
3940
from workos.utils.request_helper import DEFAULT_LIST_RESPONSE_LIMIT
@@ -54,6 +55,7 @@
5455
AuthorizationResource,
5556
AuthorizationResourceType,
5657
User,
58+
UserManagementSession,
5759
ObjectDigest,
5860
Warrant,
5961
WarrantQueryResult,

workos/types/user_management/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@
99
from .password_reset import *
1010
from .user_management_provider_type import *
1111
from .user import *
12+
from .session import *

workos/types/user_management/list_filters.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,7 @@ class OrganizationMembershipsListFilters(ListArgs, total=False):
2323

2424
class AuthenticationFactorsListFilters(ListArgs, total=False):
2525
user_id: str
26+
27+
28+
class SessionsListFilters(ListArgs, total=False):
29+
user_id: str

workos/types/user_management/session.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,34 @@ class RefreshWithSessionCookieErrorResponse(WorkOSModel):
4646
class SessionConfig(TypedDict, total=False):
4747
seal_session: bool
4848
cookie_password: str
49+
50+
51+
AuthMethodType = Literal[
52+
"external_auth",
53+
"impersonation",
54+
"magic_code",
55+
"migrated_session",
56+
"oauth",
57+
"passkey",
58+
"password",
59+
"sso",
60+
"unknown",
61+
]
62+
63+
64+
class Session(WorkOSModel):
65+
"""Representation of a WorkOS User Management Session."""
66+
67+
object: Literal["session"]
68+
id: str
69+
user_id: str
70+
organization_id: Optional[str] = None
71+
status: str
72+
auth_method: AuthMethodType
73+
impersonator: Optional[Impersonator] = None
74+
ip_address: Optional[str] = None
75+
user_agent: Optional[str] = None
76+
expires_at: str
77+
ended_at: Optional[str] = None
78+
created_at: str
79+
updated_at: str

workos/user_management.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
from workos.types.user_management.password_hash_type import PasswordHashType
4949
from workos.types.user_management.screen_hint import ScreenHintType
5050
from workos.types.user_management.session import SessionConfig
51+
from workos.types.user_management.session import Session as UserManagementSession
5152
from workos.types.user_management.user_management_provider_type import (
5253
UserManagementProviderType,
5354
)
@@ -86,6 +87,8 @@
8687
MAGIC_AUTH_PATH = "user_management/magic_auth"
8788
USER_SEND_MAGIC_AUTH_PATH = "user_management/magic_auth/send"
8889
USER_AUTH_FACTORS_PATH = "user_management/users/{0}/auth_factors"
90+
USER_SESSIONS_PATH = "user_management/users/{0}/sessions"
91+
SESSIONS_REVOKE_PATH = "user_management/sessions/revoke"
8992
EMAIL_VERIFICATION_DETAIL_PATH = "user_management/email_verification/{0}"
9093
INVITATION_PATH = "user_management/invitations"
9194
INVITATION_DETAIL_PATH = "user_management/invitations/{0}"
@@ -109,6 +112,12 @@
109112
Invitation, InvitationsListFilters, ListMetadata
110113
]
111114

115+
from workos.types.user_management.list_filters import SessionsListFilters
116+
117+
SessionsListResource = WorkOSListResource[
118+
UserManagementSession, SessionsListFilters, ListMetadata
119+
]
120+
112121

113122
class UserManagementModule(Protocol):
114123
"""Offers methods for using the WorkOS User Management API."""
@@ -720,6 +729,20 @@ def verify_email(self, *, user_id: str, code: str) -> SyncOrAsync[User]:
720729
"""
721730
...
722731

732+
def list_sessions(
733+
self,
734+
*,
735+
user_id: str,
736+
limit: Optional[int] = None,
737+
before: Optional[str] = None,
738+
after: Optional[str] = None,
739+
order: Optional[PaginationOrder] = "desc",
740+
) -> SyncOrAsync["SessionsListResource"]: ...
741+
742+
def revoke_session(
743+
self, *, session_id: str
744+
) -> SyncOrAsync[UserManagementSession]: ...
745+
723746
def get_magic_auth(self, magic_auth_id: str) -> SyncOrAsync[MagicAuth]:
724747
"""Get the details of a Magic Auth object.
725748
@@ -1377,6 +1400,54 @@ def create_magic_auth(
13771400

13781401
return MagicAuth.model_validate(response)
13791402

1403+
def list_sessions(
1404+
self,
1405+
*,
1406+
user_id: str,
1407+
limit: Optional[int] = DEFAULT_LIST_RESPONSE_LIMIT,
1408+
before: Optional[str] = None,
1409+
after: Optional[str] = None,
1410+
order: Optional[PaginationOrder] = "desc",
1411+
) -> "SessionsListResource":
1412+
limit_value: int = limit if limit is not None else DEFAULT_LIST_RESPONSE_LIMIT
1413+
1414+
params: ListArgs = {
1415+
"limit": limit_value,
1416+
"before": before,
1417+
"after": after,
1418+
"order": order,
1419+
}
1420+
1421+
response = self._http_client.request(
1422+
USER_SESSIONS_PATH.format(user_id),
1423+
method=REQUEST_METHOD_GET,
1424+
params=params,
1425+
)
1426+
1427+
list_args: SessionsListFilters = {
1428+
"limit": limit_value,
1429+
"before": before,
1430+
"after": after,
1431+
"user_id": user_id,
1432+
}
1433+
if order is not None:
1434+
list_args["order"] = order
1435+
1436+
return SessionsListResource(
1437+
list_method=self.list_sessions,
1438+
list_args=list_args,
1439+
**ListPage[UserManagementSession](**response).model_dump(),
1440+
)
1441+
1442+
def revoke_session(self, *, session_id: str) -> UserManagementSession:
1443+
json = {"session_id": session_id}
1444+
1445+
response = self._http_client.request(
1446+
SESSIONS_REVOKE_PATH, method=REQUEST_METHOD_POST, json=json
1447+
)
1448+
1449+
return UserManagementSession.model_validate(response)
1450+
13801451
def enroll_auth_factor(
13811452
self,
13821453
*,
@@ -2033,6 +2104,54 @@ async def create_magic_auth(
20332104

20342105
return MagicAuth.model_validate(response)
20352106

2107+
async def list_sessions(
2108+
self,
2109+
*,
2110+
user_id: str,
2111+
limit: Optional[int] = DEFAULT_LIST_RESPONSE_LIMIT,
2112+
before: Optional[str] = None,
2113+
after: Optional[str] = None,
2114+
order: Optional[PaginationOrder] = "desc",
2115+
) -> "SessionsListResource":
2116+
limit_value: int = limit if limit is not None else DEFAULT_LIST_RESPONSE_LIMIT
2117+
2118+
params: ListArgs = {
2119+
"limit": limit_value,
2120+
"before": before,
2121+
"after": after,
2122+
"order": order,
2123+
}
2124+
2125+
response = await self._http_client.request(
2126+
USER_SESSIONS_PATH.format(user_id),
2127+
method=REQUEST_METHOD_GET,
2128+
params=params,
2129+
)
2130+
2131+
list_args: SessionsListFilters = {
2132+
"limit": limit_value,
2133+
"before": before,
2134+
"after": after,
2135+
"user_id": user_id,
2136+
}
2137+
if order is not None:
2138+
list_args["order"] = order
2139+
2140+
return SessionsListResource(
2141+
list_method=self.list_sessions,
2142+
list_args=list_args,
2143+
**ListPage[UserManagementSession](**response).model_dump(),
2144+
)
2145+
2146+
async def revoke_session(self, *, session_id: str) -> UserManagementSession:
2147+
json = {"session_id": session_id}
2148+
2149+
response = await self._http_client.request(
2150+
SESSIONS_REVOKE_PATH, method=REQUEST_METHOD_POST, json=json
2151+
)
2152+
2153+
return UserManagementSession.model_validate(response)
2154+
20362155
async def enroll_auth_factor(
20372156
self,
20382157
*,

0 commit comments

Comments
 (0)