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="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")
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 tokensprivate_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="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")
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": "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.

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
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