Skip to main content

Hawcx Python backend SDK — step-up flows, MFA enforcement, phone updates, and any /v1/management/* endpoint via delegation crypto

Project description

hawcx-oauth-client

Customer-backend SDK for Hawcx's management API (/v1/management/*) — step-up authentication flows, MFA enforcement, phone updates, and policy lookups. Wraps Hawcx's delegation protocol (Ed25519 + ECIES) behind a single Hawcx client.

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

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
    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 and exists because most customers don't need that flexibility. Reach for these only if you do.

from hawcx_oauth_client import StepUpClient

# Same behaviour as Hawcx, but with every option exposed:
client = StepUpClient.from_secret_key(
    secret_key="hwx_sk_v1_...",
    base_url="https://api.hawcx.com",
    api_key="tenant-a",
    tenant_header_name="X-Config-Id",
    tenant_header_value="tenant-a",
    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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

hawcx_oauth_client-1.2.0.tar.gz (45.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

hawcx_oauth_client-1.2.0-py3-none-any.whl (44.0 kB view details)

Uploaded Python 3

File details

Details for the file hawcx_oauth_client-1.2.0.tar.gz.

File metadata

  • Download URL: hawcx_oauth_client-1.2.0.tar.gz
  • Upload date:
  • Size: 45.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.2

File hashes

Hashes for hawcx_oauth_client-1.2.0.tar.gz
Algorithm Hash digest
SHA256 7d37cb81e72828df8c33ca74bedc4f4415075484adb65f29ac6ec42df0a02c23
MD5 a13091c07529be7e023cfaccf4b8e20b
BLAKE2b-256 09a60529981b2b95ada12980581d4a3db71f9248d04a871824de1d04a151ac23

See more details on using hashes here.

File details

Details for the file hawcx_oauth_client-1.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for hawcx_oauth_client-1.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7d7870be95959e63846c1e8defaf1c6d2dccdc00aece1da573399eb30200fcc5
MD5 0bca8cded74cfdc4d7e29547cf76ce3d
BLAKE2b-256 0c337d9c1682c3b8001e9805437776f6731e2d76e02be8f800e44b2ae6495f16

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page