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— thehwx_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 withauthlib: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 fromhx_authlong before this release. Usehawcx.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:
- 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.
- 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).
- 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)
| File | Size | Uploaded | |
|---|---|---|---|
| hawcx_oauth_client-2.0.0.tar.gz | 71.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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