From a5e0ac1425a45642fa61298c76f69c09af848800 Mon Sep 17 00:00:00 2001 From: Aleksandr Balakirev Date: Sat, 26 Oct 2024 21:32:15 +0200 Subject: [PATCH 01/30] Add FAPI URL and BAPI URL to config. --- .github/workflows/python-package.yml | 2 + src/corbado_python_sdk/config.py | 61 +++++++++++----------------- tests/unit/test_config.py | 19 ++++++--- tests/unit/test_session_service.py | 10 ++++- tests/utils/utils.py | 4 +- tox.ini | 2 +- 6 files changed, 51 insertions(+), 47 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index ab57341..d646d4f 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -49,8 +49,10 @@ jobs: tox run env: CORBADO_BACKEND_API: ${{ vars.CORBADO_BACKEND_API }} + CORBADO_FRONTEND_API: ${{ vars.CORBADO_FRONTEND_API }} CORBADO_API_SECRET: ${{ secrets.CORBADO_API_SECRET }} CORBADO_PROJECT_ID: ${{ secrets.CORBADO_PROJECT_ID }} + build: diff --git a/src/corbado_python_sdk/config.py b/src/corbado_python_sdk/config.py index a794259..eb7b6c4 100644 --- a/src/corbado_python_sdk/config.py +++ b/src/corbado_python_sdk/config.py @@ -16,7 +16,8 @@ class Config(BaseModel): Attributes: project_id (str): The unique identifier for the project. api_secret (str): The secret key used to authenticate API requests. - backend_api (str): The base URL for the backend API. Defaults to "https://backendapi.cloud.corbado.io/v2". + backend_api (str): The base URL for the backend API. + frontend_api (str): The base URL for the frontend API. short_session_cookie_name (str): The name of the cookie for short session management. Defaults to "cbo_short_session". """ @@ -28,14 +29,16 @@ class Config(BaseModel): project_id: str api_secret: str - backend_api: str = "https://backendapi.cloud.corbado.io/v2" short_session_cookie_name: str = "cbo_short_session" cname: Optional[Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]] = None _issuer: Optional[Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]] = None - _frontend_api: Optional[str] = None + frontend_api: str + backend_api: str - @field_validator("backend_api") + @field_validator( + "backend_api", + ) @classmethod def validate_backend_api(cls, backend_api: str) -> str: """Validate the backend API URL and ensure it ends with '/v2'. @@ -43,14 +46,10 @@ def validate_backend_api(cls, backend_api: str) -> str: Args: backend_api (str): Backend API URL to validate. - Raises: - ValueError: _description_ - Returns: str: Validated backend API URL ending with '/v2'. """ - if not validators.url_validator(backend_api): - raise ValueError(f'Invalid URL "{backend_api}" provided for backend API.') + backend_api = validators.url_validator(url=backend_api) # Append '/v2' if not already present if not backend_api.endswith("/v2"): @@ -58,6 +57,21 @@ def validate_backend_api(cls, backend_api: str) -> str: return backend_api + @field_validator( + "frontend_api", + ) + @classmethod + def validate_frontend_api(cls, frontend_api: str) -> str: + """Validate the frontend API URL. + + Args: + frontend_api (str): Frontend API URL to validate. + + Returns: + str: Validated frontend API. + """ + return validators.url_validator(url=frontend_api) + @field_validator("project_id") @classmethod def project_id_validator(cls, project_id: str) -> str: @@ -121,32 +135,3 @@ def issuer(self, issuer: str) -> None: issuer (str): issuer to set. """ self._issuer = issuer - - @property - def frontend_api(self) -> str: - """Get Frontend API. - - Returns: - str: Frontend API - """ - if not self._frontend_api: - self._frontend_api = "https://" + self.project_id + ".frontendapi.corbado.io" - return self._frontend_api - - @frontend_api.setter - def frontend_api(self, frontend_api: str) -> None: - """Set Frontend API. Use it to override default value. - - Args: - frontend_api (str): Frontend API to set. - """ - self._frontend_api = validators.url_validator(url=frontend_api) # validate url - - # ------- Internal --------------# - def set_assignment_validation(self, validate: bool) -> None: - """Only use it if you know what you do. Sets assignment validation. - - Args: - validate (bool): Enable/disable validation - """ - self.model_config["validate_assignment"] = validate diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 3465eba..5db4e6d 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -9,8 +9,12 @@ def test_set_frontend_api(self): for frontend_api, valid in self.provide_urls(): try: - config = Config(project_id="pro-123", api_secret="corbado1_123") - config.frontend_api = frontend_api + Config( + project_id="pro-123", + api_secret="corbado1_123", + frontend_api=frontend_api, + backend_api="https://backendapi.cloud.corbado.io/", + ) error = False except ValueError: error = True @@ -19,16 +23,19 @@ def test_set_frontend_api(self): def test_set_backend_api(self): for backend_api, valid in self.provide_urls(): try: - config = Config(project_id="pro-123", api_secret="corbado1_123") + config = Config( + project_id="pro-123", api_secret="corbado1_123", backend_api=backend_api, frontend_api="https://test.com/" + ) config.backend_api = backend_api error = False except ValueError: error = True self.assertEqual(valid, second=not error) - def test_get_frontend_api(self): - config = Config(project_id="pro-123", api_secret="corbado1_123") - self.assertEqual("https://pro-123.frontendapi.corbado.io", config.frontend_api) + def test_get_backend_api(self): + test_url = "https://backendapi.cloud.corbado.io/" + config = Config(project_id="pro-123", api_secret="corbado1_123", frontend_api="https://test.com/", backend_api=test_url) + self.assertTrue(config.backend_api.endswith("/v2")) def provide_urls(self): return [ diff --git a/tests/unit/test_session_service.py b/tests/unit/test_session_service.py index 962bec4..3f41b39 100644 --- a/tests/unit/test_session_service.py +++ b/tests/unit/test_session_service.py @@ -1,5 +1,6 @@ # type: ignore import os +import test import unittest from time import time from unittest.mock import AsyncMock, MagicMock, patch @@ -168,7 +169,14 @@ def test_init_parameters(self): class TestSessionServiceConfiguration(TestBase): def test_set_cname_expect_issuer_changed(self): test_cname = "cname.test.com" - config: Config = Config(api_secret="corbado1_XXX", project_id="pro-55", cname=test_cname) + config: Config = Config( + api_secret="corbado1_XXX", + project_id="pro-55", + backend_api="https://backendapi.cloud.corbado.io/", + frontend_api="https://test.com/", + cname=test_cname, + ) + sdk = CorbadoSDK(config=config) sessions: SessionService = sdk.sessions self.assertEqual("https://" + test_cname, sessions.issuer) diff --git a/tests/utils/utils.py b/tests/utils/utils.py index 56e892b..092e05c 100644 --- a/tests/utils/utils.py +++ b/tests/utils/utils.py @@ -16,6 +16,7 @@ class TestUtils: CORBADO_API_SECRET: str = "CORBADO_API_SECRET" CORBADO_PROJECT_ID: str = "CORBADO_PROJECT_ID" CORBADO_BACKEND_API: str = "CORBADO_BACKEND_API" + CORBADO_FRONTEND_API: str = "CORBADO_FRONTEND_API" @classmethod def instantiate_sdk(cls) -> CorbadoSDK: @@ -23,7 +24,8 @@ def instantiate_sdk(cls) -> CorbadoSDK: config: Config = Config( api_secret=os.getenv(key=TestUtils.CORBADO_API_SECRET, default="missing CORBADO_API_SECRET"), project_id=os.getenv(key=TestUtils.CORBADO_PROJECT_ID, default="missing CORBADO_PROJECT_ID"), - backend_api=os.getenv(key=TestUtils.CORBADO_BACKEND_API, default="https://backendapi.cloud.corbado.io/v2"), + backend_api=os.getenv(key=TestUtils.CORBADO_BACKEND_API, default="missing CORBADO_BACKEND_API"), + frontend_api=os.getenv(key=TestUtils.CORBADO_FRONTEND_API, default="missing CORBADO_FRONTEND_API"), ) return CorbadoSDK(config=config) diff --git a/tox.ini b/tox.ini index 8df5df5..19d8af8 100644 --- a/tox.ini +++ b/tox.ini @@ -20,7 +20,7 @@ python = [testenv] deps = pytest commands = pytest {posargs} -passenv=CORBADO_PROJECT_ID,CORBADO_API_SECRET,CORBADO_BACKEND_API +passenv=CORBADO_PROJECT_ID,CORBADO_API_SECRET,CORBADO_BACKEND_API,CORBADO_FRONTEND_API [testenv:flake8] skipdist = true From 541d81bf1ee6b3b6b7cd6fa5e52271b3a67de215 Mon Sep 17 00:00:00 2001 From: Aleksandr Balakirev Date: Sat, 26 Oct 2024 22:40:54 +0200 Subject: [PATCH 02/30] Adapt issuer validation --- src/corbado_python_sdk/corbado_sdk.py | 1 + .../implementation/session_service.py | 66 ++++++++++++++++--- tests/unit/test_session_service.py | 10 +-- 3 files changed, 62 insertions(+), 15 deletions(-) diff --git a/src/corbado_python_sdk/corbado_sdk.py b/src/corbado_python_sdk/corbado_sdk.py index da75bd0..398e7b8 100644 --- a/src/corbado_python_sdk/corbado_sdk.py +++ b/src/corbado_python_sdk/corbado_sdk.py @@ -73,6 +73,7 @@ def sessions(self) -> SessionService: short_session_cookie_name=self.config.short_session_cookie_name, issuer=self.config.issuer, jwks_uri=self.config.frontend_api + "/.well-known/jwks", + project_id=self.config.project_id, ) return self._sessions diff --git a/src/corbado_python_sdk/services/implementation/session_service.py b/src/corbado_python_sdk/services/implementation/session_service.py index af48a9e..c5444eb 100644 --- a/src/corbado_python_sdk/services/implementation/session_service.py +++ b/src/corbado_python_sdk/services/implementation/session_service.py @@ -1,7 +1,13 @@ import jwt from jwt import decode from jwt.jwks_client import PyJWKClient -from pydantic import BaseModel, ConfigDict, StrictStr, StringConstraints +from pydantic import ( + BaseModel, + ConfigDict, + StrictStr, + StringConstraints, + ValidationError, +) from typing_extensions import Annotated from corbado_python_sdk.entities.session_validation_result import ( @@ -26,13 +32,14 @@ class SessionService(BaseModel): _jwk_client (PyJWKClient): JSON Web Key (JWK) client for handling JWKS. cache_keys (bool): Flag to cache keys. Default = False. cache_jwk_set (bool): Flag to cache jwk_sets. Default = True. + project_id (str): Corbado Project Id. """ - model_config = ConfigDict( - arbitrary_types_allowed=True, - ) + model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True) + + # Fields short_session_cookie_name: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] issuer: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] jwks_uri: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] @@ -40,8 +47,10 @@ class SessionService(BaseModel): short_session_length: int = DEFAULT_SHORT_SESSION_LENGTH cache_keys: bool = False cache_jwk_set: bool = True + project_id: str _jwk_client: PyJWKClient + # Constructor def __init__(self, **kwargs) -> None: # type: ignore """ Initialize a new instance of the SessionService class. @@ -64,6 +73,7 @@ def __init__(self, **kwargs) -> None: # type: ignore cache_jwk_set=self.cache_jwk_set, ) + # Core methods def get_and_validate_short_session_value(self, short_session: StrictStr) -> SessionValidationResult: """Validate the given short-term session (represented as JWT) value. @@ -79,12 +89,17 @@ def get_and_validate_short_session_value(self, short_session: StrictStr) -> Sess try: # retrieve signing key signing_key: jwt.PyJWK = self._jwk_client.get_signing_key_from_jwt(token=short_session) + # decode short session (jwt) with signing key - payload = decode(jwt=short_session, key=signing_key.key, algorithms=["RS256"], issuer=self.issuer) + payload = decode(jwt=short_session, key=signing_key.key, algorithms=["RS256"]) # extract information from decoded payload - sub = payload.get("sub") - full_name = payload.get("name") + token_issuer: str = payload.get("iss") + sub: str = payload.get("sub") + full_name: str = payload.get("name") + + # validate issuer + self._validate_issuer(token_issuer=token_issuer, session_token=short_session) return SessionValidationResult(authenticated=True, user_id=sub, full_name=full_name) except Exception as error: @@ -105,13 +120,13 @@ def get_current_user(self, short_session: StrictStr) -> SessionValidationResult: user: SessionValidationResult = self.get_and_validate_short_session_value(short_session) return user - def set_issuer_mismatch_error(self, issuer: str) -> None: + def set_issuer_mismatch_error(self, token_issuer: str) -> None: """Set issuer mismatch error. Args: - issuer (str): issuer. + token_issuer (str): Token issuer. """ - self.last_short_session_validation_result = f"Mismatch in issuer (configured: {self.issuer}, JWT: {issuer})" + self.last_short_session_validation_result = f"Mismatch in issuer (configured: {self.issuer}, JWT: {token_issuer})" def set_validation_error(self, error: Exception) -> None: """Set validation error. @@ -120,3 +135,34 @@ def set_validation_error(self, error: Exception) -> None: error (Exception): Exception occurred. """ self.last_short_session_validation_result = f"JWT validation failed: {error}" + + # Private methods + def _validate_issuer(self, token_issuer: str, session_token: str) -> None: + """Validate issuer. + + Args: + token_issuer (str): Token issuer. + session_token (str): Session token. + + Raises: + ValidationError: If issuer is invalid. + + """ + if not token_issuer: + raise ValidationError(f"Issuer is empty. Session token: {session_token}") + + # Check for old Frontend API (without .cloud.) + expected_old: StrictStr = f"https://{self.project_id}.frontendapi.corbado.io" + if token_issuer == expected_old: + return + + # Check for new Frontend API (with .cloud.) + expected_new: StrictStr = f"https://{self.project_id}.frontendapi.cloud.corbado.io" + if token_issuer == expected_new: + return + + # Check against the configured issuer (e.g., a custom domain or CNAME) + if token_issuer != self.issuer: + raise ValidationError( + f"Issuer mismatch (configured via FrontendAPI: '{self.issuer}', JWT issuer: '{token_issuer}')", + ) diff --git a/tests/unit/test_session_service.py b/tests/unit/test_session_service.py index 3f41b39..1872fe2 100644 --- a/tests/unit/test_session_service.py +++ b/tests/unit/test_session_service.py @@ -1,6 +1,5 @@ # type: ignore import os -import test import unittest from time import time from unittest.mock import AsyncMock, MagicMock, patch @@ -69,6 +68,7 @@ def create_session_service(cls) -> SessionService: issuer="https://auth.acme.com", jwks_uri="https://example_uri.com", # does not matter, url access is mocked short_session_cookie_name="cbo_short_session", + project_id="pro-55", ) def tearDown(self) -> None: @@ -147,13 +147,13 @@ def test_generate_jwt(self): def test_init_parameters(self): test_cases = [ # Valid session service - ({"issuer": "s", "jwks_uri": "2", "short_session_cookie_name": "name"}, True), + ({"issuer": "s", "jwks_uri": "2", "short_session_cookie_name": "name", "project_id": "pro-55"}, True), # Test empty issuer - ({"issuer": "", "jwks_uri": "2", "short_session_cookie_name": "name"}, False), + ({"issuer": "", "jwks_uri": "2", "short_session_cookie_name": "name", "project_id": "pro-55"}, False), # Test empty jwks_uri - ({"issuer": "s", "jwks_uri": "", "short_session_cookie_name": "name"}, False), + ({"issuer": "s", "jwks_uri": "", "short_session_cookie_name": "name", "project_id": "pro-55"}, False), # Tesft empty short_session_cookie_name - ({"issuer": "s", "jwks_uri": "2", "short_session_cookie_name": ""}, False), + ({"issuer": "s", "jwks_uri": "2", "short_session_cookie_name": "", "project_id": "pro-55"}, False), ] for params, expected_result in test_cases: From 15d5b4ae968620294d8e08b1d42e74e31270352a Mon Sep 17 00:00:00 2001 From: Aleksandr Balakirev Date: Sun, 27 Oct 2024 11:42:35 +0100 Subject: [PATCH 03/30] Rename short-term session to session-token --- README.md | 2 +- src/corbado_python_sdk/config.py | 6 +- src/corbado_python_sdk/corbado_sdk.py | 2 +- .../generated/api/sessions_api.py | 474 ++++++------------ .../implementation/session_service.py | 42 +- src/corbado_python_sdk/utils/__init__.py | 5 + src/corbado_python_sdk/utils/constants.py | 1 + tests/unit/test_session_service.py | 18 +- 8 files changed, 185 insertions(+), 365 deletions(-) create mode 100644 src/corbado_python_sdk/utils/constants.py diff --git a/README.md b/README.md index 363b9fd..d0f02b0 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ The Corbado Python SDK raises exceptions for all errors except those that occur 'SessionService' returns 'SessionValidationResult' as result of token validation. You can check whether any errors occurred and handle them if needed: ```Python -result: SessionValidationResult = self.session_service.get_and_validate_short_session_value(short_session=token) +result: SessionValidationResult = self.session_service.get_and_validate_short_session_value(session_token=token) if result.error is not None: print(result.error) raise result.error diff --git a/src/corbado_python_sdk/config.py b/src/corbado_python_sdk/config.py index eb7b6c4..2255183 100644 --- a/src/corbado_python_sdk/config.py +++ b/src/corbado_python_sdk/config.py @@ -1,7 +1,7 @@ from pydantic import BaseModel, ConfigDict, StringConstraints, field_validator from typing_extensions import Annotated, Optional -from corbado_python_sdk.utils import validators +from corbado_python_sdk.utils import DEFAULT_SESSION_TOKEN_COOKIE_NAME, validators class Config(BaseModel): @@ -18,7 +18,7 @@ class Config(BaseModel): api_secret (str): The secret key used to authenticate API requests. backend_api (str): The base URL for the backend API. frontend_api (str): The base URL for the frontend API. - short_session_cookie_name (str): The name of the cookie for short session management. Defaults to "cbo_short_session". + session_token_cookie_name (str): The name of the cookie for short session management. Defaults to "cbo_session_token". """ # Make sure that field assignments are also validated, use "set_assignment_validation(False)" @@ -29,7 +29,7 @@ class Config(BaseModel): project_id: str api_secret: str - short_session_cookie_name: str = "cbo_short_session" + session_token_cookie_name: str = DEFAULT_SESSION_TOKEN_COOKIE_NAME cname: Optional[Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]] = None _issuer: Optional[Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]] = None diff --git a/src/corbado_python_sdk/corbado_sdk.py b/src/corbado_python_sdk/corbado_sdk.py index 398e7b8..c592bf8 100644 --- a/src/corbado_python_sdk/corbado_sdk.py +++ b/src/corbado_python_sdk/corbado_sdk.py @@ -70,7 +70,7 @@ def sessions(self) -> SessionService: """ if not self._sessions: self._sessions = SessionService( - short_session_cookie_name=self.config.short_session_cookie_name, + session_token_cookie_name=self.config.session_token_cookie_name, issuer=self.config.issuer, jwks_uri=self.config.frontend_api + "/.well-known/jwks", project_id=self.config.project_id, diff --git a/src/corbado_python_sdk/generated/api/sessions_api.py b/src/corbado_python_sdk/generated/api/sessions_api.py index c093755..a653894 100644 --- a/src/corbado_python_sdk/generated/api/sessions_api.py +++ b/src/corbado_python_sdk/generated/api/sessions_api.py @@ -13,20 +13,24 @@ """ # noqa: E501 import warnings -from pydantic import validate_call, Field, StrictFloat, StrictStr, StrictInt from typing import Any, Dict, List, Optional, Tuple, Union -from typing_extensions import Annotated -from pydantic import Field, StrictStr +from pydantic import Field, StrictFloat, StrictInt, StrictStr, validate_call from typing_extensions import Annotated -from corbado_python_sdk.generated.models.long_session import LongSession -from corbado_python_sdk.generated.models.long_session_create_req import LongSessionCreateReq -from corbado_python_sdk.generated.models.long_session_update_req import LongSessionUpdateReq -from corbado_python_sdk.generated.models.short_session import ShortSession -from corbado_python_sdk.generated.models.short_session_create_req import ShortSessionCreateReq from corbado_python_sdk.generated.api_client import ApiClient, RequestSerialized from corbado_python_sdk.generated.api_response import ApiResponse +from corbado_python_sdk.generated.models.long_session import LongSession +from corbado_python_sdk.generated.models.long_session_create_req import ( + LongSessionCreateReq, +) +from corbado_python_sdk.generated.models.long_session_update_req import ( + LongSessionUpdateReq, +) +from corbado_python_sdk.generated.models.short_session import ShortSession +from corbado_python_sdk.generated.models.short_session_create_req import ( + ShortSessionCreateReq, +) from corbado_python_sdk.generated.rest import RESTResponseType @@ -42,7 +46,6 @@ def __init__(self, api_client=None) -> None: api_client = ApiClient.get_default() self.api_client = api_client - @validate_call def long_session_create( self, @@ -51,10 +54,7 @@ def long_session_create( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[ - Annotated[StrictFloat, Field(gt=0)], - Annotated[StrictFloat, Field(gt=0)] - ] + Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -89,7 +89,7 @@ def long_session_create( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._long_session_create_serialize( user_id=user_id, @@ -97,23 +97,19 @@ def long_session_create( _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index + _host_index=_host_index, ) _response_types_map: Dict[str, Optional[str]] = { - '200': "LongSession", + "200": "LongSession", } - response_data = self.api_client.call_api( - *_param, - _request_timeout=_request_timeout - ) + response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) response_data.read() return self.api_client.response_deserialize( response_data=response_data, response_types_map=_response_types_map, ).data - @validate_call def long_session_create_with_http_info( self, @@ -122,10 +118,7 @@ def long_session_create_with_http_info( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[ - Annotated[StrictFloat, Field(gt=0)], - Annotated[StrictFloat, Field(gt=0)] - ] + Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -160,7 +153,7 @@ def long_session_create_with_http_info( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._long_session_create_serialize( user_id=user_id, @@ -168,23 +161,19 @@ def long_session_create_with_http_info( _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index + _host_index=_host_index, ) _response_types_map: Dict[str, Optional[str]] = { - '200': "LongSession", + "200": "LongSession", } - response_data = self.api_client.call_api( - *_param, - _request_timeout=_request_timeout - ) + response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) response_data.read() return self.api_client.response_deserialize( response_data=response_data, response_types_map=_response_types_map, ) - @validate_call def long_session_create_without_preload_content( self, @@ -193,10 +182,7 @@ def long_session_create_without_preload_content( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[ - Annotated[StrictFloat, Field(gt=0)], - Annotated[StrictFloat, Field(gt=0)] - ] + Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -231,7 +217,7 @@ def long_session_create_without_preload_content( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._long_session_create_serialize( user_id=user_id, @@ -239,19 +225,15 @@ def long_session_create_without_preload_content( _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index + _host_index=_host_index, ) _response_types_map: Dict[str, Optional[str]] = { - '200': "LongSession", + "200": "LongSession", } - response_data = self.api_client.call_api( - *_param, - _request_timeout=_request_timeout - ) + response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) return response_data.response - def _long_session_create_serialize( self, user_id, @@ -264,21 +246,18 @@ def _long_session_create_serialize( _host = None - _collection_formats: Dict[str, str] = { - } + _collection_formats: Dict[str, str] = {} _path_params: Dict[str, str] = {} _query_params: List[Tuple[str, str]] = [] _header_params: Dict[str, Optional[str]] = _headers or {} _form_params: List[Tuple[str, str]] = [] - _files: Dict[ - str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] - ] = {} + _files: Dict[str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]]] = {} _body_params: Optional[bytes] = None # process the path parameters if user_id is not None: - _path_params['userID'] = user_id + _path_params["userID"] = user_id # process the query parameters # process the header parameters # process the form parameters @@ -286,37 +265,24 @@ def _long_session_create_serialize( if long_session_create_req is not None: _body_params = long_session_create_req - # set the HTTP header `Accept` - if 'Accept' not in _header_params: - _header_params['Accept'] = self.api_client.select_header_accept( - [ - 'application/json' - ] - ) + if "Accept" not in _header_params: + _header_params["Accept"] = self.api_client.select_header_accept(["application/json"]) # set the HTTP header `Content-Type` if _content_type: - _header_params['Content-Type'] = _content_type + _header_params["Content-Type"] = _content_type else: - _default_content_type = ( - self.api_client.select_header_content_type( - [ - 'application/json' - ] - ) - ) + _default_content_type = self.api_client.select_header_content_type(["application/json"]) if _default_content_type is not None: - _header_params['Content-Type'] = _default_content_type + _header_params["Content-Type"] = _default_content_type # authentication setting - _auth_settings: List[str] = [ - 'basicAuth' - ] + _auth_settings: List[str] = ["basicAuth"] return self.api_client.param_serialize( - method='POST', - resource_path='/users/{userID}/longSessions', + method="POST", + resource_path="/users/{userID}/longSessions", path_params=_path_params, query_params=_query_params, header_params=_header_params, @@ -326,12 +292,9 @@ def _long_session_create_serialize( auth_settings=_auth_settings, collection_formats=_collection_formats, _host=_host, - _request_auth=_request_auth + _request_auth=_request_auth, ) - - - @validate_call def long_session_get( self, @@ -339,10 +302,7 @@ def long_session_get( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[ - Annotated[StrictFloat, Field(gt=0)], - Annotated[StrictFloat, Field(gt=0)] - ] + Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -375,30 +335,26 @@ def long_session_get( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._long_session_get_serialize( long_session_id=long_session_id, _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index + _host_index=_host_index, ) _response_types_map: Dict[str, Optional[str]] = { - '200': "LongSession", + "200": "LongSession", } - response_data = self.api_client.call_api( - *_param, - _request_timeout=_request_timeout - ) + response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) response_data.read() return self.api_client.response_deserialize( response_data=response_data, response_types_map=_response_types_map, ).data - @validate_call def long_session_get_with_http_info( self, @@ -406,10 +362,7 @@ def long_session_get_with_http_info( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[ - Annotated[StrictFloat, Field(gt=0)], - Annotated[StrictFloat, Field(gt=0)] - ] + Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -442,30 +395,26 @@ def long_session_get_with_http_info( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._long_session_get_serialize( long_session_id=long_session_id, _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index + _host_index=_host_index, ) _response_types_map: Dict[str, Optional[str]] = { - '200': "LongSession", + "200": "LongSession", } - response_data = self.api_client.call_api( - *_param, - _request_timeout=_request_timeout - ) + response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) response_data.read() return self.api_client.response_deserialize( response_data=response_data, response_types_map=_response_types_map, ) - @validate_call def long_session_get_without_preload_content( self, @@ -473,10 +422,7 @@ def long_session_get_without_preload_content( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[ - Annotated[StrictFloat, Field(gt=0)], - Annotated[StrictFloat, Field(gt=0)] - ] + Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -509,26 +455,22 @@ def long_session_get_without_preload_content( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._long_session_get_serialize( long_session_id=long_session_id, _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index + _host_index=_host_index, ) _response_types_map: Dict[str, Optional[str]] = { - '200': "LongSession", + "200": "LongSession", } - response_data = self.api_client.call_api( - *_param, - _request_timeout=_request_timeout - ) + response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) return response_data.response - def _long_session_get_serialize( self, long_session_id, @@ -540,44 +482,33 @@ def _long_session_get_serialize( _host = None - _collection_formats: Dict[str, str] = { - } + _collection_formats: Dict[str, str] = {} _path_params: Dict[str, str] = {} _query_params: List[Tuple[str, str]] = [] _header_params: Dict[str, Optional[str]] = _headers or {} _form_params: List[Tuple[str, str]] = [] - _files: Dict[ - str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] - ] = {} + _files: Dict[str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]]] = {} _body_params: Optional[bytes] = None # process the path parameters if long_session_id is not None: - _path_params['longSessionID'] = long_session_id + _path_params["longSessionID"] = long_session_id # process the query parameters # process the header parameters # process the form parameters # process the body parameter - # set the HTTP header `Accept` - if 'Accept' not in _header_params: - _header_params['Accept'] = self.api_client.select_header_accept( - [ - 'application/json' - ] - ) - + if "Accept" not in _header_params: + _header_params["Accept"] = self.api_client.select_header_accept(["application/json"]) # authentication setting - _auth_settings: List[str] = [ - 'basicAuth' - ] + _auth_settings: List[str] = ["basicAuth"] return self.api_client.param_serialize( - method='GET', - resource_path='/longSessions/{longSessionID}', + method="GET", + resource_path="/longSessions/{longSessionID}", path_params=_path_params, query_params=_query_params, header_params=_header_params, @@ -587,12 +518,9 @@ def _long_session_get_serialize( auth_settings=_auth_settings, collection_formats=_collection_formats, _host=_host, - _request_auth=_request_auth + _request_auth=_request_auth, ) - - - @validate_call def long_session_update( self, @@ -602,10 +530,7 @@ def long_session_update( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[ - Annotated[StrictFloat, Field(gt=0)], - Annotated[StrictFloat, Field(gt=0)] - ] + Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -642,7 +567,7 @@ def long_session_update( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._long_session_update_serialize( user_id=user_id, @@ -651,23 +576,19 @@ def long_session_update( _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index + _host_index=_host_index, ) _response_types_map: Dict[str, Optional[str]] = { - '200': "LongSession", + "200": "LongSession", } - response_data = self.api_client.call_api( - *_param, - _request_timeout=_request_timeout - ) + response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) response_data.read() return self.api_client.response_deserialize( response_data=response_data, response_types_map=_response_types_map, ).data - @validate_call def long_session_update_with_http_info( self, @@ -677,10 +598,7 @@ def long_session_update_with_http_info( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[ - Annotated[StrictFloat, Field(gt=0)], - Annotated[StrictFloat, Field(gt=0)] - ] + Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -717,7 +635,7 @@ def long_session_update_with_http_info( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._long_session_update_serialize( user_id=user_id, @@ -726,23 +644,19 @@ def long_session_update_with_http_info( _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index + _host_index=_host_index, ) _response_types_map: Dict[str, Optional[str]] = { - '200': "LongSession", + "200": "LongSession", } - response_data = self.api_client.call_api( - *_param, - _request_timeout=_request_timeout - ) + response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) response_data.read() return self.api_client.response_deserialize( response_data=response_data, response_types_map=_response_types_map, ) - @validate_call def long_session_update_without_preload_content( self, @@ -752,10 +666,7 @@ def long_session_update_without_preload_content( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[ - Annotated[StrictFloat, Field(gt=0)], - Annotated[StrictFloat, Field(gt=0)] - ] + Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -792,7 +703,7 @@ def long_session_update_without_preload_content( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._long_session_update_serialize( user_id=user_id, @@ -801,19 +712,15 @@ def long_session_update_without_preload_content( _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index + _host_index=_host_index, ) _response_types_map: Dict[str, Optional[str]] = { - '200': "LongSession", + "200": "LongSession", } - response_data = self.api_client.call_api( - *_param, - _request_timeout=_request_timeout - ) + response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) return response_data.response - def _long_session_update_serialize( self, user_id, @@ -827,23 +734,20 @@ def _long_session_update_serialize( _host = None - _collection_formats: Dict[str, str] = { - } + _collection_formats: Dict[str, str] = {} _path_params: Dict[str, str] = {} _query_params: List[Tuple[str, str]] = [] _header_params: Dict[str, Optional[str]] = _headers or {} _form_params: List[Tuple[str, str]] = [] - _files: Dict[ - str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] - ] = {} + _files: Dict[str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]]] = {} _body_params: Optional[bytes] = None # process the path parameters if user_id is not None: - _path_params['userID'] = user_id + _path_params["userID"] = user_id if long_session_id is not None: - _path_params['longSessionID'] = long_session_id + _path_params["longSessionID"] = long_session_id # process the query parameters # process the header parameters # process the form parameters @@ -851,37 +755,24 @@ def _long_session_update_serialize( if long_session_update_req is not None: _body_params = long_session_update_req - # set the HTTP header `Accept` - if 'Accept' not in _header_params: - _header_params['Accept'] = self.api_client.select_header_accept( - [ - 'application/json' - ] - ) + if "Accept" not in _header_params: + _header_params["Accept"] = self.api_client.select_header_accept(["application/json"]) # set the HTTP header `Content-Type` if _content_type: - _header_params['Content-Type'] = _content_type + _header_params["Content-Type"] = _content_type else: - _default_content_type = ( - self.api_client.select_header_content_type( - [ - 'application/json' - ] - ) - ) + _default_content_type = self.api_client.select_header_content_type(["application/json"]) if _default_content_type is not None: - _header_params['Content-Type'] = _default_content_type + _header_params["Content-Type"] = _default_content_type # authentication setting - _auth_settings: List[str] = [ - 'basicAuth' - ] + _auth_settings: List[str] = ["basicAuth"] return self.api_client.param_serialize( - method='PATCH', - resource_path='/users/{userID}/longSessions/{longSessionID}', + method="PATCH", + resource_path="/users/{userID}/longSessions/{longSessionID}", path_params=_path_params, query_params=_query_params, header_params=_header_params, @@ -891,12 +782,9 @@ def _long_session_update_serialize( auth_settings=_auth_settings, collection_formats=_collection_formats, _host=_host, - _request_auth=_request_auth + _request_auth=_request_auth, ) - - - @validate_call def short_session_create( self, @@ -906,10 +794,7 @@ def short_session_create( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[ - Annotated[StrictFloat, Field(gt=0)], - Annotated[StrictFloat, Field(gt=0)] - ] + Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -946,7 +831,7 @@ def short_session_create( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._short_session_create_serialize( user_id=user_id, @@ -955,23 +840,19 @@ def short_session_create( _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index + _host_index=_host_index, ) _response_types_map: Dict[str, Optional[str]] = { - '200': "ShortSession", + "200": "ShortSession", } - response_data = self.api_client.call_api( - *_param, - _request_timeout=_request_timeout - ) + response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) response_data.read() return self.api_client.response_deserialize( response_data=response_data, response_types_map=_response_types_map, ).data - @validate_call def short_session_create_with_http_info( self, @@ -981,10 +862,7 @@ def short_session_create_with_http_info( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[ - Annotated[StrictFloat, Field(gt=0)], - Annotated[StrictFloat, Field(gt=0)] - ] + Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -1021,7 +899,7 @@ def short_session_create_with_http_info( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._short_session_create_serialize( user_id=user_id, @@ -1030,23 +908,19 @@ def short_session_create_with_http_info( _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index + _host_index=_host_index, ) _response_types_map: Dict[str, Optional[str]] = { - '200': "ShortSession", + "200": "ShortSession", } - response_data = self.api_client.call_api( - *_param, - _request_timeout=_request_timeout - ) + response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) response_data.read() return self.api_client.response_deserialize( response_data=response_data, response_types_map=_response_types_map, ) - @validate_call def short_session_create_without_preload_content( self, @@ -1056,10 +930,7 @@ def short_session_create_without_preload_content( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[ - Annotated[StrictFloat, Field(gt=0)], - Annotated[StrictFloat, Field(gt=0)] - ] + Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -1096,7 +967,7 @@ def short_session_create_without_preload_content( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._short_session_create_serialize( user_id=user_id, @@ -1105,19 +976,15 @@ def short_session_create_without_preload_content( _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index + _host_index=_host_index, ) _response_types_map: Dict[str, Optional[str]] = { - '200': "ShortSession", + "200": "ShortSession", } - response_data = self.api_client.call_api( - *_param, - _request_timeout=_request_timeout - ) + response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) return response_data.response - def _short_session_create_serialize( self, user_id, @@ -1131,23 +998,20 @@ def _short_session_create_serialize( _host = None - _collection_formats: Dict[str, str] = { - } + _collection_formats: Dict[str, str] = {} _path_params: Dict[str, str] = {} _query_params: List[Tuple[str, str]] = [] _header_params: Dict[str, Optional[str]] = _headers or {} _form_params: List[Tuple[str, str]] = [] - _files: Dict[ - str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] - ] = {} + _files: Dict[str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]]] = {} _body_params: Optional[bytes] = None # process the path parameters if user_id is not None: - _path_params['userID'] = user_id + _path_params["userID"] = user_id if long_session_id is not None: - _path_params['longSessionID'] = long_session_id + _path_params["longSessionID"] = long_session_id # process the query parameters # process the header parameters # process the form parameters @@ -1155,37 +1019,24 @@ def _short_session_create_serialize( if short_session_create_req is not None: _body_params = short_session_create_req - # set the HTTP header `Accept` - if 'Accept' not in _header_params: - _header_params['Accept'] = self.api_client.select_header_accept( - [ - 'application/json' - ] - ) + if "Accept" not in _header_params: + _header_params["Accept"] = self.api_client.select_header_accept(["application/json"]) # set the HTTP header `Content-Type` if _content_type: - _header_params['Content-Type'] = _content_type + _header_params["Content-Type"] = _content_type else: - _default_content_type = ( - self.api_client.select_header_content_type( - [ - 'application/json' - ] - ) - ) + _default_content_type = self.api_client.select_header_content_type(["application/json"]) if _default_content_type is not None: - _header_params['Content-Type'] = _default_content_type + _header_params["Content-Type"] = _default_content_type # authentication setting - _auth_settings: List[str] = [ - 'basicAuth' - ] + _auth_settings: List[str] = ["basicAuth"] return self.api_client.param_serialize( - method='POST', - resource_path='/users/{userID}/longSessions/{longSessionID}/shortSessions', + method="POST", + resource_path="/users/{userID}/longSessions/{longSessionID}/shortSessions", path_params=_path_params, query_params=_query_params, header_params=_header_params, @@ -1195,12 +1046,9 @@ def _short_session_create_serialize( auth_settings=_auth_settings, collection_formats=_collection_formats, _host=_host, - _request_auth=_request_auth + _request_auth=_request_auth, ) - - - @validate_call def user_long_session_get( self, @@ -1209,10 +1057,7 @@ def user_long_session_get( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[ - Annotated[StrictFloat, Field(gt=0)], - Annotated[StrictFloat, Field(gt=0)] - ] + Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -1247,7 +1092,7 @@ def user_long_session_get( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._user_long_session_get_serialize( user_id=user_id, @@ -1255,23 +1100,19 @@ def user_long_session_get( _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index + _host_index=_host_index, ) _response_types_map: Dict[str, Optional[str]] = { - '200': "LongSession", + "200": "LongSession", } - response_data = self.api_client.call_api( - *_param, - _request_timeout=_request_timeout - ) + response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) response_data.read() return self.api_client.response_deserialize( response_data=response_data, response_types_map=_response_types_map, ).data - @validate_call def user_long_session_get_with_http_info( self, @@ -1280,10 +1121,7 @@ def user_long_session_get_with_http_info( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[ - Annotated[StrictFloat, Field(gt=0)], - Annotated[StrictFloat, Field(gt=0)] - ] + Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -1318,7 +1156,7 @@ def user_long_session_get_with_http_info( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._user_long_session_get_serialize( user_id=user_id, @@ -1326,23 +1164,19 @@ def user_long_session_get_with_http_info( _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index + _host_index=_host_index, ) _response_types_map: Dict[str, Optional[str]] = { - '200': "LongSession", + "200": "LongSession", } - response_data = self.api_client.call_api( - *_param, - _request_timeout=_request_timeout - ) + response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) response_data.read() return self.api_client.response_deserialize( response_data=response_data, response_types_map=_response_types_map, ) - @validate_call def user_long_session_get_without_preload_content( self, @@ -1351,10 +1185,7 @@ def user_long_session_get_without_preload_content( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[ - Annotated[StrictFloat, Field(gt=0)], - Annotated[StrictFloat, Field(gt=0)] - ] + Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -1389,7 +1220,7 @@ def user_long_session_get_without_preload_content( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._user_long_session_get_serialize( user_id=user_id, @@ -1397,19 +1228,15 @@ def user_long_session_get_without_preload_content( _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index + _host_index=_host_index, ) _response_types_map: Dict[str, Optional[str]] = { - '200': "LongSession", + "200": "LongSession", } - response_data = self.api_client.call_api( - *_param, - _request_timeout=_request_timeout - ) + response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) return response_data.response - def _user_long_session_get_serialize( self, user_id, @@ -1422,46 +1249,35 @@ def _user_long_session_get_serialize( _host = None - _collection_formats: Dict[str, str] = { - } + _collection_formats: Dict[str, str] = {} _path_params: Dict[str, str] = {} _query_params: List[Tuple[str, str]] = [] _header_params: Dict[str, Optional[str]] = _headers or {} _form_params: List[Tuple[str, str]] = [] - _files: Dict[ - str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] - ] = {} + _files: Dict[str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]]] = {} _body_params: Optional[bytes] = None # process the path parameters if user_id is not None: - _path_params['userID'] = user_id + _path_params["userID"] = user_id if long_session_id is not None: - _path_params['longSessionID'] = long_session_id + _path_params["longSessionID"] = long_session_id # process the query parameters # process the header parameters # process the form parameters # process the body parameter - # set the HTTP header `Accept` - if 'Accept' not in _header_params: - _header_params['Accept'] = self.api_client.select_header_accept( - [ - 'application/json' - ] - ) - + if "Accept" not in _header_params: + _header_params["Accept"] = self.api_client.select_header_accept(["application/json"]) # authentication setting - _auth_settings: List[str] = [ - 'basicAuth' - ] + _auth_settings: List[str] = ["basicAuth"] return self.api_client.param_serialize( - method='GET', - resource_path='/users/{userID}/longSessions/{longSessionID}', + method="GET", + resource_path="/users/{userID}/longSessions/{longSessionID}", path_params=_path_params, query_params=_query_params, header_params=_header_params, @@ -1471,7 +1287,5 @@ def _user_long_session_get_serialize( auth_settings=_auth_settings, collection_formats=_collection_formats, _host=_host, - _request_auth=_request_auth + _request_auth=_request_auth, ) - - diff --git a/src/corbado_python_sdk/services/implementation/session_service.py b/src/corbado_python_sdk/services/implementation/session_service.py index c5444eb..778b423 100644 --- a/src/corbado_python_sdk/services/implementation/session_service.py +++ b/src/corbado_python_sdk/services/implementation/session_service.py @@ -14,7 +14,7 @@ SessionValidationResult, ) -DEFAULT_SHORT_SESSION_LENGTH = 300 +DEFAULT_SESSION_TOKEN_LENGTH = 300 class SessionService(BaseModel): @@ -24,11 +24,11 @@ class SessionService(BaseModel): Attributes: model_config (ConfigDict): Configuration dictionary for the model. - short_session_cookie_name (str): Name of the short session cookie. + session_token_cookie_name (str): Name of the short session cookie. issuer (str): Issuer of the session tokens. jwks_uri (str): URI of the JSON Web Key Set (JWKS) endpoint. - last_short_session_validation_result (str): Result of the last short session validation. - short_session_length (int): Length of short session in seconds. Default = 300 + last_session_token_validation_result (str): Result of the last short session validation. + session_token_cookie_length (int): Length of short session in seconds. Default = 300 _jwk_client (PyJWKClient): JSON Web Key (JWK) client for handling JWKS. cache_keys (bool): Flag to cache keys. Default = False. cache_jwk_set (bool): Flag to cache jwk_sets. Default = True. @@ -40,11 +40,11 @@ class SessionService(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True) # Fields - short_session_cookie_name: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] + session_token_cookie_name: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] issuer: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] jwks_uri: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] - last_short_session_validation_result: str = "" - short_session_length: int = DEFAULT_SHORT_SESSION_LENGTH + last_session_token_validation_result: str = "" + session_token_cookie_length: int = DEFAULT_SESSION_TOKEN_LENGTH cache_keys: bool = False cache_jwk_set: bool = True project_id: str @@ -58,8 +58,8 @@ def __init__(self, **kwargs) -> None: # type: ignore Args: **kwargs: Additional keyword arguments to initialize the SessionService. These keyword arguments should include values for the attributes defined in the class, - such as 'short_session_cookie_name', 'issuer', 'jwks_uri', 'last_short_session_validation_result', - 'cache_keys',cache_jwk_set and 'short_session_length'. + such as 'session_token_cookie_name', 'issuer', 'jwks_uri', 'last_session_token_validation_result', + 'cache_keys',cache_jwk_set and 'session_token_cookie_length'. Raises: Any errors raised during the initialization process. @@ -69,29 +69,29 @@ def __init__(self, **kwargs) -> None: # type: ignore self._jwk_client = PyJWKClient( uri=self.jwks_uri, cache_keys=self.cache_keys, - lifespan=self.short_session_length, + lifespan=self.session_token_cookie_length, cache_jwk_set=self.cache_jwk_set, ) # Core methods - def get_and_validate_short_session_value(self, short_session: StrictStr) -> SessionValidationResult: + def get_and_validate_short_session_value(self, session_token: StrictStr) -> SessionValidationResult: """Validate the given short-term session (represented as JWT) value. Args: - short_session (StrictStr): jwt + session_token (StrictStr): jwt Returns: SessionValidationResult: SessionValidationResult with authenticated=True on success, otherwise with authenticated=False """ - if not short_session: + if not session_token: return SessionValidationResult(authenticated=False) try: # retrieve signing key - signing_key: jwt.PyJWK = self._jwk_client.get_signing_key_from_jwt(token=short_session) + signing_key: jwt.PyJWK = self._jwk_client.get_signing_key_from_jwt(token=session_token) # decode short session (jwt) with signing key - payload = decode(jwt=short_session, key=signing_key.key, algorithms=["RS256"]) + payload = decode(jwt=session_token, key=signing_key.key, algorithms=["RS256"]) # extract information from decoded payload token_issuer: str = payload.get("iss") @@ -99,7 +99,7 @@ def get_and_validate_short_session_value(self, short_session: StrictStr) -> Sess full_name: str = payload.get("name") # validate issuer - self._validate_issuer(token_issuer=token_issuer, session_token=short_session) + self._validate_issuer(token_issuer=token_issuer, session_token=session_token) return SessionValidationResult(authenticated=True, user_id=sub, full_name=full_name) except Exception as error: @@ -107,17 +107,17 @@ def get_and_validate_short_session_value(self, short_session: StrictStr) -> Sess self.set_validation_error(error) return SessionValidationResult(authenticated=False, error=error) - def get_current_user(self, short_session: StrictStr) -> SessionValidationResult: + def get_current_user(self, session_token: StrictStr) -> SessionValidationResult: """Return current user for the short session. Args: - short_session (StrictStr): Short session. + session_token (StrictStr): Short session. Returns: SessionValidationResult: SessionValidationResult with authenticated=True on success, otherwise with authenticated=False. """ - user: SessionValidationResult = self.get_and_validate_short_session_value(short_session) + user: SessionValidationResult = self.get_and_validate_short_session_value(session_token) return user def set_issuer_mismatch_error(self, token_issuer: str) -> None: @@ -126,7 +126,7 @@ def set_issuer_mismatch_error(self, token_issuer: str) -> None: Args: token_issuer (str): Token issuer. """ - self.last_short_session_validation_result = f"Mismatch in issuer (configured: {self.issuer}, JWT: {token_issuer})" + self.last_session_token_validation_result = f"Mismatch in issuer (configured: {self.issuer}, JWT: {token_issuer})" def set_validation_error(self, error: Exception) -> None: """Set validation error. @@ -134,7 +134,7 @@ def set_validation_error(self, error: Exception) -> None: Args: error (Exception): Exception occurred. """ - self.last_short_session_validation_result = f"JWT validation failed: {error}" + self.last_session_token_validation_result = f"JWT validation failed: {error}" # Private methods def _validate_issuer(self, token_issuer: str, session_token: str) -> None: diff --git a/src/corbado_python_sdk/utils/__init__.py b/src/corbado_python_sdk/utils/__init__.py index e69de29..743c92c 100644 --- a/src/corbado_python_sdk/utils/__init__.py +++ b/src/corbado_python_sdk/utils/__init__.py @@ -0,0 +1,5 @@ +from .constants import ( + DEFAULT_SESSION_TOKEN_COOKIE_NAME as DEFAULT_SESSION_TOKEN_COOKIE_NAME, +) + +__all__ = ["DEFAULT_SESSION_TOKEN_COOKIE_NAME"] diff --git a/src/corbado_python_sdk/utils/constants.py b/src/corbado_python_sdk/utils/constants.py new file mode 100644 index 0000000..1dda130 --- /dev/null +++ b/src/corbado_python_sdk/utils/constants.py @@ -0,0 +1 @@ +DEFAULT_SESSION_TOKEN_COOKIE_NAME = "cbo_session_token" # noqa: S105 diff --git a/tests/unit/test_session_service.py b/tests/unit/test_session_service.py index 1872fe2..b5a54db 100644 --- a/tests/unit/test_session_service.py +++ b/tests/unit/test_session_service.py @@ -65,9 +65,9 @@ def create_session_service(cls) -> SessionService: SessionService: SessionService instance """ return SessionService( + session_token_cookie_name="cbo_session_token", issuer="https://auth.acme.com", jwks_uri="https://example_uri.com", # does not matter, url access is mocked - short_session_cookie_name="cbo_short_session", project_id="pro-55", ) @@ -119,7 +119,7 @@ def _generate_jwt(cls, iss: str, exp: int, nbf: int) -> str: class TestSessionService(TestBase): def test_get_and_validate_short_session_value(self): for valid, token in self._provide_jwts(): - result: SessionValidationResult = self.session_service.get_and_validate_short_session_value(short_session=token) + result: SessionValidationResult = self.session_service.get_and_validate_short_session_value(session_token=token) self.assertEqual(first=valid, second=result.authenticated) self.assertEqual(first=valid, second=result.error is None) @@ -130,10 +130,10 @@ def test_get_and_validate_short_session_value(self): def test_cache_jwk_set_used_expect_reduced_urlopen_calls(self): jwt: str = self._generate_jwt(iss="https://auth.acme.com", exp=int(time()) + 100, nbf=int(time()) - 100) - self.session_service.get_and_validate_short_session_value(short_session=jwt) + self.session_service.get_and_validate_short_session_value(session_token=jwt) num_calls: int = self.mock_urlopen.call_count for _i in range(3): - self.session_service.get_and_validate_short_session_value(short_session=jwt) + self.session_service.get_and_validate_short_session_value(session_token=jwt) self.assertEqual(num_calls, self.mock_urlopen.call_count) def test_generate_jwt(self): @@ -147,13 +147,13 @@ def test_generate_jwt(self): def test_init_parameters(self): test_cases = [ # Valid session service - ({"issuer": "s", "jwks_uri": "2", "short_session_cookie_name": "name", "project_id": "pro-55"}, True), + ({"issuer": "s", "jwks_uri": "2", "session_token_cookie_name": "name", "project_id": "pro-55"}, True), # Test empty issuer - ({"issuer": "", "jwks_uri": "2", "short_session_cookie_name": "name", "project_id": "pro-55"}, False), + ({"issuer": "", "jwks_uri": "2", "session_token_cookie_name": "name", "project_id": "pro-55"}, False), # Test empty jwks_uri - ({"issuer": "s", "jwks_uri": "", "short_session_cookie_name": "name", "project_id": "pro-55"}, False), - # Tesft empty short_session_cookie_name - ({"issuer": "s", "jwks_uri": "2", "short_session_cookie_name": "", "project_id": "pro-55"}, False), + ({"issuer": "s", "jwks_uri": "", "session_token_cookie_name": "name", "project_id": "pro-55"}, False), + # Tesft empty session_token_cookie_name + ({"issuer": "s", "jwks_uri": "2", "session_token_cookie_name": "", "project_id": "pro-55"}, False), ] for params, expected_result in test_cases: From e9709c06bf15523368172892bbbf7635923801c9 Mon Sep 17 00:00:00 2001 From: Aleksandr Balakirev Date: Sun, 27 Oct 2024 11:44:28 +0100 Subject: [PATCH 04/30] Bump release version to 2.0.0 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 9c1218c..359a5b9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.3 \ No newline at end of file +2.0.0 \ No newline at end of file From 4472ab85be8f4f880a85a9ff0bf4c7bce897493b Mon Sep 17 00:00:00 2001 From: Aleksandr Balakirev Date: Sun, 27 Oct 2024 11:50:32 +0100 Subject: [PATCH 05/30] Rename get_and_validate_short_session_value to validate_token --- README.md | 2 +- .../services/implementation/session_service.py | 15 +-------------- tests/unit/test_session_service.py | 8 ++++---- 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index d0f02b0..d930780 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ The Corbado Python SDK raises exceptions for all errors except those that occur 'SessionService' returns 'SessionValidationResult' as result of token validation. You can check whether any errors occurred and handle them if needed: ```Python -result: SessionValidationResult = self.session_service.get_and_validate_short_session_value(session_token=token) +result: SessionValidationResult = self.session_service.validate_token(session_token=token) if result.error is not None: print(result.error) raise result.error diff --git a/src/corbado_python_sdk/services/implementation/session_service.py b/src/corbado_python_sdk/services/implementation/session_service.py index 778b423..8f4b798 100644 --- a/src/corbado_python_sdk/services/implementation/session_service.py +++ b/src/corbado_python_sdk/services/implementation/session_service.py @@ -74,7 +74,7 @@ def __init__(self, **kwargs) -> None: # type: ignore ) # Core methods - def get_and_validate_short_session_value(self, session_token: StrictStr) -> SessionValidationResult: + def validate_token(self, session_token: StrictStr) -> SessionValidationResult: """Validate the given short-term session (represented as JWT) value. Args: @@ -107,19 +107,6 @@ def get_and_validate_short_session_value(self, session_token: StrictStr) -> Sess self.set_validation_error(error) return SessionValidationResult(authenticated=False, error=error) - def get_current_user(self, session_token: StrictStr) -> SessionValidationResult: - """Return current user for the short session. - - Args: - session_token (StrictStr): Short session. - - Returns: - SessionValidationResult: SessionValidationResult with authenticated=True on success, otherwise with - authenticated=False. - """ - user: SessionValidationResult = self.get_and_validate_short_session_value(session_token) - return user - def set_issuer_mismatch_error(self, token_issuer: str) -> None: """Set issuer mismatch error. diff --git a/tests/unit/test_session_service.py b/tests/unit/test_session_service.py index b5a54db..60ec0cc 100644 --- a/tests/unit/test_session_service.py +++ b/tests/unit/test_session_service.py @@ -117,9 +117,9 @@ def _generate_jwt(cls, iss: str, exp: int, nbf: int) -> str: class TestSessionService(TestBase): - def test_get_and_validate_short_session_value(self): + def test_validate_token(self): for valid, token in self._provide_jwts(): - result: SessionValidationResult = self.session_service.get_and_validate_short_session_value(session_token=token) + result: SessionValidationResult = self.session_service.validate_token(session_token=token) self.assertEqual(first=valid, second=result.authenticated) self.assertEqual(first=valid, second=result.error is None) @@ -130,10 +130,10 @@ def test_get_and_validate_short_session_value(self): def test_cache_jwk_set_used_expect_reduced_urlopen_calls(self): jwt: str = self._generate_jwt(iss="https://auth.acme.com", exp=int(time()) + 100, nbf=int(time()) - 100) - self.session_service.get_and_validate_short_session_value(session_token=jwt) + self.session_service.validate_token(session_token=jwt) num_calls: int = self.mock_urlopen.call_count for _i in range(3): - self.session_service.get_and_validate_short_session_value(session_token=jwt) + self.session_service.validate_token(session_token=jwt) self.assertEqual(num_calls, self.mock_urlopen.call_count) def test_generate_jwt(self): From c8b0c6887f54be305085c354a65270badfae48c2 Mon Sep 17 00:00:00 2001 From: Aleksandr Balakirev Date: Sun, 27 Oct 2024 13:58:50 +0100 Subject: [PATCH 06/30] Change validate_token error handling, add new exception type for session service --- README.md | 9 +-- src/corbado_python_sdk/__init__.py | 5 +- src/corbado_python_sdk/entities/__init__.py | 5 +- .../entities/session_validation_result.py | 14 ---- src/corbado_python_sdk/exceptions/__init__.py | 3 +- .../exceptions/token_validation_exception.py | 62 ++++++++++++++++ .../implementation/session_service.py | 73 +++++++++++-------- tests/unit/test_session_service.py | 38 +++------- 8 files changed, 130 insertions(+), 79 deletions(-) delete mode 100644 src/corbado_python_sdk/entities/session_validation_result.py create mode 100644 src/corbado_python_sdk/exceptions/token_validation_exception.py diff --git a/README.md b/README.md index d930780..a849acc 100644 --- a/README.md +++ b/README.md @@ -63,13 +63,12 @@ The Corbado Python SDK raises exceptions for all errors except those that occur - `ServerException` for server errors (server side) - `StandardException` for everything else (client side) -'SessionService' returns 'SessionValidationResult' as result of token validation. You can check whether any errors occurred and handle them if needed: +'SessionService' returns 'UserEntity' as result of token validation. You can check whether any errors occurred and handle them if needed: ```Python -result: SessionValidationResult = self.session_service.validate_token(session_token=token) - if result.error is not None: - print(result.error) - raise result.error +result: UserEntity = self.session_service.validate_token(session_token=token) + print(result.error) + raise result.error ``` If the Backend API returns a HTTP status code other than 200, the Corbado Python SDK throws a `ServerException`. The `ServerException`class provides convenient methods to access all important data: diff --git a/src/corbado_python_sdk/__init__.py b/src/corbado_python_sdk/__init__.py index ab9ceb2..ef958cb 100644 --- a/src/corbado_python_sdk/__init__.py +++ b/src/corbado_python_sdk/__init__.py @@ -1,8 +1,8 @@ from .config import Config as Config from .corbado_sdk import CorbadoSDK as CorbadoSDK -from .entities import SessionValidationResult as SessionValidationResult from .entities import UserEntity as UserEntity from .exceptions import StandardException as StandardException +from .exceptions import TokenValidationException, ValidationErrorType from .generated import ( Identifier, IdentifierCreateReq, @@ -15,7 +15,8 @@ from .services import IdentifierService, SessionService, UserService __all__ = [ - "SessionValidationResult", + "TokenValidationException", + "ValidationErrorType", "IdentifierCreateReq", "Identifier", "IdentifierStatus", diff --git a/src/corbado_python_sdk/entities/__init__.py b/src/corbado_python_sdk/entities/__init__.py index 483134d..7eafb37 100644 --- a/src/corbado_python_sdk/entities/__init__.py +++ b/src/corbado_python_sdk/entities/__init__.py @@ -1,4 +1,5 @@ -from .session_validation_result import SessionValidationResult +from corbado_python_sdk.generated import UserStatus + from .user_entity import UserEntity as UserEntity -__all__ = ["UserEntity", "SessionValidationResult"] +__all__ = ["UserEntity", "UserStatus"] diff --git a/src/corbado_python_sdk/entities/session_validation_result.py b/src/corbado_python_sdk/entities/session_validation_result.py deleted file mode 100644 index 289863c..0000000 --- a/src/corbado_python_sdk/entities/session_validation_result.py +++ /dev/null @@ -1,14 +0,0 @@ -from pydantic import BaseModel, ConfigDict, Field -from typing_extensions import Optional - - -class SessionValidationResult(BaseModel): - """Result class for SessionService validation.""" - - model_config = ConfigDict( - arbitrary_types_allowed=True, - ) - authenticated: bool = Field(default=False, description="Indicates success of validation by session service.") - user_id: Optional[str] = Field(default=None, description="The user ID.") - full_name: Optional[str] = Field(default=None, description="The full name.") - error: Optional[Exception] = Field(default=None, description="Error occurred during validation.") diff --git a/src/corbado_python_sdk/exceptions/__init__.py b/src/corbado_python_sdk/exceptions/__init__.py index cc34dd7..a066eb6 100644 --- a/src/corbado_python_sdk/exceptions/__init__.py +++ b/src/corbado_python_sdk/exceptions/__init__.py @@ -1,4 +1,5 @@ from .server_exception import ServerException as ServerException from .standard_exception import StandardException as StandardException +from .token_validation_exception import TokenValidationException, ValidationErrorType -__all__ = ["ServerException", "StandardException"] +__all__ = ["ServerException", "StandardException", "TokenValidationException", "ValidationErrorType"] diff --git a/src/corbado_python_sdk/exceptions/token_validation_exception.py b/src/corbado_python_sdk/exceptions/token_validation_exception.py new file mode 100644 index 0000000..e6b75ac --- /dev/null +++ b/src/corbado_python_sdk/exceptions/token_validation_exception.py @@ -0,0 +1,62 @@ +from enum import Enum + +from typing_extensions import Optional + + +class ValidationErrorType(Enum): + """ + Enum representing types of validation errors. + + Attributes: + MISSING_FIELD (str): Indicates a required field is missing. + INVALID_FORMAT (str): Indicates an incorrect format in the data. + VALUE_OUT_OF_RANGE (str): Indicates a value is outside the acceptable range. + UNAUTHORIZED_ACCESS (str): Indicates unauthorized access attempt. + """ + + INVALID_TOKEN = "Invalid token" # noqa s105 + SIGNING_KEY_ERROR = "Could not retrieve signing key" + EMPTY_SESSION_TOKEN = "Session token is empty" # noqa s105 + EMPTY_ISSUER = "Issuer is empty" + ISSUER_MISSMATCH = "Token issuer does not match" + + +class TokenValidationException(Exception): + """Custom exception class for handling validation errors. + + This exception wraps around other exceptions to provide additional context + regarding validation failures. + + Attributes: + message (str): The custom error message describing the validation error. + error_type (ValidationErrorType): Enum value indicating the type of validation error. + original_exception (Optional[Exception]): The original exception that caused this error, if any. + """ + + def __init__(self, message: str, error_type: ValidationErrorType, original_exception: Optional[Exception] = None): + """Initialize ValidationError with message, error type, and optional original exception. + + Args: + message (str): A description of the error. + error_type (ValidationErrorType): The specific type of validation error. + original_exception (Optional[Exception], optional): The original exception that caused + this error, if available. Defaults to None. + """ + super().__init__(message) + self.message: str = message + self.error_type: ValidationErrorType = error_type + self.original_exception: Exception | None = original_exception + + def __str__(self) -> str: + """Return a string representation of the validation error. + + Includes the error type, custom message, and details of the original exception + if it is available. + + Returns: + str: Formatted string containing error type, message, and any original exception details. + """ + base_message: str = f"[{self.error_type.value}] {self.message}" + if self.original_exception: + return f"{base_message} | Caused by: {repr(self.original_exception)}" + return base_message diff --git a/src/corbado_python_sdk/services/implementation/session_service.py b/src/corbado_python_sdk/services/implementation/session_service.py index 8f4b798..5d19f47 100644 --- a/src/corbado_python_sdk/services/implementation/session_service.py +++ b/src/corbado_python_sdk/services/implementation/session_service.py @@ -1,17 +1,13 @@ import jwt from jwt import decode from jwt.jwks_client import PyJWKClient -from pydantic import ( - BaseModel, - ConfigDict, - StrictStr, - StringConstraints, - ValidationError, -) +from pydantic import BaseModel, ConfigDict, StrictStr, StringConstraints from typing_extensions import Annotated -from corbado_python_sdk.entities.session_validation_result import ( - SessionValidationResult, +from corbado_python_sdk.entities import UserEntity, UserStatus +from corbado_python_sdk.exceptions.token_validation_exception import ( + TokenValidationException, + ValidationErrorType, ) DEFAULT_SESSION_TOKEN_LENGTH = 300 @@ -74,40 +70,56 @@ def __init__(self, **kwargs) -> None: # type: ignore ) # Core methods - def validate_token(self, session_token: StrictStr) -> SessionValidationResult: + def validate_token(self, session_token: StrictStr) -> UserEntity: """Validate the given short-term session (represented as JWT) value. Args: session_token (StrictStr): jwt + Raises: + TokenValidationException: If token is invalid. + Returns: - SessionValidationResult: SessionValidationResult with authenticated=True on success, - otherwise with authenticated=False + UserEntity: User Entity. """ if not session_token: - return SessionValidationResult(authenticated=False) + raise TokenValidationException( + error_type=ValidationErrorType.EMPTY_SESSION_TOKEN, message=ValidationErrorType.EMPTY_SESSION_TOKEN.name + ) + + # retrieve signing key try: - # retrieve signing key signing_key: jwt.PyJWK = self._jwk_client.get_signing_key_from_jwt(token=session_token) + except Exception as error: + self._set_validation_error(error=error) + raise TokenValidationException( + error_type=ValidationErrorType.SIGNING_KEY_ERROR, + message=f"Could not retrieve signing key: {session_token}. See original_exception for further information.", + original_exception=error, + ) - # decode short session (jwt) with signing key + # decode short session (jwt) with signing key + try: payload = decode(jwt=session_token, key=signing_key.key, algorithms=["RS256"]) # extract information from decoded payload token_issuer: str = payload.get("iss") sub: str = payload.get("sub") full_name: str = payload.get("name") - - # validate issuer - self._validate_issuer(token_issuer=token_issuer, session_token=session_token) - - return SessionValidationResult(authenticated=True, user_id=sub, full_name=full_name) except Exception as error: - # return unauthenticated user on error - self.set_validation_error(error) - return SessionValidationResult(authenticated=False, error=error) + self._set_validation_error(error=error) + raise TokenValidationException( + error_type=ValidationErrorType.INVALID_TOKEN, + message=f"Error occured during token decode: {session_token}. See original_exception for further information.", + original_exception=error, + ) + + # validate issuer + self._validate_issuer(token_issuer=token_issuer, session_token=session_token) + # TODO: Retrieve user status + return UserEntity(fullName=full_name, userID=sub, status=UserStatus.ACTIVE) - def set_issuer_mismatch_error(self, token_issuer: str) -> None: + def _set_issuer_mismatch_error(self, token_issuer: str) -> None: """Set issuer mismatch error. Args: @@ -115,7 +127,7 @@ def set_issuer_mismatch_error(self, token_issuer: str) -> None: """ self.last_session_token_validation_result = f"Mismatch in issuer (configured: {self.issuer}, JWT: {token_issuer})" - def set_validation_error(self, error: Exception) -> None: + def _set_validation_error(self, error: Exception) -> None: """Set validation error. Args: @@ -132,11 +144,13 @@ def _validate_issuer(self, token_issuer: str, session_token: str) -> None: session_token (str): Session token. Raises: - ValidationError: If issuer is invalid. + TokenValidationException: If issuer is invalid. """ if not token_issuer: - raise ValidationError(f"Issuer is empty. Session token: {session_token}") + raise TokenValidationException( + error_type=ValidationErrorType.EMPTY_ISSUER, message=f"Issuer is empty. Session token: {session_token}" + ) # Check for old Frontend API (without .cloud.) expected_old: StrictStr = f"https://{self.project_id}.frontendapi.corbado.io" @@ -150,6 +164,7 @@ def _validate_issuer(self, token_issuer: str, session_token: str) -> None: # Check against the configured issuer (e.g., a custom domain or CNAME) if token_issuer != self.issuer: - raise ValidationError( - f"Issuer mismatch (configured via FrontendAPI: '{self.issuer}', JWT issuer: '{token_issuer}')", + raise TokenValidationException( + error_type=ValidationErrorType.ISSUER_MISSMATCH, + message=f"Issuer mismatch (configured via FrontendAPI: '{self.issuer}', JWT issuer: '{token_issuer}')", ) diff --git a/tests/unit/test_session_service.py b/tests/unit/test_session_service.py index 60ec0cc..ae2ab9d 100644 --- a/tests/unit/test_session_service.py +++ b/tests/unit/test_session_service.py @@ -7,12 +7,7 @@ from jwt import encode from pydantic import ValidationError -from corbado_python_sdk import ( - Config, - CorbadoSDK, - SessionService, - SessionValidationResult, -) +from corbado_python_sdk import Config, CorbadoSDK, SessionService, UserEntity TEST_NAME = "Test Name" TEST_EMAIL = "test@email.com" @@ -77,20 +72,6 @@ def tearDown(self) -> None: def _provide_jwts(self): """Provide list of jwts with expected test results.""" return [ - # JWT with invalid format - (False, "invalid"), - # JWT signed with wrong algorithm (HS256 instead of RS256) - ( - False, - """eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6 - IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.dyt0CoTl4WoVjAHI9Q_CwSKhl6d_9rhM3NrXuJttkao""", - ), - # Not before (nfb) in future - (False, self._generate_jwt(iss="https://auth.acme.com", exp=int(time()) + 100, nbf=int(time()) + 100)), - # Expired (exp) - (False, self._generate_jwt(iss="https://auth.acme.com", exp=int(time()) - 100, nbf=int(time()) - 100)), - # Invalid issuer (iss) - (False, self._generate_jwt(iss="https://invalid.com", exp=int(time()) + 100, nbf=int(time()) - 100)), # Success (True, self._generate_jwt(iss="https://auth.acme.com", exp=int(time()) + 100, nbf=int(time()) - 100)), ] @@ -119,14 +100,19 @@ def _generate_jwt(cls, iss: str, exp: int, nbf: int) -> str: class TestSessionService(TestBase): def test_validate_token(self): for valid, token in self._provide_jwts(): - result: SessionValidationResult = self.session_service.validate_token(session_token=token) - - self.assertEqual(first=valid, second=result.authenticated) - self.assertEqual(first=valid, second=result.error is None) - if valid: + result: UserEntity = self.session_service.validate_token(session_token=token) + self.assertEqual(first=TEST_NAME, second=result.full_name) - self.assertEqual(TEST_USER_ID, result.user_id) + self.assertEqual(first=TEST_USER_ID, second=result.user_id) + else: + with self.assertRaises(ValidationError) as context: + # Code that should raise the ValidationError + self.session_service.validate_token(session_token=token) + + # Optionally, you can check the message or attributes of the exception + self.assertEqual(context.exception.message, "Input value is incorrect") + # self.assertEqual(context.exception.error_type, ValidationErrorType.INVALID_FORMAT) def test_cache_jwk_set_used_expect_reduced_urlopen_calls(self): jwt: str = self._generate_jwt(iss="https://auth.acme.com", exp=int(time()) + 100, nbf=int(time()) - 100) From 8f8d625cb9e9e627385320e9a1766ce68f8789fb Mon Sep 17 00:00:00 2001 From: Aleksandr Balakirev Date: Sun, 27 Oct 2024 14:40:29 +0100 Subject: [PATCH 07/30] Adapt existing token validation tests to new error handling --- .../exceptions/token_validation_exception.py | 12 ++-- .../implementation/session_service.py | 4 +- tests/unit/test_session_service.py | 58 ++++++++++++++++--- 3 files changed, 59 insertions(+), 15 deletions(-) diff --git a/src/corbado_python_sdk/exceptions/token_validation_exception.py b/src/corbado_python_sdk/exceptions/token_validation_exception.py index e6b75ac..0d9f308 100644 --- a/src/corbado_python_sdk/exceptions/token_validation_exception.py +++ b/src/corbado_python_sdk/exceptions/token_validation_exception.py @@ -7,11 +7,15 @@ class ValidationErrorType(Enum): """ Enum representing types of validation errors. + This enum categorizes various validation errors that may occur during + token validation processes. + Attributes: - MISSING_FIELD (str): Indicates a required field is missing. - INVALID_FORMAT (str): Indicates an incorrect format in the data. - VALUE_OUT_OF_RANGE (str): Indicates a value is outside the acceptable range. - UNAUTHORIZED_ACCESS (str): Indicates unauthorized access attempt. + INVALID_TOKEN (str): Indicates that the token is invalid. More information in 'original_exception'. + SIGNING_KEY_ERROR (str): Indicates that the signing key could not be retrieved. More information in 'original_exception'. + EMPTY_SESSION_TOKEN (str): Indicates that the session token is empty. + EMPTY_ISSUER (str): Indicates that the issuer is empty. + ISSUER_MISSMATCH (str): Indicates that the token issuer does not match the expected issuer. """ INVALID_TOKEN = "Invalid token" # noqa s105 diff --git a/src/corbado_python_sdk/services/implementation/session_service.py b/src/corbado_python_sdk/services/implementation/session_service.py index 5d19f47..c06d278 100644 --- a/src/corbado_python_sdk/services/implementation/session_service.py +++ b/src/corbado_python_sdk/services/implementation/session_service.py @@ -94,7 +94,7 @@ def validate_token(self, session_token: StrictStr) -> UserEntity: self._set_validation_error(error=error) raise TokenValidationException( error_type=ValidationErrorType.SIGNING_KEY_ERROR, - message=f"Could not retrieve signing key: {session_token}. See original_exception for further information.", + message=f"Could not retrieve signing key: {session_token}. See original_exception for further information: {str(error)}", original_exception=error, ) @@ -110,7 +110,7 @@ def validate_token(self, session_token: StrictStr) -> UserEntity: self._set_validation_error(error=error) raise TokenValidationException( error_type=ValidationErrorType.INVALID_TOKEN, - message=f"Error occured during token decode: {session_token}. See original_exception for further information.", + message=f"Error occured during token decode: {session_token}. See original_exception for further information: {str(error)}", original_exception=error, ) diff --git a/tests/unit/test_session_service.py b/tests/unit/test_session_service.py index ae2ab9d..b86a704 100644 --- a/tests/unit/test_session_service.py +++ b/tests/unit/test_session_service.py @@ -4,10 +4,22 @@ from time import time from unittest.mock import AsyncMock, MagicMock, patch -from jwt import encode +from jwt import ( + DecodeError, + ExpiredSignatureError, + ImmatureSignatureError, + PyJWKClientError, + encode, +) from pydantic import ValidationError -from corbado_python_sdk import Config, CorbadoSDK, SessionService, UserEntity +from corbado_python_sdk import ( + Config, + CorbadoSDK, + SessionService, + TokenValidationException, + UserEntity, +) TEST_NAME = "Test Name" TEST_EMAIL = "test@email.com" @@ -72,8 +84,34 @@ def tearDown(self) -> None: def _provide_jwts(self): """Provide list of jwts with expected test results.""" return [ + # JWT with invalid format + (False, "invalid", DecodeError, "Not enough segments"), + # JWT signed with wrong algorithm (HS256 instead of RS256) + ( + False, + """eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6 + IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.dyt0CoTl4WoVjAHI9Q_CwSKhl6d_9rhM3NrXuJttkao""", + PyJWKClientError, + 'Unable to find a signing key that matches: "None"', + ), + # Not before (nfb) in future + ( + False, + self._generate_jwt(iss="https://auth.acme.com", exp=int(time()) + 100, nbf=int(time()) + 100), + ImmatureSignatureError, + "The token is not yet valid (nbf)", + ), + # Expired (exp) + ( + False, + self._generate_jwt(iss="https://auth.acme.com", exp=int(time()) - 100, nbf=int(time()) - 100), + ExpiredSignatureError, + "Signature has expired", + ), + # Invalid issuer (iss) + (False, self._generate_jwt(iss="https://invalid.com", exp=int(time()) + 100, nbf=int(time()) - 100), None, None), # Success - (True, self._generate_jwt(iss="https://auth.acme.com", exp=int(time()) + 100, nbf=int(time()) - 100)), + (True, self._generate_jwt(iss="https://auth.acme.com", exp=int(time()) + 100, nbf=int(time()) - 100), None, None), ] @classmethod @@ -99,20 +137,22 @@ def _generate_jwt(cls, iss: str, exp: int, nbf: int) -> str: class TestSessionService(TestBase): def test_validate_token(self): - for valid, token in self._provide_jwts(): + for valid, token, expected_original_error, expected_original_error_message in self._provide_jwts(): if valid: result: UserEntity = self.session_service.validate_token(session_token=token) self.assertEqual(first=TEST_NAME, second=result.full_name) self.assertEqual(first=TEST_USER_ID, second=result.user_id) else: - with self.assertRaises(ValidationError) as context: + with self.assertRaises(TokenValidationException) as context: # Code that should raise the ValidationError self.session_service.validate_token(session_token=token) - - # Optionally, you can check the message or attributes of the exception - self.assertEqual(context.exception.message, "Input value is incorrect") - # self.assertEqual(context.exception.error_type, ValidationErrorType.INVALID_FORMAT) + if expected_original_error: + print(f"type: { print(context.exception.original_exception.__class__)}") + print(f"exception: {context.exception.original_exception}") + print(f"Token: {token}") + self.assertIsInstance(context.exception.original_exception, expected_original_error) + self.assertEqual(str(context.exception.original_exception), expected_original_error_message) def test_cache_jwk_set_used_expect_reduced_urlopen_calls(self): jwt: str = self._generate_jwt(iss="https://auth.acme.com", exp=int(time()) + 100, nbf=int(time()) - 100) From 0e3e17c05f75fbb92e0ac6f2f828eb86946fcee4 Mon Sep 17 00:00:00 2001 From: Aleksandr Balakirev Date: Sun, 27 Oct 2024 15:18:59 +0100 Subject: [PATCH 08/30] Add new tests to session service for issuer validation and false private key --- tests/unit/test_data/invalidPrivateKey.pem | 51 +++++++++++++++ tests/unit/test_session_service.py | 72 ++++++++++++++++++---- 2 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 tests/unit/test_data/invalidPrivateKey.pem diff --git a/tests/unit/test_data/invalidPrivateKey.pem b/tests/unit/test_data/invalidPrivateKey.pem new file mode 100644 index 0000000..42bff99 --- /dev/null +++ b/tests/unit/test_data/invalidPrivateKey.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKQIBAAKCAgEA0QNn8WNk03FIA+q8rEbJGpzFfQ8HZbtB/WTSqfIkDO/07L2i +Z3us17KfmXbJZnmmYhsXLVvgUHqs+ZCTi9l+4i+5gDAaq9I1eZ8zEe7Hb/fIddrt +TdnJEhP1uvCurUbjBZYVPe8E0Ii9kcIfTs0GbgBZ1fmr2O72LIYC9odQPxdEeWPh +8mheVLY/hjqPbBOGHj8hx0YvFIdKxrpX42YLB0skYquZqPmjtvFylGjxEVznMo9X +jEzuIREAAjHMNvksLp6WYCTQWxxrJ1ZEr0hDoxKnbR3VzY96K3cLJV2P1RgG4B7D +JroWeCrrSjyl9Y02f834XPy20JuMEf/1JDyiBRXV3pTZ3kaCxiXkzBSKMy7783ps +Xd5wxm7jQRjZnQEGoWERIvdaDBJqdrL9fn7Zl6BhcCd1H40AmcvvBdvZqbU5qRYY +VODDoLC6MdQbXIyRWoQVwurAD5Qd0OLj4VP+0rvktxYj0exKqLaRaI1Wy6KLkUHW +5HRl5tB+YRYckb5zTi+sMhbipTOarJDoQQAWrnBNrSYgXOuKzCIn7KQt2nn+n6at +kIYpPIseaUWaNjbj8qVJtQOtw7eFKFJHtG2fC9eaz8dYzXs9lFrNfe1pHhbcl7h0 +XEXjfF/UVo7xKRUb9k4Y4L/+slO3HaEw3nG2L1YVs5w+3cypD0Z83jiu7NMCAwEA +AQKCAgA6HuhYh3sSEP4lmLC+dzCU5eNE01kLQPcor1ClUWAU+OTysAjpHcsCb/lR +g2adyFYsEbxwMIw/1N0kyL8+tExEOWEr9DR2cw3LtClPY0ayLATiQO6cJ2NyU3qR +cTwhkwSuGP3vKvf7xQpx7bknAHw5On/AsJzIxipOVzWbuCToZl/0IYJDFiKlC8i+ +WqpnM0aoQoXyE+Ijozt353lpEocekBaeGXmxMzoDNUDXZBck7OCdfp6ptPrC1ydZ +dN+EqeVPUWofch0+OLjTR3nyA/ZK8nT8Mr+xWaRvUbsXuNy2NOg0illZ+4UkBUtI +aXT8wzfUckIOGXf8qkRY/QP9/55NqwkZC1OhUcAWC+YsAekaZpdrIz9y41454zGa +fG7KUB4z0fhxpAxkX8vYZtr+cGNdIW3CURJUeQbZr9LZGxpWtZSi/gnWFLVHoPc8 +V8aZF0LF9p89q62HrjFzw8pHlkif2nz3cyXiof2q7H0IIxgrxpW4efiW9UzTFec0 +IyUgmxglCDzZmOSwoGqrsU45GkOgDvOwwR1UOPBaaAe/GllFs/z9VByL4wxP7Lht +VrkJ7ZRFKyZIDl5SPhLA6cw4tjAZT7arqnj7YqeO2T8QI+ocYVNKzrEGI/ls8eRM +Wf6yNWfMbxOStkn/vfLEZLcvBjv6rGgk6kva93Rl48/ea7b3AQKCAQEA67+p+ZvD +Jtpw/uR42tAAtx0ErT+DiIhkiPadafOOQxg4uM9sUbVWeugZvj6J+yeaybIVUh+Y +z9KBDulkvV2NCTkyVlSOzlMACgfIzEQ43CNFu3ZFiZ+ax68iNU7WShrahUyIZy7k +weMzf24GfU54J6BxVQEw8nypXSdtbW64b3YddjojWGpJyyj+/F5DEZDge3woUM3g +M6xvSwQ076Lw4+W0kvxRcav/s5hMgC9fu34Rh1Qp+grvkR2n0dc/Sdi7IBy0dP9G +VUyppJQZCnaR/SATv6fEV6UJOgaIi4v3iqgTrXpPXosMhgB8p6MVH14R3lSV2JYv +ZCs9xZCJgkMGewKCAQEA4vfOR3RR46CU0L9CQUweBP5f7hDliLABrnjr27hWOwCP +FqpmdR+IsZQA/enPRPZZXy9qIiLgtSeKX1iqI2GQpf+rThTai6Zlc7FTBKcHZlFZ +4JlcaUkXImL3pqRkQobVVPtGDoQ9vkqB4krgJoEJjVPgGRjkFb/ERHIBrF6uYGWz +h0xFuqxeCInIKIQAgnWzn5UQHbitZx2ylJSrw3vaV1oUasKNhsaxI9MQMlXtFvTJ +hokqGfQux0Lha80/Y3t6TLuH3S9qcL6MwF4fZPBLqK/7vYWAfEwlq/eibx/L5HC2 +emkM2/3KGlIg+2n/TMug1XBozKNwGoJZ0RTBjsnPiQKCAQBScaG4yA80XDod+bqG +Ey3c28oR6F9QdRprh4s/TUyYDDVx9CAWZwtKSjcOgAJzEcOg+6VXJfOj4iL+GwL+ +8mNrMVR4OnQClaYy28wOG2GjIra1O0RQP7+6S3X0HJD2M3PuH/in1Q9s+s68prag +RbvhZ8uahaMfehckm7A6D/lyCWV+PhC0T99kq/6YNULRNtA6fw3+TDE3APueyWbV +0y7ZFDDVImY9EYCHxyAhx+97uzfYEsfnSMHFBxD0YokggynVrhQxwkBPflLpWl1z +eknNstUVNk+EsxOJ9lT0MEuuct8G0PpGvuvvROZTHcY6RQSMsxoAN211Y2Xj3Isq +QdRXAoIBAQCnaw832e+Frq/XAx2GOPVzzYdJfiEyIFI9NmkUG/dPcmwZ1fvZrTcR +dhD9Ovhu8CCge8MYBw4l0lLZXfeyPYeLSvo7kFpc5JK1MjoAlox4xs07RSTLbOSp +ssLmoyBpijfWhbr5fkZlFWSVXZHWFRzMqPAG0zt7Z4cIzee6E+UgqiQxSuusdPSv +66I/cI9HrDahIj1PDyiphVhzDO9eNP+rq4Cf936ZyfpkaI3r80H9yvbUTF1H47yV +sxnAgRee9DgUXcgsOYuUOppsU4Fo3W+RsK5K8C0Egs7O+M1Sluu5SALybIbVx3DK +1YYKrfdp2FJ83nwPZweMJlbGST81aluxAoIBAQC71LTZLbWN/kb/p92712IB+ahw +EMATYL7VUGRg7k560j+DQy8/vKqUAI+fZdDwBQGEP7hGxPDeyz8xrslC35bp/IWe +4gt3yBqJByAgjWsaStgsRSgLUJLSds3Ew4JL+b8MGtOpUVnH1I3km3PPTykYS1Ah +WhNpB7nNuq12IrRobexW5pjpcUGKo2bh4a/cuJFDGb0onQg0KvRdPgpQKGqJut/p +nFLAsPZjYSzYuaPHXA6sC7iL9qVOMsy5GMyOvWb2upLhf1eOwAjkgZUcjf7fmOnp +pW8BoDlWyE2abVflcgJrZM4AxwHJkGQDfQ0uvlziIvHG+7M1/iz7RR+mIHbQ +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/tests/unit/test_session_service.py b/tests/unit/test_session_service.py index b86a704..e824e23 100644 --- a/tests/unit/test_session_service.py +++ b/tests/unit/test_session_service.py @@ -8,6 +8,7 @@ DecodeError, ExpiredSignatureError, ImmatureSignatureError, + InvalidSignatureError, PyJWKClientError, encode, ) @@ -38,6 +39,8 @@ class TestBase(unittest.TestCase): """ + private_key = None + invalid_private_key = None mock_urlopen = None @classmethod @@ -52,6 +55,17 @@ def setUpClass(cls) -> None: except FileNotFoundError: raise FileNotFoundError("Failed to read private key file") + private_key_path: str = os.path.join(os.path.dirname(__file__), "test_data", "privateKey.pem") + invalid_key_path: str = os.path.join(os.path.dirname(__file__), "test_data", "invalidPrivateKey.pem") + + try: + with open(file=private_key_path, mode="rb") as private_key_file: + cls.private_key: bytes = private_key_file.read() + with open(file=invalid_key_path, mode="rb") as private_key_file: + cls.invalid_private_key: bytes = private_key_file.read() + except FileNotFoundError: + raise FileNotFoundError("Failed to read private key file") + def setUp(self) -> None: self.my_patch = patch("urllib.request.urlopen") self.mock_urlopen: MagicMock | AsyncMock = self.my_patch.start() @@ -110,12 +124,49 @@ def _provide_jwts(self): ), # Invalid issuer (iss) (False, self._generate_jwt(iss="https://invalid.com", exp=int(time()) + 100, nbf=int(time()) - 100), None, None), + # Use false private key + ( + False, + self._generate_jwt(valid_key=False, iss="https://auth.acme.com", exp=int(time()) + 100, nbf=int(time()) - 100), + InvalidSignatureError, + "Signature verification failed", + ), + # Invalid issuer (false project id) + ( + False, + self._generate_jwt( + iss="https://pro-12.frontendapi.cloud.corbado.io", exp=int(time()) + 100, nbf=int(time()) - 100 + ), + None, + None, + ), + # Success with old Frontend API URL in config + ( + True, + self._generate_jwt( + iss="https://pro-55.frontendapi.cloud.corbado.io", exp=int(time()) + 100, nbf=int(time()) - 100 + ), + None, + None, + ), + # Success with old Frontend API URL in config (2) + ( + True, + self._generate_jwt(iss="https://pro-55.frontendapi.corbado.io", exp=int(time()) + 100, nbf=int(time()) - 100), + None, + None, + ), # Success - (True, self._generate_jwt(iss="https://auth.acme.com", exp=int(time()) + 100, nbf=int(time()) - 100), None, None), + ( + True, + self._generate_jwt(iss="https://auth.acme.com", exp=int(time()) + 100, nbf=int(time()) - 100), + None, + None, + ), ] @classmethod - def _generate_jwt(cls, iss: str, exp: int, nbf: int) -> str: + def _generate_jwt(cls, iss: str, exp: int, nbf: int, valid_key: bool = True) -> str: payload = { "iss": iss, "iat": int(time()), @@ -125,14 +176,9 @@ def _generate_jwt(cls, iss: str, exp: int, nbf: int) -> str: "name": TEST_NAME, } - private_key_path: str = os.path.join(os.path.dirname(__file__), "test_data", "privateKey.pem") - try: - with open(file=private_key_path, mode="rb") as private_key_file: - private_key = private_key_file.read() - except FileNotFoundError: - raise FileNotFoundError("Failed to read private key file") - - return encode(payload, private_key, algorithm="RS256", headers={"kid": "kid123"}) + if valid_key: + return encode(payload, key=cls.private_key, algorithm="RS256", headers={"kid": "kid123"}) + return encode(payload, key=cls.invalid_private_key, algorithm="RS256", headers={"kid": "kid123"}) class TestSessionService(TestBase): @@ -148,9 +194,9 @@ def test_validate_token(self): # Code that should raise the ValidationError self.session_service.validate_token(session_token=token) if expected_original_error: - print(f"type: { print(context.exception.original_exception.__class__)}") - print(f"exception: {context.exception.original_exception}") - print(f"Token: {token}") + # print(f"type: { print(context.exception.original_exception.__class__)}") + # print(f"exception: {context.exception.original_exception}") + # print(f"Token: {token}") self.assertIsInstance(context.exception.original_exception, expected_original_error) self.assertEqual(str(context.exception.original_exception), expected_original_error_message) From 338ede36c5427d8c887f4ce94e22d9af04135152 Mon Sep 17 00:00:00 2001 From: Aleksandr Balakirev Date: Sun, 27 Oct 2024 15:33:33 +0100 Subject: [PATCH 09/30] Add new test for issuer validation in session service --- tests/unit/test_session_service.py | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/unit/test_session_service.py b/tests/unit/test_session_service.py index e824e23..3fe8ce8 100644 --- a/tests/unit/test_session_service.py +++ b/tests/unit/test_session_service.py @@ -92,6 +92,31 @@ def create_session_service(cls) -> SessionService: project_id="pro-55", ) + @classmethod + def create_session_service_with_frontend_api_and_cname( + cls, cname, frontend_api="https://corbado1_xxx.frontendapi.corbado.io" + ) -> SessionService: + """Create test configuration of SessionService. + + Warning! You should normally use SessionService from CorbadoSDK for non-test purposes. + + Returns: + SessionService: SessionService instance + """ + config = Config( + api_secret="corbado1_xxx", + backend_api="https://backend.com", + frontend_api="https://corbado1_xxx.frontendapi.corbado.io", + project_id="pro-55", + cname=cname, + ) + return SessionService( + session_token_cookie_name=config.session_token_cookie_name, + issuer=config.issuer, + jwks_uri="https://example_uri.com", # does not matter, url access is mocked + project_id=config.project_id, + ) + def tearDown(self) -> None: self.my_patch.stop() @@ -140,6 +165,13 @@ def _provide_jwts(self): None, None, ), + # Invalid issuer 2 (false project id) + ( + False, + self._generate_jwt(iss="https://pro-12.frontendapi.corbado.io", exp=int(time()) + 100, nbf=int(time()) - 100), + None, + None, + ), # Success with old Frontend API URL in config ( True, From 96372a469e6d4f3368f24786300a0b8b374c6292 Mon Sep 17 00:00:00 2001 From: Aleksandr Balakirev <55588266+alexbalakirev@users.noreply.github.com> Date: Sun, 27 Oct 2024 15:49:43 +0100 Subject: [PATCH 10/30] Update README.md --- README.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a849acc..6b9d7cc 100644 --- a/README.md +++ b/README.md @@ -57,18 +57,13 @@ user_service: UserService = sdk.users ### Error handling -The Corbado Python SDK raises exceptions for all errors except those that occur in the session service during token validation (See example below on how to catch those errors). The following exceptions are thrown: +The Corbado Python SDK raises exceptions for all errors. The following exceptions are thrown: - `ValidationError` for failed validations (client side) - `ServerException` for server errors (server side) +- `TokenValidationException` for errors in session service (client side) - `StandardException` for everything else (client side) -'SessionService' returns 'UserEntity' as result of token validation. You can check whether any errors occurred and handle them if needed: - -```Python -result: UserEntity = self.session_service.validate_token(session_token=token) - print(result.error) - raise result.error ``` If the Backend API returns a HTTP status code other than 200, the Corbado Python SDK throws a `ServerException`. The `ServerException`class provides convenient methods to access all important data: From 112dc08d0ea9ad58225d2c753d9328dd6f6309fa Mon Sep 17 00:00:00 2001 From: Aleksandr Balakirev Date: Sun, 3 Nov 2024 21:56:03 +0100 Subject: [PATCH 11/30] Remove session_token_cookie_name --- src/corbado_python_sdk/config.py | 6 ++---- src/corbado_python_sdk/corbado_sdk.py | 1 - .../services/implementation/session_service.py | 4 +--- src/corbado_python_sdk/utils/__init__.py | 5 ----- src/corbado_python_sdk/utils/constants.py | 1 - tests/unit/test_session_service.py | 10 +++------- 6 files changed, 6 insertions(+), 21 deletions(-) diff --git a/src/corbado_python_sdk/config.py b/src/corbado_python_sdk/config.py index 2255183..e829b7a 100644 --- a/src/corbado_python_sdk/config.py +++ b/src/corbado_python_sdk/config.py @@ -1,7 +1,7 @@ from pydantic import BaseModel, ConfigDict, StringConstraints, field_validator from typing_extensions import Annotated, Optional -from corbado_python_sdk.utils import DEFAULT_SESSION_TOKEN_COOKIE_NAME, validators +from corbado_python_sdk.utils import validators class Config(BaseModel): @@ -16,9 +16,8 @@ class Config(BaseModel): Attributes: project_id (str): The unique identifier for the project. api_secret (str): The secret key used to authenticate API requests. - backend_api (str): The base URL for the backend API. frontend_api (str): The base URL for the frontend API. - session_token_cookie_name (str): The name of the cookie for short session management. Defaults to "cbo_session_token". + backend_api (str): The base URL for the backend API. """ # Make sure that field assignments are also validated, use "set_assignment_validation(False)" @@ -29,7 +28,6 @@ class Config(BaseModel): project_id: str api_secret: str - session_token_cookie_name: str = DEFAULT_SESSION_TOKEN_COOKIE_NAME cname: Optional[Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]] = None _issuer: Optional[Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]] = None diff --git a/src/corbado_python_sdk/corbado_sdk.py b/src/corbado_python_sdk/corbado_sdk.py index c592bf8..fb8448b 100644 --- a/src/corbado_python_sdk/corbado_sdk.py +++ b/src/corbado_python_sdk/corbado_sdk.py @@ -70,7 +70,6 @@ def sessions(self) -> SessionService: """ if not self._sessions: self._sessions = SessionService( - session_token_cookie_name=self.config.session_token_cookie_name, issuer=self.config.issuer, jwks_uri=self.config.frontend_api + "/.well-known/jwks", project_id=self.config.project_id, diff --git a/src/corbado_python_sdk/services/implementation/session_service.py b/src/corbado_python_sdk/services/implementation/session_service.py index c06d278..58f6eac 100644 --- a/src/corbado_python_sdk/services/implementation/session_service.py +++ b/src/corbado_python_sdk/services/implementation/session_service.py @@ -20,7 +20,6 @@ class SessionService(BaseModel): Attributes: model_config (ConfigDict): Configuration dictionary for the model. - session_token_cookie_name (str): Name of the short session cookie. issuer (str): Issuer of the session tokens. jwks_uri (str): URI of the JSON Web Key Set (JWKS) endpoint. last_session_token_validation_result (str): Result of the last short session validation. @@ -36,7 +35,6 @@ class SessionService(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True) # Fields - session_token_cookie_name: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] issuer: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] jwks_uri: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] last_session_token_validation_result: str = "" @@ -54,7 +52,7 @@ def __init__(self, **kwargs) -> None: # type: ignore Args: **kwargs: Additional keyword arguments to initialize the SessionService. These keyword arguments should include values for the attributes defined in the class, - such as 'session_token_cookie_name', 'issuer', 'jwks_uri', 'last_session_token_validation_result', + such as 'issuer', 'jwks_uri', 'last_session_token_validation_result', 'cache_keys',cache_jwk_set and 'session_token_cookie_length'. Raises: diff --git a/src/corbado_python_sdk/utils/__init__.py b/src/corbado_python_sdk/utils/__init__.py index 743c92c..e69de29 100644 --- a/src/corbado_python_sdk/utils/__init__.py +++ b/src/corbado_python_sdk/utils/__init__.py @@ -1,5 +0,0 @@ -from .constants import ( - DEFAULT_SESSION_TOKEN_COOKIE_NAME as DEFAULT_SESSION_TOKEN_COOKIE_NAME, -) - -__all__ = ["DEFAULT_SESSION_TOKEN_COOKIE_NAME"] diff --git a/src/corbado_python_sdk/utils/constants.py b/src/corbado_python_sdk/utils/constants.py index 1dda130..e69de29 100644 --- a/src/corbado_python_sdk/utils/constants.py +++ b/src/corbado_python_sdk/utils/constants.py @@ -1 +0,0 @@ -DEFAULT_SESSION_TOKEN_COOKIE_NAME = "cbo_session_token" # noqa: S105 diff --git a/tests/unit/test_session_service.py b/tests/unit/test_session_service.py index 3fe8ce8..24ec442 100644 --- a/tests/unit/test_session_service.py +++ b/tests/unit/test_session_service.py @@ -86,7 +86,6 @@ def create_session_service(cls) -> SessionService: SessionService: SessionService instance """ return SessionService( - session_token_cookie_name="cbo_session_token", issuer="https://auth.acme.com", jwks_uri="https://example_uri.com", # does not matter, url access is mocked project_id="pro-55", @@ -111,7 +110,6 @@ def create_session_service_with_frontend_api_and_cname( cname=cname, ) return SessionService( - session_token_cookie_name=config.session_token_cookie_name, issuer=config.issuer, jwks_uri="https://example_uri.com", # does not matter, url access is mocked project_id=config.project_id, @@ -251,13 +249,11 @@ def test_generate_jwt(self): def test_init_parameters(self): test_cases = [ # Valid session service - ({"issuer": "s", "jwks_uri": "2", "session_token_cookie_name": "name", "project_id": "pro-55"}, True), + ({"issuer": "s", "jwks_uri": "2", "project_id": "pro-55"}, True), # Test empty issuer - ({"issuer": "", "jwks_uri": "2", "session_token_cookie_name": "name", "project_id": "pro-55"}, False), + ({"issuer": "", "jwks_uri": "2", "project_id": "pro-55"}, False), # Test empty jwks_uri - ({"issuer": "s", "jwks_uri": "", "session_token_cookie_name": "name", "project_id": "pro-55"}, False), - # Tesft empty session_token_cookie_name - ({"issuer": "s", "jwks_uri": "2", "session_token_cookie_name": "", "project_id": "pro-55"}, False), + ({"issuer": "s", "jwks_uri": "", "project_id": "pro-55"}, False), ] for params, expected_result in test_cases: From 58000a8bba3e5dcb39676fa5aae94f677f5e7a69 Mon Sep 17 00:00:00 2001 From: Aleksandr Balakirev Date: Sun, 3 Nov 2024 21:59:02 +0100 Subject: [PATCH 12/30] Reorder required fields for configuration --- .github/workflows/python-package.yml | 6 +++--- src/corbado_python_sdk/config.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index d646d4f..cf679d2 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -48,10 +48,10 @@ jobs: run: | tox run env: - CORBADO_BACKEND_API: ${{ vars.CORBADO_BACKEND_API }} - CORBADO_FRONTEND_API: ${{ vars.CORBADO_FRONTEND_API }} - CORBADO_API_SECRET: ${{ secrets.CORBADO_API_SECRET }} CORBADO_PROJECT_ID: ${{ secrets.CORBADO_PROJECT_ID }} + CORBADO_API_SECRET: ${{ secrets.CORBADO_API_SECRET }} + CORBADO_FRONTEND_API: ${{ vars.CORBADO_FRONTEND_API }} + CORBADO_BACKEND_API: ${{ vars.CORBADO_BACKEND_API }} diff --git a/src/corbado_python_sdk/config.py b/src/corbado_python_sdk/config.py index e829b7a..aacf02d 100644 --- a/src/corbado_python_sdk/config.py +++ b/src/corbado_python_sdk/config.py @@ -27,12 +27,12 @@ class Config(BaseModel): # Fields project_id: str api_secret: str + frontend_api: str + backend_api: str cname: Optional[Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]] = None _issuer: Optional[Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]] = None - frontend_api: str - backend_api: str @field_validator( "backend_api", From 826c295726828771c4cf0a1d6a86e9ef27797824 Mon Sep 17 00:00:00 2001 From: Aleksandr Balakirev Date: Sun, 3 Nov 2024 22:30:07 +0100 Subject: [PATCH 13/30] Improved exception handling, renamed error codes --- .../exceptions/token_validation_exception.py | 26 ++++++++---- .../implementation/session_service.py | 41 ++++++++++++++++--- tests/unit/test_session_service.py | 3 -- 3 files changed, 52 insertions(+), 18 deletions(-) diff --git a/src/corbado_python_sdk/exceptions/token_validation_exception.py b/src/corbado_python_sdk/exceptions/token_validation_exception.py index 0d9f308..92823b4 100644 --- a/src/corbado_python_sdk/exceptions/token_validation_exception.py +++ b/src/corbado_python_sdk/exceptions/token_validation_exception.py @@ -12,17 +12,25 @@ class ValidationErrorType(Enum): Attributes: INVALID_TOKEN (str): Indicates that the token is invalid. More information in 'original_exception'. - SIGNING_KEY_ERROR (str): Indicates that the signing key could not be retrieved. More information in 'original_exception'. - EMPTY_SESSION_TOKEN (str): Indicates that the session token is empty. - EMPTY_ISSUER (str): Indicates that the issuer is empty. - ISSUER_MISSMATCH (str): Indicates that the token issuer does not match the expected issuer. + CODE_JWT_SIGNING_KEY_ERROR (str): Indicates that the signing key could not be retrieved. More information in 'original_exception'. + CODE_JWT_CODE_JWT_EMPTY_SESSION_TOKEN (str): Indicates that the session token is empty. + CODE_JWT_ISSUER_EMPTY (str): Indicates that the issuer is empty. + CODE_JWT_ISSUER_MISSMATCH (str): Indicates that the token issuer does not match the expected issuer. + CODE_JWT_BEFORE (str):Token is not yet valid. + CODE_JWT_EXPIRED (str): Token expired. + CODE_JWT_INVALID_SIGNATURE (str): Invalid Signature. + + """ - INVALID_TOKEN = "Invalid token" # noqa s105 - SIGNING_KEY_ERROR = "Could not retrieve signing key" - EMPTY_SESSION_TOKEN = "Session token is empty" # noqa s105 - EMPTY_ISSUER = "Issuer is empty" - ISSUER_MISSMATCH = "Token issuer does not match" + CODE_JWT_GENERAL = "Invalid token" # noqa s105 + CODE_JWT_SIGNING_KEY_ERROR = "Could not retrieve signing key" + CODE_JWT_EMPTY_SESSION_TOKEN = "Session token is empty" # noqa s105 + CODE_JWT_ISSUER_EMPTY = "Issuer is empty" + CODE_JWT_ISSUER_MISSMATCH = "Token issuer does not match" + CODE_JWT_BEFORE = "Token is not yet valid" + CODE_JWT_EXPIRED = "Token expired" + CODE_JWT_INVALID_SIGNATURE = "Invalid Signature" class TokenValidationException(Exception): diff --git a/src/corbado_python_sdk/services/implementation/session_service.py b/src/corbado_python_sdk/services/implementation/session_service.py index 58f6eac..b2b270f 100644 --- a/src/corbado_python_sdk/services/implementation/session_service.py +++ b/src/corbado_python_sdk/services/implementation/session_service.py @@ -1,5 +1,10 @@ import jwt -from jwt import decode +from jwt import ( + ExpiredSignatureError, + ImmatureSignatureError, + InvalidSignatureError, + decode, +) from jwt.jwks_client import PyJWKClient from pydantic import BaseModel, ConfigDict, StrictStr, StringConstraints from typing_extensions import Annotated @@ -82,7 +87,8 @@ def validate_token(self, session_token: StrictStr) -> UserEntity: """ if not session_token: raise TokenValidationException( - error_type=ValidationErrorType.EMPTY_SESSION_TOKEN, message=ValidationErrorType.EMPTY_SESSION_TOKEN.name + error_type=ValidationErrorType.CODE_JWT_EMPTY_SESSION_TOKEN, + message=ValidationErrorType.CODE_JWT_EMPTY_SESSION_TOKEN.name, ) # retrieve signing key @@ -91,7 +97,7 @@ def validate_token(self, session_token: StrictStr) -> UserEntity: except Exception as error: self._set_validation_error(error=error) raise TokenValidationException( - error_type=ValidationErrorType.SIGNING_KEY_ERROR, + error_type=ValidationErrorType.CODE_JWT_SIGNING_KEY_ERROR, message=f"Could not retrieve signing key: {session_token}. See original_exception for further information: {str(error)}", original_exception=error, ) @@ -104,10 +110,33 @@ def validate_token(self, session_token: StrictStr) -> UserEntity: token_issuer: str = payload.get("iss") sub: str = payload.get("sub") full_name: str = payload.get("name") + except ImmatureSignatureError as error: + self._set_validation_error(error=error) + raise TokenValidationException( + error_type=ValidationErrorType.CODE_JWT_BEFORE, + message=f"Error occured during token decode: {session_token}. {ValidationErrorType.CODE_JWT_BEFORE.value}", + original_exception=error, + ) + except ExpiredSignatureError as error: + self._set_validation_error(error=error) + raise TokenValidationException( + error_type=ValidationErrorType.CODE_JWT_INVALID_SIGNATURE, + message=f"Error occured during token decode: {session_token}. {ValidationErrorType.CODE_JWT_INVALID_SIGNATURE.value}", + original_exception=error, + ) + + except InvalidSignatureError as error: + self._set_validation_error(error=error) + raise TokenValidationException( + error_type=ValidationErrorType.CODE_JWT_EXPIRED, + message=f"Error occured during token decode: {session_token}. {ValidationErrorType.CODE_JWT_EXPIRED.value}", + original_exception=error, + ) + except Exception as error: self._set_validation_error(error=error) raise TokenValidationException( - error_type=ValidationErrorType.INVALID_TOKEN, + error_type=ValidationErrorType.CODE_JWT_GENERAL, message=f"Error occured during token decode: {session_token}. See original_exception for further information: {str(error)}", original_exception=error, ) @@ -147,7 +176,7 @@ def _validate_issuer(self, token_issuer: str, session_token: str) -> None: """ if not token_issuer: raise TokenValidationException( - error_type=ValidationErrorType.EMPTY_ISSUER, message=f"Issuer is empty. Session token: {session_token}" + error_type=ValidationErrorType.CODE_JWT_ISSUER_EMPTY, message=f"Issuer is empty. Session token: {session_token}" ) # Check for old Frontend API (without .cloud.) @@ -163,6 +192,6 @@ def _validate_issuer(self, token_issuer: str, session_token: str) -> None: # Check against the configured issuer (e.g., a custom domain or CNAME) if token_issuer != self.issuer: raise TokenValidationException( - error_type=ValidationErrorType.ISSUER_MISSMATCH, + error_type=ValidationErrorType.CODE_JWT_ISSUER_MISSMATCH, message=f"Issuer mismatch (configured via FrontendAPI: '{self.issuer}', JWT issuer: '{token_issuer}')", ) diff --git a/tests/unit/test_session_service.py b/tests/unit/test_session_service.py index 24ec442..a41f85d 100644 --- a/tests/unit/test_session_service.py +++ b/tests/unit/test_session_service.py @@ -224,9 +224,6 @@ def test_validate_token(self): # Code that should raise the ValidationError self.session_service.validate_token(session_token=token) if expected_original_error: - # print(f"type: { print(context.exception.original_exception.__class__)}") - # print(f"exception: {context.exception.original_exception}") - # print(f"Token: {token}") self.assertIsInstance(context.exception.original_exception, expected_original_error) self.assertEqual(str(context.exception.original_exception), expected_original_error_message) From 21caa9acc2371b1651dc1af94f31d939cf1abcd6 Mon Sep 17 00:00:00 2001 From: Aleksandr Balakirev Date: Sun, 3 Nov 2024 22:52:14 +0100 Subject: [PATCH 14/30] Change CORBADO_FRONTEND_API from variable to secret --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index cf679d2..f6ee57c 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -50,7 +50,7 @@ jobs: env: CORBADO_PROJECT_ID: ${{ secrets.CORBADO_PROJECT_ID }} CORBADO_API_SECRET: ${{ secrets.CORBADO_API_SECRET }} - CORBADO_FRONTEND_API: ${{ vars.CORBADO_FRONTEND_API }} + CORBADO_FRONTEND_API: ${{ secrets.CORBADO_FRONTEND_API }} CORBADO_BACKEND_API: ${{ vars.CORBADO_BACKEND_API }} From cc785d8bef095cf39f293aa85b4b4ec473a313ff Mon Sep 17 00:00:00 2001 From: Lukas Kratzel Date: Thu, 23 Jan 2025 10:06:23 +0100 Subject: [PATCH 15/30] reduce public fields of session service --- src/corbado_python_sdk/corbado_sdk.py | 4 +- .../implementation/session_service.py | 37 +++++++------------ tests/unit/test_session_service.py | 2 +- 3 files changed, 16 insertions(+), 27 deletions(-) diff --git a/src/corbado_python_sdk/corbado_sdk.py b/src/corbado_python_sdk/corbado_sdk.py index fb8448b..f3e7e55 100644 --- a/src/corbado_python_sdk/corbado_sdk.py +++ b/src/corbado_python_sdk/corbado_sdk.py @@ -70,8 +70,8 @@ def sessions(self) -> SessionService: """ if not self._sessions: self._sessions = SessionService( - issuer=self.config.issuer, - jwks_uri=self.config.frontend_api + "/.well-known/jwks", + _issuer=self.config.issuer, + _jwks_uri=self.config.frontend_api + "/.well-known/jwks", project_id=self.config.project_id, ) diff --git a/src/corbado_python_sdk/services/implementation/session_service.py b/src/corbado_python_sdk/services/implementation/session_service.py index b2b270f..b62b30c 100644 --- a/src/corbado_python_sdk/services/implementation/session_service.py +++ b/src/corbado_python_sdk/services/implementation/session_service.py @@ -25,28 +25,20 @@ class SessionService(BaseModel): Attributes: model_config (ConfigDict): Configuration dictionary for the model. - issuer (str): Issuer of the session tokens. - jwks_uri (str): URI of the JSON Web Key Set (JWKS) endpoint. last_session_token_validation_result (str): Result of the last short session validation. - session_token_cookie_length (int): Length of short session in seconds. Default = 300 - _jwk_client (PyJWKClient): JSON Web Key (JWK) client for handling JWKS. - cache_keys (bool): Flag to cache keys. Default = False. - cache_jwk_set (bool): Flag to cache jwk_sets. Default = True. project_id (str): Corbado Project Id. - - + _issuer (str): Issuer of the session tokens. + _jwks_uri (str): URI of the JSON Web Key Set (JWKS) endpoint. + _jwk_client (PyJWKClient): JSON Web Key (JWK) client for handling JWKS. """ model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True) # Fields - issuer: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] - jwks_uri: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] last_session_token_validation_result: str = "" - session_token_cookie_length: int = DEFAULT_SESSION_TOKEN_LENGTH - cache_keys: bool = False - cache_jwk_set: bool = True project_id: str + _issuer: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] + _jwks_uri: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] _jwk_client: PyJWKClient # Constructor @@ -57,19 +49,15 @@ def __init__(self, **kwargs) -> None: # type: ignore Args: **kwargs: Additional keyword arguments to initialize the SessionService. These keyword arguments should include values for the attributes defined in the class, - such as 'issuer', 'jwks_uri', 'last_session_token_validation_result', - 'cache_keys',cache_jwk_set and 'session_token_cookie_length'. + such as 'issuer', '_jwks_uri' and 'last_session_token_validation_result', Raises: Any errors raised during the initialization process. - """ super().__init__(**kwargs) self._jwk_client = PyJWKClient( - uri=self.jwks_uri, - cache_keys=self.cache_keys, - lifespan=self.session_token_cookie_length, - cache_jwk_set=self.cache_jwk_set, + uri=self._jwks_uri, + lifespan=DEFAULT_SESSION_TOKEN_LENGTH, ) # Core methods @@ -152,7 +140,7 @@ def _set_issuer_mismatch_error(self, token_issuer: str) -> None: Args: token_issuer (str): Token issuer. """ - self.last_session_token_validation_result = f"Mismatch in issuer (configured: {self.issuer}, JWT: {token_issuer})" + self.last_session_token_validation_result = f"Mismatch in issuer (configured: {self._issuer}, JWT: {token_issuer})" def _set_validation_error(self, error: Exception) -> None: """Set validation error. @@ -176,7 +164,8 @@ def _validate_issuer(self, token_issuer: str, session_token: str) -> None: """ if not token_issuer: raise TokenValidationException( - error_type=ValidationErrorType.CODE_JWT_ISSUER_EMPTY, message=f"Issuer is empty. Session token: {session_token}" + error_type=ValidationErrorType.CODE_JWT_ISSUER_EMPTY, + message=f"Issuer is empty. Session token: {session_token}" ) # Check for old Frontend API (without .cloud.) @@ -190,8 +179,8 @@ def _validate_issuer(self, token_issuer: str, session_token: str) -> None: return # Check against the configured issuer (e.g., a custom domain or CNAME) - if token_issuer != self.issuer: + if token_issuer != self._issuer: raise TokenValidationException( error_type=ValidationErrorType.CODE_JWT_ISSUER_MISSMATCH, - message=f"Issuer mismatch (configured via FrontendAPI: '{self.issuer}', JWT issuer: '{token_issuer}')", + message=f"Issuer mismatch (configured via FrontendAPI: '{self._issuer}', JWT issuer: '{token_issuer}')", ) diff --git a/tests/unit/test_session_service.py b/tests/unit/test_session_service.py index a41f85d..135d829 100644 --- a/tests/unit/test_session_service.py +++ b/tests/unit/test_session_service.py @@ -276,4 +276,4 @@ def test_set_cname_expect_issuer_changed(self): sdk = CorbadoSDK(config=config) sessions: SessionService = sdk.sessions - self.assertEqual("https://" + test_cname, sessions.issuer) + self.assertEqual("https://" + test_cname, sessions._issuer) From 9dcaa43af1e4115651f9764a6c8917bfc55dcc73 Mon Sep 17 00:00:00 2001 From: Lukas Kratzel Date: Thu, 23 Jan 2025 10:19:45 +0100 Subject: [PATCH 16/30] update session token validation example --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 6b9d7cc..e1a46ea 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,16 @@ The Corbado Python SDK raises exceptions for all errors. The following exception - `TokenValidationException` for errors in session service (client side) - `StandardException` for everything else (client side) +`SessionService` returns a `UserEntity` as a result of token validation. If the token is invalid for any reason, +a `TokenValidationException` is thrown. It provides detailed information on the error. +If no error, is thrown, the `session_token` is valid and you can use the `UserEntity` object. + +```Python +try: + user: UserEntity = sdk.sessions.validate_token(session_token) +except TokenValidationException: + # handle error.. + pass ``` If the Backend API returns a HTTP status code other than 200, the Corbado Python SDK throws a `ServerException`. The `ServerException`class provides convenient methods to access all important data: From edd1586fc6eb29ccb944e0ff13b1192d495676bf Mon Sep 17 00:00:00 2001 From: Lukas Kratzel Date: Thu, 23 Jan 2025 12:16:15 +0100 Subject: [PATCH 17/30] adjust workflow to use var instead of secret --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index f6ee57c..cf679d2 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -50,7 +50,7 @@ jobs: env: CORBADO_PROJECT_ID: ${{ secrets.CORBADO_PROJECT_ID }} CORBADO_API_SECRET: ${{ secrets.CORBADO_API_SECRET }} - CORBADO_FRONTEND_API: ${{ secrets.CORBADO_FRONTEND_API }} + CORBADO_FRONTEND_API: ${{ vars.CORBADO_FRONTEND_API }} CORBADO_BACKEND_API: ${{ vars.CORBADO_BACKEND_API }} From 43de3c2c710df87cb96640c28b28bc12dd422d57 Mon Sep 17 00:00:00 2001 From: Lukas Kratzel Date: Thu, 23 Jan 2025 12:16:46 +0100 Subject: [PATCH 18/30] return to public fields to make pydantic happy --- src/corbado_python_sdk/corbado_sdk.py | 4 +-- .../implementation/session_service.py | 25 ++++++++++--------- tests/unit/test_session_service.py | 2 +- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/corbado_python_sdk/corbado_sdk.py b/src/corbado_python_sdk/corbado_sdk.py index f3e7e55..fb8448b 100644 --- a/src/corbado_python_sdk/corbado_sdk.py +++ b/src/corbado_python_sdk/corbado_sdk.py @@ -70,8 +70,8 @@ def sessions(self) -> SessionService: """ if not self._sessions: self._sessions = SessionService( - _issuer=self.config.issuer, - _jwks_uri=self.config.frontend_api + "/.well-known/jwks", + issuer=self.config.issuer, + jwks_uri=self.config.frontend_api + "/.well-known/jwks", project_id=self.config.project_id, ) diff --git a/src/corbado_python_sdk/services/implementation/session_service.py b/src/corbado_python_sdk/services/implementation/session_service.py index b62b30c..76c4ba9 100644 --- a/src/corbado_python_sdk/services/implementation/session_service.py +++ b/src/corbado_python_sdk/services/implementation/session_service.py @@ -25,20 +25,20 @@ class SessionService(BaseModel): Attributes: model_config (ConfigDict): Configuration dictionary for the model. + issuer (str): Issuer of the session tokens. + jwks_uri (str): URI of the JSON Web Key Set (JWKS) endpoint. last_session_token_validation_result (str): Result of the last short session validation. - project_id (str): Corbado Project Id. - _issuer (str): Issuer of the session tokens. - _jwks_uri (str): URI of the JSON Web Key Set (JWKS) endpoint. _jwk_client (PyJWKClient): JSON Web Key (JWK) client for handling JWKS. + project_id (str): Corbado Project Id. """ model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True) # Fields + issuer: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] + jwks_uri: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] last_session_token_validation_result: str = "" project_id: str - _issuer: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] - _jwks_uri: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] _jwk_client: PyJWKClient # Constructor @@ -49,14 +49,16 @@ def __init__(self, **kwargs) -> None: # type: ignore Args: **kwargs: Additional keyword arguments to initialize the SessionService. These keyword arguments should include values for the attributes defined in the class, - such as 'issuer', '_jwks_uri' and 'last_session_token_validation_result', + such as 'issuer', 'jwks_uri', 'last_session_token_validation_result', + 'cache_keys',cache_jwk_set and 'session_token_cookie_length'. Raises: Any errors raised during the initialization process. + """ super().__init__(**kwargs) self._jwk_client = PyJWKClient( - uri=self._jwks_uri, + uri=self.jwks_uri, lifespan=DEFAULT_SESSION_TOKEN_LENGTH, ) @@ -140,7 +142,7 @@ def _set_issuer_mismatch_error(self, token_issuer: str) -> None: Args: token_issuer (str): Token issuer. """ - self.last_session_token_validation_result = f"Mismatch in issuer (configured: {self._issuer}, JWT: {token_issuer})" + self.last_session_token_validation_result = f"Mismatch in issuer (configured: {self.issuer}, JWT: {token_issuer})" def _set_validation_error(self, error: Exception) -> None: """Set validation error. @@ -164,8 +166,7 @@ def _validate_issuer(self, token_issuer: str, session_token: str) -> None: """ if not token_issuer: raise TokenValidationException( - error_type=ValidationErrorType.CODE_JWT_ISSUER_EMPTY, - message=f"Issuer is empty. Session token: {session_token}" + error_type=ValidationErrorType.CODE_JWT_ISSUER_EMPTY, message=f"Issuer is empty. Session token: {session_token}" ) # Check for old Frontend API (without .cloud.) @@ -179,8 +180,8 @@ def _validate_issuer(self, token_issuer: str, session_token: str) -> None: return # Check against the configured issuer (e.g., a custom domain or CNAME) - if token_issuer != self._issuer: + if token_issuer != self.issuer: raise TokenValidationException( error_type=ValidationErrorType.CODE_JWT_ISSUER_MISSMATCH, - message=f"Issuer mismatch (configured via FrontendAPI: '{self._issuer}', JWT issuer: '{token_issuer}')", + message=f"Issuer mismatch (configured via FrontendAPI: '{self.issuer}', JWT issuer: '{token_issuer}')", ) diff --git a/tests/unit/test_session_service.py b/tests/unit/test_session_service.py index 135d829..a41f85d 100644 --- a/tests/unit/test_session_service.py +++ b/tests/unit/test_session_service.py @@ -276,4 +276,4 @@ def test_set_cname_expect_issuer_changed(self): sdk = CorbadoSDK(config=config) sessions: SessionService = sdk.sessions - self.assertEqual("https://" + test_cname, sessions._issuer) + self.assertEqual("https://" + test_cname, sessions.issuer) From 5be424d7f8a050ce26c921ec0cdc67a88e55c949 Mon Sep 17 00:00:00 2001 From: Aleksandr Balakirev <55588266+alexbalakirev@users.noreply.github.com> Date: Thu, 23 Jan 2025 13:06:41 +0100 Subject: [PATCH 19/30] Enable CI/CD on changes on merge request --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index cf679d2..688090e 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -9,7 +9,7 @@ on: - v[0-9]+.[0-9]+.[0-9]+* pull_request: branches: [ "main","development"] - types: [opened, reopened] + types: [opened, reopened, edited] jobs: test-and-lint: From 612f4c71aa65d2c27961255621b0f79ab4ce97fd Mon Sep 17 00:00:00 2001 From: Aleksandr Balakirev <55588266+alexbalakirev@users.noreply.github.com> Date: Thu, 23 Jan 2025 13:10:30 +0100 Subject: [PATCH 20/30] Increase maximum line length on linting --- .flake8 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index 7e56a97..2961070 100644 --- a/.flake8 +++ b/.flake8 @@ -5,7 +5,7 @@ extend-ignore = D100, DC100 exclude = .git,__pycache__,docs/source/conf.py,old,build,dist,src/corbado_python_sdk/generated,env,.idea,.vscode,scripts,__init__.py,.venv,.tox,setup.*,venv_name -max-line-length = 130 +max-line-length = 150 per-file-ignores = #exclude some chekcs in test code From 8c7fcbe8886f0af13dcea4ec76d094800bcd754c Mon Sep 17 00:00:00 2001 From: Aleksandr Balakirev <55588266+alexbalakirev@users.noreply.github.com> Date: Thu, 23 Jan 2025 13:13:34 +0100 Subject: [PATCH 21/30] Run CI/CD on Github Issue naming pattern --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 688090e..292a8f2 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -4,7 +4,7 @@ name: Python package on: push: - branches: [ "main","feature/**","development"] + branches: [ "main","feature/**","development", "[0-9]+-*"] tags: - v[0-9]+.[0-9]+.[0-9]+* pull_request: From db9f2284adf27f74252153eca0f42abbe1d3c557 Mon Sep 17 00:00:00 2001 From: Lukas Kratzel Date: Thu, 23 Jan 2025 12:19:00 +0100 Subject: [PATCH 22/30] satisfy mypy --- src/corbado_python_sdk/exceptions/server_exception.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/corbado_python_sdk/exceptions/server_exception.py b/src/corbado_python_sdk/exceptions/server_exception.py index 008b7bf..3952650 100644 --- a/src/corbado_python_sdk/exceptions/server_exception.py +++ b/src/corbado_python_sdk/exceptions/server_exception.py @@ -19,7 +19,7 @@ def __init__(self, e: ApiException) -> None: StandardException: If response body is not a string. """ - __body = e.body # type: ignore + __body = e.body if not isinstance(__body, str): raise StandardException("Response body is not a string") __data: Dict[str, Any] = Util.json_decode(__body) From d372b3d496e66086e704160b34862acf8c42d10b Mon Sep 17 00:00:00 2001 From: Lukas Kratzel Date: Thu, 23 Jan 2025 13:46:21 +0100 Subject: [PATCH 23/30] update generated client and openapi specs --- local/backend_api_public_v2.yml | 1901 +++++++++++++++++ scripts/backend_api_public_v2.yml | 391 +++- scripts/common.yml | 16 +- scripts/generate-openapi.sh | 0 src/corbado_python_sdk/generated/__init__.py | 14 + .../generated/api/__init__.py | 2 + .../generated/api/passkeys_api.py | 276 +++ .../generated/api/password_managers_api.py | 300 +++ .../generated/api/sessions_api.py | 474 ++-- .../generated/api/webhook_endpoints_api.py | 823 +++++++ .../generated/api_client.py | 6 +- .../generated/configuration.py | 190 +- .../generated/exceptions.py | 17 + .../generated/models/__init__.py | 12 + .../generated/models/aaguid_details.py | 94 + .../generated/models/auth_event.py | 6 +- .../generated/models/challenge.py | 6 +- .../generated/models/challenge_create_req.py | 6 +- .../generated/models/client_information.py | 8 +- .../generated/models/connect_token_data.py | 30 +- .../connect_token_data_passkey_login.py | 88 + .../generated/models/connect_token_type.py | 1 + .../generated/models/credential.py | 16 +- .../generated/models/decision_insights.py | 93 + .../generated/models/decision_tag.py | 1 + .../generated/models/detection_insights.py | 102 + .../models/error_rsp_all_of_error.py | 2 +- .../generated/models/long_session.py | 8 +- .../models/passkey_append_start_rsp.py | 29 +- .../generated/models/passkey_challenge.py | 4 +- .../generated/models/passkey_data.py | 10 +- .../generated/models/passkey_event.py | 6 +- .../models/passkey_event_create_req.py | 6 +- .../generated/models/passkey_event_type.py | 4 + .../models/passkey_login_finish_req.py | 10 +- .../models/passkey_login_finish_rsp.py | 10 +- .../models/passkey_login_start_rsp.py | 29 +- .../models/passkey_mediation_finish_req.py | 10 +- .../models/passkey_mediation_finish_rsp.py | 10 +- .../models/passkey_post_login_req.py | 88 + .../models/passkey_post_login_rsp.py | 88 + .../generated/models/password_manager.py | 102 + .../generated/models/password_manager_list.py | 96 + .../generated/models/request_data.py | 4 +- .../generated/models/webhook_endpoint.py | 103 + .../models/webhook_endpoint_create_req.py | 93 + .../generated/models/webhook_endpoint_list.py | 96 + .../generated/models/webhook_event_type.py | 39 + src/corbado_python_sdk/generated/rest.py | 2 +- .../implementation/identifier_service.py | 5 +- 50 files changed, 5435 insertions(+), 292 deletions(-) create mode 100644 local/backend_api_public_v2.yml mode change 100644 => 100755 scripts/generate-openapi.sh create mode 100644 src/corbado_python_sdk/generated/api/password_managers_api.py create mode 100644 src/corbado_python_sdk/generated/api/webhook_endpoints_api.py create mode 100644 src/corbado_python_sdk/generated/models/aaguid_details.py create mode 100644 src/corbado_python_sdk/generated/models/connect_token_data_passkey_login.py create mode 100644 src/corbado_python_sdk/generated/models/decision_insights.py create mode 100644 src/corbado_python_sdk/generated/models/detection_insights.py create mode 100644 src/corbado_python_sdk/generated/models/passkey_post_login_req.py create mode 100644 src/corbado_python_sdk/generated/models/passkey_post_login_rsp.py create mode 100644 src/corbado_python_sdk/generated/models/password_manager.py create mode 100644 src/corbado_python_sdk/generated/models/password_manager_list.py create mode 100644 src/corbado_python_sdk/generated/models/webhook_endpoint.py create mode 100644 src/corbado_python_sdk/generated/models/webhook_endpoint_create_req.py create mode 100644 src/corbado_python_sdk/generated/models/webhook_endpoint_list.py create mode 100644 src/corbado_python_sdk/generated/models/webhook_event_type.py diff --git a/local/backend_api_public_v2.yml b/local/backend_api_public_v2.yml new file mode 100644 index 0000000..eca7ac3 --- /dev/null +++ b/local/backend_api_public_v2.yml @@ -0,0 +1,1901 @@ +openapi: 3.0.3 + +################################################################### +# General # +################################################################### +info: + version: 2.0.0 + title: Corbado Backend API + description: | + + # Introduction + This documentation gives an overview of all Corbado Backend API calls to implement passwordless authentication with Passkeys. + + contact: + name: Corbado team + email: support@corbado.com + url: https://www.corbado.com + +servers: + - url: https://backendapi.corbado.io/v2 + +tags: + - name: Users + description: All API calls to manage users + - name: Sessions + description: All API calls to manage long and short sessions + - name: Challenges + description: All API calls to manage challenges + - name: Identifiers + description: All API calls to manage login identifiers + - name: Passkeys + description: All API calls for passkey flows + - name: AuthEvents + description: All API calls to manage authentication events + - name: PasskeyEvents + description: All API calls to manage passkey events + - name: ProjectConfig + description: All API calls to manage project configurations + - name: ConnectTokens + description: All API calls to manage connect tokens (they are used for Corbado Connect) + - name: PasskeyChallenges + description: All API calls to manage passkey challenges + +paths: + /users: + post: + description: Creates a new user + operationId: UserCreate + tags: + - Users + security: + - basicAuth: [ ] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/userCreateReq' + responses: + '200': + description: User has been created + content: + application/json: + schema: + $ref: '#/components/schemas/user' + default: + $ref: '#/components/responses/error' + + /users/{userID}: + get: + description: Returns a user + operationId: UserGet + tags: + - Users + security: + - basicAuth: [ ] + parameters: + - $ref: '#/components/parameters/userID' + responses: + '200': + description: User has been returned + content: + application/json: + schema: + $ref: '#/components/schemas/user' + default: + $ref: '#/components/responses/error' + patch: + description: Updates a user + operationId: UserUpdate + tags: + - Users + security: + - basicAuth: [ ] + parameters: + - $ref: '#/components/parameters/userID' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/userUpdateReq' + responses: + '200': + description: User has been updated + content: + application/json: + schema: + $ref: '#/components/schemas/user' + default: + $ref: '#/components/responses/error' + delete: + description: Deletes a user + operationId: UserDelete + tags: + - Users + security: + - basicAuth: [ ] + parameters: + - $ref: '#/components/parameters/userID' + responses: + '200': + $ref: '#/components/responses/200' + default: + $ref: '#/components/responses/error' + + /users/{userID}/longSessions: + post: + description: Create a new long session + operationId: LongSessionCreate + tags: + - Sessions + security: + - basicAuth: [ ] + parameters: + - $ref: '#/components/parameters/userID' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/longSessionCreateReq' + responses: + '200': + description: Long session has been created + content: + application/json: + schema: + $ref: '#/components/schemas/longSession' + default: + $ref: '#/components/responses/error' + + /users/{userID}/longSessions/{longSessionID}: + get: + description: Retrieves a long session by ID and user ID + operationId: UserLongSessionGet + tags: + - Sessions + security: + - basicAuth: [ ] + parameters: + - $ref: '#/components/parameters/userID' + - $ref: '#/components/parameters/longSessionID' + responses: + '200': + description: Long session has been returned + content: + application/json: + schema: + $ref: '#/components/schemas/longSession' + default: + $ref: '#/components/responses/error' + patch: + description: Updates long session status + operationId: LongSessionUpdate + tags: + - Sessions + security: + - basicAuth: [ ] + parameters: + - $ref: '#/components/parameters/userID' + - $ref: '#/components/parameters/longSessionID' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/longSessionUpdateReq' + responses: + '200': + description: Long session has been updated + content: + application/json: + schema: + $ref: '#/components/schemas/longSession' + default: + $ref: '#/components/responses/error' + + /users/{userID}/longSessions/{longSessionID}/shortSessions: + post: + description: Create a new short session + operationId: ShortSessionCreate + tags: + - Sessions + security: + - basicAuth: [ ] + parameters: + - $ref: '#/components/parameters/userID' + - $ref: '#/components/parameters/longSessionID' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/shortSessionCreateReq' + responses: + '200': + description: Short session has been created + content: + application/json: + schema: + $ref: '#/components/schemas/shortSession' + default: + $ref: '#/components/responses/error' + + /users/{userID}/challenges: + post: + description: Create a new challenge to verify a login identifier + operationId: ChallengeCreate + tags: + - Challenges + security: + - basicAuth: [ ] + parameters: + - $ref: '#/components/parameters/userID' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/challengeCreateReq' + responses: + '200': + description: Challenge has been created + content: + application/json: + schema: + $ref: '#/components/schemas/challenge' + default: + $ref: '#/components/responses/error' + + /users/{userID}/challenges/{challengeID}: + patch: + description: Updates a challenge (e.g. from pending to completed) + operationId: ChallengeUpdate + tags: + - Challenges + security: + - basicAuth: [ ] + parameters: + - $ref: '#/components/parameters/userID' + - $ref: '#/components/parameters/challengeID' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/challengeUpdateReq' + responses: + '200': + description: Challenge has been updated + content: + application/json: + schema: + $ref: '#/components/schemas/challenge' + default: + $ref: '#/components/responses/error' + + /users/{userID}/identifiers: + post: + description: Create a new login identifier + operationId: IdentifierCreate + tags: + - Identifiers + security: + - basicAuth: [ ] + parameters: + - $ref: '#/components/parameters/userID' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/identifierCreateReq' + responses: + '200': + description: Identifier has been created + content: + application/json: + schema: + $ref: '#/components/schemas/identifier' + default: + $ref: '#/components/responses/error' + + /users/{userID}/identifiers/{identifierID}: + delete: + description: Delete an existing login identifier + operationId: IdentifierDelete + tags: + - Identifiers + security: + - basicAuth: [ ] + parameters: + - $ref: '#/components/parameters/userID' + - $ref: '#/components/parameters/identifierID' + responses: + '200': + $ref: '#/components/responses/200' + default: + $ref: '#/components/responses/error' + patch: + description: Updates a login identifier (e.g. from pending to verified) + operationId: IdentifierUpdate + tags: + - Identifiers + security: + - basicAuth: [ ] + parameters: + - $ref: '#/components/parameters/userID' + - $ref: '#/components/parameters/identifierID' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/identifierUpdateReq' + responses: + '200': + description: Identifier has been updated + content: + application/json: + schema: + $ref: '#/components/schemas/identifier' + default: + $ref: '#/components/responses/error' + + /users/{userID}/socialAccounts: + get: + description: Returns a list of social accounts + operationId: UserSocialAccountList + tags: + - Users + security: + - basicAuth: [ ] + parameters: + - $ref: '#/components/parameters/userID' + - $ref: 'common.yml#/components/parameters/sort' + - $ref: 'common.yml#/components/parameters/filter' + - $ref: 'common.yml#/components/parameters/page' + - $ref: 'common.yml#/components/parameters/pageSize' + responses: + '200': + description: List of social accounts + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/socialAccount' + default: + $ref: '#/components/responses/error' + post: + description: Creates a new social account + operationId: SocialAccountCreate + tags: + - Users + security: + - basicAuth: [ ] + parameters: + - $ref: '#/components/parameters/userID' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/socialAccountCreateReq' + responses: + '200': + description: Social account has been created + content: + application/json: + schema: + $ref: '#/components/schemas/socialAccount' + default: + $ref: '#/components/responses/error' + + /users/{userID}/credentials: + get: + description: Returns a list of credentials (passkeys) + operationId: CredentialList + tags: + - Users + security: + - basicAuth: [ ] + parameters: + - $ref: '#/components/parameters/userID' + - $ref: 'common.yml#/components/parameters/sort' + - $ref: 'common.yml#/components/parameters/filter' + - $ref: 'common.yml#/components/parameters/page' + - $ref: 'common.yml#/components/parameters/pageSize' + responses: + '200': + description: List of credentials (passkeys) + content: + application/json: + schema: + $ref: '#/components/schemas/credentialList' + default: + $ref: '#/components/responses/error' + + /users/{userID}/credentials/{credentialID}: + delete: + description: Deletes an existing credential (passkey) + operationId: CredentialDelete + tags: + - Users + security: + - basicAuth: [ ] + parameters: + - $ref: '#/components/parameters/userID' + - $ref: '#/components/parameters/credentialID' + responses: + '200': + $ref: '#/components/responses/200' + default: + $ref: '#/components/responses/error' + + /users/{userID}/authEvents: + post: + description: Create a new authentication event for a user + operationId: AuthEventCreate + tags: + - AuthEvents + security: + - basicAuth: [ ] + parameters: + - $ref: '#/components/parameters/userID' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/authEventCreateReq' + responses: + '200': + description: Auth event has been created + content: + application/json: + schema: + $ref: '#/components/schemas/authEvent' + default: + $ref: '#/components/responses/error' + + /users/{userID}/passkeyEvents: + post: + description: Create a new passkey event for a user + operationId: PasskeyEventCreate + tags: + - PasskeyEvents + security: + - basicAuth: [ ] + parameters: + - $ref: '#/components/parameters/userID' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/passkeyEventCreateReq' + responses: + '200': + description: Passkey event has been created + content: + application/json: + schema: + $ref: '#/components/schemas/passkeyEvent' + default: + $ref: '#/components/responses/error' + get: + description: Returns a list of matching passkey events + operationId: PasskeyEventList + tags: + - PasskeyEvents + security: + - basicAuth: [ ] + parameters: + - $ref: 'common.yml#/components/parameters/sort' + - $ref: 'common.yml#/components/parameters/filter' + - $ref: 'common.yml#/components/parameters/page' + - $ref: 'common.yml#/components/parameters/pageSize' + - $ref: '#/components/parameters/userID' + responses: + '200': + description: List of all matching passkey events + content: + application/json: + schema: + $ref: '#/components/schemas/passkeyEventList' + default: + $ref: '#/components/responses/error' + + /users/{userID}/passkeyEvents/{passkeyEventID}: + delete: + description: Deletes an existing passkey event + operationId: PasskeyEventDelete + tags: + - PasskeyEvents + security: + - basicAuth: [ ] + parameters: + - $ref: '#/components/parameters/userID' + - $ref: '#/components/parameters/passkeyEventID' + responses: + '200': + $ref: '#/components/responses/200' + default: + $ref: '#/components/responses/error' + + /users/{userID}/passkeyChallenges: + get: + description: Returns a list of matching passkey challenges + operationId: PasskeyChallengeList + tags: + - PasskeyChallenges + security: + - basicAuth: [ ] + parameters: + - $ref: 'common.yml#/components/parameters/sort' + - $ref: 'common.yml#/components/parameters/filter' + - $ref: 'common.yml#/components/parameters/page' + - $ref: 'common.yml#/components/parameters/pageSize' + - $ref: '#/components/parameters/userID' + responses: + '200': + description: List of all matching passkey challenges + content: + application/json: + schema: + $ref: '#/components/schemas/passkeyChallengeList' + default: + $ref: '#/components/responses/error' + + /users/{userID}/passkeyChallenges/{passkeyChallengeID}: + patch: + description: Updates a passkey challenge + operationId: PasskeyChallengeUpdate + tags: + - PasskeyChallenges + security: + - basicAuth: [ ] + parameters: + - $ref: '#/components/parameters/userID' + - $ref: '#/components/parameters/passkeyChallengeID' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/passkeyChallengeUpdateReq' + responses: + '200': + description: Passkey challenge has been updated + content: + application/json: + schema: + $ref: '#/components/schemas/passkeyChallenge' + default: + $ref: '#/components/responses/error' + + /longSessions/{longSessionID}: + get: + description: Retrieves a long session by ID + operationId: LongSessionGet + tags: + - Sessions + security: + - basicAuth: [ ] + parameters: + - $ref: '#/components/parameters/longSessionID' + responses: + '200': + description: Long session has been returned + content: + application/json: + schema: + $ref: '#/components/schemas/longSession' + default: + $ref: '#/components/responses/error' + + /identifiers: + get: + description: Returns a list of matching identifiers + operationId: IdentifierList + tags: + - Identifiers + security: + - basicAuth: [ ] + parameters: + - $ref: 'common.yml#/components/parameters/sort' + - $ref: 'common.yml#/components/parameters/filter' + - $ref: 'common.yml#/components/parameters/page' + - $ref: 'common.yml#/components/parameters/pageSize' + responses: + '200': + description: List of all matching identifiers + content: + application/json: + schema: + $ref: '#/components/schemas/identifierList' + default: + $ref: '#/components/responses/error' + + /socialAccounts: + get: + description: Returns a list of social accounts + operationId: SocialAccountList + tags: + - Users + security: + - basicAuth: [ ] + parameters: + - $ref: 'common.yml#/components/parameters/sort' + - $ref: 'common.yml#/components/parameters/filter' + - $ref: 'common.yml#/components/parameters/page' + - $ref: 'common.yml#/components/parameters/pageSize' + responses: + '200': + description: List of social accounts + content: + application/json: + schema: + $ref: '#/components/schemas/socialAccountList' + default: + $ref: '#/components/responses/error' + + /projectConfig/cname: + put: + description: Update project config CNAME and generates new SSL certificate + operationId: ProjectConfigUpdateCNAME + tags: + - ProjectConfig + security: + - basicAuth: [ ] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/projectConfigUpdateCnameReq' + responses: + '200': + $ref: '#/components/responses/200' + default: + $ref: '#/components/responses/error' + + /connectTokens: + post: + description: Create a new connect token + operationId: ConnectTokenCreate + tags: + - ConnectTokens + security: + - basicAuth: [ ] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/connectTokenCreateReq' + responses: + '200': + description: Connect token has been created + content: + application/json: + schema: + $ref: '#/components/schemas/connectToken' + default: + $ref: '#/components/responses/error' + get: + description: Returns a list of matching append tokens + operationId: ConnectTokenList + tags: + - ConnectTokens + security: + - basicAuth: [ ] + parameters: + - $ref: 'common.yml#/components/parameters/sort' + - $ref: 'common.yml#/components/parameters/filter' + - $ref: 'common.yml#/components/parameters/page' + - $ref: 'common.yml#/components/parameters/pageSize' + responses: + '200': + description: List of all matching append tokens + content: + application/json: + schema: + $ref: '#/components/schemas/connectTokenList' + default: + $ref: '#/components/responses/error' + + /connectTokens/{connectTokenID}: + patch: + description: Updates an existing append token + operationId: ConnectTokenUpdate + tags: + - ConnectTokens + security: + - basicAuth: [ ] + parameters: + - $ref: '#/components/parameters/connectTokenID' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/connectTokenUpdateReq' + responses: + '200': + $ref: '#/components/responses/200' + default: + $ref: '#/components/responses/error' + delete: + description: Deletes an existing append token + operationId: ConnectTokenDelete + tags: + - ConnectTokens + security: + - basicAuth: [ ] + parameters: + - $ref: '#/components/parameters/connectTokenID' + responses: + '200': + $ref: '#/components/responses/200' + default: + $ref: '#/components/responses/error' + + ################################################################### + # Outliers (non-rest calls) # + ################################################################### + + /passkey/append/start: + post: + description: Starts a challenge for creating a new passkey + operationId: PasskeyAppendStart + tags: + - Passkeys + security: + - basicAuth: [ ] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/passkeyAppendStartReq' + responses: + '200': + description: Passkey append challenge has been created + content: + application/json: + schema: + $ref: '#/components/schemas/passkeyAppendStartRsp' + default: + $ref: '#/components/responses/error' + + /passkey/append/finish: + post: + description: Completes a challenge for creating a new passkey + operationId: PasskeyAppendFinish + tags: + - Passkeys + security: + - basicAuth: [ ] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/passkeyAppendFinishReq' + responses: + '200': + description: Passkey append succeeded + content: + application/json: + schema: + $ref: '#/components/schemas/passkeyAppendFinishRsp' + default: + $ref: '#/components/responses/error' + + /passkey/login/start: + post: + description: Starts a challenge for an existing passkey + operationId: PasskeyLoginStart + tags: + - Passkeys + security: + - basicAuth: [ ] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/passkeyLoginStartReq' + responses: + '200': + description: Passkey login challenge has been created + content: + application/json: + schema: + $ref: '#/components/schemas/passkeyLoginStartRsp' + default: + $ref: '#/components/responses/error' + + /passkey/login/finish: + post: + description: Completes a challenge for an existing passkey + operationId: PasskeyLoginFinish + tags: + - Passkeys + security: + - basicAuth: [ ] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/passkeyLoginFinishReq' + responses: + '200': + description: Passkey login succeeded + content: + application/json: + schema: + $ref: '#/components/schemas/passkeyLoginFinishRsp' + default: + $ref: '#/components/responses/error' + + /passkey/mediation/start: + post: + description: Starts a challenge for an existing passkey (Conditional UI) + operationId: PasskeyMediationStart + tags: + - Passkeys + security: + - basicAuth: [ ] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/passkeyMediationStartReq' + responses: + '200': + description: Passkey login challenge has been created + content: + application/json: + schema: + $ref: '#/components/schemas/passkeyMediationStartRsp' + default: + $ref: '#/components/responses/error' + + /passkey/mediation/finish: + post: + description: Completes a challenge for an existing passkey (Conditional UI) + operationId: PasskeyMediationFinish + tags: + - Passkeys + security: + - basicAuth: [ ] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/passkeyMediationFinishReq' + responses: + '200': + description: Passkey mediation has been success, thus we can return a userID + content: + application/json: + schema: + $ref: '#/components/schemas/passkeyMediationFinishRsp' + default: + $ref: '#/components/responses/error' + +components: + securitySchemes: + basicAuth: + type: http + scheme: basic + + parameters: + userID: + name: userID + in: path + description: ID of user + required: true + schema: + type: string + + challengeID: + name: challengeID + in: path + description: ID of challenge + required: true + schema: + type: string + + longSessionID: + name: longSessionID + in: path + description: ID of long session + required: true + schema: + type: string + + identifierID: + name: identifierID + in: path + description: ID of login identifier + required: true + schema: + type: string + + credentialID: + name: credentialID + in: path + description: ID of credential + required: true + schema: + type: string + + connectTokenID: + name: connectTokenID + in: path + description: ID of an append token + required: true + schema: + type: string + + passkeyChallengeID: + name: passkeyChallengeID + in: path + description: ID of a passkey challenge + required: true + schema: + type: string + + passkeyEventID: + name: passkeyEventID + in: path + description: ID of a passkey event + required: true + schema: + type: string + + schemas: + ################################################################### + # Request/Response bodies # + ################################################################### + userCreateReq: + type: object + required: + - status + properties: + fullName: + type: string + status: + $ref: '#/components/schemas/userStatus' + explicitWebauthnID: + type: string + description: For connect projects, the webauthnID can be explicitly set for a user + + userUpdateReq: + type: object + properties: + fullName: + type: string + status: + $ref: '#/components/schemas/userStatus' + + longSessionCreateReq: + type: object + required: + - appType + - identifierValue + properties: + appType: + $ref: 'common.yml#/components/schemas/appType' + identifierValue: + type: string + + longSessionUpdateReq: + type: object + required: + - status + properties: + status: + $ref: '#/components/schemas/longSessionStatus' + + shortSessionCreateReq: + type: object + required: + - appType + - issuer + properties: + appType: + $ref: 'common.yml#/components/schemas/appType' + issuer: + type: string + + challengeCreateReq: + type: object + required: + - challengeType + - identifierValue + - clientInformation + properties: + challengeType: + $ref: '#/components/schemas/challengeType' + identifierValue: + type: string + challengeMetadata: + type: object + clientInformation: + $ref: '#/components/schemas/clientInformation' + + challengeUpdateReq: + type: object + required: + - value + properties: + value: + type: string + + identifierCreateReq: + type: object + required: + - identifierType + - identifierValue + - status + properties: + identifierType: + $ref: '#/components/schemas/identifierType' + identifierValue: + type: string + status: + $ref: '#/components/schemas/identifierStatus' + + identifierUpdateReq: + type: object + required: + - status + properties: + status: + $ref: '#/components/schemas/identifierStatus' + + passkeyAppendStartReq: + type: object + required: + - userID + - processID + - username + - clientInformation + - passkeyIntelFlags + properties: + userID: + $ref: 'common.yml#/components/schemas/userID' + processID: + type: string + username: + type: string + clientInformation: + $ref: '#/components/schemas/clientInformation' + passkeyIntelFlags: + $ref: '#/components/schemas/passkeyIntelFlags' + + passkeyAppendStartRsp: + type: object + required: + - appendAllow + - detectionTags + - decisionTag + - attestationOptions + properties: + appendAllow: + type: boolean + detectionTags: + type: array + items: + $ref: '#/components/schemas/detectionTag' + decisionTag: + $ref: '#/components/schemas/decisionTag' + attestationOptions: + type: string + example: '{"publicKey":{"challenge":"2m6...0w9/MgW...KE=","rp":{"name":"demo","id":"localhost"},"user":{"name":"example@mail.com","id":"dXN...zk5"},"pubKeyCredParams":[{"type":"public-key","alg":-7},{"type":"public-key","alg":-35},{"type":"public-key","alg":-36},{"type":"public-key","alg":-257},{"type":"public-key","alg":-258},{"type":"public-key","alg":-259},{"type":"public-key","alg":-37},{"type":"public-key","alg":-38},{"type":"public-key","alg":-39},{"type":"public-key","alg":-8}],"authenticatorSelection":{"authenticatorAttachment":"platform","requireResidentKey":false,"userVerification":"required"},"timeout":60000,"attestation":"none"}}' + + passkeyAppendFinishReq: + type: object + required: + - userID + - processID + - attestationResponse + - clientInformation + properties: + userID: + $ref: 'common.yml#/components/schemas/userID' + processID: + type: string + attestationResponse: + type: string + example: '{"type":"public-key","id":"JM6...J_Q","rawId":"JM6...J_Q","authenticatorAttachment":null,"response":{"clientDataJSON":"eyJ...ZX0","authenticatorData":"SZY...AAQ","signature":"Ni7...YAg","userHandle":"dXN...zk5"},"clientExtensionResults":{}}' + clientInformation: + $ref: '#/components/schemas/clientInformation' + sendNotification: + type: boolean + + passkeyAppendFinishRsp: + type: object + required: + - passkeyData + properties: + passkeyData: + $ref: '#/components/schemas/passkeyData' + + passkeyLoginStartReq: + type: object + required: + - userID + - clientInformation + - crossDeviceAuthenticationStrategy + - processID + properties: + userID: + $ref: 'common.yml#/components/schemas/userID' + clientInformation: + $ref: '#/components/schemas/clientInformation' + crossDeviceAuthenticationStrategy: + $ref: '#/components/schemas/crossDeviceAuthenticationStrategy' + processID: + type: string + + passkeyLoginStartRsp: + type: object + required: + - loginAllow + - detectionTags + - decisionTag + - assertionOptions + - isCDACandidate + properties: + loginAllow: + type: boolean + detectionTags: + type: array + items: + $ref: '#/components/schemas/detectionTag' + decisionTag: + $ref: '#/components/schemas/decisionTag' + assertionOptions: + type: string + isCDACandidate: + type: boolean + + passkeyLoginFinishReq: + type: object + required: + - userID + - assertionResponse + - clientInformation + - processID + properties: + userID: + $ref: 'common.yml#/components/schemas/userID' + assertionResponse: + type: string + clientInformation: + $ref: '#/components/schemas/clientInformation' + processID: + type: string + + passkeyLoginFinishRsp: + type: object + required: + - passkeyData + properties: + passkeyData: + $ref: '#/components/schemas/passkeyData' + + passkeyMediationStartReq: + type: object + required: + - clientInformation + properties: + clientInformation: + $ref: '#/components/schemas/clientInformation' + + passkeyMediationStartRsp: + type: object + required: + - loginAllow + - assertionOptions + properties: + loginAllow: + type: boolean + assertionOptions: + type: string + + passkeyMediationFinishReq: + type: object + required: + - assertionResponse + - clientInformation + - processID + properties: + assertionResponse: + type: string + clientInformation: + $ref: '#/components/schemas/clientInformation' + processID: + type: string + + passkeyMediationFinishRsp: + type: object + required: + - passkeyData + properties: + passkeyData: + $ref: '#/components/schemas/passkeyData' + + connectTokenCreateReq: + type: object + required: + - type + - data + properties: + type: + $ref: '#/components/schemas/connectTokenType' + data: + $ref: '#/components/schemas/connectTokenData' + maxLifetimeInSeconds: + type: integer + example: 3600 + + connectTokenUpdateReq: + type: object + required: + - status + properties: + status: + $ref: '#/components/schemas/connectTokenStatus' + + ################################################################### + # Entities # + ################################################################### + + passkeyData: + type: object + required: + - id + - userID + - username + - ceremonyType + - challengeID + properties: + id: + type: string + userID: + type: string + username: + type: string + ceremonyType: + type: string + enum: [ 'local', 'cda', 'security-key' ] + challengeID: + type: string + + user: + type: object + required: + - userID + - status + properties: + userID: + type: string + fullName: + type: string + status: + $ref: '#/components/schemas/userStatus' + explicitWebauthnID: + type: string + + longSession: + type: object + required: + - longSessionID + - userID + - identifierValue + - status + - expires + properties: + longSessionID: + type: string + userID: + type: string + identifierValue: + type: string + status: + $ref: '#/components/schemas/longSessionStatus' + expires: + type: string + + shortSession: + type: object + required: + - value + properties: + value: + type: string + + identifier: + type: object + required: + - identifierID + - type + - value + - status + - userID + properties: + identifierID: + type: string + type: + $ref: '#/components/schemas/identifierType' + value: + type: string + status: + $ref: '#/components/schemas/identifierStatus' + userID: + type: string + + identifierList: + type: object + required: + - identifiers + - paging + properties: + identifiers: + type: array + items: + $ref: '#/components/schemas/identifier' + paging: + $ref: 'common.yml#/components/schemas/paging' + + socialAccountCreateReq: + type: object + required: + - providerType + - identifierValue + - foreignID + - avatarURL + - fullName + properties: + providerType: + $ref: 'common.yml#/components/schemas/socialProviderType' + identifierValue: + type: string + foreignID: + type: string + avatarURL: + type: string + fullName: + type: string + + socialAccount: + type: object + required: + - socialAccountID + - providerType + - identifierValue + - userID + - foreignID + - avatarURL + - fullName + properties: + socialAccountID: + type: string + providerType: + type: string + identifierValue: + type: string + userID: + type: string + foreignID: + type: string + avatarURL: + type: string + fullName: + type: string + + socialAccountList: + type: object + required: + - socialAccounts + - paging + properties: + socialAccounts: + type: array + items: + $ref: '#/components/schemas/socialAccount' + paging: + $ref: 'common.yml#/components/schemas/paging' + + credential: + type: object + required: + - id + - credentialID + - attestationType + - transport + - backupEligible + - backupState + - authenticatorAAGUID + - sourceOS + - sourceBrowser + - lastUsed + - created + - status + properties: + id: + type: string + example: "cre-12345" + credentialID: + type: string + attestationType: + type: string + transport: + type: array + items: + type: string + enum: [ 'usb', 'nfc', 'ble', 'internal', 'hybrid', 'smart-card' ] + backupEligible: + type: boolean + backupState: + type: boolean + authenticatorAAGUID: + type: string + sourceOS: + type: string + sourceBrowser: + type: string + lastUsed: + type: string + description: Timestamp of when the passkey was last used in yyyy-MM-dd'T'HH:mm:ss format + created: + $ref: 'common.yml#/components/schemas/created' + status: + type: string + enum: [ 'pending', 'active' ] + description: "Status" + + credentialList: + type: object + required: + - credentials + - paging + properties: + credentials: + type: array + items: + $ref: '#/components/schemas/credential' + paging: + $ref: 'common.yml#/components/schemas/paging' + + challenge: + type: object + required: + - challengeID + - type + - identifierValue + - value + - status + properties: + challengeID: + type: string + type: + $ref: '#/components/schemas/challengeType' + identifierValue: + type: string + value: + type: string + status: + $ref: '#/components/schemas/challengeStatus' + + authEventCreateReq: + type: object + required: + - username + - eventType + - method + - status + - clientInformation + properties: + username: + type: string + eventType: + $ref: '#/components/schemas/authEventType' + method: + $ref: '#/components/schemas/authEventMethod' + status: + $ref: '#/components/schemas/authEventStatus' + clientInformation: + $ref: '#/components/schemas/clientInformation' + + authEvent: + type: object + required: + - authEventID + - userID + - username + - eventType + - method + - created + - status + properties: + authEventID: + type: string + userID: + $ref: 'common.yml#/components/schemas/userID' + username: + type: string + eventType: + $ref: '#/components/schemas/authEventType' + method: + $ref: '#/components/schemas/authEventMethod' + created: + $ref: 'common.yml#/components/schemas/created' + status: + $ref: '#/components/schemas/authEventStatus' + + passkeyEventCreateReq: + type: object + required: + - eventType + properties: + eventType: + $ref: '#/components/schemas/passkeyEventType' + expires: + type: integer + processID: + type: string + clientEnvID: + type: string + credentialID: + type: string + + passkeyEvent: + type: object + required: + - passkeyEventID + - userID + - eventType + - created + properties: + passkeyEventID: + type: string + userID: + $ref: 'common.yml#/components/schemas/userID' + eventType: + $ref: '#/components/schemas/passkeyEventType' + clientEnvID: + type: string + processID: + type: string + credentialID: + type: string + expires: + type: integer + created: + $ref: 'common.yml#/components/schemas/created' + + passkeyEventList: + type: object + required: + - passkeyEvents + - paging + properties: + passkeyEvents: + type: array + items: + $ref: '#/components/schemas/passkeyEvent' + paging: + $ref: 'common.yml#/components/schemas/paging' + + projectConfigUpdateCnameReq: + type: object + required: + - cname + properties: + cname: + type: string + + detectionTag: + type: object + required: + - category + - name + properties: + category: + type: string + enum: [ 'support', 'clientEnv', 'history', 'passkey' ] + name: + type: string + + decisionTag: + type: string + enum: + - env-no-pk-support + - user-no-pks + - user-login-blacklisted + - user-security-key + - user-positive-env-history + - user-negative-env-history + - env-blacklisted + - user-platform-pk-high-confidence + - user-cross-platform-pk-high-confidence + - user-env-no-pks + - default-deny + - passkey-list-initiated-process + - user-append-blacklisted + - process-pk-login-sk-completed + - process-pk-login-platform-completed + - process-pk-login-not-offered + - process-pk-login-incomplete + - process-pk-login-cross-platform-completed + + clientInformation: + type: object + required: + - remoteAddress + - userAgent + - userVerifyingPlatformAuthenticatorAvailable + properties: + remoteAddress: + description: Client's IP address + type: string + example: '::ffff:172.18.0.1' + userAgent: + description: Client's user agent + type: string + example: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36' + clientEnvHandle: + description: Client's environment handle + type: string + javascriptFingerprint: + description: Client's fingerprint + type: string + javaScriptHighEntropy: + $ref: '#/components/schemas/javaScriptHighEntropy' + bluetoothAvailable: + description: Client's Bluetooth availability + type: boolean + passwordManagerAvailable: + description: Client's password manager availability + type: boolean + userVerifyingPlatformAuthenticatorAvailable: + type: boolean + + passkeyIntelFlags: + type: object + required: + - forcePasskeyAppend + properties: + forcePasskeyAppend: + type: boolean + + javaScriptHighEntropy: + type: object + required: + - platform + - platformVersion + - mobile + properties: + platform: + type: string + platformVersion: + type: string + mobile: + type: boolean + + passkeyChallengeUpdateReq: + type: object + required: + - status + properties: + status: + $ref: '#/components/schemas/passkeyChallengeStatus' + + passkeyChallengeList: + type: object + required: + - passkeyChallenges + - paging + properties: + passkeyChallenges: + type: array + items: + $ref: '#/components/schemas/passkeyChallenge' + paging: + $ref: 'common.yml#/components/schemas/paging' + + passkeyChallenge: + type: object + required: + - challengeID + - type + - value + - status + - created + - expires + properties: + challengeID: + type: string + type: + $ref: '#/components/schemas/passkeyChallengeType' + value: + type: string + status: + $ref: '#/components/schemas/passkeyChallengeStatus' + created: + type: integer + format: int64 + expires: + type: integer + format: int64 + + passkeyChallengeType: + type: string + enum: [ 'register', 'authenticate' ] + + passkeyChallengeStatus: + type: string + enum: [ 'pending', 'completed', 'consumed' ] + + userStatus: + type: string + enum: [ 'pending', 'active', 'disabled' ] + + longSessionStatus: + type: string + enum: [ 'active', 'logged_out', 'expired', 'inactivity_reached', 'revoked' ] + + challengeType: + type: string + enum: [ 'email_otp', 'email_link', 'sms_otp' ] + + challengeStatus: + type: string + enum: [ 'pending', 'completed', 'expired' ] + + identifierType: + type: string + enum: [ 'email', 'phone', 'username' ] + + identifierStatus: + type: string + enum: [ 'pending', 'primary', 'verified' ] + + crossDeviceAuthenticationStrategy: + type: string + enum: [ 'standard', 'minimize', 'maximize' ] + + connectTokenStatus: + type: string + enum: [ 'initial', 'consumed' ] + + connectTokenType: + type: string + enum: [ 'passkey-append', 'passkey-delete', 'passkey-list' ] + + authEventMethod: + type: string + enum: [ 'password', 'email_otp', 'email_link', 'phone_otp', 'passkey', 'social_github', 'social_google', 'social_microsoft' ] + + authEventType: + type: string + enum: [ 'sign_up', 'login', 'new_passkey_added' ] + + authEventStatus: + type: string + enum: [ 'success', 'failure' ] + + passkeyEventType: + type: string + enum: + - user-login-blacklisted + - login-explicit-abort + - login-error + - login-one-tap-switch + - user-append-after-cross-platform-blacklisted + - user-append-after-login-error-blacklisted + + connectToken: + type: object + required: + - id + - tokenType + - data + - connectTokenStatus + - expires + properties: + id: + type: string + tokenType: + $ref: '#/components/schemas/connectTokenType' + data: + $ref: '#/components/schemas/connectTokenData' + connectTokenStatus: + $ref: '#/components/schemas/connectTokenStatus' + secret: + type: string + expires: + type: integer + + connectTokenDataPasskeyAppend: + type: object + required: + - displayName + - identifier + properties: + displayName: + type: string + identifier: + type: string + + connectTokenDataPasskeyDelete: + type: object + required: + - identifier + properties: + identifier: + type: string + + connectTokenDataPasskeyList: + type: object + required: + - identifier + properties: + identifier: + type: string + + connectTokenData: + type: object + oneOf: + - $ref: '#/components/schemas/connectTokenDataPasskeyAppend' + - $ref: '#/components/schemas/connectTokenDataPasskeyDelete' + - $ref: '#/components/schemas/connectTokenDataPasskeyList' + + connectTokenList: + type: object + required: + - connectTokens + - paging + properties: + connectTokens: + type: array + items: + $ref: '#/components/schemas/connectToken' + paging: + $ref: 'common.yml#/components/schemas/paging' + + responses: + ################################################################### + # Responses: Error # + ################################################################### + error: + description: Error + content: + application/json: + schema: + $ref: 'common.yml#/components/schemas/errorRsp' + '200': + description: Operation succeeded + content: + application/json: + schema: + $ref: 'common.yml#/components/schemas/genericRsp' + +################################################################### +# Security # +################################################################### +security: + - basicAuth: [ ] diff --git a/scripts/backend_api_public_v2.yml b/scripts/backend_api_public_v2.yml index eca7ac3..2c337be 100644 --- a/scripts/backend_api_public_v2.yml +++ b/scripts/backend_api_public_v2.yml @@ -40,6 +40,10 @@ tags: description: All API calls to manage connect tokens (they are used for Corbado Connect) - name: PasskeyChallenges description: All API calls to manage passkey challenges + - name: WebhookEndpoints + description: All API calls to manage webhook endpoints + - name: PasswordManagers + description: All API calls to manage password managers paths: /users: @@ -577,6 +581,26 @@ paths: default: $ref: '#/components/responses/error' + /users/{userID}/passwordManagers: + get: + description: Returns a list of password managers + operationId: PasswordManagerList + tags: + - PasswordManagers + security: + - basicAuth: [ ] + parameters: + - $ref: '#/components/parameters/userID' + responses: + '200': + description: List of all matching password managers + content: + application/json: + schema: + $ref: '#/components/schemas/passwordManagerList' + default: + $ref: '#/components/responses/error' + /longSessions/{longSessionID}: get: description: Retrieves a long session by ID @@ -744,6 +768,63 @@ paths: default: $ref: '#/components/responses/error' + /webhookEndpoints: + get: + description: Returns a list of webhook endpoints + operationId: WebhookEndpointList + tags: + - WebhookEndpoints + security: + - basicAuth: [ ] + responses: + '200': + description: List of webhook endpoints + content: + application/json: + schema: + $ref: '#/components/schemas/webhookEndpointList' + default: + $ref: '#/components/responses/error' + post: + description: Creates a new webhook endpoint + operationId: WebhookEndpointCreate + tags: + - WebhookEndpoints + security: + - basicAuth: [ ] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/webhookEndpointCreateReq' + responses: + '200': + description: Webhook endpoint has been created + content: + application/json: + schema: + $ref: '#/components/schemas/webhookEndpoint' + default: + $ref: '#/components/responses/error' + + /webhookEndpoints/{webhookEndpointID}: + delete: + description: Deletes an existing webhook endpoint + operationId: WebhookEndpointDelete + tags: + - WebhookEndpoints + security: + - basicAuth: [ ] + parameters: + - $ref: '#/components/parameters/webhookEndpointID' + responses: + '200': + $ref: '#/components/responses/200' + default: + $ref: '#/components/responses/error' + + ################################################################### # Outliers (non-rest calls) # ################################################################### @@ -844,6 +925,30 @@ paths: default: $ref: '#/components/responses/error' + /passkey/postLogin: + post: + description: Explicitly runs the post-login action + operationId: PasskeyPostLogin + tags: + - Passkeys + security: + - basicAuth: [ ] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/passkeyPostLoginReq' + responses: + '200': + description: Post Passkey Login succeeded + content: + application/json: + schema: + $ref: '#/components/schemas/passkeyPostLoginRsp' + default: + $ref: '#/components/responses/error' + /passkey/mediation/start: post: description: Starts a challenge for an existing passkey (Conditional UI) @@ -961,7 +1066,15 @@ components: description: ID of a passkey event required: true schema: - type: string + type: string + + webhookEndpointID: + name: webhookEndpointID + in: path + description: ID of a webhook endpoint + required: true + schema: + type: string schemas: ################################################################### @@ -1031,6 +1144,8 @@ components: type: string challengeMetadata: type: object + lifetimeSeconds: + type: integer clientInformation: $ref: '#/components/schemas/clientInformation' @@ -1088,21 +1203,19 @@ components: type: object required: - appendAllow - - detectionTags - - decisionTag - attestationOptions + - detectionInsights + - decisionInsights properties: appendAllow: type: boolean - detectionTags: - type: array - items: - $ref: '#/components/schemas/detectionTag' - decisionTag: - $ref: '#/components/schemas/decisionTag' attestationOptions: type: string example: '{"publicKey":{"challenge":"2m6...0w9/MgW...KE=","rp":{"name":"demo","id":"localhost"},"user":{"name":"example@mail.com","id":"dXN...zk5"},"pubKeyCredParams":[{"type":"public-key","alg":-7},{"type":"public-key","alg":-35},{"type":"public-key","alg":-36},{"type":"public-key","alg":-257},{"type":"public-key","alg":-258},{"type":"public-key","alg":-259},{"type":"public-key","alg":-37},{"type":"public-key","alg":-38},{"type":"public-key","alg":-39},{"type":"public-key","alg":-8}],"authenticatorSelection":{"authenticatorAttachment":"platform","requireResidentKey":false,"userVerification":"required"},"timeout":60000,"attestation":"none"}}' + detectionInsights: + $ref: '#/components/schemas/detectionInsights' + decisionInsights: + $ref: '#/components/schemas/decisionInsights' passkeyAppendFinishReq: type: object @@ -1153,23 +1266,18 @@ components: type: object required: - loginAllow - - detectionTags - - decisionTag - assertionOptions - - isCDACandidate + - detectionInsights + - decisionInsights properties: loginAllow: type: boolean - detectionTags: - type: array - items: - $ref: '#/components/schemas/detectionTag' - decisionTag: - $ref: '#/components/schemas/decisionTag' assertionOptions: type: string - isCDACandidate: - type: boolean + detectionInsights: + $ref: '#/components/schemas/detectionInsights' + decisionInsights: + $ref: '#/components/schemas/decisionInsights' passkeyLoginFinishReq: type: object @@ -1187,6 +1295,8 @@ components: $ref: '#/components/schemas/clientInformation' processID: type: string + signPasskeyData: + type: boolean passkeyLoginFinishRsp: type: object @@ -1195,6 +1305,24 @@ components: properties: passkeyData: $ref: '#/components/schemas/passkeyData' + signedPasskeyData: + type: string + + passkeyPostLoginReq: + type: object + required: + - signedPasskeyData + properties: + signedPasskeyData: + type: string + + passkeyPostLoginRsp: + type: object + required: + - session + properties: + session: + type: string passkeyMediationStartReq: type: object @@ -1228,6 +1356,8 @@ components: $ref: '#/components/schemas/clientInformation' processID: type: string + signPasskeyData: + type: boolean passkeyMediationFinishRsp: type: object @@ -1236,6 +1366,8 @@ components: properties: passkeyData: $ref: '#/components/schemas/passkeyData' + signedPasskeyData: + type: string connectTokenCreateReq: type: object @@ -1259,6 +1391,22 @@ components: status: $ref: '#/components/schemas/connectTokenStatus' + webhookEndpointCreateReq: + type: object + required: + - url + - subscribedEvents + - customHeaders + properties: + url: + type: string + subscribedEvents: + type: array + items: + $ref: '#/components/schemas/webhookEventType' + customHeaders: + type: object + ################################################################### # Entities # ################################################################### @@ -1271,6 +1419,7 @@ components: - username - ceremonyType - challengeID + - aaguidDetails properties: id: type: string @@ -1283,6 +1432,8 @@ components: enum: [ 'local', 'cda', 'security-key' ] challengeID: type: string + aaguidDetails: + $ref: '#/components/schemas/aaguidDetails' user: type: object @@ -1307,6 +1458,7 @@ components: - identifierValue - status - expires + - expiresMs properties: longSessionID: type: string @@ -1318,6 +1470,9 @@ components: $ref: '#/components/schemas/longSessionStatus' expires: type: string + expiresMs: + type: integer + format: int64 shortSession: type: object @@ -1429,10 +1584,13 @@ components: - backupEligible - backupState - authenticatorAAGUID + - aaguidDetails - sourceOS - sourceBrowser - lastUsed + - lastUsedMs - created + - createdMs - status properties: id: @@ -1460,12 +1618,37 @@ components: lastUsed: type: string description: Timestamp of when the passkey was last used in yyyy-MM-dd'T'HH:mm:ss format + lastUsedMs: + type: integer + format: int64 created: $ref: 'common.yml#/components/schemas/created' + createdMs: + type: integer + format: int64 status: type: string enum: [ 'pending', 'active' ] description: "Status" + aaguidDetails: + $ref: '#/components/schemas/aaguidDetails' + + aaguidDetails: + type: object + required: + - aaguid + - name + - iconLight + - iconDark + properties: + aaguid: + type: string + name: + type: string + iconLight: + type: string + iconDark: + type: string credentialList: type: object @@ -1487,6 +1670,7 @@ components: - type - identifierValue - value + - expires - status properties: challengeID: @@ -1497,6 +1681,9 @@ components: type: string value: type: string + expires: + type: integer + format: int64 status: $ref: '#/components/schemas/challengeStatus' @@ -1529,6 +1716,7 @@ components: - eventType - method - created + - createdMs - status properties: authEventID: @@ -1543,6 +1731,9 @@ components: $ref: '#/components/schemas/authEventMethod' created: $ref: 'common.yml#/components/schemas/created' + createdMs: + type: integer + format: int64 status: $ref: '#/components/schemas/authEventStatus' @@ -1561,6 +1752,8 @@ components: type: string credentialID: type: string + challenge: + type: string passkeyEvent: type: object @@ -1569,6 +1762,7 @@ components: - userID - eventType - created + - createdMs properties: passkeyEventID: type: string @@ -1586,6 +1780,9 @@ components: type: integer created: $ref: 'common.yml#/components/schemas/created' + createdMs: + type: integer + format: int64 passkeyEventList: type: object @@ -1641,6 +1838,7 @@ components: - process-pk-login-not-offered - process-pk-login-incomplete - process-pk-login-cross-platform-completed + - device-local-platform-passkey-experiment clientInformation: type: object @@ -1648,6 +1846,7 @@ components: - remoteAddress - userAgent - userVerifyingPlatformAuthenticatorAvailable + - conditionalMediationAvailable properties: remoteAddress: description: Client's IP address @@ -1673,6 +1872,10 @@ components: type: boolean userVerifyingPlatformAuthenticatorAvailable: type: boolean + conditionalMediationAvailable: + type: boolean + privateMode: + type: boolean passkeyIntelFlags: type: object @@ -1725,6 +1928,7 @@ components: - value - status - created + - createdMs - expires properties: challengeID: @@ -1738,6 +1942,9 @@ components: created: type: integer format: int64 + createdMs: + type: integer + format: int64 expires: type: integer format: int64 @@ -1784,7 +1991,7 @@ components: connectTokenType: type: string - enum: [ 'passkey-append', 'passkey-delete', 'passkey-list' ] + enum: [ 'passkey-append', 'passkey-delete', 'passkey-list', 'passkey-login' ] authEventMethod: type: string @@ -1798,15 +2005,23 @@ components: type: string enum: [ 'success', 'failure' ] + webhookEventType: + type: string + enum: [ 'passkey-login.completed', 'passkey.created', 'passkey.deleted' ] + passkeyEventType: type: string enum: - user-login-blacklisted - login-explicit-abort - login-error + - login-error-untyped - login-one-tap-switch - user-append-after-cross-platform-blacklisted - user-append-after-login-error-blacklisted + - append-credential-exists + - append-explicit-abort + - append-error connectToken: type: object @@ -1857,12 +2072,21 @@ components: identifier: type: string + connectTokenDataPasskeyLogin: + type: object + required: + - identifier + properties: + identifier: + type: string + connectTokenData: type: object oneOf: - $ref: '#/components/schemas/connectTokenDataPasskeyAppend' - $ref: '#/components/schemas/connectTokenDataPasskeyDelete' - $ref: '#/components/schemas/connectTokenDataPasskeyList' + - $ref: '#/components/schemas/connectTokenDataPasskeyLogin' connectTokenList: type: object @@ -1877,6 +2101,131 @@ components: paging: $ref: 'common.yml#/components/schemas/paging' + webhookEndpointList: + type: object + required: + - webhookEndpoints + properties: + webhookEndpoints: + type: array + items: + $ref: '#/components/schemas/webhookEndpoint' + + webhookEndpoint: + type: object + required: + - id + - url + - customHeaders + - subscribedEvents + - created + - createdMs + - updated + - updatedMs + properties: + id: + type: string + url: + type: string + customHeaders: + type: object + subscribedEvents: + type: array + items: + $ref: '#/components/schemas/webhookEventType' + created: + type: string + createdMs: + type: integer + format: int64 + updated: + type: string + updatedMs: + type: integer + format: int64 + + detectionInsights: + type: object + required: + - tags + - credentialIds + - clientEnvIds + - isCDACandidate + - passwordManagerIds + properties: + tags: + type: array + items: + $ref: '#/components/schemas/detectionTag' + credentialIds: + type: array + items: + type: string + clientEnvIds: + type: array + items: + type: string + passwordManagerIds: + type: array + items: + type: string + + decisionInsights: + type: object + required: + - tag + - isCDACandidate + - experiments + properties: + tag: + $ref: '#/components/schemas/decisionTag' + isCDACandidate: + type: boolean + experiments: + type: array + items: + type: string + + passwordManagerList: + type: object + required: + - passwordManagers + properties: + passwordManagers: + type: array + items: + $ref: '#/components/schemas/passwordManager' + + passwordManager: + type: object + required: + - id + - userID + - clientEnvID + - credentialID + - aaguid + - status + - score + - createdMs + properties: + id: + type: string + userID: + type: string + clientEnvID: + type: string + credentialID: + type: string + aaguid: + type: string + status: + type: string + score: + type: integer + createdMs: + type: integer + format: int64 + responses: ################################################################### # Responses: Error # diff --git a/scripts/common.yml b/scripts/common.yml index 79dc241..827d6ff 100644 --- a/scripts/common.yml +++ b/scripts/common.yml @@ -89,7 +89,8 @@ components: filterEmail: summary: Filter for one email address value: - - name:eq:mail@exammple.com + - identifierType:eq:email + - identifierValue:eq:mail@example.com filterTimepoint: summary: timePoint after 20/07/2021 value: @@ -373,10 +374,15 @@ components: type: string loginIdentifierType: - description: Login Identifier type + description: Login Identifier type (deprecated) type: string enum: [ 'email', 'phone_number', 'custom' ] + identifierType: + description: Login Identifier type + type: string + enum: [ 'email', 'phone', 'username' ] + appType: description: Application type type: string @@ -392,7 +398,6 @@ components: type: object required: - requestID - - link properties: requestID: $ref: '#/components/schemas/requestID' @@ -409,7 +414,7 @@ components: - useAsLoginIdentifier properties: type: - $ref: '#/components/schemas/loginIdentifierType' + $ref: '#/components/schemas/identifierType' enforceVerification: type: string enum: [ none, signup, at_first_login ] @@ -476,6 +481,8 @@ components: $ref: '#/components/schemas/loginIdentifierConfig' p25: $ref: '#/components/schemas/socialProviderType' + p26: + $ref: '#/components/schemas/identifierType' genericRsp: type: object @@ -513,7 +520,6 @@ components: type: object required: - type - - links properties: type: description: Type of error diff --git a/scripts/generate-openapi.sh b/scripts/generate-openapi.sh old mode 100644 new mode 100755 diff --git a/src/corbado_python_sdk/generated/__init__.py b/src/corbado_python_sdk/generated/__init__.py index 5bf4cd4..f57af42 100644 --- a/src/corbado_python_sdk/generated/__init__.py +++ b/src/corbado_python_sdk/generated/__init__.py @@ -25,9 +25,11 @@ from corbado_python_sdk.generated.api.passkey_challenges_api import PasskeyChallengesApi from corbado_python_sdk.generated.api.passkey_events_api import PasskeyEventsApi from corbado_python_sdk.generated.api.passkeys_api import PasskeysApi +from corbado_python_sdk.generated.api.password_managers_api import PasswordManagersApi from corbado_python_sdk.generated.api.project_config_api import ProjectConfigApi from corbado_python_sdk.generated.api.sessions_api import SessionsApi from corbado_python_sdk.generated.api.users_api import UsersApi +from corbado_python_sdk.generated.api.webhook_endpoints_api import WebhookEndpointsApi # import ApiClient from corbado_python_sdk.generated.api_response import ApiResponse @@ -41,6 +43,7 @@ from corbado_python_sdk.generated.exceptions import ApiException # import models into sdk package +from corbado_python_sdk.generated.models.aaguid_details import AaguidDetails from corbado_python_sdk.generated.models.app_type import AppType from corbado_python_sdk.generated.models.auth_event import AuthEvent from corbado_python_sdk.generated.models.auth_event_create_req import AuthEventCreateReq @@ -59,6 +62,7 @@ from corbado_python_sdk.generated.models.connect_token_data_passkey_append import ConnectTokenDataPasskeyAppend from corbado_python_sdk.generated.models.connect_token_data_passkey_delete import ConnectTokenDataPasskeyDelete from corbado_python_sdk.generated.models.connect_token_data_passkey_list import ConnectTokenDataPasskeyList +from corbado_python_sdk.generated.models.connect_token_data_passkey_login import ConnectTokenDataPasskeyLogin from corbado_python_sdk.generated.models.connect_token_list import ConnectTokenList from corbado_python_sdk.generated.models.connect_token_status import ConnectTokenStatus from corbado_python_sdk.generated.models.connect_token_type import ConnectTokenType @@ -66,7 +70,9 @@ from corbado_python_sdk.generated.models.credential import Credential from corbado_python_sdk.generated.models.credential_list import CredentialList from corbado_python_sdk.generated.models.cross_device_authentication_strategy import CrossDeviceAuthenticationStrategy +from corbado_python_sdk.generated.models.decision_insights import DecisionInsights from corbado_python_sdk.generated.models.decision_tag import DecisionTag +from corbado_python_sdk.generated.models.detection_insights import DetectionInsights from corbado_python_sdk.generated.models.detection_tag import DetectionTag from corbado_python_sdk.generated.models.error_rsp import ErrorRsp from corbado_python_sdk.generated.models.error_rsp_all_of_error import ErrorRspAllOfError @@ -107,6 +113,10 @@ from corbado_python_sdk.generated.models.passkey_mediation_finish_rsp import PasskeyMediationFinishRsp from corbado_python_sdk.generated.models.passkey_mediation_start_req import PasskeyMediationStartReq from corbado_python_sdk.generated.models.passkey_mediation_start_rsp import PasskeyMediationStartRsp +from corbado_python_sdk.generated.models.passkey_post_login_req import PasskeyPostLoginReq +from corbado_python_sdk.generated.models.passkey_post_login_rsp import PasskeyPostLoginRsp +from corbado_python_sdk.generated.models.password_manager import PasswordManager +from corbado_python_sdk.generated.models.password_manager_list import PasswordManagerList from corbado_python_sdk.generated.models.project_config_update_cname_req import ProjectConfigUpdateCnameReq from corbado_python_sdk.generated.models.request_data import RequestData from corbado_python_sdk.generated.models.short_session import ShortSession @@ -119,3 +129,7 @@ from corbado_python_sdk.generated.models.user_create_req import UserCreateReq from corbado_python_sdk.generated.models.user_status import UserStatus from corbado_python_sdk.generated.models.user_update_req import UserUpdateReq +from corbado_python_sdk.generated.models.webhook_endpoint import WebhookEndpoint +from corbado_python_sdk.generated.models.webhook_endpoint_create_req import WebhookEndpointCreateReq +from corbado_python_sdk.generated.models.webhook_endpoint_list import WebhookEndpointList +from corbado_python_sdk.generated.models.webhook_event_type import WebhookEventType diff --git a/src/corbado_python_sdk/generated/api/__init__.py b/src/corbado_python_sdk/generated/api/__init__.py index 1898baf..432cae8 100644 --- a/src/corbado_python_sdk/generated/api/__init__.py +++ b/src/corbado_python_sdk/generated/api/__init__.py @@ -8,7 +8,9 @@ from corbado_python_sdk.generated.api.passkey_challenges_api import PasskeyChallengesApi from corbado_python_sdk.generated.api.passkey_events_api import PasskeyEventsApi from corbado_python_sdk.generated.api.passkeys_api import PasskeysApi +from corbado_python_sdk.generated.api.password_managers_api import PasswordManagersApi from corbado_python_sdk.generated.api.project_config_api import ProjectConfigApi from corbado_python_sdk.generated.api.sessions_api import SessionsApi from corbado_python_sdk.generated.api.users_api import UsersApi +from corbado_python_sdk.generated.api.webhook_endpoints_api import WebhookEndpointsApi diff --git a/src/corbado_python_sdk/generated/api/passkeys_api.py b/src/corbado_python_sdk/generated/api/passkeys_api.py index 47a6d89..aba46aa 100644 --- a/src/corbado_python_sdk/generated/api/passkeys_api.py +++ b/src/corbado_python_sdk/generated/api/passkeys_api.py @@ -29,6 +29,8 @@ from corbado_python_sdk.generated.models.passkey_mediation_finish_rsp import PasskeyMediationFinishRsp from corbado_python_sdk.generated.models.passkey_mediation_start_req import PasskeyMediationStartReq from corbado_python_sdk.generated.models.passkey_mediation_start_rsp import PasskeyMediationStartRsp +from corbado_python_sdk.generated.models.passkey_post_login_req import PasskeyPostLoginReq +from corbado_python_sdk.generated.models.passkey_post_login_rsp import PasskeyPostLoginRsp from corbado_python_sdk.generated.api_client import ApiClient, RequestSerialized from corbado_python_sdk.generated.api_response import ApiResponse @@ -1690,3 +1692,277 @@ def _passkey_mediation_start_serialize( ) + + + @validate_call + def passkey_post_login( + self, + passkey_post_login_req: PasskeyPostLoginReq, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> PasskeyPostLoginRsp: + """passkey_post_login + + Explicitly runs the post-login action + + :param passkey_post_login_req: (required) + :type passkey_post_login_req: PasskeyPostLoginReq + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._passkey_post_login_serialize( + passkey_post_login_req=passkey_post_login_req, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PasskeyPostLoginRsp", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def passkey_post_login_with_http_info( + self, + passkey_post_login_req: PasskeyPostLoginReq, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[PasskeyPostLoginRsp]: + """passkey_post_login + + Explicitly runs the post-login action + + :param passkey_post_login_req: (required) + :type passkey_post_login_req: PasskeyPostLoginReq + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._passkey_post_login_serialize( + passkey_post_login_req=passkey_post_login_req, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PasskeyPostLoginRsp", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def passkey_post_login_without_preload_content( + self, + passkey_post_login_req: PasskeyPostLoginReq, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """passkey_post_login + + Explicitly runs the post-login action + + :param passkey_post_login_req: (required) + :type passkey_post_login_req: PasskeyPostLoginReq + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._passkey_post_login_serialize( + passkey_post_login_req=passkey_post_login_req, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PasskeyPostLoginRsp", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _passkey_post_login_serialize( + self, + passkey_post_login_req, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if passkey_post_login_req is not None: + _body_params = passkey_post_login_req + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'basicAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/passkey/postLogin', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + diff --git a/src/corbado_python_sdk/generated/api/password_managers_api.py b/src/corbado_python_sdk/generated/api/password_managers_api.py new file mode 100644 index 0000000..722cb23 --- /dev/null +++ b/src/corbado_python_sdk/generated/api/password_managers_api.py @@ -0,0 +1,300 @@ +# coding: utf-8 + +""" + Corbado Backend API + + # Introduction This documentation gives an overview of all Corbado Backend API calls to implement passwordless authentication with Passkeys. + + The version of the OpenAPI document: 2.0.0 + Contact: support@corbado.com + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + +import warnings +from pydantic import validate_call, Field, StrictFloat, StrictStr, StrictInt +from typing import Any, Dict, List, Optional, Tuple, Union +from typing_extensions import Annotated + +from pydantic import Field, StrictStr +from typing_extensions import Annotated +from corbado_python_sdk.generated.models.password_manager_list import PasswordManagerList + +from corbado_python_sdk.generated.api_client import ApiClient, RequestSerialized +from corbado_python_sdk.generated.api_response import ApiResponse +from corbado_python_sdk.generated.rest import RESTResponseType + + +class PasswordManagersApi: + """NOTE: This class is auto generated by OpenAPI Generator + Ref: https://openapi-generator.tech + + Do not edit the class manually. + """ + + def __init__(self, api_client=None) -> None: + if api_client is None: + api_client = ApiClient.get_default() + self.api_client = api_client + + + @validate_call + def password_manager_list( + self, + user_id: Annotated[StrictStr, Field(description="ID of user")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> PasswordManagerList: + """password_manager_list + + Returns a list of password managers + + :param user_id: ID of user (required) + :type user_id: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._password_manager_list_serialize( + user_id=user_id, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PasswordManagerList", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def password_manager_list_with_http_info( + self, + user_id: Annotated[StrictStr, Field(description="ID of user")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[PasswordManagerList]: + """password_manager_list + + Returns a list of password managers + + :param user_id: ID of user (required) + :type user_id: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._password_manager_list_serialize( + user_id=user_id, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PasswordManagerList", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def password_manager_list_without_preload_content( + self, + user_id: Annotated[StrictStr, Field(description="ID of user")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """password_manager_list + + Returns a list of password managers + + :param user_id: ID of user (required) + :type user_id: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._password_manager_list_serialize( + user_id=user_id, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PasswordManagerList", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _password_manager_list_serialize( + self, + user_id, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if user_id is not None: + _path_params['userID'] = user_id + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'basicAuth' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/users/{userID}/passwordManagers', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + diff --git a/src/corbado_python_sdk/generated/api/sessions_api.py b/src/corbado_python_sdk/generated/api/sessions_api.py index a653894..c093755 100644 --- a/src/corbado_python_sdk/generated/api/sessions_api.py +++ b/src/corbado_python_sdk/generated/api/sessions_api.py @@ -13,24 +13,20 @@ """ # noqa: E501 import warnings +from pydantic import validate_call, Field, StrictFloat, StrictStr, StrictInt from typing import Any, Dict, List, Optional, Tuple, Union +from typing_extensions import Annotated -from pydantic import Field, StrictFloat, StrictInt, StrictStr, validate_call +from pydantic import Field, StrictStr from typing_extensions import Annotated +from corbado_python_sdk.generated.models.long_session import LongSession +from corbado_python_sdk.generated.models.long_session_create_req import LongSessionCreateReq +from corbado_python_sdk.generated.models.long_session_update_req import LongSessionUpdateReq +from corbado_python_sdk.generated.models.short_session import ShortSession +from corbado_python_sdk.generated.models.short_session_create_req import ShortSessionCreateReq from corbado_python_sdk.generated.api_client import ApiClient, RequestSerialized from corbado_python_sdk.generated.api_response import ApiResponse -from corbado_python_sdk.generated.models.long_session import LongSession -from corbado_python_sdk.generated.models.long_session_create_req import ( - LongSessionCreateReq, -) -from corbado_python_sdk.generated.models.long_session_update_req import ( - LongSessionUpdateReq, -) -from corbado_python_sdk.generated.models.short_session import ShortSession -from corbado_python_sdk.generated.models.short_session_create_req import ( - ShortSessionCreateReq, -) from corbado_python_sdk.generated.rest import RESTResponseType @@ -46,6 +42,7 @@ def __init__(self, api_client=None) -> None: api_client = ApiClient.get_default() self.api_client = api_client + @validate_call def long_session_create( self, @@ -54,7 +51,10 @@ def long_session_create( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -89,7 +89,7 @@ def long_session_create( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._long_session_create_serialize( user_id=user_id, @@ -97,19 +97,23 @@ def long_session_create( _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index, + _host_index=_host_index ) _response_types_map: Dict[str, Optional[str]] = { - "200": "LongSession", + '200': "LongSession", } - response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) response_data.read() return self.api_client.response_deserialize( response_data=response_data, response_types_map=_response_types_map, ).data + @validate_call def long_session_create_with_http_info( self, @@ -118,7 +122,10 @@ def long_session_create_with_http_info( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -153,7 +160,7 @@ def long_session_create_with_http_info( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._long_session_create_serialize( user_id=user_id, @@ -161,19 +168,23 @@ def long_session_create_with_http_info( _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index, + _host_index=_host_index ) _response_types_map: Dict[str, Optional[str]] = { - "200": "LongSession", + '200': "LongSession", } - response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) response_data.read() return self.api_client.response_deserialize( response_data=response_data, response_types_map=_response_types_map, ) + @validate_call def long_session_create_without_preload_content( self, @@ -182,7 +193,10 @@ def long_session_create_without_preload_content( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -217,7 +231,7 @@ def long_session_create_without_preload_content( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._long_session_create_serialize( user_id=user_id, @@ -225,15 +239,19 @@ def long_session_create_without_preload_content( _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index, + _host_index=_host_index ) _response_types_map: Dict[str, Optional[str]] = { - "200": "LongSession", + '200': "LongSession", } - response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) return response_data.response + def _long_session_create_serialize( self, user_id, @@ -246,18 +264,21 @@ def _long_session_create_serialize( _host = None - _collection_formats: Dict[str, str] = {} + _collection_formats: Dict[str, str] = { + } _path_params: Dict[str, str] = {} _query_params: List[Tuple[str, str]] = [] _header_params: Dict[str, Optional[str]] = _headers or {} _form_params: List[Tuple[str, str]] = [] - _files: Dict[str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]]] = {} + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} _body_params: Optional[bytes] = None # process the path parameters if user_id is not None: - _path_params["userID"] = user_id + _path_params['userID'] = user_id # process the query parameters # process the header parameters # process the form parameters @@ -265,24 +286,37 @@ def _long_session_create_serialize( if long_session_create_req is not None: _body_params = long_session_create_req + # set the HTTP header `Accept` - if "Accept" not in _header_params: - _header_params["Accept"] = self.api_client.select_header_accept(["application/json"]) + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) # set the HTTP header `Content-Type` if _content_type: - _header_params["Content-Type"] = _content_type + _header_params['Content-Type'] = _content_type else: - _default_content_type = self.api_client.select_header_content_type(["application/json"]) + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) if _default_content_type is not None: - _header_params["Content-Type"] = _default_content_type + _header_params['Content-Type'] = _default_content_type # authentication setting - _auth_settings: List[str] = ["basicAuth"] + _auth_settings: List[str] = [ + 'basicAuth' + ] return self.api_client.param_serialize( - method="POST", - resource_path="/users/{userID}/longSessions", + method='POST', + resource_path='/users/{userID}/longSessions', path_params=_path_params, query_params=_query_params, header_params=_header_params, @@ -292,9 +326,12 @@ def _long_session_create_serialize( auth_settings=_auth_settings, collection_formats=_collection_formats, _host=_host, - _request_auth=_request_auth, + _request_auth=_request_auth ) + + + @validate_call def long_session_get( self, @@ -302,7 +339,10 @@ def long_session_get( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -335,26 +375,30 @@ def long_session_get( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._long_session_get_serialize( long_session_id=long_session_id, _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index, + _host_index=_host_index ) _response_types_map: Dict[str, Optional[str]] = { - "200": "LongSession", + '200': "LongSession", } - response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) response_data.read() return self.api_client.response_deserialize( response_data=response_data, response_types_map=_response_types_map, ).data + @validate_call def long_session_get_with_http_info( self, @@ -362,7 +406,10 @@ def long_session_get_with_http_info( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -395,26 +442,30 @@ def long_session_get_with_http_info( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._long_session_get_serialize( long_session_id=long_session_id, _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index, + _host_index=_host_index ) _response_types_map: Dict[str, Optional[str]] = { - "200": "LongSession", + '200': "LongSession", } - response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) response_data.read() return self.api_client.response_deserialize( response_data=response_data, response_types_map=_response_types_map, ) + @validate_call def long_session_get_without_preload_content( self, @@ -422,7 +473,10 @@ def long_session_get_without_preload_content( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -455,22 +509,26 @@ def long_session_get_without_preload_content( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._long_session_get_serialize( long_session_id=long_session_id, _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index, + _host_index=_host_index ) _response_types_map: Dict[str, Optional[str]] = { - "200": "LongSession", + '200': "LongSession", } - response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) return response_data.response + def _long_session_get_serialize( self, long_session_id, @@ -482,33 +540,44 @@ def _long_session_get_serialize( _host = None - _collection_formats: Dict[str, str] = {} + _collection_formats: Dict[str, str] = { + } _path_params: Dict[str, str] = {} _query_params: List[Tuple[str, str]] = [] _header_params: Dict[str, Optional[str]] = _headers or {} _form_params: List[Tuple[str, str]] = [] - _files: Dict[str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]]] = {} + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} _body_params: Optional[bytes] = None # process the path parameters if long_session_id is not None: - _path_params["longSessionID"] = long_session_id + _path_params['longSessionID'] = long_session_id # process the query parameters # process the header parameters # process the form parameters # process the body parameter + # set the HTTP header `Accept` - if "Accept" not in _header_params: - _header_params["Accept"] = self.api_client.select_header_accept(["application/json"]) + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + # authentication setting - _auth_settings: List[str] = ["basicAuth"] + _auth_settings: List[str] = [ + 'basicAuth' + ] return self.api_client.param_serialize( - method="GET", - resource_path="/longSessions/{longSessionID}", + method='GET', + resource_path='/longSessions/{longSessionID}', path_params=_path_params, query_params=_query_params, header_params=_header_params, @@ -518,9 +587,12 @@ def _long_session_get_serialize( auth_settings=_auth_settings, collection_formats=_collection_formats, _host=_host, - _request_auth=_request_auth, + _request_auth=_request_auth ) + + + @validate_call def long_session_update( self, @@ -530,7 +602,10 @@ def long_session_update( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -567,7 +642,7 @@ def long_session_update( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._long_session_update_serialize( user_id=user_id, @@ -576,19 +651,23 @@ def long_session_update( _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index, + _host_index=_host_index ) _response_types_map: Dict[str, Optional[str]] = { - "200": "LongSession", + '200': "LongSession", } - response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) response_data.read() return self.api_client.response_deserialize( response_data=response_data, response_types_map=_response_types_map, ).data + @validate_call def long_session_update_with_http_info( self, @@ -598,7 +677,10 @@ def long_session_update_with_http_info( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -635,7 +717,7 @@ def long_session_update_with_http_info( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._long_session_update_serialize( user_id=user_id, @@ -644,19 +726,23 @@ def long_session_update_with_http_info( _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index, + _host_index=_host_index ) _response_types_map: Dict[str, Optional[str]] = { - "200": "LongSession", + '200': "LongSession", } - response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) response_data.read() return self.api_client.response_deserialize( response_data=response_data, response_types_map=_response_types_map, ) + @validate_call def long_session_update_without_preload_content( self, @@ -666,7 +752,10 @@ def long_session_update_without_preload_content( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -703,7 +792,7 @@ def long_session_update_without_preload_content( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._long_session_update_serialize( user_id=user_id, @@ -712,15 +801,19 @@ def long_session_update_without_preload_content( _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index, + _host_index=_host_index ) _response_types_map: Dict[str, Optional[str]] = { - "200": "LongSession", + '200': "LongSession", } - response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) return response_data.response + def _long_session_update_serialize( self, user_id, @@ -734,20 +827,23 @@ def _long_session_update_serialize( _host = None - _collection_formats: Dict[str, str] = {} + _collection_formats: Dict[str, str] = { + } _path_params: Dict[str, str] = {} _query_params: List[Tuple[str, str]] = [] _header_params: Dict[str, Optional[str]] = _headers or {} _form_params: List[Tuple[str, str]] = [] - _files: Dict[str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]]] = {} + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} _body_params: Optional[bytes] = None # process the path parameters if user_id is not None: - _path_params["userID"] = user_id + _path_params['userID'] = user_id if long_session_id is not None: - _path_params["longSessionID"] = long_session_id + _path_params['longSessionID'] = long_session_id # process the query parameters # process the header parameters # process the form parameters @@ -755,24 +851,37 @@ def _long_session_update_serialize( if long_session_update_req is not None: _body_params = long_session_update_req + # set the HTTP header `Accept` - if "Accept" not in _header_params: - _header_params["Accept"] = self.api_client.select_header_accept(["application/json"]) + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) # set the HTTP header `Content-Type` if _content_type: - _header_params["Content-Type"] = _content_type + _header_params['Content-Type'] = _content_type else: - _default_content_type = self.api_client.select_header_content_type(["application/json"]) + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) if _default_content_type is not None: - _header_params["Content-Type"] = _default_content_type + _header_params['Content-Type'] = _default_content_type # authentication setting - _auth_settings: List[str] = ["basicAuth"] + _auth_settings: List[str] = [ + 'basicAuth' + ] return self.api_client.param_serialize( - method="PATCH", - resource_path="/users/{userID}/longSessions/{longSessionID}", + method='PATCH', + resource_path='/users/{userID}/longSessions/{longSessionID}', path_params=_path_params, query_params=_query_params, header_params=_header_params, @@ -782,9 +891,12 @@ def _long_session_update_serialize( auth_settings=_auth_settings, collection_formats=_collection_formats, _host=_host, - _request_auth=_request_auth, + _request_auth=_request_auth ) + + + @validate_call def short_session_create( self, @@ -794,7 +906,10 @@ def short_session_create( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -831,7 +946,7 @@ def short_session_create( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._short_session_create_serialize( user_id=user_id, @@ -840,19 +955,23 @@ def short_session_create( _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index, + _host_index=_host_index ) _response_types_map: Dict[str, Optional[str]] = { - "200": "ShortSession", + '200': "ShortSession", } - response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) response_data.read() return self.api_client.response_deserialize( response_data=response_data, response_types_map=_response_types_map, ).data + @validate_call def short_session_create_with_http_info( self, @@ -862,7 +981,10 @@ def short_session_create_with_http_info( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -899,7 +1021,7 @@ def short_session_create_with_http_info( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._short_session_create_serialize( user_id=user_id, @@ -908,19 +1030,23 @@ def short_session_create_with_http_info( _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index, + _host_index=_host_index ) _response_types_map: Dict[str, Optional[str]] = { - "200": "ShortSession", + '200': "ShortSession", } - response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) response_data.read() return self.api_client.response_deserialize( response_data=response_data, response_types_map=_response_types_map, ) + @validate_call def short_session_create_without_preload_content( self, @@ -930,7 +1056,10 @@ def short_session_create_without_preload_content( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -967,7 +1096,7 @@ def short_session_create_without_preload_content( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._short_session_create_serialize( user_id=user_id, @@ -976,15 +1105,19 @@ def short_session_create_without_preload_content( _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index, + _host_index=_host_index ) _response_types_map: Dict[str, Optional[str]] = { - "200": "ShortSession", + '200': "ShortSession", } - response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) return response_data.response + def _short_session_create_serialize( self, user_id, @@ -998,20 +1131,23 @@ def _short_session_create_serialize( _host = None - _collection_formats: Dict[str, str] = {} + _collection_formats: Dict[str, str] = { + } _path_params: Dict[str, str] = {} _query_params: List[Tuple[str, str]] = [] _header_params: Dict[str, Optional[str]] = _headers or {} _form_params: List[Tuple[str, str]] = [] - _files: Dict[str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]]] = {} + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} _body_params: Optional[bytes] = None # process the path parameters if user_id is not None: - _path_params["userID"] = user_id + _path_params['userID'] = user_id if long_session_id is not None: - _path_params["longSessionID"] = long_session_id + _path_params['longSessionID'] = long_session_id # process the query parameters # process the header parameters # process the form parameters @@ -1019,24 +1155,37 @@ def _short_session_create_serialize( if short_session_create_req is not None: _body_params = short_session_create_req + # set the HTTP header `Accept` - if "Accept" not in _header_params: - _header_params["Accept"] = self.api_client.select_header_accept(["application/json"]) + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) # set the HTTP header `Content-Type` if _content_type: - _header_params["Content-Type"] = _content_type + _header_params['Content-Type'] = _content_type else: - _default_content_type = self.api_client.select_header_content_type(["application/json"]) + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) if _default_content_type is not None: - _header_params["Content-Type"] = _default_content_type + _header_params['Content-Type'] = _default_content_type # authentication setting - _auth_settings: List[str] = ["basicAuth"] + _auth_settings: List[str] = [ + 'basicAuth' + ] return self.api_client.param_serialize( - method="POST", - resource_path="/users/{userID}/longSessions/{longSessionID}/shortSessions", + method='POST', + resource_path='/users/{userID}/longSessions/{longSessionID}/shortSessions', path_params=_path_params, query_params=_query_params, header_params=_header_params, @@ -1046,9 +1195,12 @@ def _short_session_create_serialize( auth_settings=_auth_settings, collection_formats=_collection_formats, _host=_host, - _request_auth=_request_auth, + _request_auth=_request_auth ) + + + @validate_call def user_long_session_get( self, @@ -1057,7 +1209,10 @@ def user_long_session_get( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -1092,7 +1247,7 @@ def user_long_session_get( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._user_long_session_get_serialize( user_id=user_id, @@ -1100,19 +1255,23 @@ def user_long_session_get( _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index, + _host_index=_host_index ) _response_types_map: Dict[str, Optional[str]] = { - "200": "LongSession", + '200': "LongSession", } - response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) response_data.read() return self.api_client.response_deserialize( response_data=response_data, response_types_map=_response_types_map, ).data + @validate_call def user_long_session_get_with_http_info( self, @@ -1121,7 +1280,10 @@ def user_long_session_get_with_http_info( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -1156,7 +1318,7 @@ def user_long_session_get_with_http_info( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._user_long_session_get_serialize( user_id=user_id, @@ -1164,19 +1326,23 @@ def user_long_session_get_with_http_info( _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index, + _host_index=_host_index ) _response_types_map: Dict[str, Optional[str]] = { - "200": "LongSession", + '200': "LongSession", } - response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) response_data.read() return self.api_client.response_deserialize( response_data=response_data, response_types_map=_response_types_map, ) + @validate_call def user_long_session_get_without_preload_content( self, @@ -1185,7 +1351,10 @@ def user_long_session_get_without_preload_content( _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], - Tuple[Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] ] = None, _request_auth: Optional[Dict[StrictStr, Any]] = None, _content_type: Optional[StrictStr] = None, @@ -1220,7 +1389,7 @@ def user_long_session_get_without_preload_content( in the spec for a single request. :type _host_index: int, optional :return: Returns the result object. - """ # noqa: E501 + """ # noqa: E501 _param = self._user_long_session_get_serialize( user_id=user_id, @@ -1228,15 +1397,19 @@ def user_long_session_get_without_preload_content( _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, - _host_index=_host_index, + _host_index=_host_index ) _response_types_map: Dict[str, Optional[str]] = { - "200": "LongSession", + '200': "LongSession", } - response_data = self.api_client.call_api(*_param, _request_timeout=_request_timeout) + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) return response_data.response + def _user_long_session_get_serialize( self, user_id, @@ -1249,35 +1422,46 @@ def _user_long_session_get_serialize( _host = None - _collection_formats: Dict[str, str] = {} + _collection_formats: Dict[str, str] = { + } _path_params: Dict[str, str] = {} _query_params: List[Tuple[str, str]] = [] _header_params: Dict[str, Optional[str]] = _headers or {} _form_params: List[Tuple[str, str]] = [] - _files: Dict[str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]]] = {} + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} _body_params: Optional[bytes] = None # process the path parameters if user_id is not None: - _path_params["userID"] = user_id + _path_params['userID'] = user_id if long_session_id is not None: - _path_params["longSessionID"] = long_session_id + _path_params['longSessionID'] = long_session_id # process the query parameters # process the header parameters # process the form parameters # process the body parameter + # set the HTTP header `Accept` - if "Accept" not in _header_params: - _header_params["Accept"] = self.api_client.select_header_accept(["application/json"]) + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + # authentication setting - _auth_settings: List[str] = ["basicAuth"] + _auth_settings: List[str] = [ + 'basicAuth' + ] return self.api_client.param_serialize( - method="GET", - resource_path="/users/{userID}/longSessions/{longSessionID}", + method='GET', + resource_path='/users/{userID}/longSessions/{longSessionID}', path_params=_path_params, query_params=_query_params, header_params=_header_params, @@ -1287,5 +1471,7 @@ def _user_long_session_get_serialize( auth_settings=_auth_settings, collection_formats=_collection_formats, _host=_host, - _request_auth=_request_auth, + _request_auth=_request_auth ) + + diff --git a/src/corbado_python_sdk/generated/api/webhook_endpoints_api.py b/src/corbado_python_sdk/generated/api/webhook_endpoints_api.py new file mode 100644 index 0000000..ecbc7f0 --- /dev/null +++ b/src/corbado_python_sdk/generated/api/webhook_endpoints_api.py @@ -0,0 +1,823 @@ +# coding: utf-8 + +""" + Corbado Backend API + + # Introduction This documentation gives an overview of all Corbado Backend API calls to implement passwordless authentication with Passkeys. + + The version of the OpenAPI document: 2.0.0 + Contact: support@corbado.com + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + +import warnings +from pydantic import validate_call, Field, StrictFloat, StrictStr, StrictInt +from typing import Any, Dict, List, Optional, Tuple, Union +from typing_extensions import Annotated + +from pydantic import Field, StrictStr +from typing_extensions import Annotated +from corbado_python_sdk.generated.models.generic_rsp import GenericRsp +from corbado_python_sdk.generated.models.webhook_endpoint import WebhookEndpoint +from corbado_python_sdk.generated.models.webhook_endpoint_create_req import WebhookEndpointCreateReq +from corbado_python_sdk.generated.models.webhook_endpoint_list import WebhookEndpointList + +from corbado_python_sdk.generated.api_client import ApiClient, RequestSerialized +from corbado_python_sdk.generated.api_response import ApiResponse +from corbado_python_sdk.generated.rest import RESTResponseType + + +class WebhookEndpointsApi: + """NOTE: This class is auto generated by OpenAPI Generator + Ref: https://openapi-generator.tech + + Do not edit the class manually. + """ + + def __init__(self, api_client=None) -> None: + if api_client is None: + api_client = ApiClient.get_default() + self.api_client = api_client + + + @validate_call + def webhook_endpoint_create( + self, + webhook_endpoint_create_req: WebhookEndpointCreateReq, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> WebhookEndpoint: + """webhook_endpoint_create + + Creates a new webhook endpoint + + :param webhook_endpoint_create_req: (required) + :type webhook_endpoint_create_req: WebhookEndpointCreateReq + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._webhook_endpoint_create_serialize( + webhook_endpoint_create_req=webhook_endpoint_create_req, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "WebhookEndpoint", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def webhook_endpoint_create_with_http_info( + self, + webhook_endpoint_create_req: WebhookEndpointCreateReq, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[WebhookEndpoint]: + """webhook_endpoint_create + + Creates a new webhook endpoint + + :param webhook_endpoint_create_req: (required) + :type webhook_endpoint_create_req: WebhookEndpointCreateReq + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._webhook_endpoint_create_serialize( + webhook_endpoint_create_req=webhook_endpoint_create_req, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "WebhookEndpoint", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def webhook_endpoint_create_without_preload_content( + self, + webhook_endpoint_create_req: WebhookEndpointCreateReq, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """webhook_endpoint_create + + Creates a new webhook endpoint + + :param webhook_endpoint_create_req: (required) + :type webhook_endpoint_create_req: WebhookEndpointCreateReq + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._webhook_endpoint_create_serialize( + webhook_endpoint_create_req=webhook_endpoint_create_req, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "WebhookEndpoint", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _webhook_endpoint_create_serialize( + self, + webhook_endpoint_create_req, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if webhook_endpoint_create_req is not None: + _body_params = webhook_endpoint_create_req + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'basicAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/webhookEndpoints', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def webhook_endpoint_delete( + self, + webhook_endpoint_id: Annotated[StrictStr, Field(description="ID of a webhook endpoint")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> GenericRsp: + """webhook_endpoint_delete + + Deletes an existing webhook endpoint + + :param webhook_endpoint_id: ID of a webhook endpoint (required) + :type webhook_endpoint_id: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._webhook_endpoint_delete_serialize( + webhook_endpoint_id=webhook_endpoint_id, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "GenericRsp", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def webhook_endpoint_delete_with_http_info( + self, + webhook_endpoint_id: Annotated[StrictStr, Field(description="ID of a webhook endpoint")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[GenericRsp]: + """webhook_endpoint_delete + + Deletes an existing webhook endpoint + + :param webhook_endpoint_id: ID of a webhook endpoint (required) + :type webhook_endpoint_id: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._webhook_endpoint_delete_serialize( + webhook_endpoint_id=webhook_endpoint_id, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "GenericRsp", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def webhook_endpoint_delete_without_preload_content( + self, + webhook_endpoint_id: Annotated[StrictStr, Field(description="ID of a webhook endpoint")], + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """webhook_endpoint_delete + + Deletes an existing webhook endpoint + + :param webhook_endpoint_id: ID of a webhook endpoint (required) + :type webhook_endpoint_id: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._webhook_endpoint_delete_serialize( + webhook_endpoint_id=webhook_endpoint_id, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "GenericRsp", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _webhook_endpoint_delete_serialize( + self, + webhook_endpoint_id, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if webhook_endpoint_id is not None: + _path_params['webhookEndpointID'] = webhook_endpoint_id + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'basicAuth' + ] + + return self.api_client.param_serialize( + method='DELETE', + resource_path='/webhookEndpoints/{webhookEndpointID}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + def webhook_endpoint_list( + self, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> WebhookEndpointList: + """webhook_endpoint_list + + Returns a list of webhook endpoints + + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._webhook_endpoint_list_serialize( + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "WebhookEndpointList", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def webhook_endpoint_list_with_http_info( + self, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[WebhookEndpointList]: + """webhook_endpoint_list + + Returns a list of webhook endpoints + + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._webhook_endpoint_list_serialize( + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "WebhookEndpointList", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def webhook_endpoint_list_without_preload_content( + self, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """webhook_endpoint_list + + Returns a list of webhook endpoints + + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._webhook_endpoint_list_serialize( + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "WebhookEndpointList", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _webhook_endpoint_list_serialize( + self, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'basicAuth' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/webhookEndpoints', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + diff --git a/src/corbado_python_sdk/generated/api_client.py b/src/corbado_python_sdk/generated/api_client.py index 9624bed..fe11a5e 100644 --- a/src/corbado_python_sdk/generated/api_client.py +++ b/src/corbado_python_sdk/generated/api_client.py @@ -405,12 +405,12 @@ def deserialize(self, response_text: str, response_type: str, content_type: Opti data = json.loads(response_text) except ValueError: data = response_text - elif content_type.startswith("application/json"): + elif re.match(r'^application/(json|[\w!#$&.+-^_]+\+json)\s*(;|$)', content_type, re.IGNORECASE): if response_text == "": data = "" else: data = json.loads(response_text) - elif content_type.startswith("text/plain"): + elif re.match(r'^text\/[a-z.+-]+\s*(;|$)', content_type, re.IGNORECASE): data = response_text else: raise ApiException( @@ -518,7 +518,7 @@ def parameters_to_url_query(self, params, collection_formats): if k in collection_formats: collection_format = collection_formats[k] if collection_format == 'multi': - new_params.extend((k, str(value)) for value in v) + new_params.extend((k, quote(str(value))) for value in v) else: if collection_format == 'ssv': delimiter = ' ' diff --git a/src/corbado_python_sdk/generated/configuration.py b/src/corbado_python_sdk/generated/configuration.py index 65a1db8..bf8a711 100644 --- a/src/corbado_python_sdk/generated/configuration.py +++ b/src/corbado_python_sdk/generated/configuration.py @@ -14,14 +14,16 @@ import copy +import http.client as httplib import logging from logging import FileHandler import multiprocessing import sys -from typing import Optional +from typing import Any, ClassVar, Dict, List, Literal, Optional, TypedDict +from typing_extensions import NotRequired, Self + import urllib3 -import http.client as httplib JSON_SCHEMA_VALIDATION_KEYWORDS = { 'multipleOf', 'maximum', 'exclusiveMaximum', @@ -29,6 +31,107 @@ 'minLength', 'pattern', 'maxItems', 'minItems' } +ServerVariablesT = Dict[str, str] + +GenericAuthSetting = TypedDict( + "GenericAuthSetting", + { + "type": str, + "in": str, + "key": str, + "value": str, + }, +) + + +OAuth2AuthSetting = TypedDict( + "OAuth2AuthSetting", + { + "type": Literal["oauth2"], + "in": Literal["header"], + "key": Literal["Authorization"], + "value": str, + }, +) + + +APIKeyAuthSetting = TypedDict( + "APIKeyAuthSetting", + { + "type": Literal["api_key"], + "in": str, + "key": str, + "value": Optional[str], + }, +) + + +BasicAuthSetting = TypedDict( + "BasicAuthSetting", + { + "type": Literal["basic"], + "in": Literal["header"], + "key": Literal["Authorization"], + "value": Optional[str], + }, +) + + +BearerFormatAuthSetting = TypedDict( + "BearerFormatAuthSetting", + { + "type": Literal["bearer"], + "in": Literal["header"], + "format": Literal["JWT"], + "key": Literal["Authorization"], + "value": str, + }, +) + + +BearerAuthSetting = TypedDict( + "BearerAuthSetting", + { + "type": Literal["bearer"], + "in": Literal["header"], + "key": Literal["Authorization"], + "value": str, + }, +) + + +HTTPSignatureAuthSetting = TypedDict( + "HTTPSignatureAuthSetting", + { + "type": Literal["http-signature"], + "in": Literal["header"], + "key": Literal["Authorization"], + "value": None, + }, +) + + +AuthSettings = TypedDict( + "AuthSettings", + { + "basicAuth": BasicAuthSetting, + }, + total=False, +) + + +class HostSettingVariable(TypedDict): + description: str + default_value: str + enum_values: List[str] + + +class HostSetting(TypedDict): + url: str + description: str + variables: NotRequired[Dict[str, HostSettingVariable]] + + class Configuration: """This class contains various settings of the API client. @@ -79,20 +182,26 @@ class Configuration: """ - _default = None - - def __init__(self, host=None, - api_key=None, api_key_prefix=None, - username=None, password=None, - access_token=None, - server_index=None, server_variables=None, - server_operation_index=None, server_operation_variables=None, - ignore_operation_servers=False, - ssl_ca_cert=None, - retries=None, - *, - debug: Optional[bool] = None - ) -> None: + _default: ClassVar[Optional[Self]] = None + + def __init__( + self, + host: Optional[str]=None, + api_key: Optional[Dict[str, str]]=None, + api_key_prefix: Optional[Dict[str, str]]=None, + username: Optional[str]=None, + password: Optional[str]=None, + access_token: Optional[str]=None, + server_index: Optional[int]=None, + server_variables: Optional[ServerVariablesT]=None, + server_operation_index: Optional[Dict[int, int]]=None, + server_operation_variables: Optional[Dict[int, ServerVariablesT]]=None, + ignore_operation_servers: bool=False, + ssl_ca_cert: Optional[str]=None, + retries: Optional[int] = None, + *, + debug: Optional[bool] = None, + ) -> None: """Constructor """ self._base_path = "https://backendapi.corbado.io/v2" if host is None else host @@ -216,7 +325,7 @@ def __init__(self, host=None, """date format """ - def __deepcopy__(self, memo): + def __deepcopy__(self, memo: Dict[int, Any]) -> Self: cls = self.__class__ result = cls.__new__(cls) memo[id(self)] = result @@ -230,11 +339,11 @@ def __deepcopy__(self, memo): result.debug = self.debug return result - def __setattr__(self, name, value): + def __setattr__(self, name: str, value: Any) -> None: object.__setattr__(self, name, value) @classmethod - def set_default(cls, default): + def set_default(cls, default: Optional[Self]) -> None: """Set default instance of configuration. It stores default configuration, which can be @@ -245,7 +354,7 @@ def set_default(cls, default): cls._default = default @classmethod - def get_default_copy(cls): + def get_default_copy(cls) -> Self: """Deprecated. Please use `get_default` instead. Deprecated. Please use `get_default` instead. @@ -255,7 +364,7 @@ def get_default_copy(cls): return cls.get_default() @classmethod - def get_default(cls): + def get_default(cls) -> Self: """Return the default configuration. This method returns newly created, based on default constructor, @@ -265,11 +374,11 @@ def get_default(cls): :return: The configuration object. """ if cls._default is None: - cls._default = Configuration() + cls._default = cls() return cls._default @property - def logger_file(self): + def logger_file(self) -> Optional[str]: """The logger file. If the logger_file is None, then add stream handler and remove file @@ -281,7 +390,7 @@ def logger_file(self): return self.__logger_file @logger_file.setter - def logger_file(self, value): + def logger_file(self, value: Optional[str]) -> None: """The logger file. If the logger_file is None, then add stream handler and remove file @@ -300,7 +409,7 @@ def logger_file(self, value): logger.addHandler(self.logger_file_handler) @property - def debug(self): + def debug(self) -> bool: """Debug status :param value: The debug status, True or False. @@ -309,7 +418,7 @@ def debug(self): return self.__debug @debug.setter - def debug(self, value): + def debug(self, value: bool) -> None: """Debug status :param value: The debug status, True or False. @@ -331,7 +440,7 @@ def debug(self, value): httplib.HTTPConnection.debuglevel = 0 @property - def logger_format(self): + def logger_format(self) -> str: """The logger format. The logger_formatter will be updated when sets logger_format. @@ -342,7 +451,7 @@ def logger_format(self): return self.__logger_format @logger_format.setter - def logger_format(self, value): + def logger_format(self, value: str) -> None: """The logger format. The logger_formatter will be updated when sets logger_format. @@ -353,7 +462,7 @@ def logger_format(self, value): self.__logger_format = value self.logger_formatter = logging.Formatter(self.__logger_format) - def get_api_key_with_prefix(self, identifier, alias=None): + def get_api_key_with_prefix(self, identifier: str, alias: Optional[str]=None) -> Optional[str]: """Gets API key (with prefix if set). :param identifier: The identifier of apiKey. @@ -370,7 +479,9 @@ def get_api_key_with_prefix(self, identifier, alias=None): else: return key - def get_basic_auth_token(self): + return None + + def get_basic_auth_token(self) -> Optional[str]: """Gets HTTP basic authentication header (string). :return: The token for basic HTTP authentication. @@ -385,12 +496,12 @@ def get_basic_auth_token(self): basic_auth=username + ':' + password ).get('authorization') - def auth_settings(self): + def auth_settings(self)-> AuthSettings: """Gets Auth Settings dict for api client. :return: The Auth Settings information dict. """ - auth = {} + auth: AuthSettings = {} if self.username is not None and self.password is not None: auth['basicAuth'] = { 'type': 'basic', @@ -400,7 +511,7 @@ def auth_settings(self): } return auth - def to_debug_report(self): + def to_debug_report(self) -> str: """Gets the essential information for debugging. :return: The report for debugging. @@ -412,7 +523,7 @@ def to_debug_report(self): "SDK Package Version: 1.0.0".\ format(env=sys.platform, pyversion=sys.version) - def get_host_settings(self): + def get_host_settings(self) -> List[HostSetting]: """Gets an array of host settings :return: An array of host settings @@ -424,7 +535,12 @@ def get_host_settings(self): } ] - def get_host_from_settings(self, index, variables=None, servers=None): + def get_host_from_settings( + self, + index: Optional[int], + variables: Optional[ServerVariablesT]=None, + servers: Optional[List[HostSetting]]=None, + ) -> str: """Gets host URL based on the index and variables :param index: array index of the host settings :param variables: hash of variable and the corresponding value @@ -464,12 +580,12 @@ def get_host_from_settings(self, index, variables=None, servers=None): return url @property - def host(self): + def host(self) -> str: """Return generated host.""" return self.get_host_from_settings(self.server_index, variables=self.server_variables) @host.setter - def host(self, value): + def host(self, value: str) -> None: """Fix base path.""" self._base_path = value self.server_index = None diff --git a/src/corbado_python_sdk/generated/exceptions.py b/src/corbado_python_sdk/generated/exceptions.py index 96599dc..c4ab103 100644 --- a/src/corbado_python_sdk/generated/exceptions.py +++ b/src/corbado_python_sdk/generated/exceptions.py @@ -151,6 +151,13 @@ def from_response( if http_resp.status == 404: raise NotFoundException(http_resp=http_resp, body=body, data=data) + # Added new conditions for 409 and 422 + if http_resp.status == 409: + raise ConflictException(http_resp=http_resp, body=body, data=data) + + if http_resp.status == 422: + raise UnprocessableEntityException(http_resp=http_resp, body=body, data=data) + if 500 <= http_resp.status <= 599: raise ServiceException(http_resp=http_resp, body=body, data=data) raise ApiException(http_resp=http_resp, body=body, data=data) @@ -189,6 +196,16 @@ class ServiceException(ApiException): pass +class ConflictException(ApiException): + """Exception for HTTP 409 Conflict.""" + pass + + +class UnprocessableEntityException(ApiException): + """Exception for HTTP 422 Unprocessable Entity.""" + pass + + def render_path(path_to_item): """Returns a string representation of a path""" result = "" diff --git a/src/corbado_python_sdk/generated/models/__init__.py b/src/corbado_python_sdk/generated/models/__init__.py index 5e32aed..aba7041 100644 --- a/src/corbado_python_sdk/generated/models/__init__.py +++ b/src/corbado_python_sdk/generated/models/__init__.py @@ -15,6 +15,7 @@ # import models into model package +from corbado_python_sdk.generated.models.aaguid_details import AaguidDetails from corbado_python_sdk.generated.models.app_type import AppType from corbado_python_sdk.generated.models.auth_event import AuthEvent from corbado_python_sdk.generated.models.auth_event_create_req import AuthEventCreateReq @@ -33,6 +34,7 @@ from corbado_python_sdk.generated.models.connect_token_data_passkey_append import ConnectTokenDataPasskeyAppend from corbado_python_sdk.generated.models.connect_token_data_passkey_delete import ConnectTokenDataPasskeyDelete from corbado_python_sdk.generated.models.connect_token_data_passkey_list import ConnectTokenDataPasskeyList +from corbado_python_sdk.generated.models.connect_token_data_passkey_login import ConnectTokenDataPasskeyLogin from corbado_python_sdk.generated.models.connect_token_list import ConnectTokenList from corbado_python_sdk.generated.models.connect_token_status import ConnectTokenStatus from corbado_python_sdk.generated.models.connect_token_type import ConnectTokenType @@ -40,7 +42,9 @@ from corbado_python_sdk.generated.models.credential import Credential from corbado_python_sdk.generated.models.credential_list import CredentialList from corbado_python_sdk.generated.models.cross_device_authentication_strategy import CrossDeviceAuthenticationStrategy +from corbado_python_sdk.generated.models.decision_insights import DecisionInsights from corbado_python_sdk.generated.models.decision_tag import DecisionTag +from corbado_python_sdk.generated.models.detection_insights import DetectionInsights from corbado_python_sdk.generated.models.detection_tag import DetectionTag from corbado_python_sdk.generated.models.error_rsp import ErrorRsp from corbado_python_sdk.generated.models.error_rsp_all_of_error import ErrorRspAllOfError @@ -81,6 +85,10 @@ from corbado_python_sdk.generated.models.passkey_mediation_finish_rsp import PasskeyMediationFinishRsp from corbado_python_sdk.generated.models.passkey_mediation_start_req import PasskeyMediationStartReq from corbado_python_sdk.generated.models.passkey_mediation_start_rsp import PasskeyMediationStartRsp +from corbado_python_sdk.generated.models.passkey_post_login_req import PasskeyPostLoginReq +from corbado_python_sdk.generated.models.passkey_post_login_rsp import PasskeyPostLoginRsp +from corbado_python_sdk.generated.models.password_manager import PasswordManager +from corbado_python_sdk.generated.models.password_manager_list import PasswordManagerList from corbado_python_sdk.generated.models.project_config_update_cname_req import ProjectConfigUpdateCnameReq from corbado_python_sdk.generated.models.request_data import RequestData from corbado_python_sdk.generated.models.short_session import ShortSession @@ -93,3 +101,7 @@ from corbado_python_sdk.generated.models.user_create_req import UserCreateReq from corbado_python_sdk.generated.models.user_status import UserStatus from corbado_python_sdk.generated.models.user_update_req import UserUpdateReq +from corbado_python_sdk.generated.models.webhook_endpoint import WebhookEndpoint +from corbado_python_sdk.generated.models.webhook_endpoint_create_req import WebhookEndpointCreateReq +from corbado_python_sdk.generated.models.webhook_endpoint_list import WebhookEndpointList +from corbado_python_sdk.generated.models.webhook_event_type import WebhookEventType diff --git a/src/corbado_python_sdk/generated/models/aaguid_details.py b/src/corbado_python_sdk/generated/models/aaguid_details.py new file mode 100644 index 0000000..a53dc39 --- /dev/null +++ b/src/corbado_python_sdk/generated/models/aaguid_details.py @@ -0,0 +1,94 @@ +# coding: utf-8 + +""" + Corbado Backend API + + # Introduction This documentation gives an overview of all Corbado Backend API calls to implement passwordless authentication with Passkeys. + + The version of the OpenAPI document: 2.0.0 + Contact: support@corbado.com + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class AaguidDetails(BaseModel): + """ + AaguidDetails + """ # noqa: E501 + aaguid: StrictStr + name: StrictStr + icon_light: StrictStr = Field(alias="iconLight") + icon_dark: StrictStr = Field(alias="iconDark") + __properties: ClassVar[List[str]] = ["aaguid", "name", "iconLight", "iconDark"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of AaguidDetails from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of AaguidDetails from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "aaguid": obj.get("aaguid"), + "name": obj.get("name"), + "iconLight": obj.get("iconLight"), + "iconDark": obj.get("iconDark") + }) + return _obj + + diff --git a/src/corbado_python_sdk/generated/models/auth_event.py b/src/corbado_python_sdk/generated/models/auth_event.py index a47e3d5..3ffabd3 100644 --- a/src/corbado_python_sdk/generated/models/auth_event.py +++ b/src/corbado_python_sdk/generated/models/auth_event.py @@ -18,7 +18,7 @@ import re # noqa: F401 import json -from pydantic import BaseModel, ConfigDict, Field, StrictStr +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr from typing import Any, ClassVar, Dict, List from corbado_python_sdk.generated.models.auth_event_method import AuthEventMethod from corbado_python_sdk.generated.models.auth_event_status import AuthEventStatus @@ -36,8 +36,9 @@ class AuthEvent(BaseModel): event_type: AuthEventType = Field(alias="eventType") method: AuthEventMethod created: StrictStr = Field(description="Timestamp of when the entity was created in yyyy-MM-dd'T'HH:mm:ss format") + created_ms: StrictInt = Field(alias="createdMs") status: AuthEventStatus - __properties: ClassVar[List[str]] = ["authEventID", "userID", "username", "eventType", "method", "created", "status"] + __properties: ClassVar[List[str]] = ["authEventID", "userID", "username", "eventType", "method", "created", "createdMs", "status"] model_config = ConfigDict( populate_by_name=True, @@ -96,6 +97,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "eventType": obj.get("eventType"), "method": obj.get("method"), "created": obj.get("created"), + "createdMs": obj.get("createdMs"), "status": obj.get("status") }) return _obj diff --git a/src/corbado_python_sdk/generated/models/challenge.py b/src/corbado_python_sdk/generated/models/challenge.py index e2497ab..66a7d6c 100644 --- a/src/corbado_python_sdk/generated/models/challenge.py +++ b/src/corbado_python_sdk/generated/models/challenge.py @@ -18,7 +18,7 @@ import re # noqa: F401 import json -from pydantic import BaseModel, ConfigDict, Field, StrictStr +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr from typing import Any, ClassVar, Dict, List from corbado_python_sdk.generated.models.challenge_status import ChallengeStatus from corbado_python_sdk.generated.models.challenge_type import ChallengeType @@ -33,8 +33,9 @@ class Challenge(BaseModel): type: ChallengeType identifier_value: StrictStr = Field(alias="identifierValue") value: StrictStr + expires: StrictInt status: ChallengeStatus - __properties: ClassVar[List[str]] = ["challengeID", "type", "identifierValue", "value", "status"] + __properties: ClassVar[List[str]] = ["challengeID", "type", "identifierValue", "value", "expires", "status"] model_config = ConfigDict( populate_by_name=True, @@ -91,6 +92,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "type": obj.get("type"), "identifierValue": obj.get("identifierValue"), "value": obj.get("value"), + "expires": obj.get("expires"), "status": obj.get("status") }) return _obj diff --git a/src/corbado_python_sdk/generated/models/challenge_create_req.py b/src/corbado_python_sdk/generated/models/challenge_create_req.py index 0ef2850..3004b77 100644 --- a/src/corbado_python_sdk/generated/models/challenge_create_req.py +++ b/src/corbado_python_sdk/generated/models/challenge_create_req.py @@ -18,7 +18,7 @@ import re # noqa: F401 import json -from pydantic import BaseModel, ConfigDict, Field, StrictStr +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr from typing import Any, ClassVar, Dict, List, Optional from corbado_python_sdk.generated.models.challenge_type import ChallengeType from corbado_python_sdk.generated.models.client_information import ClientInformation @@ -32,8 +32,9 @@ class ChallengeCreateReq(BaseModel): challenge_type: ChallengeType = Field(alias="challengeType") identifier_value: StrictStr = Field(alias="identifierValue") challenge_metadata: Optional[Dict[str, Any]] = Field(default=None, alias="challengeMetadata") + lifetime_seconds: Optional[StrictInt] = Field(default=None, alias="lifetimeSeconds") client_information: ClientInformation = Field(alias="clientInformation") - __properties: ClassVar[List[str]] = ["challengeType", "identifierValue", "challengeMetadata", "clientInformation"] + __properties: ClassVar[List[str]] = ["challengeType", "identifierValue", "challengeMetadata", "lifetimeSeconds", "clientInformation"] model_config = ConfigDict( populate_by_name=True, @@ -92,6 +93,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "challengeType": obj.get("challengeType"), "identifierValue": obj.get("identifierValue"), "challengeMetadata": obj.get("challengeMetadata"), + "lifetimeSeconds": obj.get("lifetimeSeconds"), "clientInformation": ClientInformation.from_dict(obj["clientInformation"]) if obj.get("clientInformation") is not None else None }) return _obj diff --git a/src/corbado_python_sdk/generated/models/client_information.py b/src/corbado_python_sdk/generated/models/client_information.py index 5a594e3..b8d9263 100644 --- a/src/corbado_python_sdk/generated/models/client_information.py +++ b/src/corbado_python_sdk/generated/models/client_information.py @@ -36,7 +36,9 @@ class ClientInformation(BaseModel): bluetooth_available: Optional[StrictBool] = Field(default=None, description="Client's Bluetooth availability", alias="bluetoothAvailable") password_manager_available: Optional[StrictBool] = Field(default=None, description="Client's password manager availability", alias="passwordManagerAvailable") user_verifying_platform_authenticator_available: StrictBool = Field(alias="userVerifyingPlatformAuthenticatorAvailable") - __properties: ClassVar[List[str]] = ["remoteAddress", "userAgent", "clientEnvHandle", "javascriptFingerprint", "javaScriptHighEntropy", "bluetoothAvailable", "passwordManagerAvailable", "userVerifyingPlatformAuthenticatorAvailable"] + conditional_mediation_available: StrictBool = Field(alias="conditionalMediationAvailable") + private_mode: Optional[StrictBool] = Field(default=None, alias="privateMode") + __properties: ClassVar[List[str]] = ["remoteAddress", "userAgent", "clientEnvHandle", "javascriptFingerprint", "javaScriptHighEntropy", "bluetoothAvailable", "passwordManagerAvailable", "userVerifyingPlatformAuthenticatorAvailable", "conditionalMediationAvailable", "privateMode"] model_config = ConfigDict( populate_by_name=True, @@ -99,7 +101,9 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "javaScriptHighEntropy": JavaScriptHighEntropy.from_dict(obj["javaScriptHighEntropy"]) if obj.get("javaScriptHighEntropy") is not None else None, "bluetoothAvailable": obj.get("bluetoothAvailable"), "passwordManagerAvailable": obj.get("passwordManagerAvailable"), - "userVerifyingPlatformAuthenticatorAvailable": obj.get("userVerifyingPlatformAuthenticatorAvailable") + "userVerifyingPlatformAuthenticatorAvailable": obj.get("userVerifyingPlatformAuthenticatorAvailable"), + "conditionalMediationAvailable": obj.get("conditionalMediationAvailable"), + "privateMode": obj.get("privateMode") }) return _obj diff --git a/src/corbado_python_sdk/generated/models/connect_token_data.py b/src/corbado_python_sdk/generated/models/connect_token_data.py index b2e691c..6b0d6d0 100644 --- a/src/corbado_python_sdk/generated/models/connect_token_data.py +++ b/src/corbado_python_sdk/generated/models/connect_token_data.py @@ -21,11 +21,12 @@ from corbado_python_sdk.generated.models.connect_token_data_passkey_append import ConnectTokenDataPasskeyAppend from corbado_python_sdk.generated.models.connect_token_data_passkey_delete import ConnectTokenDataPasskeyDelete from corbado_python_sdk.generated.models.connect_token_data_passkey_list import ConnectTokenDataPasskeyList +from corbado_python_sdk.generated.models.connect_token_data_passkey_login import ConnectTokenDataPasskeyLogin from pydantic import StrictStr, Field from typing import Union, List, Set, Optional, Dict from typing_extensions import Literal, Self -CONNECTTOKENDATA_ONE_OF_SCHEMAS = ["ConnectTokenDataPasskeyAppend", "ConnectTokenDataPasskeyDelete", "ConnectTokenDataPasskeyList"] +CONNECTTOKENDATA_ONE_OF_SCHEMAS = ["ConnectTokenDataPasskeyAppend", "ConnectTokenDataPasskeyDelete", "ConnectTokenDataPasskeyList", "ConnectTokenDataPasskeyLogin"] class ConnectTokenData(BaseModel): """ @@ -37,8 +38,10 @@ class ConnectTokenData(BaseModel): oneof_schema_2_validator: Optional[ConnectTokenDataPasskeyDelete] = None # data type: ConnectTokenDataPasskeyList oneof_schema_3_validator: Optional[ConnectTokenDataPasskeyList] = None - actual_instance: Optional[Union[ConnectTokenDataPasskeyAppend, ConnectTokenDataPasskeyDelete, ConnectTokenDataPasskeyList]] = None - one_of_schemas: Set[str] = { "ConnectTokenDataPasskeyAppend", "ConnectTokenDataPasskeyDelete", "ConnectTokenDataPasskeyList" } + # data type: ConnectTokenDataPasskeyLogin + oneof_schema_4_validator: Optional[ConnectTokenDataPasskeyLogin] = None + actual_instance: Optional[Union[ConnectTokenDataPasskeyAppend, ConnectTokenDataPasskeyDelete, ConnectTokenDataPasskeyList, ConnectTokenDataPasskeyLogin]] = None + one_of_schemas: Set[str] = { "ConnectTokenDataPasskeyAppend", "ConnectTokenDataPasskeyDelete", "ConnectTokenDataPasskeyList", "ConnectTokenDataPasskeyLogin" } model_config = ConfigDict( validate_assignment=True, @@ -76,12 +79,17 @@ def actual_instance_must_validate_oneof(cls, v): error_messages.append(f"Error! Input type `{type(v)}` is not `ConnectTokenDataPasskeyList`") else: match += 1 + # validate data type: ConnectTokenDataPasskeyLogin + if not isinstance(v, ConnectTokenDataPasskeyLogin): + error_messages.append(f"Error! Input type `{type(v)}` is not `ConnectTokenDataPasskeyLogin`") + else: + match += 1 if match > 1: # more than 1 match - raise ValueError("Multiple matches found when setting `actual_instance` in ConnectTokenData with oneOf schemas: ConnectTokenDataPasskeyAppend, ConnectTokenDataPasskeyDelete, ConnectTokenDataPasskeyList. Details: " + ", ".join(error_messages)) + raise ValueError("Multiple matches found when setting `actual_instance` in ConnectTokenData with oneOf schemas: ConnectTokenDataPasskeyAppend, ConnectTokenDataPasskeyDelete, ConnectTokenDataPasskeyList, ConnectTokenDataPasskeyLogin. Details: " + ", ".join(error_messages)) elif match == 0: # no match - raise ValueError("No match found when setting `actual_instance` in ConnectTokenData with oneOf schemas: ConnectTokenDataPasskeyAppend, ConnectTokenDataPasskeyDelete, ConnectTokenDataPasskeyList. Details: " + ", ".join(error_messages)) + raise ValueError("No match found when setting `actual_instance` in ConnectTokenData with oneOf schemas: ConnectTokenDataPasskeyAppend, ConnectTokenDataPasskeyDelete, ConnectTokenDataPasskeyList, ConnectTokenDataPasskeyLogin. Details: " + ", ".join(error_messages)) else: return v @@ -114,13 +122,19 @@ def from_json(cls, json_str: str) -> Self: match += 1 except (ValidationError, ValueError) as e: error_messages.append(str(e)) + # deserialize data into ConnectTokenDataPasskeyLogin + try: + instance.actual_instance = ConnectTokenDataPasskeyLogin.from_json(json_str) + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) if match > 1: # more than 1 match - raise ValueError("Multiple matches found when deserializing the JSON string into ConnectTokenData with oneOf schemas: ConnectTokenDataPasskeyAppend, ConnectTokenDataPasskeyDelete, ConnectTokenDataPasskeyList. Details: " + ", ".join(error_messages)) + raise ValueError("Multiple matches found when deserializing the JSON string into ConnectTokenData with oneOf schemas: ConnectTokenDataPasskeyAppend, ConnectTokenDataPasskeyDelete, ConnectTokenDataPasskeyList, ConnectTokenDataPasskeyLogin. Details: " + ", ".join(error_messages)) elif match == 0: # no match - raise ValueError("No match found when deserializing the JSON string into ConnectTokenData with oneOf schemas: ConnectTokenDataPasskeyAppend, ConnectTokenDataPasskeyDelete, ConnectTokenDataPasskeyList. Details: " + ", ".join(error_messages)) + raise ValueError("No match found when deserializing the JSON string into ConnectTokenData with oneOf schemas: ConnectTokenDataPasskeyAppend, ConnectTokenDataPasskeyDelete, ConnectTokenDataPasskeyList, ConnectTokenDataPasskeyLogin. Details: " + ", ".join(error_messages)) else: return instance @@ -134,7 +148,7 @@ def to_json(self) -> str: else: return json.dumps(self.actual_instance) - def to_dict(self) -> Optional[Union[Dict[str, Any], ConnectTokenDataPasskeyAppend, ConnectTokenDataPasskeyDelete, ConnectTokenDataPasskeyList]]: + def to_dict(self) -> Optional[Union[Dict[str, Any], ConnectTokenDataPasskeyAppend, ConnectTokenDataPasskeyDelete, ConnectTokenDataPasskeyList, ConnectTokenDataPasskeyLogin]]: """Returns the dict representation of the actual instance""" if self.actual_instance is None: return None diff --git a/src/corbado_python_sdk/generated/models/connect_token_data_passkey_login.py b/src/corbado_python_sdk/generated/models/connect_token_data_passkey_login.py new file mode 100644 index 0000000..f55b4e1 --- /dev/null +++ b/src/corbado_python_sdk/generated/models/connect_token_data_passkey_login.py @@ -0,0 +1,88 @@ +# coding: utf-8 + +""" + Corbado Backend API + + # Introduction This documentation gives an overview of all Corbado Backend API calls to implement passwordless authentication with Passkeys. + + The version of the OpenAPI document: 2.0.0 + Contact: support@corbado.com + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictStr +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class ConnectTokenDataPasskeyLogin(BaseModel): + """ + ConnectTokenDataPasskeyLogin + """ # noqa: E501 + identifier: StrictStr + __properties: ClassVar[List[str]] = ["identifier"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of ConnectTokenDataPasskeyLogin from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of ConnectTokenDataPasskeyLogin from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "identifier": obj.get("identifier") + }) + return _obj + + diff --git a/src/corbado_python_sdk/generated/models/connect_token_type.py b/src/corbado_python_sdk/generated/models/connect_token_type.py index 114cc08..13d0517 100644 --- a/src/corbado_python_sdk/generated/models/connect_token_type.py +++ b/src/corbado_python_sdk/generated/models/connect_token_type.py @@ -30,6 +30,7 @@ class ConnectTokenType(str, Enum): PASSKEY_MINUS_APPEND = 'passkey-append' PASSKEY_MINUS_DELETE = 'passkey-delete' PASSKEY_MINUS_LIST = 'passkey-list' + PASSKEY_MINUS_LOGIN = 'passkey-login' @classmethod def from_json(cls, json_str: str) -> Self: diff --git a/src/corbado_python_sdk/generated/models/credential.py b/src/corbado_python_sdk/generated/models/credential.py index 38e28e9..8f8c066 100644 --- a/src/corbado_python_sdk/generated/models/credential.py +++ b/src/corbado_python_sdk/generated/models/credential.py @@ -18,8 +18,9 @@ import re # noqa: F401 import json -from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr, field_validator +from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictInt, StrictStr, field_validator from typing import Any, ClassVar, Dict, List +from corbado_python_sdk.generated.models.aaguid_details import AaguidDetails from typing import Optional, Set from typing_extensions import Self @@ -37,9 +38,12 @@ class Credential(BaseModel): source_os: StrictStr = Field(alias="sourceOS") source_browser: StrictStr = Field(alias="sourceBrowser") last_used: StrictStr = Field(description="Timestamp of when the passkey was last used in yyyy-MM-dd'T'HH:mm:ss format", alias="lastUsed") + last_used_ms: StrictInt = Field(alias="lastUsedMs") created: StrictStr = Field(description="Timestamp of when the entity was created in yyyy-MM-dd'T'HH:mm:ss format") + created_ms: StrictInt = Field(alias="createdMs") status: StrictStr = Field(description="Status") - __properties: ClassVar[List[str]] = ["id", "credentialID", "attestationType", "transport", "backupEligible", "backupState", "authenticatorAAGUID", "sourceOS", "sourceBrowser", "lastUsed", "created", "status"] + aaguid_details: AaguidDetails = Field(alias="aaguidDetails") + __properties: ClassVar[List[str]] = ["id", "credentialID", "attestationType", "transport", "backupEligible", "backupState", "authenticatorAAGUID", "sourceOS", "sourceBrowser", "lastUsed", "lastUsedMs", "created", "createdMs", "status", "aaguidDetails"] @field_validator('transport') def transport_validate_enum(cls, value): @@ -95,6 +99,9 @@ def to_dict(self) -> Dict[str, Any]: exclude=excluded_fields, exclude_none=True, ) + # override the default output from pydantic by calling `to_dict()` of aaguid_details + if self.aaguid_details: + _dict['aaguidDetails'] = self.aaguid_details.to_dict() return _dict @classmethod @@ -117,8 +124,11 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "sourceOS": obj.get("sourceOS"), "sourceBrowser": obj.get("sourceBrowser"), "lastUsed": obj.get("lastUsed"), + "lastUsedMs": obj.get("lastUsedMs"), "created": obj.get("created"), - "status": obj.get("status") + "createdMs": obj.get("createdMs"), + "status": obj.get("status"), + "aaguidDetails": AaguidDetails.from_dict(obj["aaguidDetails"]) if obj.get("aaguidDetails") is not None else None }) return _obj diff --git a/src/corbado_python_sdk/generated/models/decision_insights.py b/src/corbado_python_sdk/generated/models/decision_insights.py new file mode 100644 index 0000000..244a9fe --- /dev/null +++ b/src/corbado_python_sdk/generated/models/decision_insights.py @@ -0,0 +1,93 @@ +# coding: utf-8 + +""" + Corbado Backend API + + # Introduction This documentation gives an overview of all Corbado Backend API calls to implement passwordless authentication with Passkeys. + + The version of the OpenAPI document: 2.0.0 + Contact: support@corbado.com + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr +from typing import Any, ClassVar, Dict, List +from corbado_python_sdk.generated.models.decision_tag import DecisionTag +from typing import Optional, Set +from typing_extensions import Self + +class DecisionInsights(BaseModel): + """ + DecisionInsights + """ # noqa: E501 + tag: DecisionTag + is_cda_candidate: StrictBool = Field(alias="isCDACandidate") + experiments: List[StrictStr] + __properties: ClassVar[List[str]] = ["tag", "isCDACandidate", "experiments"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of DecisionInsights from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of DecisionInsights from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "tag": obj.get("tag"), + "isCDACandidate": obj.get("isCDACandidate"), + "experiments": obj.get("experiments") + }) + return _obj + + diff --git a/src/corbado_python_sdk/generated/models/decision_tag.py b/src/corbado_python_sdk/generated/models/decision_tag.py index fa6957c..cc9af36 100644 --- a/src/corbado_python_sdk/generated/models/decision_tag.py +++ b/src/corbado_python_sdk/generated/models/decision_tag.py @@ -45,6 +45,7 @@ class DecisionTag(str, Enum): PROCESS_MINUS_PK_MINUS_LOGIN_MINUS_NOT_MINUS_OFFERED = 'process-pk-login-not-offered' PROCESS_MINUS_PK_MINUS_LOGIN_MINUS_INCOMPLETE = 'process-pk-login-incomplete' PROCESS_MINUS_PK_MINUS_LOGIN_MINUS_CROSS_MINUS_PLATFORM_MINUS_COMPLETED = 'process-pk-login-cross-platform-completed' + DEVICE_MINUS_LOCAL_MINUS_PLATFORM_MINUS_PASSKEY_MINUS_EXPERIMENT = 'device-local-platform-passkey-experiment' @classmethod def from_json(cls, json_str: str) -> Self: diff --git a/src/corbado_python_sdk/generated/models/detection_insights.py b/src/corbado_python_sdk/generated/models/detection_insights.py new file mode 100644 index 0000000..a380e36 --- /dev/null +++ b/src/corbado_python_sdk/generated/models/detection_insights.py @@ -0,0 +1,102 @@ +# coding: utf-8 + +""" + Corbado Backend API + + # Introduction This documentation gives an overview of all Corbado Backend API calls to implement passwordless authentication with Passkeys. + + The version of the OpenAPI document: 2.0.0 + Contact: support@corbado.com + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List +from corbado_python_sdk.generated.models.detection_tag import DetectionTag +from typing import Optional, Set +from typing_extensions import Self + +class DetectionInsights(BaseModel): + """ + DetectionInsights + """ # noqa: E501 + tags: List[DetectionTag] + credential_ids: List[StrictStr] = Field(alias="credentialIds") + client_env_ids: List[StrictStr] = Field(alias="clientEnvIds") + password_manager_ids: List[StrictStr] = Field(alias="passwordManagerIds") + __properties: ClassVar[List[str]] = ["tags", "credentialIds", "clientEnvIds", "passwordManagerIds"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of DetectionInsights from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in tags (list) + _items = [] + if self.tags: + for _item_tags in self.tags: + if _item_tags: + _items.append(_item_tags.to_dict()) + _dict['tags'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of DetectionInsights from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "tags": [DetectionTag.from_dict(_item) for _item in obj["tags"]] if obj.get("tags") is not None else None, + "credentialIds": obj.get("credentialIds"), + "clientEnvIds": obj.get("clientEnvIds"), + "passwordManagerIds": obj.get("passwordManagerIds") + }) + return _obj + + diff --git a/src/corbado_python_sdk/generated/models/error_rsp_all_of_error.py b/src/corbado_python_sdk/generated/models/error_rsp_all_of_error.py index b6902f2..bd29bba 100644 --- a/src/corbado_python_sdk/generated/models/error_rsp_all_of_error.py +++ b/src/corbado_python_sdk/generated/models/error_rsp_all_of_error.py @@ -31,7 +31,7 @@ class ErrorRspAllOfError(BaseModel): type: StrictStr = Field(description="Type of error") details: Optional[StrictStr] = Field(default=None, description="Details of error") validation: Optional[List[ErrorRspAllOfErrorValidation]] = Field(default=None, description="Validation errors per field") - links: List[StrictStr] = Field(description="Additional links to help understand the error") + links: Optional[List[StrictStr]] = Field(default=None, description="Additional links to help understand the error") __properties: ClassVar[List[str]] = ["type", "details", "validation", "links"] model_config = ConfigDict( diff --git a/src/corbado_python_sdk/generated/models/long_session.py b/src/corbado_python_sdk/generated/models/long_session.py index bd9911f..01c85db 100644 --- a/src/corbado_python_sdk/generated/models/long_session.py +++ b/src/corbado_python_sdk/generated/models/long_session.py @@ -18,7 +18,7 @@ import re # noqa: F401 import json -from pydantic import BaseModel, ConfigDict, Field, StrictStr +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr from typing import Any, ClassVar, Dict, List from corbado_python_sdk.generated.models.long_session_status import LongSessionStatus from typing import Optional, Set @@ -33,7 +33,8 @@ class LongSession(BaseModel): identifier_value: StrictStr = Field(alias="identifierValue") status: LongSessionStatus expires: StrictStr - __properties: ClassVar[List[str]] = ["longSessionID", "userID", "identifierValue", "status", "expires"] + expires_ms: StrictInt = Field(alias="expiresMs") + __properties: ClassVar[List[str]] = ["longSessionID", "userID", "identifierValue", "status", "expires", "expiresMs"] model_config = ConfigDict( populate_by_name=True, @@ -90,7 +91,8 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "userID": obj.get("userID"), "identifierValue": obj.get("identifierValue"), "status": obj.get("status"), - "expires": obj.get("expires") + "expires": obj.get("expires"), + "expiresMs": obj.get("expiresMs") }) return _obj diff --git a/src/corbado_python_sdk/generated/models/passkey_append_start_rsp.py b/src/corbado_python_sdk/generated/models/passkey_append_start_rsp.py index 28abc10..413f134 100644 --- a/src/corbado_python_sdk/generated/models/passkey_append_start_rsp.py +++ b/src/corbado_python_sdk/generated/models/passkey_append_start_rsp.py @@ -20,8 +20,8 @@ from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr from typing import Any, ClassVar, Dict, List -from corbado_python_sdk.generated.models.decision_tag import DecisionTag -from corbado_python_sdk.generated.models.detection_tag import DetectionTag +from corbado_python_sdk.generated.models.decision_insights import DecisionInsights +from corbado_python_sdk.generated.models.detection_insights import DetectionInsights from typing import Optional, Set from typing_extensions import Self @@ -30,10 +30,10 @@ class PasskeyAppendStartRsp(BaseModel): PasskeyAppendStartRsp """ # noqa: E501 append_allow: StrictBool = Field(alias="appendAllow") - detection_tags: List[DetectionTag] = Field(alias="detectionTags") - decision_tag: DecisionTag = Field(alias="decisionTag") attestation_options: StrictStr = Field(alias="attestationOptions") - __properties: ClassVar[List[str]] = ["appendAllow", "detectionTags", "decisionTag", "attestationOptions"] + detection_insights: DetectionInsights = Field(alias="detectionInsights") + decision_insights: DecisionInsights = Field(alias="decisionInsights") + __properties: ClassVar[List[str]] = ["appendAllow", "attestationOptions", "detectionInsights", "decisionInsights"] model_config = ConfigDict( populate_by_name=True, @@ -74,13 +74,12 @@ def to_dict(self) -> Dict[str, Any]: exclude=excluded_fields, exclude_none=True, ) - # override the default output from pydantic by calling `to_dict()` of each item in detection_tags (list) - _items = [] - if self.detection_tags: - for _item_detection_tags in self.detection_tags: - if _item_detection_tags: - _items.append(_item_detection_tags.to_dict()) - _dict['detectionTags'] = _items + # override the default output from pydantic by calling `to_dict()` of detection_insights + if self.detection_insights: + _dict['detectionInsights'] = self.detection_insights.to_dict() + # override the default output from pydantic by calling `to_dict()` of decision_insights + if self.decision_insights: + _dict['decisionInsights'] = self.decision_insights.to_dict() return _dict @classmethod @@ -94,9 +93,9 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: _obj = cls.model_validate({ "appendAllow": obj.get("appendAllow"), - "detectionTags": [DetectionTag.from_dict(_item) for _item in obj["detectionTags"]] if obj.get("detectionTags") is not None else None, - "decisionTag": obj.get("decisionTag"), - "attestationOptions": obj.get("attestationOptions") + "attestationOptions": obj.get("attestationOptions"), + "detectionInsights": DetectionInsights.from_dict(obj["detectionInsights"]) if obj.get("detectionInsights") is not None else None, + "decisionInsights": DecisionInsights.from_dict(obj["decisionInsights"]) if obj.get("decisionInsights") is not None else None }) return _obj diff --git a/src/corbado_python_sdk/generated/models/passkey_challenge.py b/src/corbado_python_sdk/generated/models/passkey_challenge.py index 545ed0f..e5077a9 100644 --- a/src/corbado_python_sdk/generated/models/passkey_challenge.py +++ b/src/corbado_python_sdk/generated/models/passkey_challenge.py @@ -34,8 +34,9 @@ class PasskeyChallenge(BaseModel): value: StrictStr status: PasskeyChallengeStatus created: StrictInt + created_ms: StrictInt = Field(alias="createdMs") expires: StrictInt - __properties: ClassVar[List[str]] = ["challengeID", "type", "value", "status", "created", "expires"] + __properties: ClassVar[List[str]] = ["challengeID", "type", "value", "status", "created", "createdMs", "expires"] model_config = ConfigDict( populate_by_name=True, @@ -93,6 +94,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "value": obj.get("value"), "status": obj.get("status"), "created": obj.get("created"), + "createdMs": obj.get("createdMs"), "expires": obj.get("expires") }) return _obj diff --git a/src/corbado_python_sdk/generated/models/passkey_data.py b/src/corbado_python_sdk/generated/models/passkey_data.py index a97d6ce..31039ba 100644 --- a/src/corbado_python_sdk/generated/models/passkey_data.py +++ b/src/corbado_python_sdk/generated/models/passkey_data.py @@ -20,6 +20,7 @@ from pydantic import BaseModel, ConfigDict, Field, StrictStr, field_validator from typing import Any, ClassVar, Dict, List +from corbado_python_sdk.generated.models.aaguid_details import AaguidDetails from typing import Optional, Set from typing_extensions import Self @@ -32,7 +33,8 @@ class PasskeyData(BaseModel): username: StrictStr ceremony_type: StrictStr = Field(alias="ceremonyType") challenge_id: StrictStr = Field(alias="challengeID") - __properties: ClassVar[List[str]] = ["id", "userID", "username", "ceremonyType", "challengeID"] + aaguid_details: AaguidDetails = Field(alias="aaguidDetails") + __properties: ClassVar[List[str]] = ["id", "userID", "username", "ceremonyType", "challengeID", "aaguidDetails"] @field_validator('ceremony_type') def ceremony_type_validate_enum(cls, value): @@ -80,6 +82,9 @@ def to_dict(self) -> Dict[str, Any]: exclude=excluded_fields, exclude_none=True, ) + # override the default output from pydantic by calling `to_dict()` of aaguid_details + if self.aaguid_details: + _dict['aaguidDetails'] = self.aaguid_details.to_dict() return _dict @classmethod @@ -96,7 +101,8 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "userID": obj.get("userID"), "username": obj.get("username"), "ceremonyType": obj.get("ceremonyType"), - "challengeID": obj.get("challengeID") + "challengeID": obj.get("challengeID"), + "aaguidDetails": AaguidDetails.from_dict(obj["aaguidDetails"]) if obj.get("aaguidDetails") is not None else None }) return _obj diff --git a/src/corbado_python_sdk/generated/models/passkey_event.py b/src/corbado_python_sdk/generated/models/passkey_event.py index de8fdd7..2134936 100644 --- a/src/corbado_python_sdk/generated/models/passkey_event.py +++ b/src/corbado_python_sdk/generated/models/passkey_event.py @@ -36,7 +36,8 @@ class PasskeyEvent(BaseModel): credential_id: Optional[StrictStr] = Field(default=None, alias="credentialID") expires: Optional[StrictInt] = None created: StrictStr = Field(description="Timestamp of when the entity was created in yyyy-MM-dd'T'HH:mm:ss format") - __properties: ClassVar[List[str]] = ["passkeyEventID", "userID", "eventType", "clientEnvID", "processID", "credentialID", "expires", "created"] + created_ms: StrictInt = Field(alias="createdMs") + __properties: ClassVar[List[str]] = ["passkeyEventID", "userID", "eventType", "clientEnvID", "processID", "credentialID", "expires", "created", "createdMs"] model_config = ConfigDict( populate_by_name=True, @@ -96,7 +97,8 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "processID": obj.get("processID"), "credentialID": obj.get("credentialID"), "expires": obj.get("expires"), - "created": obj.get("created") + "created": obj.get("created"), + "createdMs": obj.get("createdMs") }) return _obj diff --git a/src/corbado_python_sdk/generated/models/passkey_event_create_req.py b/src/corbado_python_sdk/generated/models/passkey_event_create_req.py index 1618776..71a6c56 100644 --- a/src/corbado_python_sdk/generated/models/passkey_event_create_req.py +++ b/src/corbado_python_sdk/generated/models/passkey_event_create_req.py @@ -33,7 +33,8 @@ class PasskeyEventCreateReq(BaseModel): process_id: Optional[StrictStr] = Field(default=None, alias="processID") client_env_id: Optional[StrictStr] = Field(default=None, alias="clientEnvID") credential_id: Optional[StrictStr] = Field(default=None, alias="credentialID") - __properties: ClassVar[List[str]] = ["eventType", "expires", "processID", "clientEnvID", "credentialID"] + challenge: Optional[StrictStr] = None + __properties: ClassVar[List[str]] = ["eventType", "expires", "processID", "clientEnvID", "credentialID", "challenge"] model_config = ConfigDict( populate_by_name=True, @@ -90,7 +91,8 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "expires": obj.get("expires"), "processID": obj.get("processID"), "clientEnvID": obj.get("clientEnvID"), - "credentialID": obj.get("credentialID") + "credentialID": obj.get("credentialID"), + "challenge": obj.get("challenge") }) return _obj diff --git a/src/corbado_python_sdk/generated/models/passkey_event_type.py b/src/corbado_python_sdk/generated/models/passkey_event_type.py index facc682..f573e1a 100644 --- a/src/corbado_python_sdk/generated/models/passkey_event_type.py +++ b/src/corbado_python_sdk/generated/models/passkey_event_type.py @@ -30,9 +30,13 @@ class PasskeyEventType(str, Enum): USER_MINUS_LOGIN_MINUS_BLACKLISTED = 'user-login-blacklisted' LOGIN_MINUS_EXPLICIT_MINUS_ABORT = 'login-explicit-abort' LOGIN_MINUS_ERROR = 'login-error' + LOGIN_MINUS_ERROR_MINUS_UNTYPED = 'login-error-untyped' LOGIN_MINUS_ONE_MINUS_TAP_MINUS_SWITCH = 'login-one-tap-switch' USER_MINUS_APPEND_MINUS_AFTER_MINUS_CROSS_MINUS_PLATFORM_MINUS_BLACKLISTED = 'user-append-after-cross-platform-blacklisted' USER_MINUS_APPEND_MINUS_AFTER_MINUS_LOGIN_MINUS_ERROR_MINUS_BLACKLISTED = 'user-append-after-login-error-blacklisted' + APPEND_MINUS_CREDENTIAL_MINUS_EXISTS = 'append-credential-exists' + APPEND_MINUS_EXPLICIT_MINUS_ABORT = 'append-explicit-abort' + APPEND_MINUS_ERROR = 'append-error' @classmethod def from_json(cls, json_str: str) -> Self: diff --git a/src/corbado_python_sdk/generated/models/passkey_login_finish_req.py b/src/corbado_python_sdk/generated/models/passkey_login_finish_req.py index a05efe1..590e621 100644 --- a/src/corbado_python_sdk/generated/models/passkey_login_finish_req.py +++ b/src/corbado_python_sdk/generated/models/passkey_login_finish_req.py @@ -18,8 +18,8 @@ import re # noqa: F401 import json -from pydantic import BaseModel, ConfigDict, Field, StrictStr -from typing import Any, ClassVar, Dict, List +from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr +from typing import Any, ClassVar, Dict, List, Optional from corbado_python_sdk.generated.models.client_information import ClientInformation from typing import Optional, Set from typing_extensions import Self @@ -32,7 +32,8 @@ class PasskeyLoginFinishReq(BaseModel): assertion_response: StrictStr = Field(alias="assertionResponse") client_information: ClientInformation = Field(alias="clientInformation") process_id: StrictStr = Field(alias="processID") - __properties: ClassVar[List[str]] = ["userID", "assertionResponse", "clientInformation", "processID"] + sign_passkey_data: Optional[StrictBool] = Field(default=None, alias="signPasskeyData") + __properties: ClassVar[List[str]] = ["userID", "assertionResponse", "clientInformation", "processID", "signPasskeyData"] model_config = ConfigDict( populate_by_name=True, @@ -91,7 +92,8 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "userID": obj.get("userID"), "assertionResponse": obj.get("assertionResponse"), "clientInformation": ClientInformation.from_dict(obj["clientInformation"]) if obj.get("clientInformation") is not None else None, - "processID": obj.get("processID") + "processID": obj.get("processID"), + "signPasskeyData": obj.get("signPasskeyData") }) return _obj diff --git a/src/corbado_python_sdk/generated/models/passkey_login_finish_rsp.py b/src/corbado_python_sdk/generated/models/passkey_login_finish_rsp.py index a5ea05c..fd8f499 100644 --- a/src/corbado_python_sdk/generated/models/passkey_login_finish_rsp.py +++ b/src/corbado_python_sdk/generated/models/passkey_login_finish_rsp.py @@ -18,8 +18,8 @@ import re # noqa: F401 import json -from pydantic import BaseModel, ConfigDict, Field -from typing import Any, ClassVar, Dict, List +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List, Optional from corbado_python_sdk.generated.models.passkey_data import PasskeyData from typing import Optional, Set from typing_extensions import Self @@ -29,7 +29,8 @@ class PasskeyLoginFinishRsp(BaseModel): PasskeyLoginFinishRsp """ # noqa: E501 passkey_data: PasskeyData = Field(alias="passkeyData") - __properties: ClassVar[List[str]] = ["passkeyData"] + signed_passkey_data: Optional[StrictStr] = Field(default=None, alias="signedPasskeyData") + __properties: ClassVar[List[str]] = ["passkeyData", "signedPasskeyData"] model_config = ConfigDict( populate_by_name=True, @@ -85,7 +86,8 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: return cls.model_validate(obj) _obj = cls.model_validate({ - "passkeyData": PasskeyData.from_dict(obj["passkeyData"]) if obj.get("passkeyData") is not None else None + "passkeyData": PasskeyData.from_dict(obj["passkeyData"]) if obj.get("passkeyData") is not None else None, + "signedPasskeyData": obj.get("signedPasskeyData") }) return _obj diff --git a/src/corbado_python_sdk/generated/models/passkey_login_start_rsp.py b/src/corbado_python_sdk/generated/models/passkey_login_start_rsp.py index 00e7296..2108966 100644 --- a/src/corbado_python_sdk/generated/models/passkey_login_start_rsp.py +++ b/src/corbado_python_sdk/generated/models/passkey_login_start_rsp.py @@ -20,8 +20,8 @@ from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr from typing import Any, ClassVar, Dict, List -from corbado_python_sdk.generated.models.decision_tag import DecisionTag -from corbado_python_sdk.generated.models.detection_tag import DetectionTag +from corbado_python_sdk.generated.models.decision_insights import DecisionInsights +from corbado_python_sdk.generated.models.detection_insights import DetectionInsights from typing import Optional, Set from typing_extensions import Self @@ -30,11 +30,10 @@ class PasskeyLoginStartRsp(BaseModel): PasskeyLoginStartRsp """ # noqa: E501 login_allow: StrictBool = Field(alias="loginAllow") - detection_tags: List[DetectionTag] = Field(alias="detectionTags") - decision_tag: DecisionTag = Field(alias="decisionTag") assertion_options: StrictStr = Field(alias="assertionOptions") - is_cda_candidate: StrictBool = Field(alias="isCDACandidate") - __properties: ClassVar[List[str]] = ["loginAllow", "detectionTags", "decisionTag", "assertionOptions", "isCDACandidate"] + detection_insights: DetectionInsights = Field(alias="detectionInsights") + decision_insights: DecisionInsights = Field(alias="decisionInsights") + __properties: ClassVar[List[str]] = ["loginAllow", "assertionOptions", "detectionInsights", "decisionInsights"] model_config = ConfigDict( populate_by_name=True, @@ -75,13 +74,12 @@ def to_dict(self) -> Dict[str, Any]: exclude=excluded_fields, exclude_none=True, ) - # override the default output from pydantic by calling `to_dict()` of each item in detection_tags (list) - _items = [] - if self.detection_tags: - for _item_detection_tags in self.detection_tags: - if _item_detection_tags: - _items.append(_item_detection_tags.to_dict()) - _dict['detectionTags'] = _items + # override the default output from pydantic by calling `to_dict()` of detection_insights + if self.detection_insights: + _dict['detectionInsights'] = self.detection_insights.to_dict() + # override the default output from pydantic by calling `to_dict()` of decision_insights + if self.decision_insights: + _dict['decisionInsights'] = self.decision_insights.to_dict() return _dict @classmethod @@ -95,10 +93,9 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: _obj = cls.model_validate({ "loginAllow": obj.get("loginAllow"), - "detectionTags": [DetectionTag.from_dict(_item) for _item in obj["detectionTags"]] if obj.get("detectionTags") is not None else None, - "decisionTag": obj.get("decisionTag"), "assertionOptions": obj.get("assertionOptions"), - "isCDACandidate": obj.get("isCDACandidate") + "detectionInsights": DetectionInsights.from_dict(obj["detectionInsights"]) if obj.get("detectionInsights") is not None else None, + "decisionInsights": DecisionInsights.from_dict(obj["decisionInsights"]) if obj.get("decisionInsights") is not None else None }) return _obj diff --git a/src/corbado_python_sdk/generated/models/passkey_mediation_finish_req.py b/src/corbado_python_sdk/generated/models/passkey_mediation_finish_req.py index e4b4088..bb97ad3 100644 --- a/src/corbado_python_sdk/generated/models/passkey_mediation_finish_req.py +++ b/src/corbado_python_sdk/generated/models/passkey_mediation_finish_req.py @@ -18,8 +18,8 @@ import re # noqa: F401 import json -from pydantic import BaseModel, ConfigDict, Field, StrictStr -from typing import Any, ClassVar, Dict, List +from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr +from typing import Any, ClassVar, Dict, List, Optional from corbado_python_sdk.generated.models.client_information import ClientInformation from typing import Optional, Set from typing_extensions import Self @@ -31,7 +31,8 @@ class PasskeyMediationFinishReq(BaseModel): assertion_response: StrictStr = Field(alias="assertionResponse") client_information: ClientInformation = Field(alias="clientInformation") process_id: StrictStr = Field(alias="processID") - __properties: ClassVar[List[str]] = ["assertionResponse", "clientInformation", "processID"] + sign_passkey_data: Optional[StrictBool] = Field(default=None, alias="signPasskeyData") + __properties: ClassVar[List[str]] = ["assertionResponse", "clientInformation", "processID", "signPasskeyData"] model_config = ConfigDict( populate_by_name=True, @@ -89,7 +90,8 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: _obj = cls.model_validate({ "assertionResponse": obj.get("assertionResponse"), "clientInformation": ClientInformation.from_dict(obj["clientInformation"]) if obj.get("clientInformation") is not None else None, - "processID": obj.get("processID") + "processID": obj.get("processID"), + "signPasskeyData": obj.get("signPasskeyData") }) return _obj diff --git a/src/corbado_python_sdk/generated/models/passkey_mediation_finish_rsp.py b/src/corbado_python_sdk/generated/models/passkey_mediation_finish_rsp.py index 16d0878..2894c03 100644 --- a/src/corbado_python_sdk/generated/models/passkey_mediation_finish_rsp.py +++ b/src/corbado_python_sdk/generated/models/passkey_mediation_finish_rsp.py @@ -18,8 +18,8 @@ import re # noqa: F401 import json -from pydantic import BaseModel, ConfigDict, Field -from typing import Any, ClassVar, Dict, List +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List, Optional from corbado_python_sdk.generated.models.passkey_data import PasskeyData from typing import Optional, Set from typing_extensions import Self @@ -29,7 +29,8 @@ class PasskeyMediationFinishRsp(BaseModel): PasskeyMediationFinishRsp """ # noqa: E501 passkey_data: PasskeyData = Field(alias="passkeyData") - __properties: ClassVar[List[str]] = ["passkeyData"] + signed_passkey_data: Optional[StrictStr] = Field(default=None, alias="signedPasskeyData") + __properties: ClassVar[List[str]] = ["passkeyData", "signedPasskeyData"] model_config = ConfigDict( populate_by_name=True, @@ -85,7 +86,8 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: return cls.model_validate(obj) _obj = cls.model_validate({ - "passkeyData": PasskeyData.from_dict(obj["passkeyData"]) if obj.get("passkeyData") is not None else None + "passkeyData": PasskeyData.from_dict(obj["passkeyData"]) if obj.get("passkeyData") is not None else None, + "signedPasskeyData": obj.get("signedPasskeyData") }) return _obj diff --git a/src/corbado_python_sdk/generated/models/passkey_post_login_req.py b/src/corbado_python_sdk/generated/models/passkey_post_login_req.py new file mode 100644 index 0000000..fb5f2a2 --- /dev/null +++ b/src/corbado_python_sdk/generated/models/passkey_post_login_req.py @@ -0,0 +1,88 @@ +# coding: utf-8 + +""" + Corbado Backend API + + # Introduction This documentation gives an overview of all Corbado Backend API calls to implement passwordless authentication with Passkeys. + + The version of the OpenAPI document: 2.0.0 + Contact: support@corbado.com + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class PasskeyPostLoginReq(BaseModel): + """ + PasskeyPostLoginReq + """ # noqa: E501 + signed_passkey_data: StrictStr = Field(alias="signedPasskeyData") + __properties: ClassVar[List[str]] = ["signedPasskeyData"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of PasskeyPostLoginReq from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of PasskeyPostLoginReq from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "signedPasskeyData": obj.get("signedPasskeyData") + }) + return _obj + + diff --git a/src/corbado_python_sdk/generated/models/passkey_post_login_rsp.py b/src/corbado_python_sdk/generated/models/passkey_post_login_rsp.py new file mode 100644 index 0000000..d8c83e1 --- /dev/null +++ b/src/corbado_python_sdk/generated/models/passkey_post_login_rsp.py @@ -0,0 +1,88 @@ +# coding: utf-8 + +""" + Corbado Backend API + + # Introduction This documentation gives an overview of all Corbado Backend API calls to implement passwordless authentication with Passkeys. + + The version of the OpenAPI document: 2.0.0 + Contact: support@corbado.com + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictStr +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class PasskeyPostLoginRsp(BaseModel): + """ + PasskeyPostLoginRsp + """ # noqa: E501 + session: StrictStr + __properties: ClassVar[List[str]] = ["session"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of PasskeyPostLoginRsp from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of PasskeyPostLoginRsp from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "session": obj.get("session") + }) + return _obj + + diff --git a/src/corbado_python_sdk/generated/models/password_manager.py b/src/corbado_python_sdk/generated/models/password_manager.py new file mode 100644 index 0000000..9ea7390 --- /dev/null +++ b/src/corbado_python_sdk/generated/models/password_manager.py @@ -0,0 +1,102 @@ +# coding: utf-8 + +""" + Corbado Backend API + + # Introduction This documentation gives an overview of all Corbado Backend API calls to implement passwordless authentication with Passkeys. + + The version of the OpenAPI document: 2.0.0 + Contact: support@corbado.com + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class PasswordManager(BaseModel): + """ + PasswordManager + """ # noqa: E501 + id: StrictStr + user_id: StrictStr = Field(alias="userID") + client_env_id: StrictStr = Field(alias="clientEnvID") + credential_id: StrictStr = Field(alias="credentialID") + aaguid: StrictStr + status: StrictStr + score: StrictInt + created_ms: StrictInt = Field(alias="createdMs") + __properties: ClassVar[List[str]] = ["id", "userID", "clientEnvID", "credentialID", "aaguid", "status", "score", "createdMs"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of PasswordManager from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of PasswordManager from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "id": obj.get("id"), + "userID": obj.get("userID"), + "clientEnvID": obj.get("clientEnvID"), + "credentialID": obj.get("credentialID"), + "aaguid": obj.get("aaguid"), + "status": obj.get("status"), + "score": obj.get("score"), + "createdMs": obj.get("createdMs") + }) + return _obj + + diff --git a/src/corbado_python_sdk/generated/models/password_manager_list.py b/src/corbado_python_sdk/generated/models/password_manager_list.py new file mode 100644 index 0000000..daec0a8 --- /dev/null +++ b/src/corbado_python_sdk/generated/models/password_manager_list.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + Corbado Backend API + + # Introduction This documentation gives an overview of all Corbado Backend API calls to implement passwordless authentication with Passkeys. + + The version of the OpenAPI document: 2.0.0 + Contact: support@corbado.com + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field +from typing import Any, ClassVar, Dict, List +from corbado_python_sdk.generated.models.password_manager import PasswordManager +from typing import Optional, Set +from typing_extensions import Self + +class PasswordManagerList(BaseModel): + """ + PasswordManagerList + """ # noqa: E501 + password_managers: List[PasswordManager] = Field(alias="passwordManagers") + __properties: ClassVar[List[str]] = ["passwordManagers"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of PasswordManagerList from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in password_managers (list) + _items = [] + if self.password_managers: + for _item_password_managers in self.password_managers: + if _item_password_managers: + _items.append(_item_password_managers.to_dict()) + _dict['passwordManagers'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of PasswordManagerList from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "passwordManagers": [PasswordManager.from_dict(_item) for _item in obj["passwordManagers"]] if obj.get("passwordManagers") is not None else None + }) + return _obj + + diff --git a/src/corbado_python_sdk/generated/models/request_data.py b/src/corbado_python_sdk/generated/models/request_data.py index e1ece9b..f5eb7ec 100644 --- a/src/corbado_python_sdk/generated/models/request_data.py +++ b/src/corbado_python_sdk/generated/models/request_data.py @@ -19,7 +19,7 @@ import json from pydantic import BaseModel, ConfigDict, Field, StrictStr -from typing import Any, ClassVar, Dict, List +from typing import Any, ClassVar, Dict, List, Optional from typing import Optional, Set from typing_extensions import Self @@ -28,7 +28,7 @@ class RequestData(BaseModel): Data about the request itself, can be used for debugging """ # noqa: E501 request_id: StrictStr = Field(description="Unique ID of request, you can provide your own while making the request, if not the ID will be randomly generated on server side", alias="requestID") - link: StrictStr = Field(description="Link to dashboard with details about request") + link: Optional[StrictStr] = Field(default=None, description="Link to dashboard with details about request") __properties: ClassVar[List[str]] = ["requestID", "link"] model_config = ConfigDict( diff --git a/src/corbado_python_sdk/generated/models/webhook_endpoint.py b/src/corbado_python_sdk/generated/models/webhook_endpoint.py new file mode 100644 index 0000000..4c700a3 --- /dev/null +++ b/src/corbado_python_sdk/generated/models/webhook_endpoint.py @@ -0,0 +1,103 @@ +# coding: utf-8 + +""" + Corbado Backend API + + # Introduction This documentation gives an overview of all Corbado Backend API calls to implement passwordless authentication with Passkeys. + + The version of the OpenAPI document: 2.0.0 + Contact: support@corbado.com + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List +from corbado_python_sdk.generated.models.webhook_event_type import WebhookEventType +from typing import Optional, Set +from typing_extensions import Self + +class WebhookEndpoint(BaseModel): + """ + WebhookEndpoint + """ # noqa: E501 + id: StrictStr + url: StrictStr + custom_headers: Dict[str, Any] = Field(alias="customHeaders") + subscribed_events: List[WebhookEventType] = Field(alias="subscribedEvents") + created: StrictStr + created_ms: StrictInt = Field(alias="createdMs") + updated: StrictStr + updated_ms: StrictInt = Field(alias="updatedMs") + __properties: ClassVar[List[str]] = ["id", "url", "customHeaders", "subscribedEvents", "created", "createdMs", "updated", "updatedMs"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of WebhookEndpoint from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of WebhookEndpoint from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "id": obj.get("id"), + "url": obj.get("url"), + "customHeaders": obj.get("customHeaders"), + "subscribedEvents": obj.get("subscribedEvents"), + "created": obj.get("created"), + "createdMs": obj.get("createdMs"), + "updated": obj.get("updated"), + "updatedMs": obj.get("updatedMs") + }) + return _obj + + diff --git a/src/corbado_python_sdk/generated/models/webhook_endpoint_create_req.py b/src/corbado_python_sdk/generated/models/webhook_endpoint_create_req.py new file mode 100644 index 0000000..86f652f --- /dev/null +++ b/src/corbado_python_sdk/generated/models/webhook_endpoint_create_req.py @@ -0,0 +1,93 @@ +# coding: utf-8 + +""" + Corbado Backend API + + # Introduction This documentation gives an overview of all Corbado Backend API calls to implement passwordless authentication with Passkeys. + + The version of the OpenAPI document: 2.0.0 + Contact: support@corbado.com + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List +from corbado_python_sdk.generated.models.webhook_event_type import WebhookEventType +from typing import Optional, Set +from typing_extensions import Self + +class WebhookEndpointCreateReq(BaseModel): + """ + WebhookEndpointCreateReq + """ # noqa: E501 + url: StrictStr + subscribed_events: List[WebhookEventType] = Field(alias="subscribedEvents") + custom_headers: Dict[str, Any] = Field(alias="customHeaders") + __properties: ClassVar[List[str]] = ["url", "subscribedEvents", "customHeaders"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of WebhookEndpointCreateReq from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of WebhookEndpointCreateReq from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "url": obj.get("url"), + "subscribedEvents": obj.get("subscribedEvents"), + "customHeaders": obj.get("customHeaders") + }) + return _obj + + diff --git a/src/corbado_python_sdk/generated/models/webhook_endpoint_list.py b/src/corbado_python_sdk/generated/models/webhook_endpoint_list.py new file mode 100644 index 0000000..25e6b47 --- /dev/null +++ b/src/corbado_python_sdk/generated/models/webhook_endpoint_list.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + Corbado Backend API + + # Introduction This documentation gives an overview of all Corbado Backend API calls to implement passwordless authentication with Passkeys. + + The version of the OpenAPI document: 2.0.0 + Contact: support@corbado.com + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field +from typing import Any, ClassVar, Dict, List +from corbado_python_sdk.generated.models.webhook_endpoint import WebhookEndpoint +from typing import Optional, Set +from typing_extensions import Self + +class WebhookEndpointList(BaseModel): + """ + WebhookEndpointList + """ # noqa: E501 + webhook_endpoints: List[WebhookEndpoint] = Field(alias="webhookEndpoints") + __properties: ClassVar[List[str]] = ["webhookEndpoints"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of WebhookEndpointList from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in webhook_endpoints (list) + _items = [] + if self.webhook_endpoints: + for _item_webhook_endpoints in self.webhook_endpoints: + if _item_webhook_endpoints: + _items.append(_item_webhook_endpoints.to_dict()) + _dict['webhookEndpoints'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of WebhookEndpointList from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "webhookEndpoints": [WebhookEndpoint.from_dict(_item) for _item in obj["webhookEndpoints"]] if obj.get("webhookEndpoints") is not None else None + }) + return _obj + + diff --git a/src/corbado_python_sdk/generated/models/webhook_event_type.py b/src/corbado_python_sdk/generated/models/webhook_event_type.py new file mode 100644 index 0000000..92d7c81 --- /dev/null +++ b/src/corbado_python_sdk/generated/models/webhook_event_type.py @@ -0,0 +1,39 @@ +# coding: utf-8 + +""" + Corbado Backend API + + # Introduction This documentation gives an overview of all Corbado Backend API calls to implement passwordless authentication with Passkeys. + + The version of the OpenAPI document: 2.0.0 + Contact: support@corbado.com + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import json +from enum import Enum +from typing_extensions import Self + + +class WebhookEventType(str, Enum): + """ + WebhookEventType + """ + + """ + allowed enum values + """ + PASSKEY_MINUS_LOGIN_DOT_COMPLETED = 'passkey-login.completed' + PASSKEY_DOT_CREATED = 'passkey.created' + PASSKEY_DOT_DELETED = 'passkey.deleted' + + @classmethod + def from_json(cls, json_str: str) -> Self: + """Create an instance of WebhookEventType from a JSON string""" + return cls(json.loads(json_str)) + + diff --git a/src/corbado_python_sdk/generated/rest.py b/src/corbado_python_sdk/generated/rest.py index d49d772..a621bf2 100644 --- a/src/corbado_python_sdk/generated/rest.py +++ b/src/corbado_python_sdk/generated/rest.py @@ -226,7 +226,7 @@ def request( headers=headers, preload_content=False ) - elif headers['Content-Type'] == 'text/plain' and isinstance(body, bool): + elif headers['Content-Type'].startswith('text/') and isinstance(body, bool): request_body = "true" if body else "false" r = self.pool_manager.request( method, diff --git a/src/corbado_python_sdk/services/implementation/identifier_service.py b/src/corbado_python_sdk/services/implementation/identifier_service.py index 581802e..2750135 100644 --- a/src/corbado_python_sdk/services/implementation/identifier_service.py +++ b/src/corbado_python_sdk/services/implementation/identifier_service.py @@ -94,9 +94,10 @@ def list_identifiers( encoded_type: StrictStr = urllib.parse.quote_plus(identifier_type) filters.append(f"identifierType:eq:{encoded_type}") if identifier_value: - encoded_value: StrictStr = urllib.parse.quote_plus(identifier_value) + # encoded_value: StrictStr = urllib.parse.quote_plus(identifier_value) filters = filters or [] - filters.append(f"identifierValue:eq:{encoded_value}") + # only works when the identifier_value is not url encoded + filters.append(f"identifierValue:eq:{identifier_value}") try: return self.client.identifier_list(sort=sort, filter=filters, page=page, page_size=page_size) except ApiException as e: From e99addab2c16c9dfbb4eb7013d28856ac1352729 Mon Sep 17 00:00:00 2001 From: Lukas Kratzel Date: Thu, 23 Jan 2025 14:21:06 +0100 Subject: [PATCH 24/30] enforce no trailing slashes in config urls --- src/corbado_python_sdk/utils/validators.py | 3 +++ tests/unit/test_config.py | 9 +++++---- tests/unit/test_session_service.py | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/corbado_python_sdk/utils/validators.py b/src/corbado_python_sdk/utils/validators.py index 2de236e..84a8604 100644 --- a/src/corbado_python_sdk/utils/validators.py +++ b/src/corbado_python_sdk/utils/validators.py @@ -27,6 +27,9 @@ def url_validator(url: str) -> str: if parts.username: raise ValueError("Assert failed: username needs to be empty") + if parts.path: + raise ValueError("Assert failed: path needs to be empty") + if parts.password: raise ValueError("Assert failed: password needs to be empty") diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 5db4e6d..db15e3e 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -13,7 +13,7 @@ def test_set_frontend_api(self): project_id="pro-123", api_secret="corbado1_123", frontend_api=frontend_api, - backend_api="https://backendapi.cloud.corbado.io/", + backend_api="https://backendapi.cloud.corbado.io", ) error = False except ValueError: @@ -24,7 +24,7 @@ def test_set_backend_api(self): for backend_api, valid in self.provide_urls(): try: config = Config( - project_id="pro-123", api_secret="corbado1_123", backend_api=backend_api, frontend_api="https://test.com/" + project_id="pro-123", api_secret="corbado1_123", backend_api=backend_api, frontend_api="https://test.com" ) config.backend_api = backend_api error = False @@ -33,8 +33,8 @@ def test_set_backend_api(self): self.assertEqual(valid, second=not error) def test_get_backend_api(self): - test_url = "https://backendapi.cloud.corbado.io/" - config = Config(project_id="pro-123", api_secret="corbado1_123", frontend_api="https://test.com/", backend_api=test_url) + test_url = "https://backendapi.cloud.corbado.io" + config = Config(project_id="pro-123", api_secret="corbado1_123", frontend_api="https://test.com", backend_api=test_url) self.assertTrue(config.backend_api.endswith("/v2")) def provide_urls(self): @@ -44,6 +44,7 @@ def provide_urls(self): ("http://auth.acme.com", False), # Only HTTPS ("https://user@auth.acme.com", False), # No user ("https://user:pass@auth.acme.com", False), # No user no password + ("https://auth.acme.com/", False), # No path ("https://auth.acme.com?xxx", False), # No query string ("https://auth.acme.com#xxx", False), # No fragment ("https://auth.acme.com", True), diff --git a/tests/unit/test_session_service.py b/tests/unit/test_session_service.py index a41f85d..79f0eca 100644 --- a/tests/unit/test_session_service.py +++ b/tests/unit/test_session_service.py @@ -270,7 +270,7 @@ def test_set_cname_expect_issuer_changed(self): api_secret="corbado1_XXX", project_id="pro-55", backend_api="https://backendapi.cloud.corbado.io/", - frontend_api="https://test.com/", + frontend_api="https://test.com", cname=test_cname, ) From 73a73ad0c6a66a4557d260ce11d4bf60653c1141 Mon Sep 17 00:00:00 2001 From: Lukas Kratzel Date: Thu, 23 Jan 2025 14:28:16 +0100 Subject: [PATCH 25/30] remove last_session_token_validation_result from session_service.py --- .../implementation/session_service.py | 24 +++---------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/src/corbado_python_sdk/services/implementation/session_service.py b/src/corbado_python_sdk/services/implementation/session_service.py index 76c4ba9..43adc3f 100644 --- a/src/corbado_python_sdk/services/implementation/session_service.py +++ b/src/corbado_python_sdk/services/implementation/session_service.py @@ -27,7 +27,6 @@ class SessionService(BaseModel): model_config (ConfigDict): Configuration dictionary for the model. issuer (str): Issuer of the session tokens. jwks_uri (str): URI of the JSON Web Key Set (JWKS) endpoint. - last_session_token_validation_result (str): Result of the last short session validation. _jwk_client (PyJWKClient): JSON Web Key (JWK) client for handling JWKS. project_id (str): Corbado Project Id. """ @@ -37,7 +36,6 @@ class SessionService(BaseModel): # Fields issuer: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] jwks_uri: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] - last_session_token_validation_result: str = "" project_id: str _jwk_client: PyJWKClient @@ -49,8 +47,7 @@ def __init__(self, **kwargs) -> None: # type: ignore Args: **kwargs: Additional keyword arguments to initialize the SessionService. These keyword arguments should include values for the attributes defined in the class, - such as 'issuer', 'jwks_uri', 'last_session_token_validation_result', - 'cache_keys',cache_jwk_set and 'session_token_cookie_length'. + such as 'issuer', 'jwks_uri', 'cache_keys', 'cache_jwk_set' and 'session_token_cookie_length'. Raises: Any errors raised during the initialization process. @@ -136,22 +133,6 @@ def validate_token(self, session_token: StrictStr) -> UserEntity: # TODO: Retrieve user status return UserEntity(fullName=full_name, userID=sub, status=UserStatus.ACTIVE) - def _set_issuer_mismatch_error(self, token_issuer: str) -> None: - """Set issuer mismatch error. - - Args: - token_issuer (str): Token issuer. - """ - self.last_session_token_validation_result = f"Mismatch in issuer (configured: {self.issuer}, JWT: {token_issuer})" - - def _set_validation_error(self, error: Exception) -> None: - """Set validation error. - - Args: - error (Exception): Exception occurred. - """ - self.last_session_token_validation_result = f"JWT validation failed: {error}" - # Private methods def _validate_issuer(self, token_issuer: str, session_token: str) -> None: """Validate issuer. @@ -166,7 +147,8 @@ def _validate_issuer(self, token_issuer: str, session_token: str) -> None: """ if not token_issuer: raise TokenValidationException( - error_type=ValidationErrorType.CODE_JWT_ISSUER_EMPTY, message=f"Issuer is empty. Session token: {session_token}" + error_type=ValidationErrorType.CODE_JWT_ISSUER_EMPTY, + message=f"Issuer is empty. Session token: {session_token}" ) # Check for old Frontend API (without .cloud.) From f842777a56dcc2e5508f0737cf12741170d93d96 Mon Sep 17 00:00:00 2001 From: Lukas Kratzel Date: Thu, 23 Jan 2025 14:32:28 +0100 Subject: [PATCH 26/30] satisfy flake8 --- tests/unit/test_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index db15e3e..59aab1f 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -44,7 +44,7 @@ def provide_urls(self): ("http://auth.acme.com", False), # Only HTTPS ("https://user@auth.acme.com", False), # No user ("https://user:pass@auth.acme.com", False), # No user no password - ("https://auth.acme.com/", False), # No path + ("https://auth.acme.com/", False), # No path ("https://auth.acme.com?xxx", False), # No query string ("https://auth.acme.com#xxx", False), # No fragment ("https://auth.acme.com", True), From 28345731576d19ebabc1300b5e59306d15a43c8c Mon Sep 17 00:00:00 2001 From: Lukas Kratzel Date: Thu, 23 Jan 2025 14:39:52 +0100 Subject: [PATCH 27/30] remove all usages of _set_validation_error --- .../services/implementation/session_service.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/corbado_python_sdk/services/implementation/session_service.py b/src/corbado_python_sdk/services/implementation/session_service.py index 43adc3f..f752ba5 100644 --- a/src/corbado_python_sdk/services/implementation/session_service.py +++ b/src/corbado_python_sdk/services/implementation/session_service.py @@ -82,7 +82,6 @@ def validate_token(self, session_token: StrictStr) -> UserEntity: try: signing_key: jwt.PyJWK = self._jwk_client.get_signing_key_from_jwt(token=session_token) except Exception as error: - self._set_validation_error(error=error) raise TokenValidationException( error_type=ValidationErrorType.CODE_JWT_SIGNING_KEY_ERROR, message=f"Could not retrieve signing key: {session_token}. See original_exception for further information: {str(error)}", @@ -98,14 +97,12 @@ def validate_token(self, session_token: StrictStr) -> UserEntity: sub: str = payload.get("sub") full_name: str = payload.get("name") except ImmatureSignatureError as error: - self._set_validation_error(error=error) raise TokenValidationException( error_type=ValidationErrorType.CODE_JWT_BEFORE, message=f"Error occured during token decode: {session_token}. {ValidationErrorType.CODE_JWT_BEFORE.value}", original_exception=error, ) except ExpiredSignatureError as error: - self._set_validation_error(error=error) raise TokenValidationException( error_type=ValidationErrorType.CODE_JWT_INVALID_SIGNATURE, message=f"Error occured during token decode: {session_token}. {ValidationErrorType.CODE_JWT_INVALID_SIGNATURE.value}", @@ -113,7 +110,6 @@ def validate_token(self, session_token: StrictStr) -> UserEntity: ) except InvalidSignatureError as error: - self._set_validation_error(error=error) raise TokenValidationException( error_type=ValidationErrorType.CODE_JWT_EXPIRED, message=f"Error occured during token decode: {session_token}. {ValidationErrorType.CODE_JWT_EXPIRED.value}", @@ -121,7 +117,6 @@ def validate_token(self, session_token: StrictStr) -> UserEntity: ) except Exception as error: - self._set_validation_error(error=error) raise TokenValidationException( error_type=ValidationErrorType.CODE_JWT_GENERAL, message=f"Error occured during token decode: {session_token}. See original_exception for further information: {str(error)}", From ccddb451e18a3029517414f90518b55d7340a92c Mon Sep 17 00:00:00 2001 From: Lukas Kratzel Date: Thu, 23 Jan 2025 14:42:03 +0100 Subject: [PATCH 28/30] adjust test_set_cname_expect_issuer_changed to new url validation expectations --- tests/unit/test_session_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_session_service.py b/tests/unit/test_session_service.py index 79f0eca..6c8122e 100644 --- a/tests/unit/test_session_service.py +++ b/tests/unit/test_session_service.py @@ -269,7 +269,7 @@ def test_set_cname_expect_issuer_changed(self): config: Config = Config( api_secret="corbado1_XXX", project_id="pro-55", - backend_api="https://backendapi.cloud.corbado.io/", + backend_api="https://backendapi.cloud.corbado.io", frontend_api="https://test.com", cname=test_cname, ) From 3ede93c1c97651ff1e916ca17117ed3d6a6444fb Mon Sep 17 00:00:00 2001 From: Lukas Kratzel Date: Thu, 23 Jan 2025 14:45:33 +0100 Subject: [PATCH 29/30] satisfy mypy --- src/corbado_python_sdk/exceptions/token_validation_exception.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/corbado_python_sdk/exceptions/token_validation_exception.py b/src/corbado_python_sdk/exceptions/token_validation_exception.py index 92823b4..7d0515f 100644 --- a/src/corbado_python_sdk/exceptions/token_validation_exception.py +++ b/src/corbado_python_sdk/exceptions/token_validation_exception.py @@ -57,7 +57,7 @@ def __init__(self, message: str, error_type: ValidationErrorType, original_excep super().__init__(message) self.message: str = message self.error_type: ValidationErrorType = error_type - self.original_exception: Exception | None = original_exception + self.original_exception: Optional[Exception] = original_exception def __str__(self) -> str: """Return a string representation of the validation error. From fe9f6cc5d2055f1e854375f0aa719416b2e327b5 Mon Sep 17 00:00:00 2001 From: Lukas Kratzel Date: Thu, 23 Jan 2025 15:38:40 +0100 Subject: [PATCH 30/30] remove unnecessary file --- local/backend_api_public_v2.yml | 1901 ------------------------------- 1 file changed, 1901 deletions(-) delete mode 100644 local/backend_api_public_v2.yml diff --git a/local/backend_api_public_v2.yml b/local/backend_api_public_v2.yml deleted file mode 100644 index eca7ac3..0000000 --- a/local/backend_api_public_v2.yml +++ /dev/null @@ -1,1901 +0,0 @@ -openapi: 3.0.3 - -################################################################### -# General # -################################################################### -info: - version: 2.0.0 - title: Corbado Backend API - description: | - - # Introduction - This documentation gives an overview of all Corbado Backend API calls to implement passwordless authentication with Passkeys. - - contact: - name: Corbado team - email: support@corbado.com - url: https://www.corbado.com - -servers: - - url: https://backendapi.corbado.io/v2 - -tags: - - name: Users - description: All API calls to manage users - - name: Sessions - description: All API calls to manage long and short sessions - - name: Challenges - description: All API calls to manage challenges - - name: Identifiers - description: All API calls to manage login identifiers - - name: Passkeys - description: All API calls for passkey flows - - name: AuthEvents - description: All API calls to manage authentication events - - name: PasskeyEvents - description: All API calls to manage passkey events - - name: ProjectConfig - description: All API calls to manage project configurations - - name: ConnectTokens - description: All API calls to manage connect tokens (they are used for Corbado Connect) - - name: PasskeyChallenges - description: All API calls to manage passkey challenges - -paths: - /users: - post: - description: Creates a new user - operationId: UserCreate - tags: - - Users - security: - - basicAuth: [ ] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/userCreateReq' - responses: - '200': - description: User has been created - content: - application/json: - schema: - $ref: '#/components/schemas/user' - default: - $ref: '#/components/responses/error' - - /users/{userID}: - get: - description: Returns a user - operationId: UserGet - tags: - - Users - security: - - basicAuth: [ ] - parameters: - - $ref: '#/components/parameters/userID' - responses: - '200': - description: User has been returned - content: - application/json: - schema: - $ref: '#/components/schemas/user' - default: - $ref: '#/components/responses/error' - patch: - description: Updates a user - operationId: UserUpdate - tags: - - Users - security: - - basicAuth: [ ] - parameters: - - $ref: '#/components/parameters/userID' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/userUpdateReq' - responses: - '200': - description: User has been updated - content: - application/json: - schema: - $ref: '#/components/schemas/user' - default: - $ref: '#/components/responses/error' - delete: - description: Deletes a user - operationId: UserDelete - tags: - - Users - security: - - basicAuth: [ ] - parameters: - - $ref: '#/components/parameters/userID' - responses: - '200': - $ref: '#/components/responses/200' - default: - $ref: '#/components/responses/error' - - /users/{userID}/longSessions: - post: - description: Create a new long session - operationId: LongSessionCreate - tags: - - Sessions - security: - - basicAuth: [ ] - parameters: - - $ref: '#/components/parameters/userID' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/longSessionCreateReq' - responses: - '200': - description: Long session has been created - content: - application/json: - schema: - $ref: '#/components/schemas/longSession' - default: - $ref: '#/components/responses/error' - - /users/{userID}/longSessions/{longSessionID}: - get: - description: Retrieves a long session by ID and user ID - operationId: UserLongSessionGet - tags: - - Sessions - security: - - basicAuth: [ ] - parameters: - - $ref: '#/components/parameters/userID' - - $ref: '#/components/parameters/longSessionID' - responses: - '200': - description: Long session has been returned - content: - application/json: - schema: - $ref: '#/components/schemas/longSession' - default: - $ref: '#/components/responses/error' - patch: - description: Updates long session status - operationId: LongSessionUpdate - tags: - - Sessions - security: - - basicAuth: [ ] - parameters: - - $ref: '#/components/parameters/userID' - - $ref: '#/components/parameters/longSessionID' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/longSessionUpdateReq' - responses: - '200': - description: Long session has been updated - content: - application/json: - schema: - $ref: '#/components/schemas/longSession' - default: - $ref: '#/components/responses/error' - - /users/{userID}/longSessions/{longSessionID}/shortSessions: - post: - description: Create a new short session - operationId: ShortSessionCreate - tags: - - Sessions - security: - - basicAuth: [ ] - parameters: - - $ref: '#/components/parameters/userID' - - $ref: '#/components/parameters/longSessionID' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/shortSessionCreateReq' - responses: - '200': - description: Short session has been created - content: - application/json: - schema: - $ref: '#/components/schemas/shortSession' - default: - $ref: '#/components/responses/error' - - /users/{userID}/challenges: - post: - description: Create a new challenge to verify a login identifier - operationId: ChallengeCreate - tags: - - Challenges - security: - - basicAuth: [ ] - parameters: - - $ref: '#/components/parameters/userID' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/challengeCreateReq' - responses: - '200': - description: Challenge has been created - content: - application/json: - schema: - $ref: '#/components/schemas/challenge' - default: - $ref: '#/components/responses/error' - - /users/{userID}/challenges/{challengeID}: - patch: - description: Updates a challenge (e.g. from pending to completed) - operationId: ChallengeUpdate - tags: - - Challenges - security: - - basicAuth: [ ] - parameters: - - $ref: '#/components/parameters/userID' - - $ref: '#/components/parameters/challengeID' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/challengeUpdateReq' - responses: - '200': - description: Challenge has been updated - content: - application/json: - schema: - $ref: '#/components/schemas/challenge' - default: - $ref: '#/components/responses/error' - - /users/{userID}/identifiers: - post: - description: Create a new login identifier - operationId: IdentifierCreate - tags: - - Identifiers - security: - - basicAuth: [ ] - parameters: - - $ref: '#/components/parameters/userID' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/identifierCreateReq' - responses: - '200': - description: Identifier has been created - content: - application/json: - schema: - $ref: '#/components/schemas/identifier' - default: - $ref: '#/components/responses/error' - - /users/{userID}/identifiers/{identifierID}: - delete: - description: Delete an existing login identifier - operationId: IdentifierDelete - tags: - - Identifiers - security: - - basicAuth: [ ] - parameters: - - $ref: '#/components/parameters/userID' - - $ref: '#/components/parameters/identifierID' - responses: - '200': - $ref: '#/components/responses/200' - default: - $ref: '#/components/responses/error' - patch: - description: Updates a login identifier (e.g. from pending to verified) - operationId: IdentifierUpdate - tags: - - Identifiers - security: - - basicAuth: [ ] - parameters: - - $ref: '#/components/parameters/userID' - - $ref: '#/components/parameters/identifierID' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/identifierUpdateReq' - responses: - '200': - description: Identifier has been updated - content: - application/json: - schema: - $ref: '#/components/schemas/identifier' - default: - $ref: '#/components/responses/error' - - /users/{userID}/socialAccounts: - get: - description: Returns a list of social accounts - operationId: UserSocialAccountList - tags: - - Users - security: - - basicAuth: [ ] - parameters: - - $ref: '#/components/parameters/userID' - - $ref: 'common.yml#/components/parameters/sort' - - $ref: 'common.yml#/components/parameters/filter' - - $ref: 'common.yml#/components/parameters/page' - - $ref: 'common.yml#/components/parameters/pageSize' - responses: - '200': - description: List of social accounts - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/socialAccount' - default: - $ref: '#/components/responses/error' - post: - description: Creates a new social account - operationId: SocialAccountCreate - tags: - - Users - security: - - basicAuth: [ ] - parameters: - - $ref: '#/components/parameters/userID' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/socialAccountCreateReq' - responses: - '200': - description: Social account has been created - content: - application/json: - schema: - $ref: '#/components/schemas/socialAccount' - default: - $ref: '#/components/responses/error' - - /users/{userID}/credentials: - get: - description: Returns a list of credentials (passkeys) - operationId: CredentialList - tags: - - Users - security: - - basicAuth: [ ] - parameters: - - $ref: '#/components/parameters/userID' - - $ref: 'common.yml#/components/parameters/sort' - - $ref: 'common.yml#/components/parameters/filter' - - $ref: 'common.yml#/components/parameters/page' - - $ref: 'common.yml#/components/parameters/pageSize' - responses: - '200': - description: List of credentials (passkeys) - content: - application/json: - schema: - $ref: '#/components/schemas/credentialList' - default: - $ref: '#/components/responses/error' - - /users/{userID}/credentials/{credentialID}: - delete: - description: Deletes an existing credential (passkey) - operationId: CredentialDelete - tags: - - Users - security: - - basicAuth: [ ] - parameters: - - $ref: '#/components/parameters/userID' - - $ref: '#/components/parameters/credentialID' - responses: - '200': - $ref: '#/components/responses/200' - default: - $ref: '#/components/responses/error' - - /users/{userID}/authEvents: - post: - description: Create a new authentication event for a user - operationId: AuthEventCreate - tags: - - AuthEvents - security: - - basicAuth: [ ] - parameters: - - $ref: '#/components/parameters/userID' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/authEventCreateReq' - responses: - '200': - description: Auth event has been created - content: - application/json: - schema: - $ref: '#/components/schemas/authEvent' - default: - $ref: '#/components/responses/error' - - /users/{userID}/passkeyEvents: - post: - description: Create a new passkey event for a user - operationId: PasskeyEventCreate - tags: - - PasskeyEvents - security: - - basicAuth: [ ] - parameters: - - $ref: '#/components/parameters/userID' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/passkeyEventCreateReq' - responses: - '200': - description: Passkey event has been created - content: - application/json: - schema: - $ref: '#/components/schemas/passkeyEvent' - default: - $ref: '#/components/responses/error' - get: - description: Returns a list of matching passkey events - operationId: PasskeyEventList - tags: - - PasskeyEvents - security: - - basicAuth: [ ] - parameters: - - $ref: 'common.yml#/components/parameters/sort' - - $ref: 'common.yml#/components/parameters/filter' - - $ref: 'common.yml#/components/parameters/page' - - $ref: 'common.yml#/components/parameters/pageSize' - - $ref: '#/components/parameters/userID' - responses: - '200': - description: List of all matching passkey events - content: - application/json: - schema: - $ref: '#/components/schemas/passkeyEventList' - default: - $ref: '#/components/responses/error' - - /users/{userID}/passkeyEvents/{passkeyEventID}: - delete: - description: Deletes an existing passkey event - operationId: PasskeyEventDelete - tags: - - PasskeyEvents - security: - - basicAuth: [ ] - parameters: - - $ref: '#/components/parameters/userID' - - $ref: '#/components/parameters/passkeyEventID' - responses: - '200': - $ref: '#/components/responses/200' - default: - $ref: '#/components/responses/error' - - /users/{userID}/passkeyChallenges: - get: - description: Returns a list of matching passkey challenges - operationId: PasskeyChallengeList - tags: - - PasskeyChallenges - security: - - basicAuth: [ ] - parameters: - - $ref: 'common.yml#/components/parameters/sort' - - $ref: 'common.yml#/components/parameters/filter' - - $ref: 'common.yml#/components/parameters/page' - - $ref: 'common.yml#/components/parameters/pageSize' - - $ref: '#/components/parameters/userID' - responses: - '200': - description: List of all matching passkey challenges - content: - application/json: - schema: - $ref: '#/components/schemas/passkeyChallengeList' - default: - $ref: '#/components/responses/error' - - /users/{userID}/passkeyChallenges/{passkeyChallengeID}: - patch: - description: Updates a passkey challenge - operationId: PasskeyChallengeUpdate - tags: - - PasskeyChallenges - security: - - basicAuth: [ ] - parameters: - - $ref: '#/components/parameters/userID' - - $ref: '#/components/parameters/passkeyChallengeID' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/passkeyChallengeUpdateReq' - responses: - '200': - description: Passkey challenge has been updated - content: - application/json: - schema: - $ref: '#/components/schemas/passkeyChallenge' - default: - $ref: '#/components/responses/error' - - /longSessions/{longSessionID}: - get: - description: Retrieves a long session by ID - operationId: LongSessionGet - tags: - - Sessions - security: - - basicAuth: [ ] - parameters: - - $ref: '#/components/parameters/longSessionID' - responses: - '200': - description: Long session has been returned - content: - application/json: - schema: - $ref: '#/components/schemas/longSession' - default: - $ref: '#/components/responses/error' - - /identifiers: - get: - description: Returns a list of matching identifiers - operationId: IdentifierList - tags: - - Identifiers - security: - - basicAuth: [ ] - parameters: - - $ref: 'common.yml#/components/parameters/sort' - - $ref: 'common.yml#/components/parameters/filter' - - $ref: 'common.yml#/components/parameters/page' - - $ref: 'common.yml#/components/parameters/pageSize' - responses: - '200': - description: List of all matching identifiers - content: - application/json: - schema: - $ref: '#/components/schemas/identifierList' - default: - $ref: '#/components/responses/error' - - /socialAccounts: - get: - description: Returns a list of social accounts - operationId: SocialAccountList - tags: - - Users - security: - - basicAuth: [ ] - parameters: - - $ref: 'common.yml#/components/parameters/sort' - - $ref: 'common.yml#/components/parameters/filter' - - $ref: 'common.yml#/components/parameters/page' - - $ref: 'common.yml#/components/parameters/pageSize' - responses: - '200': - description: List of social accounts - content: - application/json: - schema: - $ref: '#/components/schemas/socialAccountList' - default: - $ref: '#/components/responses/error' - - /projectConfig/cname: - put: - description: Update project config CNAME and generates new SSL certificate - operationId: ProjectConfigUpdateCNAME - tags: - - ProjectConfig - security: - - basicAuth: [ ] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/projectConfigUpdateCnameReq' - responses: - '200': - $ref: '#/components/responses/200' - default: - $ref: '#/components/responses/error' - - /connectTokens: - post: - description: Create a new connect token - operationId: ConnectTokenCreate - tags: - - ConnectTokens - security: - - basicAuth: [ ] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/connectTokenCreateReq' - responses: - '200': - description: Connect token has been created - content: - application/json: - schema: - $ref: '#/components/schemas/connectToken' - default: - $ref: '#/components/responses/error' - get: - description: Returns a list of matching append tokens - operationId: ConnectTokenList - tags: - - ConnectTokens - security: - - basicAuth: [ ] - parameters: - - $ref: 'common.yml#/components/parameters/sort' - - $ref: 'common.yml#/components/parameters/filter' - - $ref: 'common.yml#/components/parameters/page' - - $ref: 'common.yml#/components/parameters/pageSize' - responses: - '200': - description: List of all matching append tokens - content: - application/json: - schema: - $ref: '#/components/schemas/connectTokenList' - default: - $ref: '#/components/responses/error' - - /connectTokens/{connectTokenID}: - patch: - description: Updates an existing append token - operationId: ConnectTokenUpdate - tags: - - ConnectTokens - security: - - basicAuth: [ ] - parameters: - - $ref: '#/components/parameters/connectTokenID' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/connectTokenUpdateReq' - responses: - '200': - $ref: '#/components/responses/200' - default: - $ref: '#/components/responses/error' - delete: - description: Deletes an existing append token - operationId: ConnectTokenDelete - tags: - - ConnectTokens - security: - - basicAuth: [ ] - parameters: - - $ref: '#/components/parameters/connectTokenID' - responses: - '200': - $ref: '#/components/responses/200' - default: - $ref: '#/components/responses/error' - - ################################################################### - # Outliers (non-rest calls) # - ################################################################### - - /passkey/append/start: - post: - description: Starts a challenge for creating a new passkey - operationId: PasskeyAppendStart - tags: - - Passkeys - security: - - basicAuth: [ ] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/passkeyAppendStartReq' - responses: - '200': - description: Passkey append challenge has been created - content: - application/json: - schema: - $ref: '#/components/schemas/passkeyAppendStartRsp' - default: - $ref: '#/components/responses/error' - - /passkey/append/finish: - post: - description: Completes a challenge for creating a new passkey - operationId: PasskeyAppendFinish - tags: - - Passkeys - security: - - basicAuth: [ ] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/passkeyAppendFinishReq' - responses: - '200': - description: Passkey append succeeded - content: - application/json: - schema: - $ref: '#/components/schemas/passkeyAppendFinishRsp' - default: - $ref: '#/components/responses/error' - - /passkey/login/start: - post: - description: Starts a challenge for an existing passkey - operationId: PasskeyLoginStart - tags: - - Passkeys - security: - - basicAuth: [ ] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/passkeyLoginStartReq' - responses: - '200': - description: Passkey login challenge has been created - content: - application/json: - schema: - $ref: '#/components/schemas/passkeyLoginStartRsp' - default: - $ref: '#/components/responses/error' - - /passkey/login/finish: - post: - description: Completes a challenge for an existing passkey - operationId: PasskeyLoginFinish - tags: - - Passkeys - security: - - basicAuth: [ ] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/passkeyLoginFinishReq' - responses: - '200': - description: Passkey login succeeded - content: - application/json: - schema: - $ref: '#/components/schemas/passkeyLoginFinishRsp' - default: - $ref: '#/components/responses/error' - - /passkey/mediation/start: - post: - description: Starts a challenge for an existing passkey (Conditional UI) - operationId: PasskeyMediationStart - tags: - - Passkeys - security: - - basicAuth: [ ] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/passkeyMediationStartReq' - responses: - '200': - description: Passkey login challenge has been created - content: - application/json: - schema: - $ref: '#/components/schemas/passkeyMediationStartRsp' - default: - $ref: '#/components/responses/error' - - /passkey/mediation/finish: - post: - description: Completes a challenge for an existing passkey (Conditional UI) - operationId: PasskeyMediationFinish - tags: - - Passkeys - security: - - basicAuth: [ ] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/passkeyMediationFinishReq' - responses: - '200': - description: Passkey mediation has been success, thus we can return a userID - content: - application/json: - schema: - $ref: '#/components/schemas/passkeyMediationFinishRsp' - default: - $ref: '#/components/responses/error' - -components: - securitySchemes: - basicAuth: - type: http - scheme: basic - - parameters: - userID: - name: userID - in: path - description: ID of user - required: true - schema: - type: string - - challengeID: - name: challengeID - in: path - description: ID of challenge - required: true - schema: - type: string - - longSessionID: - name: longSessionID - in: path - description: ID of long session - required: true - schema: - type: string - - identifierID: - name: identifierID - in: path - description: ID of login identifier - required: true - schema: - type: string - - credentialID: - name: credentialID - in: path - description: ID of credential - required: true - schema: - type: string - - connectTokenID: - name: connectTokenID - in: path - description: ID of an append token - required: true - schema: - type: string - - passkeyChallengeID: - name: passkeyChallengeID - in: path - description: ID of a passkey challenge - required: true - schema: - type: string - - passkeyEventID: - name: passkeyEventID - in: path - description: ID of a passkey event - required: true - schema: - type: string - - schemas: - ################################################################### - # Request/Response bodies # - ################################################################### - userCreateReq: - type: object - required: - - status - properties: - fullName: - type: string - status: - $ref: '#/components/schemas/userStatus' - explicitWebauthnID: - type: string - description: For connect projects, the webauthnID can be explicitly set for a user - - userUpdateReq: - type: object - properties: - fullName: - type: string - status: - $ref: '#/components/schemas/userStatus' - - longSessionCreateReq: - type: object - required: - - appType - - identifierValue - properties: - appType: - $ref: 'common.yml#/components/schemas/appType' - identifierValue: - type: string - - longSessionUpdateReq: - type: object - required: - - status - properties: - status: - $ref: '#/components/schemas/longSessionStatus' - - shortSessionCreateReq: - type: object - required: - - appType - - issuer - properties: - appType: - $ref: 'common.yml#/components/schemas/appType' - issuer: - type: string - - challengeCreateReq: - type: object - required: - - challengeType - - identifierValue - - clientInformation - properties: - challengeType: - $ref: '#/components/schemas/challengeType' - identifierValue: - type: string - challengeMetadata: - type: object - clientInformation: - $ref: '#/components/schemas/clientInformation' - - challengeUpdateReq: - type: object - required: - - value - properties: - value: - type: string - - identifierCreateReq: - type: object - required: - - identifierType - - identifierValue - - status - properties: - identifierType: - $ref: '#/components/schemas/identifierType' - identifierValue: - type: string - status: - $ref: '#/components/schemas/identifierStatus' - - identifierUpdateReq: - type: object - required: - - status - properties: - status: - $ref: '#/components/schemas/identifierStatus' - - passkeyAppendStartReq: - type: object - required: - - userID - - processID - - username - - clientInformation - - passkeyIntelFlags - properties: - userID: - $ref: 'common.yml#/components/schemas/userID' - processID: - type: string - username: - type: string - clientInformation: - $ref: '#/components/schemas/clientInformation' - passkeyIntelFlags: - $ref: '#/components/schemas/passkeyIntelFlags' - - passkeyAppendStartRsp: - type: object - required: - - appendAllow - - detectionTags - - decisionTag - - attestationOptions - properties: - appendAllow: - type: boolean - detectionTags: - type: array - items: - $ref: '#/components/schemas/detectionTag' - decisionTag: - $ref: '#/components/schemas/decisionTag' - attestationOptions: - type: string - example: '{"publicKey":{"challenge":"2m6...0w9/MgW...KE=","rp":{"name":"demo","id":"localhost"},"user":{"name":"example@mail.com","id":"dXN...zk5"},"pubKeyCredParams":[{"type":"public-key","alg":-7},{"type":"public-key","alg":-35},{"type":"public-key","alg":-36},{"type":"public-key","alg":-257},{"type":"public-key","alg":-258},{"type":"public-key","alg":-259},{"type":"public-key","alg":-37},{"type":"public-key","alg":-38},{"type":"public-key","alg":-39},{"type":"public-key","alg":-8}],"authenticatorSelection":{"authenticatorAttachment":"platform","requireResidentKey":false,"userVerification":"required"},"timeout":60000,"attestation":"none"}}' - - passkeyAppendFinishReq: - type: object - required: - - userID - - processID - - attestationResponse - - clientInformation - properties: - userID: - $ref: 'common.yml#/components/schemas/userID' - processID: - type: string - attestationResponse: - type: string - example: '{"type":"public-key","id":"JM6...J_Q","rawId":"JM6...J_Q","authenticatorAttachment":null,"response":{"clientDataJSON":"eyJ...ZX0","authenticatorData":"SZY...AAQ","signature":"Ni7...YAg","userHandle":"dXN...zk5"},"clientExtensionResults":{}}' - clientInformation: - $ref: '#/components/schemas/clientInformation' - sendNotification: - type: boolean - - passkeyAppendFinishRsp: - type: object - required: - - passkeyData - properties: - passkeyData: - $ref: '#/components/schemas/passkeyData' - - passkeyLoginStartReq: - type: object - required: - - userID - - clientInformation - - crossDeviceAuthenticationStrategy - - processID - properties: - userID: - $ref: 'common.yml#/components/schemas/userID' - clientInformation: - $ref: '#/components/schemas/clientInformation' - crossDeviceAuthenticationStrategy: - $ref: '#/components/schemas/crossDeviceAuthenticationStrategy' - processID: - type: string - - passkeyLoginStartRsp: - type: object - required: - - loginAllow - - detectionTags - - decisionTag - - assertionOptions - - isCDACandidate - properties: - loginAllow: - type: boolean - detectionTags: - type: array - items: - $ref: '#/components/schemas/detectionTag' - decisionTag: - $ref: '#/components/schemas/decisionTag' - assertionOptions: - type: string - isCDACandidate: - type: boolean - - passkeyLoginFinishReq: - type: object - required: - - userID - - assertionResponse - - clientInformation - - processID - properties: - userID: - $ref: 'common.yml#/components/schemas/userID' - assertionResponse: - type: string - clientInformation: - $ref: '#/components/schemas/clientInformation' - processID: - type: string - - passkeyLoginFinishRsp: - type: object - required: - - passkeyData - properties: - passkeyData: - $ref: '#/components/schemas/passkeyData' - - passkeyMediationStartReq: - type: object - required: - - clientInformation - properties: - clientInformation: - $ref: '#/components/schemas/clientInformation' - - passkeyMediationStartRsp: - type: object - required: - - loginAllow - - assertionOptions - properties: - loginAllow: - type: boolean - assertionOptions: - type: string - - passkeyMediationFinishReq: - type: object - required: - - assertionResponse - - clientInformation - - processID - properties: - assertionResponse: - type: string - clientInformation: - $ref: '#/components/schemas/clientInformation' - processID: - type: string - - passkeyMediationFinishRsp: - type: object - required: - - passkeyData - properties: - passkeyData: - $ref: '#/components/schemas/passkeyData' - - connectTokenCreateReq: - type: object - required: - - type - - data - properties: - type: - $ref: '#/components/schemas/connectTokenType' - data: - $ref: '#/components/schemas/connectTokenData' - maxLifetimeInSeconds: - type: integer - example: 3600 - - connectTokenUpdateReq: - type: object - required: - - status - properties: - status: - $ref: '#/components/schemas/connectTokenStatus' - - ################################################################### - # Entities # - ################################################################### - - passkeyData: - type: object - required: - - id - - userID - - username - - ceremonyType - - challengeID - properties: - id: - type: string - userID: - type: string - username: - type: string - ceremonyType: - type: string - enum: [ 'local', 'cda', 'security-key' ] - challengeID: - type: string - - user: - type: object - required: - - userID - - status - properties: - userID: - type: string - fullName: - type: string - status: - $ref: '#/components/schemas/userStatus' - explicitWebauthnID: - type: string - - longSession: - type: object - required: - - longSessionID - - userID - - identifierValue - - status - - expires - properties: - longSessionID: - type: string - userID: - type: string - identifierValue: - type: string - status: - $ref: '#/components/schemas/longSessionStatus' - expires: - type: string - - shortSession: - type: object - required: - - value - properties: - value: - type: string - - identifier: - type: object - required: - - identifierID - - type - - value - - status - - userID - properties: - identifierID: - type: string - type: - $ref: '#/components/schemas/identifierType' - value: - type: string - status: - $ref: '#/components/schemas/identifierStatus' - userID: - type: string - - identifierList: - type: object - required: - - identifiers - - paging - properties: - identifiers: - type: array - items: - $ref: '#/components/schemas/identifier' - paging: - $ref: 'common.yml#/components/schemas/paging' - - socialAccountCreateReq: - type: object - required: - - providerType - - identifierValue - - foreignID - - avatarURL - - fullName - properties: - providerType: - $ref: 'common.yml#/components/schemas/socialProviderType' - identifierValue: - type: string - foreignID: - type: string - avatarURL: - type: string - fullName: - type: string - - socialAccount: - type: object - required: - - socialAccountID - - providerType - - identifierValue - - userID - - foreignID - - avatarURL - - fullName - properties: - socialAccountID: - type: string - providerType: - type: string - identifierValue: - type: string - userID: - type: string - foreignID: - type: string - avatarURL: - type: string - fullName: - type: string - - socialAccountList: - type: object - required: - - socialAccounts - - paging - properties: - socialAccounts: - type: array - items: - $ref: '#/components/schemas/socialAccount' - paging: - $ref: 'common.yml#/components/schemas/paging' - - credential: - type: object - required: - - id - - credentialID - - attestationType - - transport - - backupEligible - - backupState - - authenticatorAAGUID - - sourceOS - - sourceBrowser - - lastUsed - - created - - status - properties: - id: - type: string - example: "cre-12345" - credentialID: - type: string - attestationType: - type: string - transport: - type: array - items: - type: string - enum: [ 'usb', 'nfc', 'ble', 'internal', 'hybrid', 'smart-card' ] - backupEligible: - type: boolean - backupState: - type: boolean - authenticatorAAGUID: - type: string - sourceOS: - type: string - sourceBrowser: - type: string - lastUsed: - type: string - description: Timestamp of when the passkey was last used in yyyy-MM-dd'T'HH:mm:ss format - created: - $ref: 'common.yml#/components/schemas/created' - status: - type: string - enum: [ 'pending', 'active' ] - description: "Status" - - credentialList: - type: object - required: - - credentials - - paging - properties: - credentials: - type: array - items: - $ref: '#/components/schemas/credential' - paging: - $ref: 'common.yml#/components/schemas/paging' - - challenge: - type: object - required: - - challengeID - - type - - identifierValue - - value - - status - properties: - challengeID: - type: string - type: - $ref: '#/components/schemas/challengeType' - identifierValue: - type: string - value: - type: string - status: - $ref: '#/components/schemas/challengeStatus' - - authEventCreateReq: - type: object - required: - - username - - eventType - - method - - status - - clientInformation - properties: - username: - type: string - eventType: - $ref: '#/components/schemas/authEventType' - method: - $ref: '#/components/schemas/authEventMethod' - status: - $ref: '#/components/schemas/authEventStatus' - clientInformation: - $ref: '#/components/schemas/clientInformation' - - authEvent: - type: object - required: - - authEventID - - userID - - username - - eventType - - method - - created - - status - properties: - authEventID: - type: string - userID: - $ref: 'common.yml#/components/schemas/userID' - username: - type: string - eventType: - $ref: '#/components/schemas/authEventType' - method: - $ref: '#/components/schemas/authEventMethod' - created: - $ref: 'common.yml#/components/schemas/created' - status: - $ref: '#/components/schemas/authEventStatus' - - passkeyEventCreateReq: - type: object - required: - - eventType - properties: - eventType: - $ref: '#/components/schemas/passkeyEventType' - expires: - type: integer - processID: - type: string - clientEnvID: - type: string - credentialID: - type: string - - passkeyEvent: - type: object - required: - - passkeyEventID - - userID - - eventType - - created - properties: - passkeyEventID: - type: string - userID: - $ref: 'common.yml#/components/schemas/userID' - eventType: - $ref: '#/components/schemas/passkeyEventType' - clientEnvID: - type: string - processID: - type: string - credentialID: - type: string - expires: - type: integer - created: - $ref: 'common.yml#/components/schemas/created' - - passkeyEventList: - type: object - required: - - passkeyEvents - - paging - properties: - passkeyEvents: - type: array - items: - $ref: '#/components/schemas/passkeyEvent' - paging: - $ref: 'common.yml#/components/schemas/paging' - - projectConfigUpdateCnameReq: - type: object - required: - - cname - properties: - cname: - type: string - - detectionTag: - type: object - required: - - category - - name - properties: - category: - type: string - enum: [ 'support', 'clientEnv', 'history', 'passkey' ] - name: - type: string - - decisionTag: - type: string - enum: - - env-no-pk-support - - user-no-pks - - user-login-blacklisted - - user-security-key - - user-positive-env-history - - user-negative-env-history - - env-blacklisted - - user-platform-pk-high-confidence - - user-cross-platform-pk-high-confidence - - user-env-no-pks - - default-deny - - passkey-list-initiated-process - - user-append-blacklisted - - process-pk-login-sk-completed - - process-pk-login-platform-completed - - process-pk-login-not-offered - - process-pk-login-incomplete - - process-pk-login-cross-platform-completed - - clientInformation: - type: object - required: - - remoteAddress - - userAgent - - userVerifyingPlatformAuthenticatorAvailable - properties: - remoteAddress: - description: Client's IP address - type: string - example: '::ffff:172.18.0.1' - userAgent: - description: Client's user agent - type: string - example: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36' - clientEnvHandle: - description: Client's environment handle - type: string - javascriptFingerprint: - description: Client's fingerprint - type: string - javaScriptHighEntropy: - $ref: '#/components/schemas/javaScriptHighEntropy' - bluetoothAvailable: - description: Client's Bluetooth availability - type: boolean - passwordManagerAvailable: - description: Client's password manager availability - type: boolean - userVerifyingPlatformAuthenticatorAvailable: - type: boolean - - passkeyIntelFlags: - type: object - required: - - forcePasskeyAppend - properties: - forcePasskeyAppend: - type: boolean - - javaScriptHighEntropy: - type: object - required: - - platform - - platformVersion - - mobile - properties: - platform: - type: string - platformVersion: - type: string - mobile: - type: boolean - - passkeyChallengeUpdateReq: - type: object - required: - - status - properties: - status: - $ref: '#/components/schemas/passkeyChallengeStatus' - - passkeyChallengeList: - type: object - required: - - passkeyChallenges - - paging - properties: - passkeyChallenges: - type: array - items: - $ref: '#/components/schemas/passkeyChallenge' - paging: - $ref: 'common.yml#/components/schemas/paging' - - passkeyChallenge: - type: object - required: - - challengeID - - type - - value - - status - - created - - expires - properties: - challengeID: - type: string - type: - $ref: '#/components/schemas/passkeyChallengeType' - value: - type: string - status: - $ref: '#/components/schemas/passkeyChallengeStatus' - created: - type: integer - format: int64 - expires: - type: integer - format: int64 - - passkeyChallengeType: - type: string - enum: [ 'register', 'authenticate' ] - - passkeyChallengeStatus: - type: string - enum: [ 'pending', 'completed', 'consumed' ] - - userStatus: - type: string - enum: [ 'pending', 'active', 'disabled' ] - - longSessionStatus: - type: string - enum: [ 'active', 'logged_out', 'expired', 'inactivity_reached', 'revoked' ] - - challengeType: - type: string - enum: [ 'email_otp', 'email_link', 'sms_otp' ] - - challengeStatus: - type: string - enum: [ 'pending', 'completed', 'expired' ] - - identifierType: - type: string - enum: [ 'email', 'phone', 'username' ] - - identifierStatus: - type: string - enum: [ 'pending', 'primary', 'verified' ] - - crossDeviceAuthenticationStrategy: - type: string - enum: [ 'standard', 'minimize', 'maximize' ] - - connectTokenStatus: - type: string - enum: [ 'initial', 'consumed' ] - - connectTokenType: - type: string - enum: [ 'passkey-append', 'passkey-delete', 'passkey-list' ] - - authEventMethod: - type: string - enum: [ 'password', 'email_otp', 'email_link', 'phone_otp', 'passkey', 'social_github', 'social_google', 'social_microsoft' ] - - authEventType: - type: string - enum: [ 'sign_up', 'login', 'new_passkey_added' ] - - authEventStatus: - type: string - enum: [ 'success', 'failure' ] - - passkeyEventType: - type: string - enum: - - user-login-blacklisted - - login-explicit-abort - - login-error - - login-one-tap-switch - - user-append-after-cross-platform-blacklisted - - user-append-after-login-error-blacklisted - - connectToken: - type: object - required: - - id - - tokenType - - data - - connectTokenStatus - - expires - properties: - id: - type: string - tokenType: - $ref: '#/components/schemas/connectTokenType' - data: - $ref: '#/components/schemas/connectTokenData' - connectTokenStatus: - $ref: '#/components/schemas/connectTokenStatus' - secret: - type: string - expires: - type: integer - - connectTokenDataPasskeyAppend: - type: object - required: - - displayName - - identifier - properties: - displayName: - type: string - identifier: - type: string - - connectTokenDataPasskeyDelete: - type: object - required: - - identifier - properties: - identifier: - type: string - - connectTokenDataPasskeyList: - type: object - required: - - identifier - properties: - identifier: - type: string - - connectTokenData: - type: object - oneOf: - - $ref: '#/components/schemas/connectTokenDataPasskeyAppend' - - $ref: '#/components/schemas/connectTokenDataPasskeyDelete' - - $ref: '#/components/schemas/connectTokenDataPasskeyList' - - connectTokenList: - type: object - required: - - connectTokens - - paging - properties: - connectTokens: - type: array - items: - $ref: '#/components/schemas/connectToken' - paging: - $ref: 'common.yml#/components/schemas/paging' - - responses: - ################################################################### - # Responses: Error # - ################################################################### - error: - description: Error - content: - application/json: - schema: - $ref: 'common.yml#/components/schemas/errorRsp' - '200': - description: Operation succeeded - content: - application/json: - schema: - $ref: 'common.yml#/components/schemas/genericRsp' - -################################################################### -# Security # -################################################################### -security: - - basicAuth: [ ]