Skip to main content

zanii-id

Python SDK for Zanii ID — OAuth 2.1 / OpenID Connect sign-in for products in the Zanii ecosystem.

Authorization Code flow with mandatory PKCE, offline ID-token verification against the issuer's JWKS, and a FastAPI integration that mounts the whole login round-trip for you.

pip install zanii-id

Use it

Configuration comes from ZANII_ISSUER, ZANII_CLIENT_ID, ZANII_CLIENT_SECRET and ZANII_REDIRECT_URI, and is validated eagerly so a misconfigured deployment fails at startup rather than on a user's first login.

from zanii_id import ZaniiClient

zanii = ZaniiClient()
req = zanii.get_authorization_url()       # keep req.state / req.nonce / req.verifier in session
# ... redirect the user to req.url, then on your callback route:
tokens = await zanii.exchange_code(code, req, received_state)
claims = zanii.verify_id_token(tokens.id_token, nonce=req.nonce)
user = await zanii.get_user(tokens.access_token)

FastAPI

from zanii_id import ZaniiClient
from zanii_id.integrations.fastapi import build_auth_router, install_zanii, require_zanii_auth

zanii = ZaniiClient()
install_zanii(app, zanii, session_secret=SECRET)
app.include_router(build_auth_router(zanii, session_secret=SECRET))

@app.get("/dashboard")
async def dashboard(user = Depends(require_zanii_auth)):
    return {"zanii_user_id": user.zanii_user_id}

That mounts /auth/login, /auth/callback and /auth/logout. require_zanii_auth refreshes a stale access token once and rotates the session cookie before giving up.

Provisioning users locally

sub is an immutable zanii_user_id. Upsert on it — never on email, which users change:

INSERT INTO users (zanii_user_id, ...) VALUES ($1, ...)
ON CONFLICT (zanii_user_id) DO NOTHING;

Agent activity

If your product records agent receipts on the Zanii ledger, stamp them with the user's subject tag so they can audit their own slice:

from zanii_id.activity import subject_tag, fetch_activity   # pip install 'zanii-id[subject]'

tag = subject_tag(user.did, client_id)     # pass to record(..., subject_tag=tag)
entries = await fetch_activity(tag)        # every receipt verified offline

Invalid receipts come back with verified=False and a flag_reason rather than being dropped — a truncated slice appends its own flagged entry, so a cut page never reads as complete.

Lifecycle webhooks

Register a lifecycle_webhook_url on your client (console or /orgs/clients) and Zanii ID POSTs account events to it, signed over the raw body with the same webhook_secret you already hold: X-Zanii-Signature: sha256=<hmac> plus X-Zanii-Event. verify_lifecycle_event checks it in constant time and returns the parsed event. Give it the raw request bytes.

from zanii_id import LifecycleSignatureError, verify_lifecycle_event

@app.post("/webhooks/zanii-id")
async def lifecycle(request: Request):
    try:
        ev = verify_lifecycle_event(await request.body(), request.headers.get("X-Zanii-Signature"), WEBHOOK_SECRET)
    except LifecycleSignatureError:
        return Response(status_code=401)
    if ev.event == "user.deleted":
        await users.erase(ev.sub)
    return Response(status_code=204)
event what to do
user.deleted erase or anonymise your copy of that sub
consent.revoked drop the local session; the next login shows the consent screen again
user.password_changed refresh tokens are already dead; drop the local session
user.email_changed update the display email; data.email, data.email_verified

Deliveries retry with backoff for about an hour (8 attempts). Answer 2xx quickly and do the work afterwards. sub is the identifier you receive at login, so key on it as usual.

Two-step verification and amr

Every id_token carries acr (urn:zanii:mfa when the session has a second factor, urn:zanii:pwd otherwise) and amr (["pwd"], ["pwd","otp"] or ["webauthn"]). claims = zanii.verify_id_token(tokens.id_token) gives you claims.acr. To require a second factor on a route, start the flow with acr_values=ACR_MFA:

from zanii_id import ACR_MFA
req = zanii.get_authorization_url(acr_values=ACR_MFA)   # or prompt="login", max_age=0

Enrolled users are challenged in place; users with no second factor come back with error=unmet_authentication_requirements - send them to {issuer}/ui/account to enrol. With the FastAPI router, /auth/login?acr=mfa does the same.

Public clients and revocation

Leave client_secret unset for a public client and the SDK sends client_id in the token request body, as RFC 6749 requires. revoke() authenticates the same way /token does. The default scope includes offline_access, which is what a refresh token requires; drop it to get session-bound tokens only.

Notes

  • alg is pinned from the discovery document, never trusted from the token header.
  • Token POSTs are never retried; grants are single-use.
  • The JWKS cache refetches exactly once on an unseen kid, then fails hard.

Licence

Apache-2.0. See LICENSE.

Download files

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

Source Distribution

zanii_id-0.2.0.tar.gz (24.7 kB view details)

Uploaded Source

Built Distribution

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

zanii_id-0.2.0-py3-none-any.whl (20.0 kB view details)

Uploaded Python 3

File details

Details for the file zanii_id-0.2.0.tar.gz.

File metadata

  • Download URL: zanii_id-0.2.0.tar.gz
  • Upload date:
  • Size: 24.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.10

File hashes

Hashes for zanii_id-0.2.0.tar.gz
Algorithm Hash digest
SHA256 bc05b3b40d981ea4b430a6c966a7dc5c78702131916d6d5612b9d174207cdbae
MD5 2fc37c3e872ca97a9156dbc371526551
BLAKE2b-256 d6ba6c145b274ce4d0a2e116b2c52dadfb7bbd75dce6e4806b0093e403947c6d

See more details on using hashes here.

File details

Details for the file zanii_id-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: zanii_id-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 20.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.10

File hashes

Hashes for zanii_id-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d8d1f071a3fa94173a84534704b35998fedbc0083e55ee517df43e8bed5d1b19
MD5 3beeda24866b8d8402c7b32859342f7f
BLAKE2b-256 676b872a81eee483645158e5343cf8a9c5e0be5d5c6ddee75ec1c197886fae27

See more details on using hashes here.

Release history Release notifications | RSS feed

0.7.0

2 files

0.6.0

2 files

0.5.1

2 files

0.5.0

2 files

0.4.0

2 files

0.3.0

2 files

This release

0.2.0 This release

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 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