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 diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index ab57341..292a8f2 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -4,12 +4,12 @@ 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: branches: [ "main","development"] - types: [opened, reopened] + types: [opened, reopened, edited] jobs: test-and-lint: @@ -48,9 +48,11 @@ jobs: run: | tox run env: - CORBADO_BACKEND_API: ${{ vars.CORBADO_BACKEND_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 }} + build: diff --git a/README.md b/README.md index 363b9fd..e1a46ea 100644 --- a/README.md +++ b/README.md @@ -57,19 +57,23 @@ 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 'SessionValidationResult' as result of token validation. You can check whether any errors occurred and handle them if needed: +`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 -result: SessionValidationResult = self.session_service.get_and_validate_short_session_value(short_session=token) - if result.error is not None: - print(result.error) - raise result.error +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: 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 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/__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/config.py b/src/corbado_python_sdk/config.py index a794259..aacf02d 100644 --- a/src/corbado_python_sdk/config.py +++ b/src/corbado_python_sdk/config.py @@ -16,8 +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". - short_session_cookie_name (str): The name of the cookie for short session management. Defaults to "cbo_short_session". + frontend_api (str): The base URL for the frontend API. + backend_api (str): The base URL for the backend API. """ # Make sure that field assignments are also validated, use "set_assignment_validation(False)" @@ -27,15 +27,16 @@ class Config(BaseModel): # Fields project_id: str api_secret: str + frontend_api: str + backend_api: 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 - @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 +44,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 +55,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 +133,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/src/corbado_python_sdk/corbado_sdk.py b/src/corbado_python_sdk/corbado_sdk.py index da75bd0..fb8448b 100644 --- a/src/corbado_python_sdk/corbado_sdk.py +++ b/src/corbado_python_sdk/corbado_sdk.py @@ -70,9 +70,9 @@ def sessions(self) -> SessionService: """ if not self._sessions: self._sessions = 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/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/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) 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..7d0515f --- /dev/null +++ b/src/corbado_python_sdk/exceptions/token_validation_exception.py @@ -0,0 +1,74 @@ +from enum import Enum + +from typing_extensions import Optional + + +class ValidationErrorType(Enum): + """ + Enum representing types of validation errors. + + This enum categorizes various validation errors that may occur during + token validation processes. + + Attributes: + INVALID_TOKEN (str): Indicates that the token is invalid. More information in 'original_exception'. + 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. + + + """ + + 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): + """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: Optional[Exception] = 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/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/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: diff --git a/src/corbado_python_sdk/services/implementation/session_service.py b/src/corbado_python_sdk/services/implementation/session_service.py index af48a9e..f752ba5 100644 --- a/src/corbado_python_sdk/services/implementation/session_service.py +++ b/src/corbado_python_sdk/services/implementation/session_service.py @@ -1,14 +1,21 @@ 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 -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_SHORT_SESSION_LENGTH = 300 +DEFAULT_SESSION_TOKEN_LENGTH = 300 class SessionService(BaseModel): @@ -18,30 +25,21 @@ class SessionService(BaseModel): Attributes: model_config (ConfigDict): Configuration dictionary for the model. - short_session_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 _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, - ) - short_session_cookie_name: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] + 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_short_session_validation_result: str = "" - 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. @@ -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 'short_session_cookie_name', 'issuer', 'jwks_uri', 'last_short_session_validation_result', - 'cache_keys',cache_jwk_set and 'short_session_length'. + such as 'issuer', 'jwks_uri', 'cache_keys', 'cache_jwk_set' and 'session_token_cookie_length'. Raises: Any errors raised during the initialization process. @@ -59,64 +56,109 @@ def __init__(self, **kwargs) -> None: # type: ignore super().__init__(**kwargs) self._jwk_client = PyJWKClient( uri=self.jwks_uri, - cache_keys=self.cache_keys, - lifespan=self.short_session_length, - cache_jwk_set=self.cache_jwk_set, + lifespan=DEFAULT_SESSION_TOKEN_LENGTH, ) - def get_and_validate_short_session_value(self, short_session: StrictStr) -> SessionValidationResult: + # Core methods + def validate_token(self, session_token: StrictStr) -> UserEntity: """Validate the given short-term session (represented as JWT) value. Args: - short_session (StrictStr): jwt + 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 short_session: - return SessionValidationResult(authenticated=False) + if not session_token: + raise TokenValidationException( + error_type=ValidationErrorType.CODE_JWT_EMPTY_SESSION_TOKEN, + message=ValidationErrorType.CODE_JWT_EMPTY_SESSION_TOKEN.name, + ) + + # retrieve signing key + try: + signing_key: jwt.PyJWK = self._jwk_client.get_signing_key_from_jwt(token=session_token) + except Exception as 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)}", + original_exception=error, + ) + + # decode short session (jwt) with signing key 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=session_token, 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") + except ImmatureSignatureError as 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: + 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: + 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, + ) - 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) - - def get_current_user(self, short_session: StrictStr) -> SessionValidationResult: - """Return current user for the short session. + 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)}", + original_exception=error, + ) - Args: - short_session (StrictStr): Short session. + # 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) - Returns: - SessionValidationResult: SessionValidationResult with authenticated=True on success, otherwise with - authenticated=False. - """ - user: SessionValidationResult = self.get_and_validate_short_session_value(short_session) - return user - - def set_issuer_mismatch_error(self, issuer: str) -> None: - """Set issuer mismatch error. + # Private methods + def _validate_issuer(self, token_issuer: str, session_token: str) -> None: + """Validate issuer. Args: - issuer (str): issuer. - """ - self.last_short_session_validation_result = f"Mismatch in issuer (configured: {self.issuer}, JWT: {issuer})" + token_issuer (str): Token issuer. + session_token (str): Session token. - def set_validation_error(self, error: Exception) -> None: - """Set validation error. + Raises: + TokenValidationException: If issuer is invalid. - Args: - error (Exception): Exception occurred. """ - self.last_short_session_validation_result = f"JWT validation failed: {error}" + if not token_issuer: + raise TokenValidationException( + error_type=ValidationErrorType.CODE_JWT_ISSUER_EMPTY, + 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" + 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 TokenValidationException( + error_type=ValidationErrorType.CODE_JWT_ISSUER_MISSMATCH, + message=f"Issuer mismatch (configured via FrontendAPI: '{self.issuer}', JWT issuer: '{token_issuer}')", + ) diff --git a/src/corbado_python_sdk/utils/constants.py b/src/corbado_python_sdk/utils/constants.py new file mode 100644 index 0000000..e69de29 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 3465eba..59aab1f 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 [ @@ -37,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_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 962bec4..6c8122e 100644 --- a/tests/unit/test_session_service.py +++ b/tests/unit/test_session_service.py @@ -4,14 +4,22 @@ from time import time from unittest.mock import AsyncMock, MagicMock, patch -from jwt import encode +from jwt import ( + DecodeError, + ExpiredSignatureError, + ImmatureSignatureError, + InvalidSignatureError, + PyJWKClientError, + encode, +) from pydantic import ValidationError from corbado_python_sdk import ( Config, CorbadoSDK, SessionService, - SessionValidationResult, + TokenValidationException, + UserEntity, ) TEST_NAME = "Test Name" @@ -31,6 +39,8 @@ class TestBase(unittest.TestCase): """ + private_key = None + invalid_private_key = None mock_urlopen = None @classmethod @@ -45,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() @@ -67,7 +88,31 @@ def create_session_service(cls) -> SessionService: return 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", + ) + + @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( + 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: @@ -77,25 +122,81 @@ def _provide_jwts(self): """Provide list of jwts with expected test results.""" return [ # JWT with invalid format - (False, "invalid"), + (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)), + ( + 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)), + ( + 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)), + (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, + ), + # 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, + 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)), + ( + 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()), @@ -105,34 +206,33 @@ 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): - 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) - - self.assertEqual(first=valid, second=result.authenticated) - self.assertEqual(first=valid, second=result.error is None) - + def test_validate_token(self): + 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(TEST_USER_ID, result.user_id) + self.assertEqual(first=TEST_USER_ID, second=result.user_id) + else: + with self.assertRaises(TokenValidationException) as context: + # Code that should raise the ValidationError + self.session_service.validate_token(session_token=token) + if expected_original_error: + 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) - self.session_service.get_and_validate_short_session_value(short_session=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(short_session=jwt) + self.session_service.validate_token(session_token=jwt) self.assertEqual(num_calls, self.mock_urlopen.call_count) def test_generate_jwt(self): @@ -146,13 +246,11 @@ 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", "project_id": "pro-55"}, True), # Test empty issuer - ({"issuer": "", "jwks_uri": "2", "short_session_cookie_name": "name"}, False), + ({"issuer": "", "jwks_uri": "2", "project_id": "pro-55"}, False), # Test empty jwks_uri - ({"issuer": "s", "jwks_uri": "", "short_session_cookie_name": "name"}, False), - # Tesft empty short_session_cookie_name - ({"issuer": "s", "jwks_uri": "2", "short_session_cookie_name": ""}, False), + ({"issuer": "s", "jwks_uri": "", "project_id": "pro-55"}, False), ] for params, expected_result in test_cases: @@ -168,7 +266,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