Metadata-Version: 2.5
Name: nandi-identity
Version: 0.1.3
Summary: A production-oriented identity and access management foundation for Python applications.
Project-URL: Homepage, https://github.com/shubhambengani22/nandi
Project-URL: Documentation, https://github.com/shubhambengani22/nandi/blob/main/src/nandi/PUBLIC_API_DOCS.md
Project-URL: Repository, https://github.com/shubhambengani22/nandi
Project-URL: Issues, https://github.com/shubhambengani22/nandi/issues
Project-URL: Changelog, https://github.com/shubhambengani22/nandi/blob/main/CHANGELOG.md
Author: Shubham Bengani
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Keywords: authentication,authorization,django,iam,identity,security
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.0
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: cryptography<50,>=46
Requires-Dist: email-validator>=2.3.0
Provides-Extra: django
Requires-Dist: django<6.1,>=5.2; extra == 'django'
Description-Content-Type: text/markdown

# Nandi Identity

`nandi-identity` is a production-oriented identity and access-management
foundation for Python. Its import namespace is `nandi`.

The Django adapter provides an email/password identity flow with durable
security state:

- email registration and verification;
- password enrollment, authentication, change, and recovery;
- opaque, server-side sessions with rotation and global invalidation;
- transactional-outbox email delivery with retries and leases; and
- durable, keyed rate limits for Nandi and consumer-defined operations.

Nandi owns identity invariants and security workflows. Your application owns
HTTP endpoints, authorization, templates, transport security, concrete Django
models, migrations, and operational scheduling.

## Install

Nandi requires Python 3.12 or newer. Add the Django adapter with:

```bash
uv add "nandi-identity[django]"
```

The distribution is named `nandi-identity`; imports use `nandi`:

```python
import nandi

print(nandi.__version__)
```

Nandi supports Django 5.2 through 6.0. It does not install database migrations
into your project. This is intentional: your application subclasses Nandi's
abstract models and owns its migration history.

## Django installation

### 1. Add the application

```python
# settings.py
INSTALLED_APPS = [
    # ...
    "nandi.integrations.django.apps.DjangoAppConfig",
    "identity",  # the consumer app containing the concrete models
]
```

`DjangoAppConfig` validates configuration and initializes a process-local
runtime during Django startup. Do not construct a runtime per request.

### 2. Define concrete models

Create these models in an application you own, for example
`identity/models.py`:

```python
from django.db import models

from nandi.integrations.django.models import (
    AbstractAccount,
    AbstractAccountIdentifier,
    AbstractAuthenticator,
    AbstractOutboxMessage,
    AbstractPasswordAuthenticator,
    AbstractRateLimitBucket,
    AbstractSession,
    AbstractVerificationChallenge,
)


class Account(AbstractAccount):
    # Product-owned fields are allowed. Give required fields a default, make
    # them nullable, or populate them in your own account-creation policy.
    display_name = models.CharField(max_length=120, blank=True)


class AccountIdentifier(AbstractAccountIdentifier):
    pass


class Authenticator(AbstractAuthenticator):
    pass


class PasswordAuthenticator(AbstractPasswordAuthenticator):
    pass


class VerificationChallenge(AbstractVerificationChallenge):
    pass


class OutboxMessage(AbstractOutboxMessage):
    pass


class Session(AbstractSession):
    pass


class RateLimitBucket(AbstractRateLimitBucket):
    pass
```

Then point Nandi at the models and generate migrations in **your** app:

```bash
python manage.py makemigrations identity
python manage.py migrate
```

### 3. Configure Nandi

Nandi keeps keys outside the database. Each `ENV` value is the **name** of an
environment variable, whose value is supplied by your deployment platform or
secret manager. `KEY_ID` is a non-secret rotation label.

```python
# settings.py
NANDI = {
    "MODELS": {
        "ACCOUNT": "identity.Account",
        "IDENTIFIER": "identity.AccountIdentifier",
        "AUTHENTICATOR": "identity.Authenticator",
        "PASSWORD_AUTHENTICATOR": "identity.PasswordAuthenticator",
        "VERIFICATION_CHALLENGE": "identity.VerificationChallenge",
        "OUTBOX_MESSAGE": "identity.OutboxMessage",
        "SESSION": "identity.Session",
        "RATE_LIMIT_BUCKET": "identity.RateLimitBucket",
    },
    "CRYPTO": {
        "CHALLENGE_TOKEN": {
            "ACTIVE": {"KEY_ID": "2026-01", "ENV": "NANDI_CHALLENGE_KEY"},
            "FALLBACKS": [],
        },
        "OUTBOX_PAYLOAD": {
            "ACTIVE": {"KEY_ID": "2026-01", "ENV": "NANDI_OUTBOX_KEY"},
            "FALLBACKS": [],
        },
        "SESSION_TOKEN": {
            "ACTIVE": {"KEY_ID": "2026-01", "ENV": "NANDI_SESSION_KEY"},
            "FALLBACKS": [],
        },
        "RATE_LIMIT": {
            "ACTIVE": {"KEY_ID": "2026-01", "ENV": "NANDI_RATE_LIMIT_KEY"},
            "FALLBACKS": [],
        },
    },
    "EMAIL": {
        "FROM_EMAIL": "security@example.com",
        "URL_TEMPLATES": {
            "EMAIL_VERIFICATION": "https://app.example.com/verify?challenge={challenge_id}&token={token}",
            "PASSWORD_RESET": "https://app.example.com/reset?challenge={challenge_id}&token={token}",
        },
        "TEMPLATES": {
            "EMAIL_VERIFICATION": {
                "SUBJECT": "identity/email_verification_subject.txt",
                "TEXT": "identity/email_verification.txt",
                "HTML": "identity/email_verification.html",
            },
            "PASSWORD_RESET": {
                "SUBJECT": "identity/password_reset_subject.txt",
                "TEXT": "identity/password_reset.txt",
                "HTML": "identity/password_reset.html",
            },
        },
    },
}
```

