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. 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,
)

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"})

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

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.3.0.tar.gz (55.8 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.3.0-py3-none-any.whl (48.4 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: hawcx_oauth_client-1.3.0.tar.gz
  • Upload date:
  • Size: 55.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for hawcx_oauth_client-1.3.0.tar.gz
Algorithm Hash digest
SHA256 c215f519b997fa033c6c1d2060a57cb93699e546392666e0fc68c8facfa773eb
MD5 5326306a5ba57009268757001b0235dd
BLAKE2b-256 18c68f15d3032ea840a854622476a529fdc0674531429ca2c318a1a60c4a0b6e

See more details on using hashes here.

Provenance

The following attestation bundles were made for hawcx_oauth_client-1.3.0.tar.gz:

Publisher: release.yml on hawcx/hawcx_py_oauth_client

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

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

File metadata

File hashes

Hashes for hawcx_oauth_client-1.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5663ec828ce1e6446074b65eb5064d7b53badc2a827138244cee564a0f6d43de
MD5 17fe295e42a3ee012a0b63520ebce888
BLAKE2b-256 608f79822f7cc5245d6a23e13fcdab05c151bea2cb594a6ce758e3cd70df07dd

See more details on using hashes here.

Provenance

The following attestation bundles were made for hawcx_oauth_client-1.3.0-py3-none-any.whl:

Publisher: release.yml on hawcx/hawcx_py_oauth_client

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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