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
)
ParameterRequiredDescription
issuerYesYour environment's issuer URL — the Base URL from the Admin Console. Discovery resolves the token and JWKS endpoints from it.
config_idYesProject credential, sent as the X-Config-Id header on the token exchange.
client_idYesThe Admin Console value stamped as the aud claim on issued id_tokens; enforced on every verify.
timeout_secondsNoKeyword-only. HTTP timeout for the token exchange.
discovery_timeout_secondsNoKeyword-only. HTTP timeout for the one-time discovery fetch.
clock_tolerance_secondsNoKeyword-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:

MethodSignature
from_issuerawait HawcxOAuthAsync.from_issuer(issuer, config_id, client_id, *, timeout_seconds=10, discovery_timeout_seconds=5, clock_tolerance_seconds=10)
exchange_codeawait oauth.exchange_code(code, code_verifier, redirect_uri=None, expected_nonce=None) -> ExchangeResult
verify_tokenawait oauth.verify_token(token, *, nonce=None) -> dict
with_client_assertionoauth.with_client_assertion(signer) — opt into private_key_jwt (returns self; sync)
acloseawait 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")
MethodSignature
start_step_upawait hawcx.start_step_up(*, user_id, purpose, new_mfa_method=None, new_phone_number=None) -> StepUpStartTokenResponse
consume_step_upawait hawcx.consume_step_up(*, receipt) -> StepUpConsumeResponse
managementawait hawcx.management(endpoint, payload) -> dict
acloseawait 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_jwt only (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.

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, ES384 or ES512 and verifies against the JWKS;
  • its body_sha256 claim matches the SHA-256 of the exact response bytes;
  • its iat is within 300 seconds of local time, plus clock_tolerance_seconds;
  • it carries no aud, sub or nonce claim, 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.

OptionWhereDefaultDescription
verify_response_signaturewith_private_key_jwt, JwtBearerTransport, JwtBearerTransportAsyncTrueVerify X-Response-Signature on every 2xx response. Emergency escape hatch only, see below.
jwks_urlwith_private_key_jwt, JwtBearerTransport, JwtBearerTransportAsync{base_url}/.well-known/jwks.jsonJWKS the signature is verified against. Same URL rules as base_url, except that a query is allowed.
clock_tolerance_secondsJwtBearerTransport, JwtBearerTransportAsync10Extra 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.
    raise

verify_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:. Plain http: is accepted only for localhost, 127.0.0.1 and [::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")
MethodSignature
start_step_uphawcx.start_step_up(*, user_id, purpose, new_mfa_method=None, new_phone_number=None) -> StepUpStartTokenResponse
consume_step_uphawcx.consume_step_up(*, receipt) -> StepUpConsumeResponse
managementhawcx.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.) — use management_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.

FieldTypeDescription
id_tokenstrThe raw signed JWT. Do not use it as an access token.
claimsdict[str, Any]The verified claim set (sub, email, …).

StepUpStartTokenResponse

Dataclass returned by start_token / start_step_up.

FieldTypeDescription
start_tokenstrOpaque token that begins the step-up challenge.
expires_inintLifetime of the start token, in seconds.

StepUpConsumeResponse

Dataclass returned by consume_receipt / consume_step_up.

FieldTypeDescription
okboolTrue 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.

FieldTypeDescription
sp_signing_keystrService-provider Ed25519 private key (PEM).
sp_encryption_keystrService-provider X25519 private key (PEM).
idp_verify_keystrHawcx IdP Ed25519 public key (PEM).
idp_encryption_keystrHawcx IdP X25519 public key (PEM).
sp_kidstrService-provider key id.
idp_kidstrHawcx IdP key id.
signing_algLiteral["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_...")  # -> ParsedCredentials

Decodes 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