From b4d4ff4ede59828d5f08c9140d6ca4a49a67803c Mon Sep 17 00:00:00 2001 From: "d.saner@enlyze.com" Date: Tue, 4 Nov 2025 21:29:34 +0100 Subject: [PATCH 01/13] introduce ApiKey module alongside with types --- tests/test_api_keys.py | 46 ++++++++++++++++++++++++ tests/utils/fixtures/mock_api_key.py | 16 +++++++++ workos/api_key.py | 54 ++++++++++++++++++++++++++++ workos/types/api_key/__init__.py | 1 + workos/types/api_key/api_key.py | 12 +++++++ 5 files changed, 129 insertions(+) create mode 100644 tests/test_api_keys.py create mode 100644 tests/utils/fixtures/mock_api_key.py create mode 100644 workos/api_key.py create mode 100644 workos/types/api_key/__init__.py create mode 100644 workos/types/api_key/api_key.py diff --git a/tests/test_api_keys.py b/tests/test_api_keys.py new file mode 100644 index 00000000..846a3794 --- /dev/null +++ b/tests/test_api_keys.py @@ -0,0 +1,46 @@ +import pytest + +from tests.utils.fixtures.mock_api_key import MockApiKey +from tests.utils.syncify import syncify +from workos.api_key import ApiKey, AsyncApiKey +from workos.exceptions import AuthenticationException + + +@pytest.mark.sync_and_async(ApiKey, AsyncApiKey) +class TestApiKey: + @pytest.fixture + def mock_api_key_details(self): + api_key_details = MockApiKey() + return api_key_details.model_dump() + + def test_validate_api_key_with_valid_key( + self, + module_instance, + mock_api_key_details, + capture_and_mock_http_client_request, + ): + request_kwargs = capture_and_mock_http_client_request( + module_instance._http_client, mock_api_key_details, 200 + ) + + api_key_details = syncify(module_instance.validate_api_key()) + + assert request_kwargs["url"].endswith("/api_keys/validate") + assert request_kwargs["method"] == "post" + assert api_key_details.id == mock_api_key_details["id"] + assert api_key_details.name == mock_api_key_details["name"] + assert api_key_details.object == "api_key" + + def test_validate_api_key_with_invalid_key( + self, + module_instance, + mock_http_client_with_response, + ): + mock_http_client_with_response( + module_instance._http_client, + {"message": "Invalid API key", "error": "invalid_api_key"}, + 401, + ) + + with pytest.raises(AuthenticationException): + syncify(module_instance.validate_api_key()) diff --git a/tests/utils/fixtures/mock_api_key.py b/tests/utils/fixtures/mock_api_key.py new file mode 100644 index 00000000..d1f23877 --- /dev/null +++ b/tests/utils/fixtures/mock_api_key.py @@ -0,0 +1,16 @@ +import datetime + +from workos.types.api_keys import ApiKey + + +class MockApiKey(ApiKey): + def __init__(self, id="api_key_01234567890"): + now = datetime.datetime.now().isoformat() + super().__init__( + object="api_key", + id=id, + name="Development API Key", + last_used_at=now, + created_at=now, + updated_at=now, + ) diff --git a/workos/api_key.py b/workos/api_key.py new file mode 100644 index 00000000..f5eaa869 --- /dev/null +++ b/workos/api_key.py @@ -0,0 +1,54 @@ +from typing import Protocol + +from workos.types.api_keys import ApiKey +from workos.typing.sync_or_async import SyncOrAsync +from workos.utils.http_client import AsyncHTTPClient, SyncHTTPClient +from workos.utils.request_helper import REQUEST_METHOD_POST + + +class ApiKeyModule(Protocol): + def validate_api_key(self) -> SyncOrAsync[ApiKey]: + """Validates the configured API key. + + Returns: + ApiKey: The validated API key details containing + information about the key's name and usage + + Raises: + AuthenticationException: If the API key is invalid or + unauthorized (401) + NotFoundException: If the API key is not found (404) + ServerException: If the API server encounters an error + (5xx) + """ + ... + + +class ApiKey(ApiKeyModule): + _http_client: SyncHTTPClient + + def __init__(self, http_client: SyncHTTPClient): + self._http_client = http_client + + def validate_api_key(self) -> ApiKey: + response = self._http_client.request( + "api_keys/validate", + method=REQUEST_METHOD_POST, + ) + + return ApiKey.model_validate(response) + + +class AsyncApiKey(ApiKeyModule): + _http_client: AsyncHTTPClient + + def __init__(self, http_client: AsyncHTTPClient): + self._http_client = http_client + + async def validate_api_key(self) -> ApiKey: + response = await self._http_client.request( + "api_keys/validate", + method=REQUEST_METHOD_POST, + ) + + return ApiKey.model_validate(response) diff --git a/workos/types/api_key/__init__.py b/workos/types/api_key/__init__.py new file mode 100644 index 00000000..60874ecc --- /dev/null +++ b/workos/types/api_key/__init__.py @@ -0,0 +1 @@ +from .api_key import ApiKey as ApiKey # noqa: F401 diff --git a/workos/types/api_key/api_key.py b/workos/types/api_key/api_key.py new file mode 100644 index 00000000..d807e419 --- /dev/null +++ b/workos/types/api_key/api_key.py @@ -0,0 +1,12 @@ +from typing import Literal + +from workos.types.workos_model import WorkOSModel + + +class ApiKey(WorkOSModel): + object: Literal["api_key"] + id: str + name: str + last_used_at: str | None = None + created_at: str + updated_at: str From 600f0466941e5bab0cd31e72179fefa16107dafb Mon Sep 17 00:00:00 2001 From: "d.saner@enlyze.com" Date: Tue, 4 Nov 2025 21:30:58 +0100 Subject: [PATCH 02/13] wire ApiKey module through clients --- tests/test_client.py | 6 ++++++ workos/_base_client.py | 5 +++++ workos/async_client.py | 7 +++++++ workos/client.py | 7 +++++++ 4 files changed, 25 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index e23cf93c..9a2c1a81 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -37,6 +37,9 @@ def test_client_with_api_key_and_client_id_environment_variables(self): os.environ.pop("WORKOS_API_KEY") os.environ.pop("WORKOS_CLIENT_ID") + def test_initialize_api_keys(self, default_client): + assert bool(default_client.api_keys) + def test_initialize_sso(self, default_client): assert bool(default_client.sso) @@ -112,6 +115,9 @@ def test_client_with_api_key_and_client_id_environment_variables(self): os.environ.pop("WORKOS_API_KEY") os.environ.pop("WORKOS_CLIENT_ID") + def test_initialize_api_keys(self, default_client): + assert bool(default_client.api_keys) + def test_initialize_directory_sync(self, default_client): assert bool(default_client.directory_sync) diff --git a/workos/_base_client.py b/workos/_base_client.py index d805a80a..ad53cc7a 100644 --- a/workos/_base_client.py +++ b/workos/_base_client.py @@ -6,6 +6,7 @@ from workos.fga import FGAModule from workos.utils._base_http_client import DEFAULT_REQUEST_TIMEOUT from workos.utils.http_client import HTTPClient +from workos.api_key import ApiKeyModule from workos.audit_logs import AuditLogsModule from workos.directory_sync import DirectorySyncModule from workos.events import EventsModule @@ -65,6 +66,10 @@ def __init__( else int(os.getenv("WORKOS_REQUEST_TIMEOUT", DEFAULT_REQUEST_TIMEOUT)) ) + @property + @abstractmethod + def api_keys(self) -> ApiKeyModule: ... + @property @abstractmethod def audit_logs(self) -> AuditLogsModule: ... diff --git a/workos/async_client.py b/workos/async_client.py index 920c08ab..5edcab33 100644 --- a/workos/async_client.py +++ b/workos/async_client.py @@ -1,6 +1,7 @@ from typing import Optional from workos.__about__ import __version__ from workos._base_client import BaseClient +from workos.api_key import AsyncApiKey from workos.audit_logs import AuditLogsModule from workos.directory_sync import AsyncDirectorySync from workos.events import AsyncEvents @@ -45,6 +46,12 @@ def __init__( timeout=self.request_timeout, ) + @property + def api_keys(self) -> AsyncApiKey: + if not getattr(self, "_api_keys", None): + self._api_keys = AsyncApiKey(self._http_client) + return self._api_keys + @property def sso(self) -> AsyncSSO: if not getattr(self, "_sso", None): diff --git a/workos/client.py b/workos/client.py index 9c4aa154..7e3466c1 100644 --- a/workos/client.py +++ b/workos/client.py @@ -1,6 +1,7 @@ from typing import Optional from workos.__about__ import __version__ from workos._base_client import BaseClient +from workos.api_key import ApiKey from workos.audit_logs import AuditLogs from workos.directory_sync import DirectorySync from workos.fga import FGA @@ -45,6 +46,12 @@ def __init__( timeout=self.request_timeout, ) + @property + def api_keys(self) -> ApiKey: + if not getattr(self, "_api_keys", None): + self._api_keys = ApiKey(self._http_client) + return self._api_keys + @property def sso(self) -> SSO: if not getattr(self, "_sso", None): From aa5199670a310258cb9fc0d9f6e110c4f76fdf02 Mon Sep 17 00:00:00 2001 From: "d.saner@enlyze.com" Date: Thu, 13 Nov 2025 09:29:02 +0100 Subject: [PATCH 03/13] rename api_key modules to api_keys --- workos/{api_key.py => api_keys.py} | 0 workos/types/api_key/__init__.py | 1 - workos/types/api_keys/__init__.py | 1 + workos/types/{api_key/api_key.py => api_keys/api_keys.py} | 0 4 files changed, 1 insertion(+), 1 deletion(-) rename workos/{api_key.py => api_keys.py} (100%) delete mode 100644 workos/types/api_key/__init__.py create mode 100644 workos/types/api_keys/__init__.py rename workos/types/{api_key/api_key.py => api_keys/api_keys.py} (100%) diff --git a/workos/api_key.py b/workos/api_keys.py similarity index 100% rename from workos/api_key.py rename to workos/api_keys.py diff --git a/workos/types/api_key/__init__.py b/workos/types/api_key/__init__.py deleted file mode 100644 index 60874ecc..00000000 --- a/workos/types/api_key/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .api_key import ApiKey as ApiKey # noqa: F401 diff --git a/workos/types/api_keys/__init__.py b/workos/types/api_keys/__init__.py new file mode 100644 index 00000000..c03f4fee --- /dev/null +++ b/workos/types/api_keys/__init__.py @@ -0,0 +1 @@ +from .api_keys import ApiKey as ApiKey # noqa: F401 diff --git a/workos/types/api_key/api_key.py b/workos/types/api_keys/api_keys.py similarity index 100% rename from workos/types/api_key/api_key.py rename to workos/types/api_keys/api_keys.py From 6622211ecb69fd1b626643632f96473060c7773c Mon Sep 17 00:00:00 2001 From: "d.saner@enlyze.com" Date: Thu, 13 Nov 2025 09:29:35 +0100 Subject: [PATCH 04/13] extend ApiKey response model by missing fields --- tests/utils/fixtures/mock_api_key.py | 3 +++ workos/types/api_keys/api_keys.py | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/tests/utils/fixtures/mock_api_key.py b/tests/utils/fixtures/mock_api_key.py index d1f23877..7fdcc50e 100644 --- a/tests/utils/fixtures/mock_api_key.py +++ b/tests/utils/fixtures/mock_api_key.py @@ -9,7 +9,10 @@ def __init__(self, id="api_key_01234567890"): super().__init__( object="api_key", id=id, + owner={"type": "organization", "id": "org_1337"}, name="Development API Key", + obfuscated_value="api_..0", + permissions=[], last_used_at=now, created_at=now, updated_at=now, diff --git a/workos/types/api_keys/api_keys.py b/workos/types/api_keys/api_keys.py index d807e419..04bb0a22 100644 --- a/workos/types/api_keys/api_keys.py +++ b/workos/types/api_keys/api_keys.py @@ -3,10 +3,18 @@ from workos.types.workos_model import WorkOSModel +class ApiKeyOwner(WorkOSModel): + type: str + id: str + + class ApiKey(WorkOSModel): object: Literal["api_key"] id: str + owner: ApiKeyOwner name: str + obfuscated_value: str last_used_at: str | None = None + permissions: list[str] created_at: str updated_at: str From 1566ff03b4e2091399e0ebede605e342917c5557 Mon Sep 17 00:00:00 2001 From: "d.saner@enlyze.com" Date: Thu, 13 Nov 2025 09:33:03 +0100 Subject: [PATCH 05/13] pluralize ApiKeyModule, ApiKey and AsyncApiKey --- workos/_base_client.py | 15 +++++++-------- workos/api_keys.py | 7 ++++--- workos/async_client.py | 6 +++--- workos/client.py | 6 +++--- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/workos/_base_client.py b/workos/_base_client.py index ad53cc7a..326ab20d 100644 --- a/workos/_base_client.py +++ b/workos/_base_client.py @@ -1,22 +1,21 @@ -from abc import abstractmethod import os +from abc import abstractmethod from typing import Optional -from workos.__about__ import __version__ + from workos._client_configuration import ClientConfiguration -from workos.fga import FGAModule -from workos.utils._base_http_client import DEFAULT_REQUEST_TIMEOUT -from workos.utils.http_client import HTTPClient -from workos.api_key import ApiKeyModule +from workos.api_keys import ApiKeysModule from workos.audit_logs import AuditLogsModule from workos.directory_sync import DirectorySyncModule from workos.events import EventsModule +from workos.fga import FGAModule from workos.mfa import MFAModule -from workos.organizations import OrganizationsModule from workos.organization_domains import OrganizationDomainsModule +from workos.organizations import OrganizationsModule from workos.passwordless import PasswordlessModule from workos.portal import PortalModule from workos.sso import SSOModule from workos.user_management import UserManagementModule +from workos.utils._base_http_client import DEFAULT_REQUEST_TIMEOUT from workos.webhooks import WebhooksModule @@ -68,7 +67,7 @@ def __init__( @property @abstractmethod - def api_keys(self) -> ApiKeyModule: ... + def api_keys(self) -> ApiKeysModule: ... @property @abstractmethod diff --git a/workos/api_keys.py b/workos/api_keys.py index f5eaa869..6ab42735 100644 --- a/workos/api_keys.py +++ b/workos/api_keys.py @@ -6,7 +6,8 @@ from workos.utils.request_helper import REQUEST_METHOD_POST -class ApiKeyModule(Protocol): + +class ApiKeysModule(Protocol): def validate_api_key(self) -> SyncOrAsync[ApiKey]: """Validates the configured API key. @@ -24,7 +25,7 @@ def validate_api_key(self) -> SyncOrAsync[ApiKey]: ... -class ApiKey(ApiKeyModule): +class ApiKeys(ApiKeysModule): _http_client: SyncHTTPClient def __init__(self, http_client: SyncHTTPClient): @@ -39,7 +40,7 @@ def validate_api_key(self) -> ApiKey: return ApiKey.model_validate(response) -class AsyncApiKey(ApiKeyModule): +class AsyncApiKeys(ApiKeysModule): _http_client: AsyncHTTPClient def __init__(self, http_client: AsyncHTTPClient): diff --git a/workos/async_client.py b/workos/async_client.py index 5edcab33..b3d25979 100644 --- a/workos/async_client.py +++ b/workos/async_client.py @@ -1,7 +1,7 @@ from typing import Optional from workos.__about__ import __version__ from workos._base_client import BaseClient -from workos.api_key import AsyncApiKey +from workos.api_keys import AsyncApiKeys from workos.audit_logs import AuditLogsModule from workos.directory_sync import AsyncDirectorySync from workos.events import AsyncEvents @@ -47,9 +47,9 @@ def __init__( ) @property - def api_keys(self) -> AsyncApiKey: + def api_keys(self) -> AsyncApiKeys: if not getattr(self, "_api_keys", None): - self._api_keys = AsyncApiKey(self._http_client) + self._api_keys = AsyncApiKeys(self._http_client) return self._api_keys @property diff --git a/workos/client.py b/workos/client.py index 7e3466c1..6c124f51 100644 --- a/workos/client.py +++ b/workos/client.py @@ -1,7 +1,7 @@ from typing import Optional from workos.__about__ import __version__ from workos._base_client import BaseClient -from workos.api_key import ApiKey +from workos.api_keys import ApiKeys from workos.audit_logs import AuditLogs from workos.directory_sync import DirectorySync from workos.fga import FGA @@ -47,9 +47,9 @@ def __init__( ) @property - def api_keys(self) -> ApiKey: + def api_keys(self) -> ApiKeys: if not getattr(self, "_api_keys", None): - self._api_keys = ApiKey(self._http_client) + self._api_keys = ApiKeys(self._http_client) return self._api_keys @property From 1239d82a94e6c7e81767f1f170b99fd6860d025b Mon Sep 17 00:00:00 2001 From: "d.saner@enlyze.com" Date: Thu, 13 Nov 2025 09:36:39 +0100 Subject: [PATCH 06/13] remove AI slop, actually validate API key --- workos/api_keys.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/workos/api_keys.py b/workos/api_keys.py index 6ab42735..139af13b 100644 --- a/workos/api_keys.py +++ b/workos/api_keys.py @@ -8,7 +8,7 @@ class ApiKeysModule(Protocol): - def validate_api_key(self) -> SyncOrAsync[ApiKey]: + def validate_api_key(self, *, value: str) -> SyncOrAsync[ApiKey]: """Validates the configured API key. Returns: @@ -31,13 +31,12 @@ class ApiKeys(ApiKeysModule): def __init__(self, http_client: SyncHTTPClient): self._http_client = http_client - def validate_api_key(self) -> ApiKey: + def validate_api_key(self, *, value: str) -> ApiKey: response = self._http_client.request( - "api_keys/validate", - method=REQUEST_METHOD_POST, + "api_keys/validations", method=REQUEST_METHOD_POST, json={ + "value": value} ) - - return ApiKey.model_validate(response) + return ApiKey.model_validate(response["api_key"]) class AsyncApiKeys(ApiKeysModule): @@ -46,10 +45,9 @@ class AsyncApiKeys(ApiKeysModule): def __init__(self, http_client: AsyncHTTPClient): self._http_client = http_client - async def validate_api_key(self) -> ApiKey: + async def validate_api_key(self, *, value: str) -> ApiKey: response = await self._http_client.request( - "api_keys/validate", - method=REQUEST_METHOD_POST, + "api_keys/validations", method=REQUEST_METHOD_POST, json={ + "value": value} ) - - return ApiKey.model_validate(response) + return ApiKey.model_validate(response["api_key"]) From 683d508ec702557d8b893940266f601014826f9c Mon Sep 17 00:00:00 2001 From: "d.saner@enlyze.com" Date: Thu, 13 Nov 2025 09:37:10 +0100 Subject: [PATCH 07/13] make api key validation path a constant --- workos/api_keys.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/workos/api_keys.py b/workos/api_keys.py index 139af13b..88f2a6c4 100644 --- a/workos/api_keys.py +++ b/workos/api_keys.py @@ -5,6 +5,7 @@ from workos.utils.http_client import AsyncHTTPClient, SyncHTTPClient from workos.utils.request_helper import REQUEST_METHOD_POST +API_KEY_VALIDATION_PATH = "api_keys/validations" class ApiKeysModule(Protocol): @@ -33,7 +34,7 @@ def __init__(self, http_client: SyncHTTPClient): def validate_api_key(self, *, value: str) -> ApiKey: response = self._http_client.request( - "api_keys/validations", method=REQUEST_METHOD_POST, json={ + API_KEY_VALIDATION_PATH, method=REQUEST_METHOD_POST, json={ "value": value} ) return ApiKey.model_validate(response["api_key"]) @@ -47,7 +48,7 @@ def __init__(self, http_client: AsyncHTTPClient): async def validate_api_key(self, *, value: str) -> ApiKey: response = await self._http_client.request( - "api_keys/validations", method=REQUEST_METHOD_POST, json={ + API_KEY_VALIDATION_PATH, method=REQUEST_METHOD_POST, json={ "value": value} ) return ApiKey.model_validate(response["api_key"]) From b445127cc62589a21888bab5ad50dbd4a0b3e8d2 Mon Sep 17 00:00:00 2001 From: "d.saner@enlyze.com" Date: Thu, 13 Nov 2025 09:38:00 +0100 Subject: [PATCH 08/13] adapt tests --- tests/test_api_keys.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/tests/test_api_keys.py b/tests/test_api_keys.py index 846a3794..f81f13b0 100644 --- a/tests/test_api_keys.py +++ b/tests/test_api_keys.py @@ -1,34 +1,40 @@ +# type: ignore import pytest from tests.utils.fixtures.mock_api_key import MockApiKey from tests.utils.syncify import syncify -from workos.api_key import ApiKey, AsyncApiKey +from workos.api_keys import API_KEY_VALIDATION_PATH, ApiKeys, AsyncApiKeys from workos.exceptions import AuthenticationException -@pytest.mark.sync_and_async(ApiKey, AsyncApiKey) -class TestApiKey: +@pytest.mark.sync_and_async(ApiKeys, AsyncApiKeys) +class TestApiKeys: @pytest.fixture - def mock_api_key_details(self): - api_key_details = MockApiKey() - return api_key_details.model_dump() + def mock_api_key(self): + return MockApiKey().dict() + + @pytest.fixture + def api_key(self): + return "sk_my_api_key" def test_validate_api_key_with_valid_key( self, module_instance, - mock_api_key_details, + api_key, + mock_api_key, capture_and_mock_http_client_request, ): + response_body = {"api_key": mock_api_key} request_kwargs = capture_and_mock_http_client_request( - module_instance._http_client, mock_api_key_details, 200 + module_instance._http_client, response_body, 200 ) - api_key_details = syncify(module_instance.validate_api_key()) + api_key_details = syncify(module_instance.validate_api_key(value=api_key)) - assert request_kwargs["url"].endswith("/api_keys/validate") + assert request_kwargs["url"].endswith(API_KEY_VALIDATION_PATH) assert request_kwargs["method"] == "post" - assert api_key_details.id == mock_api_key_details["id"] - assert api_key_details.name == mock_api_key_details["name"] + assert api_key_details.id == mock_api_key["id"] + assert api_key_details.name == mock_api_key["name"] assert api_key_details.object == "api_key" def test_validate_api_key_with_invalid_key( @@ -43,4 +49,4 @@ def test_validate_api_key_with_invalid_key( ) with pytest.raises(AuthenticationException): - syncify(module_instance.validate_api_key()) + syncify(module_instance.validate_api_key(value="invalid-key")) From 686c98ef8ac79584ee97dcd48bd062e7a44610d6 Mon Sep 17 00:00:00 2001 From: "d.saner@enlyze.com" Date: Thu, 4 Dec 2025 12:20:18 +0100 Subject: [PATCH 09/13] backport | None notation to Optional[T] --- workos/types/api_keys/api_keys.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/workos/types/api_keys/api_keys.py b/workos/types/api_keys/api_keys.py index 04bb0a22..7dc1216c 100644 --- a/workos/types/api_keys/api_keys.py +++ b/workos/types/api_keys/api_keys.py @@ -1,4 +1,4 @@ -from typing import Literal +from typing import Literal, Optional from workos.types.workos_model import WorkOSModel @@ -14,7 +14,7 @@ class ApiKey(WorkOSModel): owner: ApiKeyOwner name: str obfuscated_value: str - last_used_at: str | None = None + last_used_at: Optional[str] = None permissions: list[str] created_at: str updated_at: str From 0e65d4605fd112ea544cb715cd1919481a3ed2e2 Mon Sep 17 00:00:00 2001 From: "d.saner@enlyze.com" Date: Thu, 4 Dec 2025 14:21:11 +0100 Subject: [PATCH 10/13] return None on invalid API key --- tests/test_api_keys.py | 8 +++----- workos/api_keys.py | 39 +++++++++++++++++++-------------------- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/tests/test_api_keys.py b/tests/test_api_keys.py index f81f13b0..fd42e706 100644 --- a/tests/test_api_keys.py +++ b/tests/test_api_keys.py @@ -4,7 +4,6 @@ from tests.utils.fixtures.mock_api_key import MockApiKey from tests.utils.syncify import syncify from workos.api_keys import API_KEY_VALIDATION_PATH, ApiKeys, AsyncApiKeys -from workos.exceptions import AuthenticationException @pytest.mark.sync_and_async(ApiKeys, AsyncApiKeys) @@ -44,9 +43,8 @@ def test_validate_api_key_with_invalid_key( ): mock_http_client_with_response( module_instance._http_client, - {"message": "Invalid API key", "error": "invalid_api_key"}, - 401, + {"api_key": None}, + 200, ) - with pytest.raises(AuthenticationException): - syncify(module_instance.validate_api_key(value="invalid-key")) + assert syncify(module_instance.validate_api_key(value="invalid-key")) is None diff --git a/workos/api_keys.py b/workos/api_keys.py index 88f2a6c4..95064402 100644 --- a/workos/api_keys.py +++ b/workos/api_keys.py @@ -1,4 +1,4 @@ -from typing import Protocol +from typing import Optional, Protocol from workos.types.api_keys import ApiKey from workos.typing.sync_or_async import SyncOrAsync @@ -6,22 +6,19 @@ from workos.utils.request_helper import REQUEST_METHOD_POST API_KEY_VALIDATION_PATH = "api_keys/validations" +RESOURCE_OBJECT_PATH = "api_key" class ApiKeysModule(Protocol): - def validate_api_key(self, *, value: str) -> SyncOrAsync[ApiKey]: - """Validates the configured API key. + def validate_api_key(self, *, value: str) -> SyncOrAsync[Optional[ApiKey]]: + """Validate an API key. + + Kwargs: + value (str): API key value Returns: - ApiKey: The validated API key details containing - information about the key's name and usage - - Raises: - AuthenticationException: If the API key is invalid or - unauthorized (401) - NotFoundException: If the API key is not found (404) - ServerException: If the API server encounters an error - (5xx) + Optional[ApiKey]: Returns ApiKey resource object + if supplied value was valid, None if it was not """ ... @@ -32,12 +29,13 @@ class ApiKeys(ApiKeysModule): def __init__(self, http_client: SyncHTTPClient): self._http_client = http_client - def validate_api_key(self, *, value: str) -> ApiKey: + def validate_api_key(self, *, value: str) -> Optional[ApiKey]: response = self._http_client.request( - API_KEY_VALIDATION_PATH, method=REQUEST_METHOD_POST, json={ - "value": value} + API_KEY_VALIDATION_PATH, method=REQUEST_METHOD_POST, json={"value": value} ) - return ApiKey.model_validate(response["api_key"]) + if response.get(RESOURCE_OBJECT_PATH) is None: + return None + return ApiKey.model_validate(response[RESOURCE_OBJECT_PATH]) class AsyncApiKeys(ApiKeysModule): @@ -46,9 +44,10 @@ class AsyncApiKeys(ApiKeysModule): def __init__(self, http_client: AsyncHTTPClient): self._http_client = http_client - async def validate_api_key(self, *, value: str) -> ApiKey: + async def validate_api_key(self, *, value: str) -> Optional[ApiKey]: response = await self._http_client.request( - API_KEY_VALIDATION_PATH, method=REQUEST_METHOD_POST, json={ - "value": value} + API_KEY_VALIDATION_PATH, method=REQUEST_METHOD_POST, json={"value": value} ) - return ApiKey.model_validate(response["api_key"]) + if response.get(RESOURCE_OBJECT_PATH) is None: + return None + return ApiKey.model_validate(response[RESOURCE_OBJECT_PATH]) From bccd817cff2713a4dfeb5f59542730b9690a44b6 Mon Sep 17 00:00:00 2001 From: "d.saner@enlyze.com" Date: Thu, 4 Dec 2025 14:50:22 +0100 Subject: [PATCH 11/13] improve naming --- workos/api_keys.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/workos/api_keys.py b/workos/api_keys.py index 95064402..38ea5abb 100644 --- a/workos/api_keys.py +++ b/workos/api_keys.py @@ -6,7 +6,7 @@ from workos.utils.request_helper import REQUEST_METHOD_POST API_KEY_VALIDATION_PATH = "api_keys/validations" -RESOURCE_OBJECT_PATH = "api_key" +RESOURCE_OBJECT_ATTRIBUTE_NAME = "api_key" class ApiKeysModule(Protocol): @@ -31,11 +31,12 @@ def __init__(self, http_client: SyncHTTPClient): def validate_api_key(self, *, value: str) -> Optional[ApiKey]: response = self._http_client.request( - API_KEY_VALIDATION_PATH, method=REQUEST_METHOD_POST, json={"value": value} + API_KEY_VALIDATION_PATH, method=REQUEST_METHOD_POST, json={ + "value": value} ) - if response.get(RESOURCE_OBJECT_PATH) is None: + if response.get(RESOURCE_OBJECT_ATTRIBUTE_NAME) is None: return None - return ApiKey.model_validate(response[RESOURCE_OBJECT_PATH]) + return ApiKey.model_validate(response[RESOURCE_OBJECT_ATTRIBUTE_NAME]) class AsyncApiKeys(ApiKeysModule): @@ -46,8 +47,9 @@ def __init__(self, http_client: AsyncHTTPClient): async def validate_api_key(self, *, value: str) -> Optional[ApiKey]: response = await self._http_client.request( - API_KEY_VALIDATION_PATH, method=REQUEST_METHOD_POST, json={"value": value} + API_KEY_VALIDATION_PATH, method=REQUEST_METHOD_POST, json={ + "value": value} ) - if response.get(RESOURCE_OBJECT_PATH) is None: + if response.get(RESOURCE_OBJECT_ATTRIBUTE_NAME) is None: return None - return ApiKey.model_validate(response[RESOURCE_OBJECT_PATH]) + return ApiKey.model_validate(response[RESOURCE_OBJECT_ATTRIBUTE_NAME]) From 63e833a21365ae4593bce0ca58f0ebe7a975acbf Mon Sep 17 00:00:00 2001 From: Nick Holden Date: Thu, 4 Dec 2025 11:52:55 -0800 Subject: [PATCH 12/13] black . --- workos/api_keys.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/workos/api_keys.py b/workos/api_keys.py index 38ea5abb..2a1416ed 100644 --- a/workos/api_keys.py +++ b/workos/api_keys.py @@ -31,8 +31,7 @@ def __init__(self, http_client: SyncHTTPClient): def validate_api_key(self, *, value: str) -> Optional[ApiKey]: response = self._http_client.request( - API_KEY_VALIDATION_PATH, method=REQUEST_METHOD_POST, json={ - "value": value} + API_KEY_VALIDATION_PATH, method=REQUEST_METHOD_POST, json={"value": value} ) if response.get(RESOURCE_OBJECT_ATTRIBUTE_NAME) is None: return None @@ -47,8 +46,7 @@ def __init__(self, http_client: AsyncHTTPClient): async def validate_api_key(self, *, value: str) -> Optional[ApiKey]: response = await self._http_client.request( - API_KEY_VALIDATION_PATH, method=REQUEST_METHOD_POST, json={ - "value": value} + API_KEY_VALIDATION_PATH, method=REQUEST_METHOD_POST, json={"value": value} ) if response.get(RESOURCE_OBJECT_ATTRIBUTE_NAME) is None: return None From 7610c603186bbe9d060659d82775fe0be0e187d4 Mon Sep 17 00:00:00 2001 From: Nick Holden Date: Thu, 4 Dec 2025 11:58:38 -0800 Subject: [PATCH 13/13] fix type --- workos/types/api_keys/api_keys.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/workos/types/api_keys/api_keys.py b/workos/types/api_keys/api_keys.py index 7dc1216c..84a9809b 100644 --- a/workos/types/api_keys/api_keys.py +++ b/workos/types/api_keys/api_keys.py @@ -1,4 +1,4 @@ -from typing import Literal, Optional +from typing import Literal, Optional, Sequence from workos.types.workos_model import WorkOSModel @@ -15,6 +15,6 @@ class ApiKey(WorkOSModel): name: str obfuscated_value: str last_used_at: Optional[str] = None - permissions: list[str] + permissions: Sequence[str] created_at: str updated_at: str