The URL templates must be absolute HTTPS URLs and contain exactly
`{challenge_id}` and `{token}`. Nandi URL-encodes both values. Each Django
template receives `challenge_url`, `expires_at`, and `recipient`.

Use independent random secrets for every crypto purpose. During a rotation,
move the former active key to `FALLBACKS`, deploy it with the new active key,
and remove it only after `identity_key_status` reports it is safe to retire.

## Use the account service

Retrieve the service from the initialized runtime in your Django view, API
handler, or application service:

```python
from nandi.integrations.django.runtime import get_runtime

accounts = get_runtime().create_account_service()
result = accounts.register_email(email="ada@example.com", source=request.META["REMOTE_ADDR"])
```

The `source` argument is an optional trusted request origin used for rate
limits. Resolve it at your HTTP boundary; never accept a client-controlled
forwarding header without proxy-aware validation.

### Registration and verification

```python
registration = accounts.register_email(email="ada@example.com", source=source)
# Send a generic response even when RegistrationUnavailableError is raised.

verification = accounts.verify_email(
    challenge_id=str(challenge_id_from_url),
    token=token_from_url,
    source=source,
)
```

`register_email` creates a pending account, an unverified email identifier,
and an outbox message. The dispatcher sends the verification message later.
`verify_email` consumes the challenge, verifies the identifier, and activates
the account in one transaction.

### Passwords and sessions

```python
accounts.set_password(account_id=str(account_id), password="a long secret")

login = accounts.authenticate_password(
    email="ada@example.com",
    password="a long secret",
    source=source,
)

response.set_cookie(
    "session",
    login.session_token.value,
    max_age=get_runtime().session_cookie_settings().max_age,
    secure=True,
    httponly=True,
    samesite="Lax",
)
```

The returned session token is a bearer secret and is available only in the
successful result. Put it in a Secure, HttpOnly cookie over HTTPS, do not log
it, and never include it in URLs. Its database representation is a keyed
digest, not the raw token.

On an authenticated request, call:

```python
session = accounts.validate_session(token=request.COOKIES["session"])
# session.account_id is authenticated; apply your own authorization next.
```

`refresh_session(token=...)` rotates the bearer token without extending the
absolute expiry. `logout_session(token=...)` revokes one session and is safe
to retry. `logout_everywhere(account_id=...)` invalidates every session for
that account by advancing its security version.

### Password recovery

```python
request_result = accounts.request_password_reset(email="ada@example.com", source=source)
assert request_result.accepted

accounts.confirm_password_reset(
    challenge_id=str(challenge_id_from_url),
    token=token_from_url,
    password="replacement secret",
    source=source,
)
```

The request method always returns `accepted=True`, including for unknown email
addresses. Preserve that generic response at the HTTP boundary to avoid
account enumeration.

## Run operations

Nandi does not start background workers. Schedule these Django commands in
your infrastructure:

```bash
python manage.py identity_dispatch_outbox
python manage.py identity_purge_sessions --revoked-retention-days 7
python manage.py identity_purge_rate_limits
python manage.py identity_key_status --fail-on-unavailable
```

Run the dispatcher frequently enough to meet your email delivery target. Run
purge commands periodically. Run key status before removing fallback secrets.

## Public API reference

The full, versioned public contract—including every public model, field,
runtime method, account-service method, rate-limit type, command, return
value, and endpoint-facing error—is in
[PUBLIC_API_DOCS.md](src/nandi/PUBLIC_API_DOCS.md).

Only documented modules are supported. Paths containing `_internal` are
implementation details and may change in patch releases.

## Security responsibilities

Nandi reduces common identity risks; it cannot secure an unsafe integration.
Consumers must enforce HTTPS, set secure cookie policy, protect state-changing
endpoints from CSRF, validate trusted client-address handling, authorize
account actions, protect configuration secrets, and monitor dispatcher and key
status jobs. See [SECURITY.md](SECURITY.md) for vulnerability reporting.

## Project information

- [Public API reference](src/nandi/PUBLIC_API_DOCS.md)
- [Release history](CHANGELOG.md)
- [Contributing](CONTRIBUTING.md)
- [License](LICENSE)
