ohauthcat 0.0.2__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.
- ohauthcat-0.0.2/.gitignore +43 -0
- ohauthcat-0.0.2/CHANGELOG.md +27 -0
- ohauthcat-0.0.2/LICENSE +35 -0
- ohauthcat-0.0.2/PKG-INFO +181 -0
- ohauthcat-0.0.2/README.md +153 -0
- ohauthcat-0.0.2/THIRD_PARTY_LICENSES +852 -0
- ohauthcat-0.0.2/examples/python/LICENSE +35 -0
- ohauthcat-0.0.2/examples/python/THIRD_PARTY_LICENSES +5413 -0
- ohauthcat-0.0.2/examples/python/pyproject.toml +28 -0
- ohauthcat-0.0.2/pyproject.toml +52 -0
- ohauthcat-0.0.2/spec/README.md +23 -0
- ohauthcat-0.0.2/src/ohauthcat/__init__.py +95 -0
- ohauthcat-0.0.2/src/ohauthcat/_version.py +6 -0
- ohauthcat-0.0.2/src/ohauthcat/access_token.py +400 -0
- ohauthcat-0.0.2/src/ohauthcat/audit.py +56 -0
- ohauthcat-0.0.2/src/ohauthcat/client.py +178 -0
- ohauthcat-0.0.2/src/ohauthcat/contract.json +1 -0
- ohauthcat-0.0.2/src/ohauthcat/custom_attributes.py +130 -0
- ohauthcat-0.0.2/src/ohauthcat/directory_csv.py +72 -0
- ohauthcat-0.0.2/src/ohauthcat/end_user.py +232 -0
- ohauthcat-0.0.2/src/ohauthcat/errors.py +135 -0
- ohauthcat-0.0.2/src/ohauthcat/existing_auth.py +1222 -0
- ohauthcat-0.0.2/src/ohauthcat/flags.py +200 -0
- ohauthcat-0.0.2/src/ohauthcat/framework.py +448 -0
- ohauthcat-0.0.2/src/ohauthcat/jwt_verifier.py +225 -0
- ohauthcat-0.0.2/src/ohauthcat/logo.py +123 -0
- ohauthcat-0.0.2/src/ohauthcat/migration.py +351 -0
- ohauthcat-0.0.2/src/ohauthcat/models.py +9246 -0
- ohauthcat-0.0.2/src/ohauthcat/passkeys.py +211 -0
- ohauthcat-0.0.2/src/ohauthcat/py.typed +0 -0
- ohauthcat-0.0.2/src/ohauthcat/resource_server.py +148 -0
- ohauthcat-0.0.2/src/ohauthcat/resources.py +10311 -0
- ohauthcat-0.0.2/src/ohauthcat/response_models.py +8758 -0
- ohauthcat-0.0.2/src/ohauthcat/ssf.py +218 -0
- ohauthcat-0.0.2/src/ohauthcat/sso.py +391 -0
- ohauthcat-0.0.2/src/ohauthcat/standby.py +137 -0
- ohauthcat-0.0.2/src/ohauthcat/transport.py +929 -0
- ohauthcat-0.0.2/src/ohauthcat/webhooks.py +178 -0
- ohauthcat-0.0.2/src/ohauthcat/wire.py +199 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# OS
|
|
2
|
+
.DS_Store
|
|
3
|
+
|
|
4
|
+
# Environment variables and secrets
|
|
5
|
+
.env
|
|
6
|
+
.env.*
|
|
7
|
+
!.env.template
|
|
8
|
+
*.pem
|
|
9
|
+
*.key
|
|
10
|
+
secrets/
|
|
11
|
+
|
|
12
|
+
# Build outputs
|
|
13
|
+
dist/
|
|
14
|
+
build/
|
|
15
|
+
coverage/
|
|
16
|
+
*.tsbuildinfo
|
|
17
|
+
|
|
18
|
+
# Node
|
|
19
|
+
node_modules/
|
|
20
|
+
|
|
21
|
+
# Python
|
|
22
|
+
.venv/
|
|
23
|
+
__pycache__/
|
|
24
|
+
*.py[cod]
|
|
25
|
+
.mypy_cache/
|
|
26
|
+
.pytest_cache/
|
|
27
|
+
.ruff_cache/
|
|
28
|
+
.coverage
|
|
29
|
+
|
|
30
|
+
# Repository baseline (handbook doc 31 §2b)
|
|
31
|
+
Thumbs.db
|
|
32
|
+
.idea/
|
|
33
|
+
.vscode/*
|
|
34
|
+
!.vscode/extensions.json
|
|
35
|
+
*.swp
|
|
36
|
+
*.p12
|
|
37
|
+
*.log
|
|
38
|
+
tmp/
|
|
39
|
+
.cache/
|
|
40
|
+
*.egg-info/
|
|
41
|
+
|
|
42
|
+
# graphify output is generated locally on demand and never committed.
|
|
43
|
+
graphify-out/
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `ohauthcat` Python SDK are recorded here. The SDK
|
|
4
|
+
follows [Semantic Versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
## Unreleased
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
10
|
+
- Request models now enforce the size limits defined by the OhAuthCat API, so
|
|
11
|
+
an oversized request fails locally with a validation error instead of being
|
|
12
|
+
sent and rejected by the API:
|
|
13
|
+
- `AttributePreviewRequest.attributes`: at most 256 keys, each with at most
|
|
14
|
+
256 values; `AttributePreviewRequest.claims`: at most 256 keys.
|
|
15
|
+
- `ConnectionForwardingUpdate.acs_urls` and `.redirect_uris`,
|
|
16
|
+
`Saml.legacy_acs_urls` and `ImportBundleDirectory.legacy_scim_urls`: at
|
|
17
|
+
most 5 entries each.
|
|
18
|
+
- `Metadata` and `ImportBundleOrganization.metadata`: at most 50 keys, each
|
|
19
|
+
value at most 500 characters.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- The metadata check was reversed: it capped metadata at 500 keys and left
|
|
24
|
+
values unbounded. It now matches the API (50 keys, values of at most 500
|
|
25
|
+
characters).
|
|
26
|
+
|
|
27
|
+
No public class, field or import path was renamed or removed.
|
ohauthcat-0.0.2/LICENSE
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
OhAuthCat SDK and CLI Licence
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Generic Technologies Private Limited. All rights reserved.
|
|
4
|
+
OhAuthCat is a product of Generic Technologies Private Limited.
|
|
5
|
+
|
|
6
|
+
This software development kit (the "Software") is licensed, not sold, under
|
|
7
|
+
the OhAuthCat SDK and CLI Licence, version 1.0, published at
|
|
8
|
+
https://ohauthcat.com/legal/sdk-licence (the "Licence"). By downloading,
|
|
9
|
+
installing or using the Software you accept the Licence. The summary below is
|
|
10
|
+
for convenience; if it differs from the Licence, the Licence prevails.
|
|
11
|
+
|
|
12
|
+
While you have an active agreement with OhAuthCat for its hosted service, you
|
|
13
|
+
may use, copy, read, modify and make derivative works of the Software, and
|
|
14
|
+
distribute it, modified or not, only as an integrated part of your own
|
|
15
|
+
application, solely to access and use the OhAuthCat service.
|
|
16
|
+
|
|
17
|
+
You must not:
|
|
18
|
+
|
|
19
|
+
1. use the Software, or any modified version or part of it, with any service
|
|
20
|
+
other than the OhAuthCat service;
|
|
21
|
+
2. distribute, publish or make the Software, or any modified version or part
|
|
22
|
+
of it, available on its own, as a standalone library, package, SDK, fork or
|
|
23
|
+
tool;
|
|
24
|
+
3. remove or alter any copyright notice, this file or THIRD_PARTY_LICENSES;
|
|
25
|
+
4. use the Software or its source code to build or help build a product or
|
|
26
|
+
service that competes with the OhAuthCat service; or
|
|
27
|
+
5. license or distribute the Software under terms that would require it to be
|
|
28
|
+
disclosed or licensed as open source or distributed free of charge.
|
|
29
|
+
|
|
30
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
31
|
+
IMPLIED, TO THE EXTENT THE LAW ALLOWS. LIABILITY IS LIMITED AS SET OUT IN THE
|
|
32
|
+
LICENCE.
|
|
33
|
+
|
|
34
|
+
Third-party components distributed with the Software are listed in
|
|
35
|
+
THIRD_PARTY_LICENSES and are governed by their own licences.
|
ohauthcat-0.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ohauthcat
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Official OhAuthCat SDK for Python
|
|
5
|
+
Project-URL: Documentation, https://docs.ohauthcat.com
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
License-File: THIRD_PARTY_LICENSES
|
|
8
|
+
Classifier: License :: Other/Proprietary License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Typing :: Typed
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Requires-Dist: cryptography<51,>=50.0.2
|
|
13
|
+
Requires-Dist: httpx<1,>=0.27
|
|
14
|
+
Requires-Dist: pydantic<3,>=2.7
|
|
15
|
+
Requires-Dist: typing-extensions<5,>=4.12
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: black==26.10.1; extra == 'dev'
|
|
18
|
+
Requires-Dist: build; extra == 'dev'
|
|
19
|
+
Requires-Dist: datamodel-code-generator==0.83.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: hatch; extra == 'dev'
|
|
21
|
+
Requires-Dist: isort==6.1.0; extra == 'dev'
|
|
22
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
24
|
+
Requires-Dist: ruff==0.16.10; extra == 'dev'
|
|
25
|
+
Requires-Dist: setuptools>=84.1.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: types-pyyaml==6.0.12.20260906; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# ohauthcat (Python)
|
|
30
|
+
|
|
31
|
+
Typed sync and async clients for the pinned OhAuthCat versioned operations.
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
pip install ohauthcat
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Before the first release
|
|
38
|
+
|
|
39
|
+
`ohauthcat` is not published to PyPI yet, so the install command above fails until the first release. Until then, install it from the source repository. The repository is private: you need read access to `bitbucket.org/the-genie-team/ohauthcat-sdk-python` over SSH. Run these commands from your project directory; the `v0.0.2` tag matches the version above (until that tag exists, replace `--branch v0.0.2` with `--branch dev` in `git clone`, and `@v0.0.2` with `@dev` in the `pip install` URL).
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
git clone --depth 1 --branch v0.0.2 git@bitbucket.org:the-genie-team/ohauthcat-sdk-python.git
|
|
43
|
+
pip install ./ohauthcat-sdk-python
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Without a clone:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
pip install "git+ssh://git@bitbucket.org/the-genie-team/ohauthcat-sdk-python.git@v0.0.2"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from ohauthcat import OhAuthCat, models
|
|
54
|
+
|
|
55
|
+
with OhAuthCat(api_key="sk_example") as client:
|
|
56
|
+
organization = client.organizations.create_organization(
|
|
57
|
+
body=models.OrganizationCreate(name="Example")
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Errors
|
|
62
|
+
|
|
63
|
+
API failures raise `OhAuthCatError` (or a subclass) with `code`, `status`, `param`, `doc_url` and `request_id`. Arguments the SDK refuses before sending anything raise `OhAuthCatValidationError`: its `code` is `request_invalid`, `param` names the argument when known and `doc_url` links the catalogue page. It is also a `ValueError`, so existing `except ValueError` handlers keep working.
|
|
64
|
+
|
|
65
|
+
## Webhooks
|
|
66
|
+
|
|
67
|
+
Verify the exact raw request body before parsing it. Pass every secret you hold during a rotation. Verification raises `WebhookVerificationError` whose `code` is, for example, `signature_invalid` or `timestamp_outside` (older than 300 seconds).
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import os
|
|
71
|
+
|
|
72
|
+
from ohauthcat import webhooks
|
|
73
|
+
from ohauthcat.webhooks import WebhookVerificationError
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def handle_webhook(raw_body: bytes, header: str) -> int:
|
|
77
|
+
"""raw_body: the exact request bytes; header: the OhAuthCat-Signature header."""
|
|
78
|
+
try:
|
|
79
|
+
event = webhooks.verify(raw_body, header, [os.environ.get("OHAUTHCAT_WEBHOOK_SECRET", "")])
|
|
80
|
+
except WebhookVerificationError as err:
|
|
81
|
+
print("rejected:", err.code)
|
|
82
|
+
return 400
|
|
83
|
+
print(event.type, event.id)
|
|
84
|
+
return 204
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`AsyncOhAuthCat` provides the same resource methods with `await` and async context management. `client.events.list()` returns a forward iterator, or async iterator. `client.portal.create_link(...)` aliases the generated portal operation. Original `models` validate requests; `response_models` tolerate additive enum strings while retaining singleton discriminators and field constraints.
|
|
88
|
+
|
|
89
|
+
## Sign-in
|
|
90
|
+
|
|
91
|
+
Configure `client_id="pk_..."` (and `environment_id="env_..."` to verify access tokens). Every sign-in needs a random `state` that you keep in the user's session and compare on the callback, or an attacker can sign your user into the attacker's account (login CSRF). `get_authorization_url` generates one when you pass none and returns it as `.state`, together with the PKCE `.code_verifier`. Refuse a missing or different state before exchanging the code. The exchange uses the secret API key, is never retried automatically (reusing a code revokes the first grant), and raises `RestartSignIn` when the user must start again.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
import hmac
|
|
95
|
+
|
|
96
|
+
from ohauthcat import OhAuthCat, RestartSignIn
|
|
97
|
+
|
|
98
|
+
REDIRECT_URI = "https://app.example.com/callback"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def start_sign_in(client: OhAuthCat, session: dict[str, str]) -> str:
|
|
102
|
+
"""Returns the URL to redirect the browser to. `session` is your server-side session."""
|
|
103
|
+
auth = client.sso.get_authorization_url(redirect_uri=REDIRECT_URI, domain="customer.com")
|
|
104
|
+
session["sso_state"] = auth.state
|
|
105
|
+
session["sso_verifier"] = auth.code_verifier
|
|
106
|
+
return auth.url
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def finish_sign_in(client: OhAuthCat, session: dict[str, str], code: str, state: str) -> str:
|
|
110
|
+
expected = session.pop("sso_state", "")
|
|
111
|
+
verifier = session.pop("sso_verifier", "")
|
|
112
|
+
if not expected or not verifier or not hmac.compare_digest(expected.encode(), state.encode()):
|
|
113
|
+
raise PermissionError("sign-in expired, start again") # never exchange the code
|
|
114
|
+
try:
|
|
115
|
+
token = client.sso.get_profile_and_token(
|
|
116
|
+
code=code, code_verifier=verifier, redirect_uri=REDIRECT_URI
|
|
117
|
+
)
|
|
118
|
+
except RestartSignIn:
|
|
119
|
+
raise PermissionError("sign-in expired, start again") from None
|
|
120
|
+
return token.profile.email # create your session for this user
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Guests: a sign-in admitted under the organization's guest policy (an email domain outside its verified domains) is identified by `token.profile.access_policy == "guest"` (`AccessPolicy.guest.value` in `ohauthcat.response_models`; response enums decode to plain strings so a value newer than this SDK never fails); for every other sign-in the field is absent. `email_domain_verified` stays true for guests, so check this field when guests must get less access.
|
|
124
|
+
|
|
125
|
+
`client.sso.verify_access_token(token)` verifies ES256 with the configured issuer/client/environment and a bounded JWKS cache fetched from the active trusted broker origin. The configured issuer remains exact when retrieval fails over. `get_sso_profile(access_token=token)` additionally checks the authenticated broker profile. Await both on the async client. Issuer defaults to `https://auth.ohauthcat.com`; explicitly configured HTTP loopback origins support local fixtures. No token-supplied issuer or key URL is trusted.
|
|
126
|
+
|
|
127
|
+
Clients own their default HTTPX clients. Injected `http_client` instances remain caller-owned. Closing the SDK disables requests and token verification. Async requests have an overall deadline and cancellation cleanup. Sync HTTPX uses per-phase timeouts and checks the total budget between chunks/retries, including JWKS lock waits; a blocked network read or custom transport may exceed that total budget before control returns.
|
|
128
|
+
|
|
129
|
+
Upload a logo from bytes or a file path:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
with OhAuthCat(api_key="sk_example") as client:
|
|
133
|
+
logo = client.logos.upload_file("logo.svg", idempotency_key="logo-2026-10")
|
|
134
|
+
# or: client.logos.upload(Path("logo.png").read_bytes())
|
|
135
|
+
client.branding.update_branding(body=models.BrandingUpdate(logo_url=str(logo.logo_url)))
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`client.logos.upload(data)` and `upload_file(path)` (awaitable on the async client) refuse, before any request, a file that is empty, larger than 256 KiB, or neither a PNG (by its signature) nor an SVG (by its root element), raising `OhAuthCatError` with code `logo_invalid`, status `0` and param `data`. `check_logo(data)` runs the same check alone. Accepted bytes are base64-encoded and sent to `upload_branding_logo`; the API still validates dimensions and SVG content, and its `logo_invalid` refusal raises the same error type with the HTTP status and message.
|
|
139
|
+
|
|
140
|
+
Install tooling in a repository-local virtual environment:
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
python3 -m venv .venv
|
|
144
|
+
.venv/bin/pip install -e '.[dev]'
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Run named test files, for example `.venv/bin/python -m pytest tests/test_transport.py`. The independent contract tests additionally need Node 24 and the development spec mock: `(cd spec-mock && pnpm install --frozen-lockfile)`.
|
|
148
|
+
|
|
149
|
+
Migration helpers are explicitly import-bound: `helper = client.migration(import_id=..., legacy_provider="workos", resolve_legacy_profile=resolver)`. Read `helper.routing.for_organization(org)` and call `helper.migrate_session(legacy_access_token=...)`; await both with the async client. Your resolver authenticates UserInfo and returns the legacy profile ID, while OhAuthCat receives only the import and that ID. Routing cache defaults to 30 seconds (maximum 60); call `helper.routing.invalidate()` after a switch or rollback. Session migration reads a fresh import. Sync resolvers must cooperate with `LegacySessionContext.remaining_seconds()` and `is_cancelled()`; async resolvers must honor cancellation, whose cleanup is awaited. See the OhAuthCat docs for the full contract and direct generated API distinction.
|
|
150
|
+
|
|
151
|
+
Automatic trusted edge failover, configurable fallback origins and request-driven dual health probes use the shared policy in handbook docs/12 §8.5. `standby=False` disables it; custom primary origins require both explicit trusted fallback origins to opt in.
|
|
152
|
+
|
|
153
|
+
## Portal links
|
|
154
|
+
|
|
155
|
+
A setup link lasts `expires_in` seconds, 300 (5 minutes) to 2,592,000 (30 days); leave it out for the default of 604,800 (7 days). A link created with a secret key opens once; revoke it to end it early, and `expires_at` on the result says when it ends. `ttl` (60 to 300 seconds) is only for sealed single-use embed links; never send both.
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
link = client.portal.create_link(
|
|
159
|
+
body=models.PortalLinkCreate(
|
|
160
|
+
organization="org_...", intents=[models.Intent.sso], expires_in=24 * 60 * 60
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
print(link.url, link.expires_at)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Development
|
|
167
|
+
|
|
168
|
+
Python 3.11 (CI also runs 3.14). From the repository root:
|
|
169
|
+
|
|
170
|
+
```sh
|
|
171
|
+
python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'
|
|
172
|
+
(cd spec-mock && pnpm install --frozen-lockfile) # Node 24; contract tests only
|
|
173
|
+
.venv/bin/ruff format --check . && .venv/bin/ruff check . && .venv/bin/mypy --strict src
|
|
174
|
+
timeout 300 .venv/bin/python -m pytest tests/test_transport.py # targeted
|
|
175
|
+
.venv/bin/python scripts/check_licenses.py
|
|
176
|
+
sh scripts/check-pin.sh && sh scripts/check-drift.sh
|
|
177
|
+
sh scripts/repin.sh <core-ref> # local only, needs ../ohauthcat-core
|
|
178
|
+
.venv/bin/python -m build
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Framework examples (FastAPI, Django, Flask) live in `examples/python`. Releases: [RELEASING.md](RELEASING.md).
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# ohauthcat (Python)
|
|
2
|
+
|
|
3
|
+
Typed sync and async clients for the pinned OhAuthCat versioned operations.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pip install ohauthcat
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
### Before the first release
|
|
10
|
+
|
|
11
|
+
`ohauthcat` is not published to PyPI yet, so the install command above fails until the first release. Until then, install it from the source repository. The repository is private: you need read access to `bitbucket.org/the-genie-team/ohauthcat-sdk-python` over SSH. Run these commands from your project directory; the `v0.0.2` tag matches the version above (until that tag exists, replace `--branch v0.0.2` with `--branch dev` in `git clone`, and `@v0.0.2` with `@dev` in the `pip install` URL).
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
git clone --depth 1 --branch v0.0.2 git@bitbucket.org:the-genie-team/ohauthcat-sdk-python.git
|
|
15
|
+
pip install ./ohauthcat-sdk-python
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Without a clone:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
pip install "git+ssh://git@bitbucket.org/the-genie-team/ohauthcat-sdk-python.git@v0.0.2"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from ohauthcat import OhAuthCat, models
|
|
26
|
+
|
|
27
|
+
with OhAuthCat(api_key="sk_example") as client:
|
|
28
|
+
organization = client.organizations.create_organization(
|
|
29
|
+
body=models.OrganizationCreate(name="Example")
|
|
30
|
+
)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Errors
|
|
34
|
+
|
|
35
|
+
API failures raise `OhAuthCatError` (or a subclass) with `code`, `status`, `param`, `doc_url` and `request_id`. Arguments the SDK refuses before sending anything raise `OhAuthCatValidationError`: its `code` is `request_invalid`, `param` names the argument when known and `doc_url` links the catalogue page. It is also a `ValueError`, so existing `except ValueError` handlers keep working.
|
|
36
|
+
|
|
37
|
+
## Webhooks
|
|
38
|
+
|
|
39
|
+
Verify the exact raw request body before parsing it. Pass every secret you hold during a rotation. Verification raises `WebhookVerificationError` whose `code` is, for example, `signature_invalid` or `timestamp_outside` (older than 300 seconds).
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import os
|
|
43
|
+
|
|
44
|
+
from ohauthcat import webhooks
|
|
45
|
+
from ohauthcat.webhooks import WebhookVerificationError
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def handle_webhook(raw_body: bytes, header: str) -> int:
|
|
49
|
+
"""raw_body: the exact request bytes; header: the OhAuthCat-Signature header."""
|
|
50
|
+
try:
|
|
51
|
+
event = webhooks.verify(raw_body, header, [os.environ.get("OHAUTHCAT_WEBHOOK_SECRET", "")])
|
|
52
|
+
except WebhookVerificationError as err:
|
|
53
|
+
print("rejected:", err.code)
|
|
54
|
+
return 400
|
|
55
|
+
print(event.type, event.id)
|
|
56
|
+
return 204
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`AsyncOhAuthCat` provides the same resource methods with `await` and async context management. `client.events.list()` returns a forward iterator, or async iterator. `client.portal.create_link(...)` aliases the generated portal operation. Original `models` validate requests; `response_models` tolerate additive enum strings while retaining singleton discriminators and field constraints.
|
|
60
|
+
|
|
61
|
+
## Sign-in
|
|
62
|
+
|
|
63
|
+
Configure `client_id="pk_..."` (and `environment_id="env_..."` to verify access tokens). Every sign-in needs a random `state` that you keep in the user's session and compare on the callback, or an attacker can sign your user into the attacker's account (login CSRF). `get_authorization_url` generates one when you pass none and returns it as `.state`, together with the PKCE `.code_verifier`. Refuse a missing or different state before exchanging the code. The exchange uses the secret API key, is never retried automatically (reusing a code revokes the first grant), and raises `RestartSignIn` when the user must start again.
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import hmac
|
|
67
|
+
|
|
68
|
+
from ohauthcat import OhAuthCat, RestartSignIn
|
|
69
|
+
|
|
70
|
+
REDIRECT_URI = "https://app.example.com/callback"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def start_sign_in(client: OhAuthCat, session: dict[str, str]) -> str:
|
|
74
|
+
"""Returns the URL to redirect the browser to. `session` is your server-side session."""
|
|
75
|
+
auth = client.sso.get_authorization_url(redirect_uri=REDIRECT_URI, domain="customer.com")
|
|
76
|
+
session["sso_state"] = auth.state
|
|
77
|
+
session["sso_verifier"] = auth.code_verifier
|
|
78
|
+
return auth.url
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def finish_sign_in(client: OhAuthCat, session: dict[str, str], code: str, state: str) -> str:
|
|
82
|
+
expected = session.pop("sso_state", "")
|
|
83
|
+
verifier = session.pop("sso_verifier", "")
|
|
84
|
+
if not expected or not verifier or not hmac.compare_digest(expected.encode(), state.encode()):
|
|
85
|
+
raise PermissionError("sign-in expired, start again") # never exchange the code
|
|
86
|
+
try:
|
|
87
|
+
token = client.sso.get_profile_and_token(
|
|
88
|
+
code=code, code_verifier=verifier, redirect_uri=REDIRECT_URI
|
|
89
|
+
)
|
|
90
|
+
except RestartSignIn:
|
|
91
|
+
raise PermissionError("sign-in expired, start again") from None
|
|
92
|
+
return token.profile.email # create your session for this user
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Guests: a sign-in admitted under the organization's guest policy (an email domain outside its verified domains) is identified by `token.profile.access_policy == "guest"` (`AccessPolicy.guest.value` in `ohauthcat.response_models`; response enums decode to plain strings so a value newer than this SDK never fails); for every other sign-in the field is absent. `email_domain_verified` stays true for guests, so check this field when guests must get less access.
|
|
96
|
+
|
|
97
|
+
`client.sso.verify_access_token(token)` verifies ES256 with the configured issuer/client/environment and a bounded JWKS cache fetched from the active trusted broker origin. The configured issuer remains exact when retrieval fails over. `get_sso_profile(access_token=token)` additionally checks the authenticated broker profile. Await both on the async client. Issuer defaults to `https://auth.ohauthcat.com`; explicitly configured HTTP loopback origins support local fixtures. No token-supplied issuer or key URL is trusted.
|
|
98
|
+
|
|
99
|
+
Clients own their default HTTPX clients. Injected `http_client` instances remain caller-owned. Closing the SDK disables requests and token verification. Async requests have an overall deadline and cancellation cleanup. Sync HTTPX uses per-phase timeouts and checks the total budget between chunks/retries, including JWKS lock waits; a blocked network read or custom transport may exceed that total budget before control returns.
|
|
100
|
+
|
|
101
|
+
Upload a logo from bytes or a file path:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
with OhAuthCat(api_key="sk_example") as client:
|
|
105
|
+
logo = client.logos.upload_file("logo.svg", idempotency_key="logo-2026-10")
|
|
106
|
+
# or: client.logos.upload(Path("logo.png").read_bytes())
|
|
107
|
+
client.branding.update_branding(body=models.BrandingUpdate(logo_url=str(logo.logo_url)))
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`client.logos.upload(data)` and `upload_file(path)` (awaitable on the async client) refuse, before any request, a file that is empty, larger than 256 KiB, or neither a PNG (by its signature) nor an SVG (by its root element), raising `OhAuthCatError` with code `logo_invalid`, status `0` and param `data`. `check_logo(data)` runs the same check alone. Accepted bytes are base64-encoded and sent to `upload_branding_logo`; the API still validates dimensions and SVG content, and its `logo_invalid` refusal raises the same error type with the HTTP status and message.
|
|
111
|
+
|
|
112
|
+
Install tooling in a repository-local virtual environment:
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
python3 -m venv .venv
|
|
116
|
+
.venv/bin/pip install -e '.[dev]'
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Run named test files, for example `.venv/bin/python -m pytest tests/test_transport.py`. The independent contract tests additionally need Node 24 and the development spec mock: `(cd spec-mock && pnpm install --frozen-lockfile)`.
|
|
120
|
+
|
|
121
|
+
Migration helpers are explicitly import-bound: `helper = client.migration(import_id=..., legacy_provider="workos", resolve_legacy_profile=resolver)`. Read `helper.routing.for_organization(org)` and call `helper.migrate_session(legacy_access_token=...)`; await both with the async client. Your resolver authenticates UserInfo and returns the legacy profile ID, while OhAuthCat receives only the import and that ID. Routing cache defaults to 30 seconds (maximum 60); call `helper.routing.invalidate()` after a switch or rollback. Session migration reads a fresh import. Sync resolvers must cooperate with `LegacySessionContext.remaining_seconds()` and `is_cancelled()`; async resolvers must honor cancellation, whose cleanup is awaited. See the OhAuthCat docs for the full contract and direct generated API distinction.
|
|
122
|
+
|
|
123
|
+
Automatic trusted edge failover, configurable fallback origins and request-driven dual health probes use the shared policy in handbook docs/12 §8.5. `standby=False` disables it; custom primary origins require both explicit trusted fallback origins to opt in.
|
|
124
|
+
|
|
125
|
+
## Portal links
|
|
126
|
+
|
|
127
|
+
A setup link lasts `expires_in` seconds, 300 (5 minutes) to 2,592,000 (30 days); leave it out for the default of 604,800 (7 days). A link created with a secret key opens once; revoke it to end it early, and `expires_at` on the result says when it ends. `ttl` (60 to 300 seconds) is only for sealed single-use embed links; never send both.
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
link = client.portal.create_link(
|
|
131
|
+
body=models.PortalLinkCreate(
|
|
132
|
+
organization="org_...", intents=[models.Intent.sso], expires_in=24 * 60 * 60
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
print(link.url, link.expires_at)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Development
|
|
139
|
+
|
|
140
|
+
Python 3.11 (CI also runs 3.14). From the repository root:
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'
|
|
144
|
+
(cd spec-mock && pnpm install --frozen-lockfile) # Node 24; contract tests only
|
|
145
|
+
.venv/bin/ruff format --check . && .venv/bin/ruff check . && .venv/bin/mypy --strict src
|
|
146
|
+
timeout 300 .venv/bin/python -m pytest tests/test_transport.py # targeted
|
|
147
|
+
.venv/bin/python scripts/check_licenses.py
|
|
148
|
+
sh scripts/check-pin.sh && sh scripts/check-drift.sh
|
|
149
|
+
sh scripts/repin.sh <core-ref> # local only, needs ../ohauthcat-core
|
|
150
|
+
.venv/bin/python -m build
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Framework examples (FastAPI, Django, Flask) live in `examples/python`. Releases: [RELEASING.md](RELEASING.md).
|