Python SDK API Reference
Complete API reference for the Hawcx Python SDK
HawcxOAuth
Initialize the OAuth client and exchange codes for verified claims.
from hawcx_oauth_client import HawcxOAuth
import os
oauth = HawcxOAuth.from_issuer(
os.environ['HAWCX_BASE_URL'], # issuer URL (Base URL from Admin Console)
os.environ['HAWCX_CONFIG_ID'],
os.environ['HAWCX_CLIENT_ID'], # expected `aud` on issued id_tokens
)from_issuer is the constructor to use
HawcxOAuth.from_issuer(...) discovers the OAuth endpoints from <issuer>/.well-known/openid-configuration and enforces iss + aud on every verify. Unlike the Node SDK, the Python client has no legacy base_url constructor — from_issuer (or its async equivalent HawcxOAuthAsync.from_issuer) is the entry point.
HawcxOAuth.from_issuer(issuer, config_id, client_id, *, timeout_seconds=..., discovery_timeout_seconds=..., clock_tolerance_seconds=...)
Discovers /.well-known/openid-configuration once at construction and enforces signature + iss + aud + exp + nbf on every exchange_code. Recommended for new integrations.
from hawcx_oauth_client import HawcxOAuth
import os
oauth = HawcxOAuth.from_issuer(
os.environ['HAWCX_BASE_URL'], # issuer URL (Base URL from Admin Console)
os.environ['HAWCX_CONFIG_ID'],
os.environ['HAWCX_CLIENT_ID'], # expected `aud` on issued id_tokens
)| Parameter | Required | Description |
|---|---|---|
issuer | Yes | Your environment's issuer URL — the Base URL from the Admin Console. Discovery resolves the token and JWKS endpoints from it. |
config_id | Yes | Project credential, sent as the X-Config-Id header on the token exchange. |
client_id | Yes | The Admin Console value stamped as the aud claim on issued id_tokens; enforced on every verify. |
timeout_seconds | No | Keyword-only. HTTP timeout for the token exchange. |
discovery_timeout_seconds | No | Keyword-only. HTTP timeout for the one-time discovery fetch. |
clock_tolerance_seconds | No | Keyword-only. Leeway applied to exp / nbf / iat. |
Raises DiscoveryError if the discovery document can't be resolved. An async equivalent (await HawcxOAuthAsync.from_issuer(...)) is also available.
exchange_code(auth_code, code_verifier)
Exchange an authorization code for verified claims.
result = oauth.exchange_code(auth_code, code_verifier)
claims = result.claims
# claims['sub'] = user ID
# claims.get('email') = verified email (if present)Returns:
result.id_token: raw JWT (do not use as access token)result.claims: verified claims
verify_token(token)
Verify a JWT and return its claims.
claims = oauth.verify_token(id_token)Async clients
Every client has an async/await twin with an identical surface:
HawcxOAuthAsync (OAuth relying party), StepUpClientAsync (management /
step-up), and HawcxAsync (the high-level step-up convenience wrapper). Each is
an async context manager — use async with so the underlying
httpx.AsyncClient is closed on exit, or call await client.aclose() yourself.
HawcxOAuthAsync
The async equivalent of HawcxOAuth. The from_issuer factory
is itself awaitable and returns an async-context-manager instance:
import asyncio
from hawcx_oauth_client import HawcxOAuthAsync
async def main():
async with await HawcxOAuthAsync.from_issuer(
issuer, # e.g. "https://api.hawcx.com"
config_id, # → X-Config-Id header on the token exchange
client_id, # → expected `aud` on issued id_tokens
) as oauth:
result = await oauth.exchange_code(auth_code, code_verifier)
print(result.id_token, result.claims)
claims = await oauth.verify_token(id_token, nonce=expected_nonce)
asyncio.run(main())Method signatures mirror the sync client:
| Method | Signature |
|---|---|
from_issuer | await HawcxOAuthAsync.from_issuer(issuer, config_id, client_id, *, timeout_seconds=10, discovery_timeout_seconds=5, clock_tolerance_seconds=10) |
exchange_code | await oauth.exchange_code(code, code_verifier, redirect_uri=None, expected_nonce=None) -> ExchangeResult |
verify_token | await oauth.verify_token(token, *, nonce=None) -> dict |
with_client_assertion | oauth.with_client_assertion(signer) — opt into private_key_jwt (returns self; sync) |
aclose | await oauth.aclose() |
StepUpClientAsync
The async equivalent of StepUpClient. Prefer
with_private_key_jwt; from_secret_key / from_keys are deprecated (they emit
a DeprecationWarning).
import os
from hawcx_oauth_client import StepUpClientAsync
async def change_mfa():
async with StepUpClientAsync.with_private_key_jwt(
oidc_signing_key=os.environ["HAWCX_OIDC_PRIVATE_KEY_PEM"],
kid=os.environ["HAWCX_PRIVATE_KEY_KID"],
client_id=os.environ["HAWCX_CLIENT_ID"],
base_url="https://api.hawcx.com",
config_id=os.environ["HAWCX_CONFIG_ID"],
) as client:
started = await client.start_token(
user_id="[email protected]",
purpose="change_mfa_method",
new_mfa_method="email_otp",
)
# ... user completes the MFA challenge, frontend returns a receipt ...
await client.consume_receipt(receipt="receipt-from-frontend")await start_token(*, user_id, purpose, new_mfa_method=None, new_phone_number=None) -> StepUpStartTokenResponse,
await consume_receipt(*, receipt) -> StepUpConsumeResponse, and
await management_request(*, endpoint, payload) -> dict mirror their sync
counterparts.
HawcxAsync
The async equivalent of the high-level Hawcx step-up wrapper. It builds a
StepUpClientAsync internally from a secret key and exposes the same
convenience surface:
from hawcx_oauth_client import HawcxAsync
async def main():
async with HawcxAsync(
config_id="your-config-id",
secret_key="hwx_sk_v1_...",
base_url="https://api.hawcx.com",
) as hawcx:
result = await hawcx.start_step_up(
user_id="[email protected]",
purpose="change_mfa_method",
new_mfa_method="email_otp",
)
await hawcx.consume_step_up(receipt="receipt-from-frontend")| Method | Signature |
|---|---|
start_step_up | await hawcx.start_step_up(*, user_id, purpose, new_mfa_method=None, new_phone_number=None) -> StepUpStartTokenResponse |
consume_step_up | await hawcx.consume_step_up(*, receipt) -> StepUpConsumeResponse |
management | await hawcx.management(endpoint, payload) -> dict |
aclose | await hawcx.aclose() |
Step-Up Client (Management API)
StepUpClient is the entry point for the management / step-up API (/v1/management/*) — start_token, consume_receipt, and a generic management_request escape hatch.
Recommended: reuse your OIDC signing key
with_private_key_jwt (Path A) is the recommended way to authenticate management API calls. It reuses the same Ed25519 private_key_jwt signing key you already registered for OIDC login — one key instead of the legacy four-key ECIES blob — and is fully standards-based (RFC 7523).
Prerequisite: enable jwt on the Management API Authentication card
Before management calls can authenticate with private_key_jwt, enable it for the project in the Admin Console → Project Settings → Management API Authentication:
- Migrating — accepts both the legacy ECIES path and signed
private_key_jwt. Use this during rollout so existing and new callers coexist. - Signed tokens —
private_key_jwtonly (after every caller has migrated).
If the project is left on Legacy (ECIES only), private_key_jwt management calls are rejected with management_auth_mode_not_allowed (HTTP 401) — even with a correctly registered key. Note: regenerating an ECIES secret-key blob resets the project to Legacy, so set this after any blob regeneration.
StepUpClient.with_private_key_jwt() (recommended)
Mints a short-lived per-request EdDSA Bearer JWT (iss = sub = client_id, aud = the full endpoint URL, ~60s lifetime, random jti, and a body_sha256 claim that binds the JWT to the exact request body bytes). Routing uses the X-Config-Id header.
import os
from hawcx_oauth_client import StepUpClient
client = StepUpClient.with_private_key_jwt(
oidc_signing_key=os.environ["HAWCX_OIDC_PRIVATE_KEY_PEM"], # Ed25519 PEM or bytes
kid=os.environ["HAWCX_PRIVATE_KEY_KID"], # key id registered in the Hawcx Admin Console
client_id=os.environ["HAWCX_CLIENT_ID"],
base_url="https://api.hawcx.com",
config_id=os.environ["HAWCX_CONFIG_ID"],
)
# Begin a step-up flow (purposes: "change_mfa_method", "change_phone_number")
result = client.start_token(
user_id="[email protected]",
purpose="change_mfa_method",
new_mfa_method="email_otp",
)
# Finalize after the user completes the MFA challenge. The receipt is returned
# to your frontend by the Hawcx SDK once the challenge succeeds; forward it to
# your backend and pass it here.
receipt = "receipt-from-frontend-after-mfa"
client.consume_receipt(receipt=receipt)
# Generic call for any /v1/management/* endpoint
client.management_request(
endpoint="/v1/management/users/mfa-enforcement",
payload={"userid": "[email protected]"},
)with_private_key_jwt also accepts api_prefix (default "/v1"), relying_party, extra_headers, jwt_ttl_seconds (default 60), timeout_seconds (default 15.0), and the two verification options below.
Response signature verification
Since 2.0.0 the SDK verifies every successful management response before returning
it. Each 2xx response carries an X-Response-Signature header, a compact JWS signed
with the same key the OIDC JWKS publishes. Its alg follows that key's EC curve:
ES256 (P-256) in dev/staging, ES512 (P-521) in production. The SDK accepts the
response only when:
- the JWS uses
ES256,ES384orES512and verifies against the JWKS; - its
body_sha256claim matches the SHA-256 of the exact response bytes; - its
iatis within 300 seconds of local time, plusclock_tolerance_seconds; - it carries no
aud,subornonceclaim, so an ID token signed by the same key cannot pass as a response signature.
Non-2xx responses are not signed and are not verified; they still raise
DelegationRequestError with their status.
| Option | Where | Default | Description |
|---|---|---|---|
verify_response_signature | with_private_key_jwt, JwtBearerTransport, JwtBearerTransportAsync | True | Verify X-Response-Signature on every 2xx response. Emergency escape hatch only, see below. |
jwks_url | with_private_key_jwt, JwtBearerTransport, JwtBearerTransportAsync | {base_url}/.well-known/jwks.json | JWKS the signature is verified against. Same URL rules as base_url, except that a query is allowed. |
clock_tolerance_seconds | JwtBearerTransport, JwtBearerTransportAsync | 10 | Extra leeway added to the 300-second iat window |
The default jwks_url works because the management API and the OIDC issuer share one
host in every Hawcx environment. Set it only if your base_url is not that host.
Verification detects a response that was changed in transit, for example by a proxy that terminates TLS. Because the keys come from the same origin as the API by default, it does not protect against someone who can impersonate that origin itself. The signature binds the response body, not the request it answers.
A failure raises ResponseSignatureError, a subclass of DelegationResponseError:
from hawcx_oauth_client import ResponseSignatureError
try:
client.consume_receipt(receipt=receipt)
except ResponseSignatureError:
# Missing or invalid signature: treat the response as untrusted. The server
# has already processed the request, so reconcile state before retrying.
raiseverify_response_signature=False is an escape hatch
verify_response_signature=False restores the 1.x behaviour: a 2xx body is trusted on
TLS alone, and a warning is logged once per process. Use it only to recover from an
outage, or in a test harness whose stubs return unsigned responses. If a gateway or
CDN in front of Hawcx strips response headers, every call fails: fix the gateway
instead.
URL rules
Since 2.0.0 every management transport validates base_url at construction
(with_private_key_jwt, from_secret_key, from_keys, Hawcx, HawcxAsync). A
base_url must:
- use
https:. Plainhttp:is accepted only forlocalhost,127.0.0.1and[::1]; - carry no userinfo, query or fragment, no backslash, whitespace or non-ASCII
characters, no percent-encoded host, no port 0, and no
./..path segment.
An invalid base_url raises ValueError. Endpoint paths passed to
management_request must start with / and may not contain . / .. segments
(literal or %2e) or an encoded / or \ (%2f, %5c); an unsafe endpoint raises
DelegationRequestError before anything is sent.
StepUpClient.from_secret_key() (legacy, deprecated)
Legacy ECIES path — deprecated
from_secret_key and from_keys use the proprietary ECIES credential blob (hwx_sk_v1_…). They are deprecated but still functional (a DeprecationWarning is emitted) and will only be removed in a future major release. Migrate to with_private_key_jwt at your leisure.
Since 2.0.0 the ECIES transport behind from_secret_key, from_keys, Hawcx and
HawcxAsync fails closed: a 2xx response missing any of X-Signature,
X-Signature-Alg, X-Enc-Alg or X-Timestamp raises
DelegationResponseError("Missing signature headers in IdP response") instead of
being returned unverified. Unsigned error responses still raise
DelegationRequestError with their status.
from hawcx_oauth_client import StepUpClient
client = StepUpClient.from_secret_key( # DeprecationWarning is emitted
secret_key="hwx_sk_v1_...",
base_url="https://api.hawcx.com",
api_key="your-config-id",
tenant_header_name="X-Config-Id",
tenant_header_value="your-config-id",
)
client.start_token(user_id="[email protected]", purpose="change_mfa_method")Hawcx (high-level step-up wrapper)
Hawcx is a thin convenience client over StepUpClient — construct it from a
secret key and call start_step_up / consume_step_up / management directly
(the sync counterpart of HawcxAsync).
from hawcx_oauth_client import Hawcx
hawcx = Hawcx(
config_id="your-config-id",
secret_key="hwx_sk_v1_...",
base_url="https://api.hawcx.com",
)
result = hawcx.start_step_up(
user_id="[email protected]",
purpose="change_mfa_method", # or "change_phone_number"
new_mfa_method="email_otp",
)
hawcx.consume_step_up(receipt="receipt-from-frontend")| Method | Signature |
|---|---|
start_step_up | hawcx.start_step_up(*, user_id, purpose, new_mfa_method=None, new_phone_number=None) -> StepUpStartTokenResponse |
consume_step_up | hawcx.consume_step_up(*, receipt) -> StepUpConsumeResponse |
management | hawcx.management(endpoint, payload) -> dict |
Backend user & device management
`HawcxDelegationClient` was removed
Earlier versions exposed a HawcxDelegationClient (with initiate_mfa_change,
verify_mfa_change, get_user_credentials, device methods) and a MfaMethod
enum. These were removed — they are no longer exported and importing them
raises ImportError. All user/device/MFA management now goes through the
Step-Up Client:
- MFA method / phone changes — use
start_token(...)+consume_receipt(...)(a user-verified step-up). - Everything else under
/v1/management/*(MFA-enforcement policy, device list/revoke, etc.) — usemanagement_request(...).
from hawcx_oauth_client import StepUpClient
import os
client = StepUpClient.with_private_key_jwt(
oidc_signing_key=os.environ["HAWCX_OIDC_PRIVATE_KEY_PEM"],
kid=os.environ["HAWCX_PRIVATE_KEY_KID"],
client_id=os.environ["HAWCX_CLIENT_ID"],
base_url="https://api.hawcx.com",
config_id=os.environ["HAWCX_CONFIG_ID"],
)
# Read a user's MFA-enforcement preference
prefs = client.management_request(
endpoint="/v1/management/users/mfa-enforcement",
payload={"userid": "[email protected]"},
)
# Update it
client.management_request(
endpoint="/v1/management/users/mfa-enforcement",
payload={"userid": "[email protected]", "mfa_enforcement": "always"},
)Types
Returned by, or accepted as arguments to, the clients above. All are importable from the package root.
ExchangeResult
Frozen dataclass returned by exchange_code.
| Field | Type | Description |
|---|---|---|
id_token | str | The raw signed JWT. Do not use it as an access token. |
claims | dict[str, Any] | The verified claim set (sub, email, …). |
StepUpStartTokenResponse
Dataclass returned by start_token / start_step_up.
| Field | Type | Description |
|---|---|---|
start_token | str | Opaque token that begins the step-up challenge. |
expires_in | int | Lifetime of the start token, in seconds. |
StepUpConsumeResponse
Dataclass returned by consume_receipt / consume_step_up.
| Field | Type | Description |
|---|---|---|
ok | bool | True when the receipt was accepted and the change applied. |
StepUpPurpose and HxAuthMfaMethod
String literal types used by the step-up calls.
StepUpPurpose = Literal["change_mfa_method", "change_phone_number"]
HxAuthMfaMethod = Literal["sms_otp", "email_otp", "totp"]DelegationKeyMaterial
The four-key ECIES bundle accepted by StepUpClient.from_keys /
StepUpClientAsync.from_keys.
| Field | Type | Description |
|---|---|---|
sp_signing_key | str | Service-provider Ed25519 private key (PEM). |
sp_encryption_key | str | Service-provider X25519 private key (PEM). |
idp_verify_key | str | Hawcx IdP Ed25519 public key (PEM). |
idp_encryption_key | str | Hawcx IdP X25519 public key (PEM). |
sp_kid | str | Service-provider key id. |
idp_kid | str | Hawcx IdP key id. |
signing_alg | Literal["ed25519", "rsa-pss-sha256"] | Signature algorithm. Defaults to "ed25519". |
ParsedCredentials
The decoded contents of a hwx_sk_v1_… secret key, returned by
parse_hawcx_secret_key. All fields are str:
kid, hkid, signing_key_pem, verify_key_pem, encrypt_key_pem,
decrypt_key_pem.
Credential helpers & client assertions
parse_hawcx_secret_key
from hawcx_oauth_client import parse_hawcx_secret_key
creds = parse_hawcx_secret_key("hwx_sk_v1_...") # -> ParsedCredentialsDecodes a Hawcx ECIES secret-key blob into its constituent PEM keys and key ids.
Raises DelegationCryptoError if the blob is malformed, the wrong version, or
carries an unexpected algorithm/key length.
generate_credential_blob
from hawcx_oauth_client import generate_credential_blob
blob = generate_credential_blob(
kid=..., hkid=...,
ed_private=..., x_private=..., # 32-byte keys
hawcx_ed_public=..., hawcx_x_public=...,
) # -> "hwx_sk_v1_<base64url>"The inverse of parse_hawcx_secret_key — packs raw 32-byte key material into a
hwx_sk_v1_… blob. Raises DelegationCryptoError if any key is not 32 bytes.
ClientAssertionSigner
Signs the private_key_jwt client assertion used by the OAuth clients'
with_client_assertion(...) opt-in. Build one from your registered OIDC signing
key and hand it to HawcxOAuth / HawcxOAuthAsync.
CLIENT_ASSERTION_TYPE
The RFC 7523 constant sent as client_assertion_type on the token request:
CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"Exception hierarchy
HawcxError is the canonical root of every SDK exception (added to align with
the Node and Java SDKs — catching it catches everything). HawcxOAuthError is a
backwards-compatible alias that still catches every SDK error, so existing
except HawcxOAuthError: handlers keep working.
HawcxError # canonical root (message, error, error_description)
└── HawcxOAuthError # back-compat alias — still catches every SDK error
├── DiscoveryError # discovery document could not be resolved
├── TokenExchangeError # token exchange failed (adds status_code)
├── TokenVerificationError # signature / claims validation failed
└── DelegationError # base for delegation / step-up errors
├── DelegationCryptoError # ECIES / signature / key-parsing failure
├── DelegationRequestError # HTTP failure (adds status_code, response_body)
└── DelegationResponseError # response signature / timestamp / decryption check failed
└── ResponseSignatureError # X-Response-Signature missing or invalid (2.0.0+)from hawcx_oauth_client import HawcxError, TokenExchangeError
try:
result = oauth.exchange_code(auth_code, code_verifier)
except TokenExchangeError as e:
log.warning("token exchange failed (HTTP %s)", e.status_code)
except HawcxError as e:
log.error("Hawcx SDK call failed: %s", e)Last updated on