Skip to main content

hawcx-oauth-client

Customer-backend SDK for Hawcx's management API (/v1/management/*) — step-up authentication flows, MFA enforcement, phone updates, and policy lookups. Authenticate with your existing OIDC private_key_jwt signing key via StepUpClient.with_private_key_jwt (one key, standards-based RFC 7523 Bearer JWT). A legacy ECIES path (Hawcx, from_secret_key) is retained for existing deployments and will be removed in a future major version.

For OIDC login/signup, the SDK also ships a relying-party client — HawcxOAuth (sync) / HawcxOAuthAsync (async) — with OIDC discovery, authorization-code exchange, id_token verification, and optional private_key_jwt client authentication. See Confidential clients (private_key_jwt) below. (A standard OIDC library such as authlib also works against Hawcx's discovery URL if you prefer.)

Confidential clients (private_key_jwt)

If your project is registered in the Hawcx admin console with Client authentication = Signing key (private_key_jwt), attach an Ed25519 signer. The SDK signs an RFC 7523 client_assertion on every token exchange (PKCE is still sent). Public clients omit the signer and keep using PKCE only.

import json, os
from hawcx_oauth_client import HawcxOAuth, ClientAssertionSigner

private_jwk = json.loads(os.environ["HAWCX_PRIVATE_JWK"])  # OKP/Ed25519, with kid

oauth = HawcxOAuth.from_issuer(
    "https://dev-demo-api.hawcx.com",
    os.environ["HAWCX_CONFIG_ID"],
    os.environ["HAWCX_CLIENT_ID"],
).with_client_assertion(ClientAssertionSigner.ed25519_from_jwk(private_jwk))

result = oauth.exchange_code(code, code_verifier)   # PKCE + signed assertion
print(result.claims["sub"])

HawcxOAuthAsync exposes the same API with await (and await HawcxOAuthAsync.from_issuer(...)). The private key must be Ed25519 (kty="OKP", crv="Ed25519", with a kid); its public JWK is what you register in the admin console. The assertion's aud is the discovered token_endpoint, and EdDSA is the only accepted algorithm. client_id becomes the assertion iss/sub and the id_token aud. If you bound a nonce at /authorize, pass expected_nonce=... to exchange_code — verification refuses a nonce-bearing token when none is supplied (OIDC Core §3.1.3.7).

Installation

pip install hawcx-oauth-client

Requires Python 3.10+.

Quick start

New deployments: prefer StepUpClient.with_private_key_jwt — see Advanced — StepUpClient below.

Existing deployments using the hwx_sk_v1_... credential blob can use the Hawcx facade (note: this path is deprecated and will be removed in a future major version):

import os
from hawcx_oauth_client import Hawcx

hawcx = Hawcx(
    config_id=os.environ["HAWCX_CONFIG_ID"],   # your tenant's API key
    secret_key=os.environ["HAWCX_SECRET_KEY"], # hwx_sk_v1_... blob (legacy ECIES)
    base_url="https://api.hawcx.com",
)

# Begin a step-up flow to change a user's MFA method.
result = hawcx.start_step_up(
    user_id="alice@example.com",
    purpose="change_mfa_method",
    new_mfa_method="email_otp",
)
print(result.start_token)    # JWT, hand to your frontend
print(result.expires_in)     # seconds until it expires (~60)

# After the user completes the MFA challenge in the browser, your frontend
# returns a receipt — finalize the change:
hawcx.consume_step_up(receipt=user_receipt)

What this SDK is for

A Hawcx customer backend uses this SDK to perform privileged operations on its users (revoke MFA, change phone, force MFA enrollment, etc.) without asking the user to re-authenticate from scratch. The protocol underneath ("delegation") ensures these operations can only happen if your backend proves it holds the customer's private signing key.

Use case Use this SDK?
Basic login/signup in your customer-facing app ❌ — use any OIDC client (authlib, oauthlib)
Step up a user mid-session to re-verify before a sensitive action ✅
Set or read a user's MFA enforcement preference ✅
Change a user's stored phone number ✅
Look up tenant signin policy from your backend ✅
Bulk-provision users from your IdP into Hawcx ❌ — use Hawcx's SCIM endpoint with any SCIM client

API

Hawcx

The primary client. Two values to construct it:

  • config_id — your tenant identifier (the API key Hawcx hands out at provisioning).
  • secret_key — the hwx_sk_v1_... credential blob.
hawcx = Hawcx(
    config_id="your-tenant-id",
    secret_key="hwx_sk_v1_...",
    base_url="https://api.hawcx.com",   # required
    # All optional:
    api_prefix="/v1",
    extra_headers={},
    timeout_seconds=15,
    clock_skew_seconds=300,
)

Methods:

# Step-up: change MFA method.
hawcx.start_step_up(
    user_id="alice@example.com",
    purpose="change_mfa_method",
    new_mfa_method="email_otp",   # one of: email_otp, sms_otp, totp
)

# Step-up: change phone number.
hawcx.start_step_up(
    user_id="alice@example.com",
    purpose="change_phone_number",
    new_phone_number="+15551234567",
)

# Step-up: finalize after the user completes the challenge.
hawcx.consume_step_up(receipt="...")

# Generic management endpoint. Use for anything under /v1/management/*.
hawcx.management(
    "/v1/management/users/mfa-enforcement",
    {"userid": "alice@example.com"},                                 # read mode
)
hawcx.management(
    "/v1/management/users/mfa-enforcement",
    {"userid": "alice@example.com", "mfa_enforcement": "always_on"}, # set mode
)

HawcxAsync

Same surface, awaitable methods. Supports async with for clean HTTP-client teardown.

import asyncio
from hawcx_oauth_client import HawcxAsync

async def main():
    async with HawcxAsync(
        config_id=os.environ["HAWCX_CONFIG_ID"],
        secret_key=os.environ["HAWCX_SECRET_KEY"],
        base_url="https://api.hawcx.com",
    ) as hawcx:
        result = await hawcx.start_step_up(
            user_id="alice@example.com",
            purpose="change_mfa_method",
            new_mfa_method="email_otp",
        )

asyncio.run(main())

Errors

All exceptions inherit from HawcxOAuthError:

Exception When
DelegationCryptoError Invalid blob, key length wrong, signature verification failed locally
DelegationRequestError Network failure, non-2xx response (carries status_code + response_body)
DelegationResponseError Hawcx response missing signature, clock skew exceeded, decryption failed
from hawcx_oauth_client import DelegationRequestError

try:
    hawcx.start_step_up(user_id=..., purpose="change_mfa_method", new_mfa_method="email_otp")
except DelegationRequestError as e:
    if e.status_code == 404:
        print("user not found")
    else:
        raise

Advanced — StepUpClient

For callers who need lower-level control (custom headers, alternate API prefix, direct transport access), StepUpClient and StepUpClientAsync are also exported. Hawcx is a thin facade over them; reach for StepUpClient directly when you need these options.

Preferred: with_private_key_jwt

Reuses your existing OIDC private_key_jwt signing key — one Ed25519 key for both OIDC login and management API calls. Each request is authenticated with a per-request signed Bearer JWT (RFC 7523); no ECIES blob required.

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="your-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"],
    # Optional:
    api_prefix="/v1",
    relying_party=None,
    extra_headers={},
    jwt_ttl_seconds=60,
    jwks_url=None,              # default: {base_url}/.well-known/jwks.json
)

