Metadata-Version: 2.4
Name: zanii-id
Version: 0.1.1
Summary: Zanii ID SDK for Python — OIDC authentication for products in the Zanii ecosystem
Author-email: Zanii <info@zanii.agency>
License-Expression: Apache-2.0
Project-URL: Homepage, https://id.zanii.agency
Project-URL: Source, https://github.com/vigilancetrent/zanii-id
Keywords: oauth2,oidc,openid-connect,authentication,sso,pkce
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Internet :: WWW/HTTP :: Session
Classifier: Topic :: Security
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.27
Requires-Dist: pyjwt[crypto]>=2.9
Requires-Dist: pydantic>=2.7
Requires-Dist: pydantic-settings>=2.3
Requires-Dist: itsdangerous>=2.2
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.110; extra == "fastapi"
Provides-Extra: subject
Requires-Dist: zanii>=0.24; extra == "subject"
Provides-Extra: dev
Requires-Dist: pytest>=8.2; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: cryptography>=42.0; extra == "dev"
Requires-Dist: fastapi>=0.110; extra == "dev"
Requires-Dist: zanii>=0.24; extra == "dev"
Dynamic: license-file

# zanii-id

Python SDK for [Zanii ID](https://id.zanii.agency) — 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.

```bash
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.

```python
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

```python
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:

```sql
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:

```python
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.

## 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`.
