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— 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,
)
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 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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c215f519b997fa033c6c1d2060a57cb93699e546392666e0fc68c8facfa773eb
|
|
| MD5 |
5326306a5ba57009268757001b0235dd
|
|
| BLAKE2b-256 |
18c68f15d3032ea840a854622476a529fdc0674531429ca2c318a1a60c4a0b6e
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hawcx_oauth_client-1.3.0.tar.gz -
Subject digest:
c215f519b997fa033c6c1d2060a57cb93699e546392666e0fc68c8facfa773eb - Sigstore transparency entry: 1968033131
- Sigstore integration time:
-
Permalink:
hawcx/hawcx_py_oauth_client@6ab91625737f54925f2f14cd12d5258e4659b049 -
Branch / Tag:
refs/tags/v1.3.0 - Owner: https://github.com/hawcx
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@6ab91625737f54925f2f14cd12d5258e4659b049 -
Trigger Event:
push
-
Statement type:
File details
Details for the file hawcx_oauth_client-1.3.0-py3-none-any.whl.
File metadata
- Download URL: hawcx_oauth_client-1.3.0-py3-none-any.whl
- Upload date:
- Size: 48.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5663ec828ce1e6446074b65eb5064d7b53badc2a827138244cee564a0f6d43de
|
|
| MD5 |
17fe295e42a3ee012a0b63520ebce888
|
|
| BLAKE2b-256 |
608f79822f7cc5245d6a23e13fcdab05c151bea2cb594a6ce758e3cd70df07dd
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hawcx_oauth_client-1.3.0-py3-none-any.whl -
Subject digest:
5663ec828ce1e6446074b65eb5064d7b53badc2a827138244cee564a0f6d43de - Sigstore transparency entry: 1968033249
- Sigstore integration time:
-
Permalink:
hawcx/hawcx_py_oauth_client@6ab91625737f54925f2f14cd12d5258e4659b049 -
Branch / Tag:
refs/tags/v1.3.0 - Owner: https://github.com/hawcx
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@6ab91625737f54925f2f14cd12d5258e4659b049 -
Trigger Event:
push
-
Statement type: