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="user@example.com",
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="user@example.com",
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="user@example.com",
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": "user@example.com"},
)Responses carry an X-Response-Signature JWS — signed with the same key and algorithm the OIDC JWKS advertises. The JWS alg follows your OIDC signing key's EC curve, so it matches whatever ${issuer}/.well-known/jwks.json advertises: ES256 (P-256) in dev/staging, ES512 (P-521) in production. You may verify it against the OIDC JWKS you already trust.
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.
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="user@example.com", 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="user@example.com",
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": "user@example.com"},
)
# Update it
client.management_request(
endpoint="/v1/management/users/mfa-enforcement",
payload={"userid": "user@example.com", "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 failedfrom 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