Metadata-Version: 2.4
Name: zanshield-sdk
Version: 1.0.0
Summary: Client library for integrating a system with ZanShield, eGAZ's multi-factor authentication service
Author-email: DevTech <abdulrahman@devtech.co.tz>
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/devtechzanzibar/zanshield-python
Project-URL: Source, https://github.com/devtechzanzibar/zanshield-python
Project-URL: Issues, https://github.com/devtechzanzibar/zanshield-python/issues
Keywords: zanshield,mfa,authentication,egaz,zanzibar
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Topic :: Security :: Cryptography
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cryptography>=41.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: build>=1.0; extra == "dev"
Requires-Dist: twine>=5.0; extra == "dev"
Dynamic: license-file

# ZanShield SDK for Python

Add ZanShield multi-factor authentication to your system's login. ZanShield is the authentication service run by eGAZ (eGovernment Authority Zanzibar).

Your system checks the user's password as usual, then hands them to ZanShield for the second factor. ZanShield decides everything from there: whether the user sets up their first method or verifies an existing one, and which methods (authenticator app, security key, email, SMS) your system allows. It then sends the user back to you with a signed acknowledgement. This library builds the ticket you send and checks the acknowledgement you get back.

## Install

```bash
pip install zanshield-sdk
```

Requires Python 3.9+. Installs [`cryptography`](https://pypi.org/project/cryptography/) automatically.

## Before you start

Your institution admin registers your system in the ZanShield administration console. You get:

- your **system code** (e.g. `PAYROLL01`) and **institution code**
- your system's **private key** (shown once; keep it on your server only, never in a browser or mobile app)
- ZanShield's **engine public key** (the admin console's "Download ZanShield public key" button)
- the **portal address**, e.g. `https://portal.zanshield.devtech.co.tz`

The **redirect URI** you register is where ZanShield sends users back to.

## Usage

```python
from zanshield_sdk import AcknowledgementRejected, ZanShieldClient, load_private_key, load_public_key

client = ZanShieldClient(
    system_code="PAYROLL01",
    institution_code="EGAZ",
    my_private_key=load_private_key(open("/secure/zanshield-private.pem").read()),
    engine_public_key=load_public_key(open("/secure/zanshield-engine-public.pem").read()),
)

# 1. After the user's password is correct:
ticket = client.create_ticket(
    user.id,
    user.email,
    full_name=user.full_name,  # optional: the portal greets the user by name
    mobile=user.phone,         # optional: without it the user can't choose SMS codes
)
return redirect(ZanShieldClient.portal_url("https://portal.zanshield.devtech.co.tz", ticket))

# 2. At your redirect URI, ZanShield sends the user back with ?ticket=...
try:
    ack = client.accept_acknowledgement(request.args["ticket"])
except AcknowledgementRejected as e:
    # Genuine, but must not sign in: e.reason is "NOT_AUTHENTICATED", "EXPIRED" or "REPLAYED".
    return show_error(e.acknowledgement.message or str(e))
start_session_for(ack.user_id)
```

`accept_acknowledgement` does every check your callback needs before it starts a session. If any check fails it raises an exception and returns nothing:

1. The ticket decrypts with your key and is signed by ZanShield. If not, it raises `ZanShieldError`, and you must never trust such a ticket.
2. `response_code` is `9000` and `is_authenticated` is true.
3. It's fresh: issued less than 3 minutes ago, configurable with `max_acknowledgement_age=`.
4. Its `txn_id` hasn't been used before, so a leaked callback URL can't be replayed into a second session.

### More than one server

By default, used `txn_id`s are remembered in memory, which protects a single process. If your application runs on several servers or workers, pass a store backed by something they share. Any object with a `mark_used(txn_id, keep_until) -> bool` method works:

```python
class RedisTxnIdStore:
    def __init__(self, redis):
        self.redis = redis

    def mark_used(self, txn_id, keep_until):
        ttl = max(1, int((keep_until - datetime.now(timezone.utc)).total_seconds()))
        return bool(self.redis.set(f"zanshield:txn:{txn_id}", 1, nx=True, ex=ttl))

client = ZanShieldClient(..., txn_id_store=RedisTxnIdStore(redis))
```

## Response codes

`ResponseCodes` has every code ZanShield uses. On an acknowledgement, only `ResponseCodes.SUCCESS` (9000) means the user verified.

## License

Apache-2.0