client.start_token(user_id="alice@example.com", purpose="change_mfa_method", new_mfa_method="email_otp")
client.consume_receipt(receipt=user_receipt)
client.management_request(endpoint="/v1/management/users/mfa-enforcement", payload={"userid": "alice@example.com"})

Every 2xx response is verified before it is returned: hx_auth signs it with an X-Response-Signature JWS (ES256/384/512, the issuer's OAuth signing key) that binds the SHA-256 of the exact response bytes, and the SDK checks it against the issuer JWKS (cached; an unknown kid refetches at most once per 30s). A missing or invalid signature raises ResponseSignatureError (a DelegationResponseError). verify_response_signature=False turns this off. It is an emergency escape hatch only, and it logs a warning.

Legacy (deprecated): ECIES via from_secret_key

Existing deployments using the hwx_sk_v1_... credential blob can continue using from_secret_key or from_keys, but these factories are deprecated and will be removed in a future major version. Migrate to with_private_key_jwt when possible.

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",
    extra_headers={"X-Custom": "..."},
)
client.start_token(...)
client.consume_receipt(...)
client.management_request(...)

Migration from 0.x

The 0.x line had two paths that are both removed in 1.x:

  • OAuth code-exchange path (exchange_code_for_claims, verify_jwt, require_oauth_claims): replaced by any standard OIDC client pointing at Hawcx's discovery URL. Example with authlib:

    from authlib.integrations.requests_client import OAuth2Session
    session = OAuth2Session(client_id=tenant_id, code_verifier=verifier)
    token = session.fetch_token(
        "https://api.hawcx.com/oauth2/token",
        code=code,
        headers={"X-Config-Id": tenant_id},
    )
    
  • HawcxDelegationClient (list_user_devices, revoke_device, initiate_mfa_change, set_suggested_mfa, etc.): those endpoints (/hc_auth/v5/*) were removed from hx_auth long before this release. Use hawcx.management(...) with the current /v1/management/* endpoints.

See CHANGELOG.md for the full breaking-change list.

Examples

For an end-to-end FastAPI backend mirroring the prod-validated hawcx_web_demo/backend/src/server.ts in Python, see EXAMPLE_USAGE.md.

Security model

Every request to /v1/management/* is:

  1. JSON-serialized then ECIES-encrypted with Hawcx's X25519 public key. Body is unreadable to anything between your process and Hawcx's hx_auth service — including Kong, load balancers, and observability sidecars.
  2. Signed with your Ed25519 private key over the encrypted body plus a timestamp. Hawcx verifies with your public key (which it stores; your private key never leaves your environment).
  3. Replay-protected: Hawcx rejects signed messages older than 5 minutes.

Responses follow the same protocol in reverse — signed by Hawcx, encrypted to your X25519 public key. The SDK verifies the signature, checks the timestamp, then decrypts. If any step fails, you get a DelegationResponseError.

Compare with Hawcx's other auth doors:

Door Auth What for
/oauth2/token X-Config-Id header (Kong API key) Standard OIDC code exchange
/v1/scim/{tid}/Users Bearer token Provisioning from external IdPs (Entra, Okta)
/v1/management/* Delegation crypto (this SDK) Customer-backend operations on users

License

MIT

Metadata

Release files for hawcx-oauth-client 2.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for hawcx-oauth-client 2.0.0
File Size Uploaded
hawcx_oauth_client-2.0.0.tar.gz 71.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for hawcx-oauth-client 2.0.0
File Interpreter ABI Platform
hawcx_oauth_client-2.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 129.9 kB

Release files / hawcx_oauth_client-2.0.0.tar.gz

Download URL hawcx_oauth_client-2.0.0.tar.gz
Size 71.4 kB
Tags Source
SHA-256 checksum
How to use checksums
c6d75108cdf8d117dbe63e1d05a36e77dc74d007cf6bfc711696509db44066ef
BLAKE2b-256 checksum
How to use checksums
7f8bced7882a07e88c9d85ca43fafe5fe769ba56fe35a157c4aefa53cd46258e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 10, 2026.

Transparency log

Release files / hawcx_oauth_client-2.0.0-py3-none-any.whl

Download URL hawcx_oauth_client-2.0.0-py3-none-any.whl
Size 58.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2f210cb45d928e45fa966c5fd7f5e5d800fcc591bdef21ba41154183e25e2bb7
BLAKE2b-256 checksum
How to use checksums
3195afe4e16e00d5124cdd02c8275981255fe0a5936b438c359dbc307be63b0a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 10, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

2.0.0 This release

2 release files

1.3.0

2 release files

1.2.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page