scalekit-sdk-python 2.16.0__tar.gz → 2.17.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/PKG-INFO +91 -2
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/README.md +82 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/_version.py +1 -1
- scalekit_sdk_python-2.17.0/scalekit/frameworks/django.py +369 -0
- scalekit_sdk_python-2.17.0/scalekit/frameworks/fastapi.py +386 -0
- scalekit_sdk_python-2.17.0/scalekit/frameworks/flask.py +368 -0
- scalekit_sdk_python-2.17.0/scalekit/middleware/csrf_state.py +57 -0
- scalekit_sdk_python-2.17.0/scalekit/middleware/protocol.py +56 -0
- scalekit_sdk_python-2.17.0/scalekit/middleware/session_crypto.py +126 -0
- scalekit_sdk_python-2.17.0/scalekit/middleware/session_manager.py +242 -0
- scalekit_sdk_python-2.17.0/scalekit/v1/webhooks/__init__.py +0 -0
- scalekit_sdk_python-2.17.0/scalekit/v1/workspaces/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit_sdk_python.egg-info/PKG-INFO +91 -2
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit_sdk_python.egg-info/SOURCES.txt +15 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit_sdk_python.egg-info/requires.txt +10 -1
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/setup.py +9 -1
- scalekit_sdk_python-2.17.0/tests/test_middleware_csrf_state.py +47 -0
- scalekit_sdk_python-2.17.0/tests/test_middleware_django.py +428 -0
- scalekit_sdk_python-2.17.0/tests/test_middleware_fastapi.py +434 -0
- scalekit_sdk_python-2.17.0/tests/test_middleware_flask.py +557 -0
- scalekit_sdk_python-2.17.0/tests/test_middleware_session_crypto.py +86 -0
- scalekit_sdk_python-2.17.0/tests/test_middleware_session_manager.py +303 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/LICENSE +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/buf/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/buf/validate/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/buf/validate/validate_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/buf/validate/validate_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/buf/validate/validate_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/pyproject.toml +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/actions.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/frameworks/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/frameworks/google_adk.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/frameworks/langchain.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/frameworks/types/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/frameworks/types/google_adk_tool.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/frameworks/util.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/custom_provider.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/mcp_config.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/mcp_connection_auth_state.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/mcp_instance.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/requests/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/requests/create_connected_account_request.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/requests/create_custom_provider_request.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/requests/delete_custom_provider_request.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/requests/list_providers_request.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/requests/mcp_request.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/requests/tool_request.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/requests/update_connected_account_request.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/requests/update_custom_provider_request.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/responses/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/responses/create_connected_account_response.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/responses/create_custom_provider_response.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/responses/create_mcp_config_response.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/responses/create_mcp_response.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/responses/create_mcp_session_token_response.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/responses/delete_connected_account_response.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/responses/delete_custom_provider_response.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/responses/delete_mcp_config_response.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/responses/delete_mcp_instance_response.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/responses/ensure_mcp_instance_response.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/responses/execute_tool_response.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/responses/get_connected_account_auth_response.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/responses/get_mcp_instance_auth_state_response.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/responses/get_mcp_instance_response.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/responses/get_mcp_response.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/responses/list_connected_accounts_response.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/responses/list_mcp_configs_response.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/responses/list_mcp_connected_accounts_response.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/responses/list_mcp_instances_response.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/responses/list_providers_response.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/responses/magic_link_response.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/responses/update_connected_account_response.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/responses/update_custom_provider_response.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/responses/update_mcp_config_response.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/responses/update_mcp_instance_response.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/responses/verify_connected_account_user_response.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/tool_input_output.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/models/tool_mapping.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/modifier.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/actions/types.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/auth.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/client.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/common/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/common/exceptions.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/common/scalekit.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/common/user.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/connected_accounts.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/connection.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/constants/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/constants/user.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/core.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/directory.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/domain.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/events.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/utils → scalekit_sdk_python-2.17.0/scalekit/frameworks}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/m2m_client.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/mcp.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1 → scalekit_sdk_python-2.17.0/scalekit/middleware}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/organization.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/passwordless.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/permissions.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/providers.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/role.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/sessions.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/token.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/tools.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/users.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/agentkit_logs → scalekit_sdk_python-2.17.0/scalekit/utils}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/utils/directory.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/auditlogs → scalekit_sdk_python-2.17.0/scalekit/v1}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/auth → scalekit_sdk_python-2.17.0/scalekit/v1/agentkit_logs}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/agentkit_logs/agentkit_analytics_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/agentkit_logs/agentkit_analytics_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/agentkit_logs/agentkit_analytics_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/agentkit_logs/agentkit_logs_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/agentkit_logs/agentkit_logs_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/agentkit_logs/agentkit_logs_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/clients → scalekit_sdk_python-2.17.0/scalekit/v1/auditlogs}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/auditlogs/auditlogs_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/auditlogs/auditlogs_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/auditlogs/auditlogs_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/commons → scalekit_sdk_python-2.17.0/scalekit/v1/auth}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/auth/auth_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/auth/auth_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/auth/auth_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/auth/passwordless_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/auth/passwordless_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/auth/passwordless_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/auth/totp_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/auth/totp_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/auth/totp_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/auth/webauthn_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/auth/webauthn_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/auth/webauthn_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/connected_accounts → scalekit_sdk_python-2.17.0/scalekit/v1/clients}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/clients/clients_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/clients/clients_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/clients/clients_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/connections → scalekit_sdk_python-2.17.0/scalekit/v1/commons}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/commons/commons_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/commons/commons_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/commons/commons_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/directories → scalekit_sdk_python-2.17.0/scalekit/v1/connected_accounts}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/connected_accounts/connected_accounts_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/connected_accounts/connected_accounts_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/connected_accounts/connected_accounts_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/domains → scalekit_sdk_python-2.17.0/scalekit/v1/connections}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/connections/connections_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/connections/connections_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/connections/connections_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/emails → scalekit_sdk_python-2.17.0/scalekit/v1/directories}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/directories/directories_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/directories/directories_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/directories/directories_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/environments → scalekit_sdk_python-2.17.0/scalekit/v1/domains}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/domains/domains_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/domains/domains_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/domains/domains_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/errdetails → scalekit_sdk_python-2.17.0/scalekit/v1/emails}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/emails/emails_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/emails/emails_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/emails/emails_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/events → scalekit_sdk_python-2.17.0/scalekit/v1/environments}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/environments/environments_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/environments/environments_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/environments/environments_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/gateway → scalekit_sdk_python-2.17.0/scalekit/v1/errdetails}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/errdetails/errdetails_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/errdetails/errdetails_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/errdetails/errdetails_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/interceptors → scalekit_sdk_python-2.17.0/scalekit/v1/events}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/events/events_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/events/events_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/events/events_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/keys → scalekit_sdk_python-2.17.0/scalekit/v1/gateway}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/gateway/gateway_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/gateway/gateway_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/gateway/gateway_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/mcp → scalekit_sdk_python-2.17.0/scalekit/v1/interceptors}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/interceptors/interceptors_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/interceptors/interceptors_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/interceptors/interceptors_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/members → scalekit_sdk_python-2.17.0/scalekit/v1/keys}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/keys/keys_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/keys/keys_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/keys/keys_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/migrations → scalekit_sdk_python-2.17.0/scalekit/v1/mcp}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/mcp/mcp_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/mcp/mcp_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/mcp/mcp_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/options → scalekit_sdk_python-2.17.0/scalekit/v1/members}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/members/members_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/members/members_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/members/members_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/organizations → scalekit_sdk_python-2.17.0/scalekit/v1/migrations}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/migrations/migrations_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/migrations/migrations_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/migrations/migrations_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/providers → scalekit_sdk_python-2.17.0/scalekit/v1/options}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/options/options_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/options/options_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/options/options_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/roles → scalekit_sdk_python-2.17.0/scalekit/v1/organizations}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/organizations/organizations_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/organizations/organizations_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/organizations/organizations_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/secrets → scalekit_sdk_python-2.17.0/scalekit/v1/providers}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/providers/providers_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/providers/providers_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/providers/providers_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/sessions → scalekit_sdk_python-2.17.0/scalekit/v1/roles}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/roles/roles_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/roles/roles_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/roles/roles_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/tokens → scalekit_sdk_python-2.17.0/scalekit/v1/secrets}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/secrets/secrets_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/secrets/secrets_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/secrets/secrets_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/tools → scalekit_sdk_python-2.17.0/scalekit/v1/sessions}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/sessions/sessions_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/sessions/sessions_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/sessions/sessions_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/user_attributes → scalekit_sdk_python-2.17.0/scalekit/v1/tokens}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/tokens/tokens_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/tokens/tokens_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/tokens/tokens_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/users → scalekit_sdk_python-2.17.0/scalekit/v1/tools}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/tools/tools_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/tools/tools_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/tools/tools_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/webhooks → scalekit_sdk_python-2.17.0/scalekit/v1/user_attributes}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/user_attributes/user_attributes_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/user_attributes/user_attributes_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/user_attributes/user_attributes_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0/scalekit/v1/workspaces → scalekit_sdk_python-2.17.0/scalekit/v1/users}/__init__.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/users/users_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/users/users_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/users/users_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/webhooks/webhooks_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/webhooks/webhooks_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/webhooks/webhooks_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/workspaces/workspaces_pb2.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/workspaces/workspaces_pb2.pyi +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/v1/workspaces/workspaces_pb2_grpc.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit/webauthn.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit_sdk_python.egg-info/dependency_links.txt +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/scalekit_sdk_python.egg-info/top_level.txt +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/setup.cfg +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/tests/test_actions.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/tests/test_connected_accounts.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/tests/test_connection.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/tests/test_directory.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/tests/test_domain.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/tests/test_events.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/tests/test_login.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/tests/test_m2m_client.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/tests/test_mcp.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/tests/test_org_roles.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/tests/test_organization.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/tests/test_organization_session_policy.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/tests/test_organization_slug_logo.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/tests/test_passwordless.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/tests/test_permissions.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/tests/test_providers.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/tests/test_role_defaults_dependent.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/tests/test_roles.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/tests/test_sessions.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/tests/test_sk819_retry_behavior.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/tests/test_tokens.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/tests/test_tools.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/tests/test_user_roles_permissions.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/tests/test_users.py +0 -0
- {scalekit_sdk_python-2.16.0 → scalekit_sdk_python-2.17.0}/tests/test_webauthn_credentials.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scalekit-sdk-python
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.17.0
|
|
4
4
|
Summary: Scalekit official Python SDK
|
|
5
5
|
Home-page: https://github.com/scalekit-inc/scalekit-sdk-python
|
|
6
6
|
Author: Team Scalekit
|
|
@@ -17,7 +17,7 @@ Requires-Dist: google>=3.0
|
|
|
17
17
|
Requires-Dist: requests>=2.34.0
|
|
18
18
|
Requires-Dist: PyJWT>=2.13.0
|
|
19
19
|
Requires-Dist: cffi>=1.15.1
|
|
20
|
-
Requires-Dist: cryptography
|
|
20
|
+
Requires-Dist: cryptography>=50.0.0
|
|
21
21
|
Requires-Dist: setuptools<83.0,>=82.0.1
|
|
22
22
|
Requires-Dist: grpcio-status<2.0,>=1.81.0
|
|
23
23
|
Requires-Dist: protoc-gen-openapiv2>=0.0.1
|
|
@@ -27,6 +27,12 @@ Requires-Dist: python-dotenv<2.0,>=1.2.2
|
|
|
27
27
|
Requires-Dist: Faker<41.0,>=33.0.0
|
|
28
28
|
Requires-Dist: pydantic>=2.13.4
|
|
29
29
|
Requires-Dist: mcp>=1.27.2
|
|
30
|
+
Provides-Extra: flask
|
|
31
|
+
Requires-Dist: flask>=2.0; extra == "flask"
|
|
32
|
+
Provides-Extra: fastapi
|
|
33
|
+
Requires-Dist: fastapi>=0.100; extra == "fastapi"
|
|
34
|
+
Provides-Extra: django
|
|
35
|
+
Requires-Dist: django>=4.2; extra == "django"
|
|
30
36
|
Dynamic: author
|
|
31
37
|
Dynamic: author-email
|
|
32
38
|
Dynamic: classifier
|
|
@@ -35,6 +41,7 @@ Dynamic: description-content-type
|
|
|
35
41
|
Dynamic: home-page
|
|
36
42
|
Dynamic: license
|
|
37
43
|
Dynamic: license-file
|
|
44
|
+
Dynamic: provides-extra
|
|
38
45
|
Dynamic: requires-dist
|
|
39
46
|
Dynamic: summary
|
|
40
47
|
|
|
@@ -178,6 +185,88 @@ Explore fully functional sample applications built with popular Python framework
|
|
|
178
185
|
|-----------|------------|-------------|
|
|
179
186
|
| **FastAPI** | [scalekit-fastapi-example](https://github.com/scalekit-developers/scalekit-fastapi-example) | Modern async Python API framework |
|
|
180
187
|
|
|
188
|
+
### Full Stack Auth — encrypted-session middleware for Flask, FastAPI, and Django
|
|
189
|
+
|
|
190
|
+
The example above is for **Modular SSO**: Scalekit brokers the OAuth exchange with your customer's own IdP via a `connection_id`, and your app owns its own session however it likes.
|
|
191
|
+
|
|
192
|
+
If instead Scalekit hosts your login UI and you want it to also manage the session lifecycle for you (**Full Stack Auth**), `scalekit-sdk-python` ships optional Flask, FastAPI, and Django extras that handle the encrypted session cookie, transparent token refresh, CSRF-safe login/callback, and full logout for you — no hand-rolled cookies, no manual refresh timing.
|
|
193
|
+
|
|
194
|
+
Register these under **Dashboard → Authentication → Redirects** before testing:
|
|
195
|
+
- **Redirect URI** — your `redirect_uri` (the `/callback` path). Scalekit rejects the exchange if this doesn't match exactly.
|
|
196
|
+
- **Post Logout Redirect URI** — where users land after full logout. A relative path gets auto-absolutized against the request host, but the resulting absolute URL must still be registered.
|
|
197
|
+
- **Initiate Login URL** — your `/login` path. Scalekit redirects here (not `/callback`) for a bookmarked login page, an IdP portal tile, or an invite/magic link — the login view already handles this correctly, including the `idp_initiated_login` case, with no extra code required.
|
|
198
|
+
|
|
199
|
+
pip install "scalekit-sdk-python[flask]" # or "scalekit-sdk-python[fastapi]" or "scalekit-sdk-python[django]"
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
# Flask
|
|
203
|
+
import os
|
|
204
|
+
from flask import Flask
|
|
205
|
+
from scalekit.frameworks.flask import ScalekitAuth
|
|
206
|
+
|
|
207
|
+
app = Flask(__name__)
|
|
208
|
+
auth = ScalekitAuth(
|
|
209
|
+
app,
|
|
210
|
+
env_url=os.environ["SCALEKIT_ENV_URL"],
|
|
211
|
+
client_id=os.environ["SCALEKIT_CLIENT_ID"],
|
|
212
|
+
client_secret=os.environ["SCALEKIT_CLIENT_SECRET"],
|
|
213
|
+
redirect_uri="https://myapp.com/callback",
|
|
214
|
+
cookie_encryption_secret=os.environ["COOKIE_ENCRYPTION_SECRET"], # openssl rand -base64 32
|
|
215
|
+
) # registers /login, /callback, /logout
|
|
216
|
+
|
|
217
|
+
@app.route("/account")
|
|
218
|
+
@auth.requires_auth
|
|
219
|
+
def account():
|
|
220
|
+
return {"email": auth.current_user["email"]}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
# FastAPI -- protect routes with Depends(), FastAPI's idiomatic mechanism
|
|
225
|
+
import os
|
|
226
|
+
from fastapi import Depends, FastAPI
|
|
227
|
+
from scalekit.frameworks.fastapi import ScalekitAuth
|
|
228
|
+
|
|
229
|
+
app = FastAPI()
|
|
230
|
+
auth = ScalekitAuth(
|
|
231
|
+
env_url=os.environ["SCALEKIT_ENV_URL"],
|
|
232
|
+
client_id=os.environ["SCALEKIT_CLIENT_ID"],
|
|
233
|
+
client_secret=os.environ["SCALEKIT_CLIENT_SECRET"],
|
|
234
|
+
redirect_uri="https://myapp.com/callback",
|
|
235
|
+
cookie_encryption_secret=os.environ["COOKIE_ENCRYPTION_SECRET"],
|
|
236
|
+
)
|
|
237
|
+
auth.install(app) # registers /login, /callback, /logout
|
|
238
|
+
|
|
239
|
+
@app.get("/account")
|
|
240
|
+
async def account(user: dict = Depends(auth.requires_auth)):
|
|
241
|
+
return {"email": user["email"]}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
# Django -- settings.py
|
|
246
|
+
import os
|
|
247
|
+
|
|
248
|
+
MIDDLEWARE = [..., "scalekit.frameworks.django.ScalekitAuthMiddleware"]
|
|
249
|
+
SCALEKIT_ENV_URL = os.environ["SCALEKIT_ENV_URL"]
|
|
250
|
+
SCALEKIT_CLIENT_ID = os.environ["SCALEKIT_CLIENT_ID"]
|
|
251
|
+
SCALEKIT_CLIENT_SECRET = os.environ["SCALEKIT_CLIENT_SECRET"]
|
|
252
|
+
SCALEKIT_REDIRECT_URI = "https://myapp.com/callback"
|
|
253
|
+
SCALEKIT_COOKIE_ENCRYPTION_SECRET = os.environ["COOKIE_ENCRYPTION_SECRET"]
|
|
254
|
+
|
|
255
|
+
# urls.py
|
|
256
|
+
from django.urls import include, path
|
|
257
|
+
urlpatterns = [path("", include("scalekit.frameworks.django")), ...] # /login, /callback, /logout
|
|
258
|
+
|
|
259
|
+
# views.py
|
|
260
|
+
from django.http import JsonResponse
|
|
261
|
+
from scalekit.frameworks.django import login_required
|
|
262
|
+
|
|
263
|
+
@login_required
|
|
264
|
+
def account(request):
|
|
265
|
+
return JsonResponse(request.scalekit_user)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
See [`examples/flask`](./examples/flask), [`examples/fastapi`](./examples/fastapi), and [`examples/django`](./examples/django) for complete, runnable versions. For a fuller production-oriented sample app, see the framework repos above.
|
|
269
|
+
|
|
181
270
|
## 🔗 Helpful Links
|
|
182
271
|
|
|
183
272
|
### 📖 Quickstart Guides
|
|
@@ -138,6 +138,88 @@ Explore fully functional sample applications built with popular Python framework
|
|
|
138
138
|
|-----------|------------|-------------|
|
|
139
139
|
| **FastAPI** | [scalekit-fastapi-example](https://github.com/scalekit-developers/scalekit-fastapi-example) | Modern async Python API framework |
|
|
140
140
|
|
|
141
|
+
### Full Stack Auth — encrypted-session middleware for Flask, FastAPI, and Django
|
|
142
|
+
|
|
143
|
+
The example above is for **Modular SSO**: Scalekit brokers the OAuth exchange with your customer's own IdP via a `connection_id`, and your app owns its own session however it likes.
|
|
144
|
+
|
|
145
|
+
If instead Scalekit hosts your login UI and you want it to also manage the session lifecycle for you (**Full Stack Auth**), `scalekit-sdk-python` ships optional Flask, FastAPI, and Django extras that handle the encrypted session cookie, transparent token refresh, CSRF-safe login/callback, and full logout for you — no hand-rolled cookies, no manual refresh timing.
|
|
146
|
+
|
|
147
|
+
Register these under **Dashboard → Authentication → Redirects** before testing:
|
|
148
|
+
- **Redirect URI** — your `redirect_uri` (the `/callback` path). Scalekit rejects the exchange if this doesn't match exactly.
|
|
149
|
+
- **Post Logout Redirect URI** — where users land after full logout. A relative path gets auto-absolutized against the request host, but the resulting absolute URL must still be registered.
|
|
150
|
+
- **Initiate Login URL** — your `/login` path. Scalekit redirects here (not `/callback`) for a bookmarked login page, an IdP portal tile, or an invite/magic link — the login view already handles this correctly, including the `idp_initiated_login` case, with no extra code required.
|
|
151
|
+
|
|
152
|
+
pip install "scalekit-sdk-python[flask]" # or "scalekit-sdk-python[fastapi]" or "scalekit-sdk-python[django]"
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
# Flask
|
|
156
|
+
import os
|
|
157
|
+
from flask import Flask
|
|
158
|
+
from scalekit.frameworks.flask import ScalekitAuth
|
|
159
|
+
|
|
160
|
+
app = Flask(__name__)
|
|
161
|
+
auth = ScalekitAuth(
|
|
162
|
+
app,
|
|
163
|
+
env_url=os.environ["SCALEKIT_ENV_URL"],
|
|
164
|
+
client_id=os.environ["SCALEKIT_CLIENT_ID"],
|
|
165
|
+
client_secret=os.environ["SCALEKIT_CLIENT_SECRET"],
|
|
166
|
+
redirect_uri="https://myapp.com/callback",
|
|
167
|
+
cookie_encryption_secret=os.environ["COOKIE_ENCRYPTION_SECRET"], # openssl rand -base64 32
|
|
168
|
+
) # registers /login, /callback, /logout
|
|
169
|
+
|
|
170
|
+
@app.route("/account")
|
|
171
|
+
@auth.requires_auth
|
|
172
|
+
def account():
|
|
173
|
+
return {"email": auth.current_user["email"]}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
# FastAPI -- protect routes with Depends(), FastAPI's idiomatic mechanism
|
|
178
|
+
import os
|
|
179
|
+
from fastapi import Depends, FastAPI
|
|
180
|
+
from scalekit.frameworks.fastapi import ScalekitAuth
|
|
181
|
+
|
|
182
|
+
app = FastAPI()
|
|
183
|
+
auth = ScalekitAuth(
|
|
184
|
+
env_url=os.environ["SCALEKIT_ENV_URL"],
|
|
185
|
+
client_id=os.environ["SCALEKIT_CLIENT_ID"],
|
|
186
|
+
client_secret=os.environ["SCALEKIT_CLIENT_SECRET"],
|
|
187
|
+
redirect_uri="https://myapp.com/callback",
|
|
188
|
+
cookie_encryption_secret=os.environ["COOKIE_ENCRYPTION_SECRET"],
|
|
189
|
+
)
|
|
190
|
+
auth.install(app) # registers /login, /callback, /logout
|
|
191
|
+
|
|
192
|
+
@app.get("/account")
|
|
193
|
+
async def account(user: dict = Depends(auth.requires_auth)):
|
|
194
|
+
return {"email": user["email"]}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
# Django -- settings.py
|
|
199
|
+
import os
|
|
200
|
+
|
|
201
|
+
MIDDLEWARE = [..., "scalekit.frameworks.django.ScalekitAuthMiddleware"]
|
|
202
|
+
SCALEKIT_ENV_URL = os.environ["SCALEKIT_ENV_URL"]
|
|
203
|
+
SCALEKIT_CLIENT_ID = os.environ["SCALEKIT_CLIENT_ID"]
|
|
204
|
+
SCALEKIT_CLIENT_SECRET = os.environ["SCALEKIT_CLIENT_SECRET"]
|
|
205
|
+
SCALEKIT_REDIRECT_URI = "https://myapp.com/callback"
|
|
206
|
+
SCALEKIT_COOKIE_ENCRYPTION_SECRET = os.environ["COOKIE_ENCRYPTION_SECRET"]
|
|
207
|
+
|
|
208
|
+
# urls.py
|
|
209
|
+
from django.urls import include, path
|
|
210
|
+
urlpatterns = [path("", include("scalekit.frameworks.django")), ...] # /login, /callback, /logout
|
|
211
|
+
|
|
212
|
+
# views.py
|
|
213
|
+
from django.http import JsonResponse
|
|
214
|
+
from scalekit.frameworks.django import login_required
|
|
215
|
+
|
|
216
|
+
@login_required
|
|
217
|
+
def account(request):
|
|
218
|
+
return JsonResponse(request.scalekit_user)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
See [`examples/flask`](./examples/flask), [`examples/fastapi`](./examples/fastapi), and [`examples/django`](./examples/django) for complete, runnable versions. For a fuller production-oriented sample app, see the framework repos above.
|
|
222
|
+
|
|
141
223
|
## 🔗 Helpful Links
|
|
142
224
|
|
|
143
225
|
### 📖 Quickstart Guides
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from functools import lru_cache, wraps
|
|
6
|
+
from typing import Any, Callable, Optional
|
|
7
|
+
from urllib.parse import quote
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from django.conf import settings as django_settings
|
|
11
|
+
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
|
12
|
+
from django.urls import path
|
|
13
|
+
except ImportError as exc: # pragma: no cover -- exercised only without the extra installed
|
|
14
|
+
raise ImportError(
|
|
15
|
+
"Django integration requires the 'django' extra: "
|
|
16
|
+
"pip install scalekit-sdk-python[django]"
|
|
17
|
+
) from exc
|
|
18
|
+
|
|
19
|
+
from scalekit.client import ScalekitClient
|
|
20
|
+
from scalekit.common.scalekit import (
|
|
21
|
+
AuthorizationUrlOptions,
|
|
22
|
+
CodeAuthenticationOptions,
|
|
23
|
+
LogoutUrlOptions,
|
|
24
|
+
)
|
|
25
|
+
from scalekit.middleware.csrf_state import (
|
|
26
|
+
RETURN_TO_COOKIE_NAME,
|
|
27
|
+
STATE_COOKIE_MAX_AGE,
|
|
28
|
+
STATE_COOKIE_NAME,
|
|
29
|
+
generate_state,
|
|
30
|
+
sanitize_return_to,
|
|
31
|
+
verify_state,
|
|
32
|
+
)
|
|
33
|
+
from scalekit.middleware.protocol import RequestAdapter, ResponseAdapter
|
|
34
|
+
from scalekit.middleware.session_manager import DEFAULT_COOKIE_NAME, SessionRefreshManager
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger("scalekit.frameworks.django")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _DjangoRequestAdapter:
|
|
40
|
+
"""RequestAdapter implementation backed by a Django HttpRequest."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, request: HttpRequest):
|
|
43
|
+
self._request = request
|
|
44
|
+
|
|
45
|
+
def get_cookie(self, name: str) -> Optional[str]:
|
|
46
|
+
return self._request.COOKIES.get(name)
|
|
47
|
+
|
|
48
|
+
def get_request_url(self) -> str:
|
|
49
|
+
return self._request.build_absolute_uri()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class _DjangoResponseAdapter:
|
|
53
|
+
"""ResponseAdapter implementation that mutates a Django HttpResponse in place."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, response: HttpResponse, *, secure: bool = True):
|
|
56
|
+
self.response = response
|
|
57
|
+
# Default for callers that don't pass `secure` explicitly (e.g.
|
|
58
|
+
# SessionRefreshManager.apply()-style callers that only know the
|
|
59
|
+
# ResponseAdapter interface, not SCALEKIT_COOKIE_SECURE) -- every
|
|
60
|
+
# call site below now constructs this with secure=config.cookie_secure.
|
|
61
|
+
self._default_secure = secure
|
|
62
|
+
|
|
63
|
+
def set_cookie(
|
|
64
|
+
self,
|
|
65
|
+
name: str,
|
|
66
|
+
value: str,
|
|
67
|
+
*,
|
|
68
|
+
max_age: Optional[int] = None,
|
|
69
|
+
secure: Optional[bool] = None,
|
|
70
|
+
http_only: bool = True,
|
|
71
|
+
same_site: str = "Lax",
|
|
72
|
+
domain: Optional[str] = None,
|
|
73
|
+
path: str = "/",
|
|
74
|
+
) -> None:
|
|
75
|
+
self.response.set_cookie(
|
|
76
|
+
name,
|
|
77
|
+
value,
|
|
78
|
+
max_age=max_age,
|
|
79
|
+
secure=self._default_secure if secure is None else secure,
|
|
80
|
+
httponly=http_only,
|
|
81
|
+
samesite=same_site,
|
|
82
|
+
domain=domain,
|
|
83
|
+
path=path,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def delete_cookie(
|
|
87
|
+
self, name: str, *, path: str = "/", domain: Optional[str] = None
|
|
88
|
+
) -> None:
|
|
89
|
+
self.response.delete_cookie(name, path=path, domain=domain)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _setting(name: str, default: Any = None, required: bool = False) -> Any:
|
|
93
|
+
value = getattr(django_settings, name, default)
|
|
94
|
+
if required and not value:
|
|
95
|
+
raise ValueError(
|
|
96
|
+
f"Django setting {name} is required to use scalekit.frameworks.django"
|
|
97
|
+
)
|
|
98
|
+
return value
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ScalekitAuthConfig:
|
|
102
|
+
"""
|
|
103
|
+
Holds the shared ScalekitClient + SessionRefreshManager, built once from
|
|
104
|
+
Django settings (all read with an SCALEKIT_ prefix, matching Django
|
|
105
|
+
convention for third-party app config):
|
|
106
|
+
|
|
107
|
+
SCALEKIT_CLIENT_ID, SCALEKIT_CLIENT_SECRET, SCALEKIT_ENV_URL,
|
|
108
|
+
SCALEKIT_REDIRECT_URI, SCALEKIT_COOKIE_ENCRYPTION_SECRET (required --
|
|
109
|
+
see scalekit.middleware.session_crypto for why there is intentionally
|
|
110
|
+
no default), SCALEKIT_COOKIE_NAME (optional), SCALEKIT_LOGIN_PATH
|
|
111
|
+
(optional, default "/login"), SCALEKIT_POST_LOGIN_REDIRECT (optional,
|
|
112
|
+
default "/"), SCALEKIT_POST_LOGOUT_REDIRECT_URI (optional, falls back
|
|
113
|
+
to SCALEKIT_POST_LOGIN_REDIRECT), SCALEKIT_FULL_LOGOUT (optional
|
|
114
|
+
bool, default True).
|
|
115
|
+
|
|
116
|
+
SCALEKIT_CLIENT (an already-constructed ScalekitClient) can be set
|
|
117
|
+
directly, primarily for tests -- normal usage constructs one from the
|
|
118
|
+
ID/secret/env_url settings above.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def __init__(self):
|
|
122
|
+
client = _setting("SCALEKIT_CLIENT", None)
|
|
123
|
+
if client is None:
|
|
124
|
+
client = ScalekitClient(
|
|
125
|
+
env_url=_setting("SCALEKIT_ENV_URL", required=True),
|
|
126
|
+
client_id=_setting("SCALEKIT_CLIENT_ID", required=True),
|
|
127
|
+
client_secret=_setting("SCALEKIT_CLIENT_SECRET", required=True),
|
|
128
|
+
)
|
|
129
|
+
self.client = client
|
|
130
|
+
self.redirect_uri = _setting("SCALEKIT_REDIRECT_URI", required=True)
|
|
131
|
+
self.login_path = _setting("SCALEKIT_LOGIN_PATH", "/login")
|
|
132
|
+
self.post_login_redirect = _setting("SCALEKIT_POST_LOGIN_REDIRECT", "/")
|
|
133
|
+
self.post_logout_redirect_uri = (
|
|
134
|
+
_setting("SCALEKIT_POST_LOGOUT_REDIRECT_URI") or self.post_login_redirect
|
|
135
|
+
)
|
|
136
|
+
self.full_logout = _setting("SCALEKIT_FULL_LOGOUT", True)
|
|
137
|
+
# Right default for production. Chrome (and some other browsers) silently
|
|
138
|
+
# drop a Secure cookie set over plain http://localhost, which looks like a
|
|
139
|
+
# session that never sticks -- set SCALEKIT_COOKIE_SECURE=False for local
|
|
140
|
+
# HTTP dev.
|
|
141
|
+
self.cookie_secure = _setting("SCALEKIT_COOKIE_SECURE", True)
|
|
142
|
+
|
|
143
|
+
# Raises immediately if the secret is missing.
|
|
144
|
+
self.manager = SessionRefreshManager(
|
|
145
|
+
self.client,
|
|
146
|
+
_setting("SCALEKIT_COOKIE_ENCRYPTION_SECRET", required=True),
|
|
147
|
+
cookie_name=_setting("SCALEKIT_COOKIE_NAME", DEFAULT_COOKIE_NAME),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@lru_cache(maxsize=1)
|
|
152
|
+
def get_config() -> ScalekitAuthConfig:
|
|
153
|
+
return ScalekitAuthConfig()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class ScalekitAuthMiddleware:
|
|
157
|
+
"""
|
|
158
|
+
Add to MIDDLEWARE in settings.py. Populates `request.scalekit_user` (None
|
|
159
|
+
if not authenticated) and transparently refreshes the session cookie on
|
|
160
|
+
every request. Mirrors Django's own convention (AuthenticationMiddleware
|
|
161
|
+
populates `request.user`, `login_required` enforces it) -- this
|
|
162
|
+
middleware does NOT itself enforce authentication; use `login_required`
|
|
163
|
+
on individual views for that.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
def __init__(self, get_response: Callable):
|
|
167
|
+
self.get_response = get_response
|
|
168
|
+
|
|
169
|
+
def __call__(self, request: HttpRequest):
|
|
170
|
+
config = get_config()
|
|
171
|
+
result = config.manager.check(_DjangoRequestAdapter(request))
|
|
172
|
+
request.scalekit_user = result.user if result.authenticated else None
|
|
173
|
+
request.scalekit_session_result = result
|
|
174
|
+
|
|
175
|
+
response = self.get_response(request)
|
|
176
|
+
|
|
177
|
+
if result.new_cookie_value:
|
|
178
|
+
_DjangoResponseAdapter(response, secure=config.cookie_secure).set_cookie(
|
|
179
|
+
config.manager.cookie_name, result.new_cookie_value
|
|
180
|
+
)
|
|
181
|
+
elif result.should_clear_cookie:
|
|
182
|
+
_DjangoResponseAdapter(response).delete_cookie(config.manager.cookie_name)
|
|
183
|
+
|
|
184
|
+
return response
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def get_session(request: HttpRequest) -> Optional[dict]:
|
|
188
|
+
"""
|
|
189
|
+
Read-only session lookup -- for use outside a `login_required`-gated view
|
|
190
|
+
(e.g. a homepage that wants to show a logged-in/logged-out state without
|
|
191
|
+
gating the whole route). Never refreshes or writes a new cookie.
|
|
192
|
+
|
|
193
|
+
Returns `{"user": ..., "expires_at": ...}`, or `None` if there's no valid
|
|
194
|
+
session. `user` is the access-token claims (not tokens themselves) --
|
|
195
|
+
this function never returns `access_token`, `refresh_token`, or
|
|
196
|
+
`id_token`.
|
|
197
|
+
|
|
198
|
+
Note: ScalekitAuthMiddleware already sets `request.scalekit_user` on
|
|
199
|
+
every request (`None` if unauthenticated), so most views can just read
|
|
200
|
+
that directly. Use `get_session()` instead when you also need
|
|
201
|
+
`expires_at`.
|
|
202
|
+
"""
|
|
203
|
+
config = get_config()
|
|
204
|
+
cookie_value = request.COOKIES.get(config.manager.cookie_name)
|
|
205
|
+
if not cookie_value:
|
|
206
|
+
return None
|
|
207
|
+
payload = config.manager.read_session(cookie_value)
|
|
208
|
+
if payload is None:
|
|
209
|
+
return None
|
|
210
|
+
return {"user": payload.get("user"), "expires_at": payload.get("expires_at")}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def login_required(view_func: Callable) -> Callable:
|
|
214
|
+
"""
|
|
215
|
+
View decorator: redirects to SCALEKIT_LOGIN_PATH with a real 302 if
|
|
216
|
+
`request.scalekit_user` isn't set. Requires ScalekitAuthMiddleware to be
|
|
217
|
+
installed. Same "real redirect, never a JSON 401" guarantee as the
|
|
218
|
+
Flask/FastAPI integrations -- a JSON 401 is exactly what a background
|
|
219
|
+
fetch/XHR would silently swallow, which is the failure mode this whole
|
|
220
|
+
design exists to avoid.
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
@wraps(view_func)
|
|
224
|
+
def wrapped(request: HttpRequest, *args, **kwargs):
|
|
225
|
+
if getattr(request, "scalekit_user", None) is None:
|
|
226
|
+
# Preserve the page the caller was trying to reach so callback_view
|
|
227
|
+
# can send them back there instead of the fixed post_login_redirect.
|
|
228
|
+
current_path = request.get_full_path()
|
|
229
|
+
location = f"{get_config().login_path}?returnTo={quote(current_path, safe='')}"
|
|
230
|
+
return HttpResponseRedirect(location)
|
|
231
|
+
return view_func(request, *args, **kwargs)
|
|
232
|
+
|
|
233
|
+
return wrapped
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def login_view(request: HttpRequest) -> HttpResponse:
|
|
237
|
+
config = get_config()
|
|
238
|
+
options = AuthorizationUrlOptions()
|
|
239
|
+
# offline_access is required to get a refresh_token back at all -- see
|
|
240
|
+
# scalekit.frameworks.flask for the full explanation.
|
|
241
|
+
options.scopes = ["openid", "profile", "email", "offline_access"]
|
|
242
|
+
|
|
243
|
+
# This view doubles as the dashboard-registered "Initiate Login URL" --
|
|
244
|
+
# see scalekit.frameworks.flask for the full reasoning.
|
|
245
|
+
idp_initiated_login = request.GET.get("idp_initiated_login")
|
|
246
|
+
if idp_initiated_login:
|
|
247
|
+
try:
|
|
248
|
+
claims = config.client.get_idp_initiated_login_claims(idp_initiated_login)
|
|
249
|
+
options.connection_id = claims.get("connection_id")
|
|
250
|
+
options.organization_id = claims.get("organization_id")
|
|
251
|
+
options.login_hint = claims.get("login_hint")
|
|
252
|
+
except Exception:
|
|
253
|
+
logger.exception(
|
|
254
|
+
"idp_initiated_login claim validation failed; falling back to normal login"
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# Bind this authorization request to the browser that started it, so
|
|
258
|
+
# callback_view can reject a forged callback carrying an attacker's own
|
|
259
|
+
# authorization code (CSRF) -- see scalekit.frameworks.flask.
|
|
260
|
+
state = generate_state()
|
|
261
|
+
options.state = state
|
|
262
|
+
url = config.client.get_authorization_url(config.redirect_uri, options)
|
|
263
|
+
response = HttpResponseRedirect(url)
|
|
264
|
+
adapter = _DjangoResponseAdapter(response, secure=config.cookie_secure)
|
|
265
|
+
adapter.set_cookie(STATE_COOKIE_NAME, state, max_age=STATE_COOKIE_MAX_AGE)
|
|
266
|
+
|
|
267
|
+
# Preserve the page the caller was trying to reach (set by
|
|
268
|
+
# login_required's redirect) so callback_view can send them back there
|
|
269
|
+
# instead of the fixed post_login_redirect -- re-validated here since
|
|
270
|
+
# query strings are always attacker-influenceable.
|
|
271
|
+
return_to = sanitize_return_to(request.GET.get("returnTo"))
|
|
272
|
+
if return_to:
|
|
273
|
+
adapter.set_cookie(RETURN_TO_COOKIE_NAME, return_to, max_age=STATE_COOKIE_MAX_AGE)
|
|
274
|
+
return response
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def callback_view(request: HttpRequest) -> HttpResponse:
|
|
278
|
+
config = get_config()
|
|
279
|
+
|
|
280
|
+
def _redirect_to_login():
|
|
281
|
+
resp = HttpResponseRedirect(config.login_path)
|
|
282
|
+
adapter = _DjangoResponseAdapter(resp, secure=config.cookie_secure)
|
|
283
|
+
adapter.delete_cookie(STATE_COOKIE_NAME)
|
|
284
|
+
adapter.delete_cookie(RETURN_TO_COOKIE_NAME)
|
|
285
|
+
return resp
|
|
286
|
+
|
|
287
|
+
# The provider redirects here with `error` (no `code`) if the user
|
|
288
|
+
# cancels consent or the request is otherwise rejected -- never reflect
|
|
289
|
+
# error/error_description into the response, it's attacker-influenced.
|
|
290
|
+
error = request.GET.get("error")
|
|
291
|
+
code = request.GET.get("code")
|
|
292
|
+
if error or not code:
|
|
293
|
+
return _redirect_to_login()
|
|
294
|
+
|
|
295
|
+
stored_state = request.COOKIES.get(STATE_COOKIE_NAME)
|
|
296
|
+
returned_state = request.GET.get("state")
|
|
297
|
+
if not verify_state(stored_state, returned_state):
|
|
298
|
+
# Missing or mismatched state -- this callback did not originate
|
|
299
|
+
# from a /login this browser actually made. Refuse the exchange.
|
|
300
|
+
return _redirect_to_login()
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
result = config.client.authenticate_with_code(
|
|
304
|
+
code, config.redirect_uri, CodeAuthenticationOptions()
|
|
305
|
+
)
|
|
306
|
+
access_token = result["access_token"]
|
|
307
|
+
# Access-token claims (not id_token claims) are the source of truth for
|
|
308
|
+
# `user` -- see scalekit.frameworks.flask for the full reasoning.
|
|
309
|
+
claims = config.client.validate_access_token_and_get_claims(access_token)
|
|
310
|
+
payload = {
|
|
311
|
+
"user": claims,
|
|
312
|
+
"access_token": access_token,
|
|
313
|
+
"refresh_token": result.get("refresh_token"),
|
|
314
|
+
"id_token": result.get("id_token"),
|
|
315
|
+
"expires_at": claims.get("exp", time.time() + (result.get("expires_in") or 300)),
|
|
316
|
+
}
|
|
317
|
+
# create_session_cookie raises if the encrypted payload exceeds the
|
|
318
|
+
# browser cookie size limit -- must stay inside this try, not just
|
|
319
|
+
# the network calls above, or this view raises with no wrapper.
|
|
320
|
+
cookie_value = config.manager.create_session_cookie(payload)
|
|
321
|
+
except Exception:
|
|
322
|
+
logger.exception("login callback failed; redirecting to login")
|
|
323
|
+
return _redirect_to_login()
|
|
324
|
+
|
|
325
|
+
return_to = sanitize_return_to(request.COOKIES.get(RETURN_TO_COOKIE_NAME))
|
|
326
|
+
response = HttpResponseRedirect(return_to or config.post_login_redirect)
|
|
327
|
+
adapter = _DjangoResponseAdapter(response, secure=config.cookie_secure)
|
|
328
|
+
adapter.set_cookie(config.manager.cookie_name, cookie_value)
|
|
329
|
+
adapter.delete_cookie(STATE_COOKIE_NAME)
|
|
330
|
+
adapter.delete_cookie(RETURN_TO_COOKIE_NAME)
|
|
331
|
+
return response
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def logout_view(request: HttpRequest) -> HttpResponse:
|
|
335
|
+
config = get_config()
|
|
336
|
+
cookie_value = request.COOKIES.get(config.manager.cookie_name)
|
|
337
|
+
id_token = None
|
|
338
|
+
if cookie_value:
|
|
339
|
+
payload = config.manager.read_session(cookie_value)
|
|
340
|
+
if payload:
|
|
341
|
+
id_token = payload.get("id_token")
|
|
342
|
+
|
|
343
|
+
redirect_url = config.post_logout_redirect_uri
|
|
344
|
+
if config.full_logout and id_token:
|
|
345
|
+
# Scalekit requires an absolute, dashboard-registered post-logout
|
|
346
|
+
# redirect URI -- absolutize a relative default against the current
|
|
347
|
+
# request, same as the Flask/FastAPI adapters.
|
|
348
|
+
absolute_redirect_uri = config.post_logout_redirect_uri
|
|
349
|
+
if not absolute_redirect_uri.startswith(("http://", "https://")):
|
|
350
|
+
absolute_redirect_uri = request.build_absolute_uri(absolute_redirect_uri)
|
|
351
|
+
options = LogoutUrlOptions()
|
|
352
|
+
options.id_token_hint = id_token
|
|
353
|
+
options.post_logout_redirect_uri = absolute_redirect_uri
|
|
354
|
+
redirect_url = config.client.get_logout_url(options)
|
|
355
|
+
|
|
356
|
+
response = HttpResponseRedirect(redirect_url)
|
|
357
|
+
_DjangoResponseAdapter(response).delete_cookie(config.manager.cookie_name)
|
|
358
|
+
return response
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
# No trailing slashes -- matches the Flask/FastAPI adapters' paths
|
|
362
|
+
# ("/login", "/callback", "/logout") so a redirect_uri registered in the
|
|
363
|
+
# Scalekit dashboard for one framework works unchanged for the others.
|
|
364
|
+
# Include via: path("", include("scalekit.frameworks.django"))
|
|
365
|
+
urlpatterns = [
|
|
366
|
+
path("login", login_view, name="scalekit_login"),
|
|
367
|
+
path("callback", callback_view, name="scalekit_callback"),
|
|
368
|
+
path("logout", logout_view, name="scalekit_logout"),
|
|
369
|
+
]
|