Skip to content

Commit 01d9bd4

Browse files
committed
Add list sessions and revoke session methods and tests
1 parent f33b90c commit 01d9bd4

File tree

8 files changed

+258
-1
lines changed

8 files changed

+258
-1
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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+
"ip_address": "192.168.1.1",
21+
"user_agent": "Mozilla/5.0",
22+
"expires_at": "2025-07-23T15:00:00.000Z",
23+
"ended_at": None,
24+
"created_at": now,
25+
"updated_at": now,
26+
}
27+
28+
29+
@pytest.mark.sync_and_async(UserManagement, AsyncUserManagement)
30+
class TestUserManagementListSessions:
31+
@pytest.fixture(autouse=True)
32+
def setup(self, module_instance: Union[UserManagement, AsyncUserManagement]):
33+
self.http_client = module_instance._http_client
34+
self.user_management = module_instance
35+
36+
def test_list_sessions_query_and_parsing(
37+
self, capture_and_mock_http_client_request
38+
):
39+
sessions = [_mock_session("session_1"), _mock_session("session_2")]
40+
response = list_response_of(data=sessions)
41+
request_kwargs = capture_and_mock_http_client_request(
42+
self.http_client, response, 200
43+
)
44+
45+
result = syncify(
46+
self.user_management.list_sessions(
47+
user_id="user_123", limit=10, before="before_id", order="desc"
48+
)
49+
)
50+
51+
assert request_kwargs["url"].endswith("user_management/users/user_123/sessions")
52+
assert request_kwargs["method"] == "get"
53+
assert request_kwargs["params"]["limit"] == 10
54+
assert request_kwargs["params"]["before"] == "before_id"
55+
assert request_kwargs["params"]["order"] == "desc"
56+
assert "after" not in request_kwargs["params"]
57+
assert len(result.data) == 2
58+
assert result.data[0].id == "session_1"
59+
assert result.list_metadata.before is None
60+
assert result.list_metadata.after is None
61+
62+
def test_list_sessions_auto_pagination(
63+
self, test_auto_pagination: TestAutoPaginationFunction
64+
):
65+
data = [_mock_session(str(i)) for i in range(40)]
66+
test_auto_pagination(
67+
http_client=self.http_client,
68+
list_function=self.user_management.list_sessions,
69+
list_function_params={"user_id": "user_123"},
70+
expected_all_page_data=data,
71+
url_path_keys=["user_id"],
72+
)
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/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from workos.client import SyncClient as WorkOSClient
22
from workos.async_client import AsyncClient as AsyncWorkOSClient
3+
from workos.types.user_management.session import Session as Session
34

4-
__all__ = ["WorkOSClient", "AsyncWorkOSClient"]
5+
__all__ = ["WorkOSClient", "AsyncWorkOSClient", "Session"]

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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,20 @@ class RefreshWithSessionCookieErrorResponse(WorkOSModel):
4646
class SessionConfig(TypedDict, total=False):
4747
seal_session: bool
4848
cookie_password: str
49+
50+
51+
class Session(WorkOSModel):
52+
"""Representation of a WorkOS User Management Session."""
53+
54+
object: Literal["session"]
55+
id: str
56+
user_id: str
57+
organization_id: Optional[str] = None
58+
status: str
59+
auth_method: Optional[str] = None
60+
ip_address: Optional[str] = None
61+
user_agent: Optional[str] = None
62+
expires_at: Optional[str] = None
63+
ended_at: Optional[str] = None
64+
created_at: str
65+
updated_at: str

workos/user_management.py

Lines changed: 113 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] = None,
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,51 @@ 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] = None,
1408+
before: Optional[str] = None,
1409+
after: Optional[str] = None,
1410+
order: Optional[PaginationOrder] = None,
1411+
) -> "SessionsListResource":
1412+
params: ListArgs = {
1413+
"limit": limit or DEFAULT_LIST_RESPONSE_LIMIT,
1414+
"before": before,
1415+
"after": after,
1416+
"order": order,
1417+
}
1418+
1419+
response = self._http_client.request(
1420+
USER_SESSIONS_PATH.format(user_id),
1421+
method=REQUEST_METHOD_GET,
1422+
params=params,
1423+
)
1424+
1425+
list_args: SessionsListFilters = {
1426+
"limit": limit or DEFAULT_LIST_RESPONSE_LIMIT,
1427+
"before": before,
1428+
"after": after,
1429+
"order": order,
1430+
"user_id": user_id,
1431+
}
1432+
1433+
return SessionsListResource(
1434+
list_method=self.list_sessions,
1435+
list_args=list_args,
1436+
**ListPage[UserManagementSession](**response).model_dump(),
1437+
)
1438+
1439+
def revoke_session(self, *, session_id: str) -> UserManagementSession:
1440+
json = {"session_id": session_id}
1441+
1442+
response = self._http_client.request(
1443+
SESSIONS_REVOKE_PATH, method=REQUEST_METHOD_POST, json=json
1444+
)
1445+
1446+
return UserManagementSession.model_validate(response)
1447+
13801448
def enroll_auth_factor(
13811449
self,
13821450
*,
@@ -2033,6 +2101,51 @@ async def create_magic_auth(
20332101

20342102
return MagicAuth.model_validate(response)
20352103

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

0 commit comments

Comments
 (0)