Metadata-Version: 2.4
Name: auth101
Version: 0.3.1
Summary: Simple email/password authentication for Django, FastAPI, and Flask
Author: Elsai
Description-Content-Type: text/markdown
Requires-Dist: passlib[argon2]
Requires-Dist: pyjwt
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Provides-Extra: sqlalchemy
Requires-Dist: sqlalchemy; extra == "sqlalchemy"
Provides-Extra: fastapi
Requires-Dist: fastapi; extra == "fastapi"
Requires-Dist: pydantic; extra == "fastapi"
Provides-Extra: flask
Requires-Dist: flask; extra == "flask"
Provides-Extra: django
Requires-Dist: django; extra == "django"
Provides-Extra: all
Requires-Dist: sqlalchemy; extra == "all"
Requires-Dist: fastapi; extra == "all"
Requires-Dist: pydantic; extra == "all"
Requires-Dist: flask; extra == "all"
Requires-Dist: django; extra == "all"

# auth101

Simple email/password authentication for Django, FastAPI, and Flask.

Configure once, mount anywhere. `auth101` uses a schema (user, account, session, verification), handles password hashing (Argon2, bcrypt fallback), JWT access/refresh tokens with a session table for revocation, and optional default Django models so you can add authentication in minutes.

## Features

- Email/password sign-up, sign-in, sign-out, and session verification
- **Access + refresh tokens** with **session table** for revocation
- Secure password hashing with Argon2 (bcrypt fallback)
- JWT access/refresh token generation and verification
- Built-in framework integrations for **FastAPI**, **Flask**, and **Django**
- Pluggable storage backends: **SQLAlchemy** or **Django ORM**

## Installation

```bash
pip install auth101
```

With extras for your framework and database:

```bash
pip install auth101[fastapi]      # FastAPI + Pydantic
pip install auth101[flask]        # Flask
pip install auth101[django]       # Django
pip install auth101[sqlalchemy]   # SQLAlchemy (any SQL database)
pip install auth101[all]          # Everything
```

## How it works

- **Auth101** is the main entry point: you pass a **secret**, a **database adapter** (SQLAlchemy or Django), and optional entity config (table names / field mappings). Table names and field mappings are configured on Auth101, not on the adapter.
- Internally, a **factory** (`auth101.factory`) resolves config and builds the object graph: the adapter is asked for four **stores** (user, account, session, verification) with the resolved table name (or Django model) and field mapping; then **TokenIssuer**, **EmailPasswordService**, and **SessionService** are wired and attached to the facade.
- **EmailPasswordService** handles sign-up and sign-in: creates/loads user and account (password hash in account with `provider_id="credential"`), creates a session row (with a unique `jti`), and issues access + refresh JWTs.
- **SessionService** handles refresh (validates refresh token, looks up session by `jti`, rotates by revoking old session and issuing new tokens), revoke, sign-out, and get-session. Refresh tokens are tied to a session row so they can be revoked.
- **Integrations** (FastAPI, Flask, Django) expose the same HTTP endpoints and call these Auth101 methods; they also provide a way to get the current user (dependency, decorator, or middleware).

**Architecture.** Auth101 uses a layered structure: **domain** (entities) → **core** / **security** / **ports** → **features** (use cases) → **adapters** and **factory** → **facade** → **integrations**. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for layers, dependency rules, and explicit composition (`create_auth101`, `build_auth101_wiring`).

## Quick Start

**1. Install and create auth** (SQLAlchemy with a DB URL):

```python
from auth101 import Auth101
from auth101.adapters import SQLAlchemyAdapter

adapter = SQLAlchemyAdapter("sqlite:///auth.db")  # or "sqlite:///:memory:" for tests
auth = Auth101(secret="change-me-in-production", database=adapter)
```

**2. For a real database**, create the auth tables once (SQLAlchemy only):

```bash
# Config module must define database (and optionally user / user_field_mapping for user table)
auth101 migrate --config myapp.auth_config
```

Or omit migrate when using `sqlite:///auth.db` — the adapter creates tables on first use.

**3. Use the API:**

```python
# Sign up → returns user + access_token + refresh_token + expires_in
result = auth.sign_up("alice@example.com", "s3cr3t")
# result["user"]: {id, name, email, email_verified, image, is_active}
# result["access_token"], result["refresh_token"], result["expires_in"]

# Sign in
result = auth.sign_in("alice@example.com", "s3cr3t")

# Call your API with the access token (Bearer)
session = auth.get_session(result["access_token"])  # {"user": {...}}

# Refresh (get new tokens; old refresh token is revoked)
auth.refresh(result["refresh_token"])

# Logout: revoke server-side session, then discard tokens
auth.revoke_session(result["refresh_token"])
auth.sign_out(result["access_token"])  # validates access token
```

## Configuration

Pass a **database adapter** (connection only; no table names or field mapping on the adapter). Configure **entity options** on **Auth101**.

### User table: two options (mutually exclusive)

- **`user`** — Customize the user table that **auth101 creates and manages**. Use `{"model_name": "users", "fields": {...}}`. **Migrate will create** this table; you only customize its name and column names.
- **`user_field_mapping`** — Use **your own** user table (you create and manage it). Must include **`table_name`** (or `model_name`) and optionally **`fields`** (auth101 field → your column). Auth101 **never creates or migrates** this table; we only use it via the mapping. If both `user` and `user_field_mapping` are provided, **`user` wins**.

Session/account/verification: if present in config, migrate skips that table; if omitted, migrate creates it with defaults.

```python
from auth101 import Auth101
from auth101.adapters import SQLAlchemyAdapter

adapter = SQLAlchemyAdapter("sqlite:///auth.db")  # adapter takes only engine/URL
auth = Auth101(secret="...", database=adapter)

# Auth101-managed user table with custom name and column names (migrate creates it)
auth = Auth101(
    secret="...",
    database=adapter,
    user={
        "model_name": "users",
        "fields": {"name": "full_name", "email": "email_address"},
    },
)
# session, account, verification omitted → migrate will create those tables with defaults

# Your own user table (you manage it); include table_name + mapping (migrate skips user)
auth = Auth101(
    secret="...",
    database=adapter,
    user_field_mapping={
        "table_name": "app_user",
        "fields": {"email": "email_address", "name": "full_name"},
    },
)

# Custom session table only
auth = Auth101(
    secret="...",
    database=adapter,
    session={"model_name": "my_sessions"},
)
# user, account, verification: user uses default (migrate creates user); migrate creates account, verification
```

| Parameter | Description |
|---|---|
| `secret` | **Required.** Secret key for signing JWT tokens. |
| `database` | **Required.** A `DatabaseAdapter` (e.g. `SQLAlchemyAdapter(url)`, `DjangoAdapter(UserModel)`). Do not pass table names to the adapter. |
| `user` | Optional. Customize the user table **auth101 creates and manages**: `{"model_name": "users", "fields": {...}}`. Migrate creates this table. |
| `session` | Optional. Entity config for session table. If present, migrate skips session. |
| `account` | Optional. Entity config for account table. If present, migrate skips account. |
| `verification` | Optional. Entity config for verification table. If present, migrate skips verification. |
| `user_field_mapping` | Optional. Use **your own** user table: `{"table_name": "app_user", "fields": {"email": "your_column", ...}}`. Auth101 never creates or migrates it. Mutually exclusive with `user`; if both, `user` wins. |
| `access_token_expires_in` | Access token TTL in minutes. Default: `60`. |
| `refresh_token_expires_in` | Refresh token TTL in minutes. Default: `10080` (7 days). |

**Explicit composition.** You can build Auth101 via the factory for tests or custom wiring: `from auth101.factory import create_auth101, build_auth101_wiring`. `create_auth101(secret=..., database=..., ...)` is equivalent to `Auth101(...)`. `build_auth101_wiring(...)` returns an `Auth101Wiring` with adapter, entity config, resolved tables/mappings, and wired services (used by the CLI for migrations). See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).

### CLI: create tables (`auth101 migrate`)

- **User table:** Migrate creates it only when auth101 manages it (`user=...` or neither). If you use your own table (`user_field_mapping` with `table_name`), migrate skips the user table.
- **Session/account/verification:** Migrate creates them when omitted from config; skips when present.

```bash
auth101 migrate --config myapp.auth_config
```

Your config module must define **either** `auth` (Auth101 instance) **or** `database` (and optionally `user_field_mapping` for your own user table):

```python
# myapp/auth_config.py (preferred)
from auth101 import Auth101
from auth101.adapters import SQLAlchemyAdapter

adapter = SQLAlchemyAdapter("sqlite:///auth.db")
auth = Auth101(secret="...", database=adapter)

# Auth101-managed user with custom name: migrate creates it
# auth = Auth101(secret="...", database=adapter, user={"model_name": "users", "fields": {...}})

# Your own user table: migrate skips user
# auth = Auth101(secret="...", database=adapter, user_field_mapping={"table_name": "app_user", "fields": {...}})
```

```python
# Legacy: database + user_field_mapping (your own user table; migrate skips user)
from auth101.adapters import SQLAlchemyAdapter
database = SQLAlchemyAdapter("sqlite:///auth.db")
user_field_mapping = None   # or {"table_name": "app_user", "fields": {"email": "email_address"}}
```

Adapters that don’t support migrate (e.g. Django) use their ORM’s migrations instead.

## Core API

All methods return plain dicts, so they work with any framework or without one.

### `auth.sign_up(email, password)`

Register a new user. Returns `{"user": {...}, "access_token": "...", "refresh_token": "...", "expires_in": N}` on success.

### `auth.sign_in(email, password)`

Authenticate an existing user. Returns `{"user": {...}, "access_token": "...", "refresh_token": "...", "expires_in": N}` on success.

### `auth.refresh(refresh_token)`

Exchange a refresh token for new access and refresh tokens (rotation; old refresh token is revoked). Returns the same shape as sign-in.

### `auth.revoke_session(refresh_token)`

Revoke the session for the given refresh token (e.g. on logout). Returns `{"success": True}`.

### `auth.sign_out(token)`

Validate an **access** token and acknowledge sign-out. Returns `{"success": True}`. Call `revoke_session(refresh_token)` to invalidate the session server-side.

### `auth.get_session(token)`

Return the user associated with an **access** token. Returns `{"user": {...}}` on success. The `user` dict has: `id`, `name`, `email`, `email_verified`, `image`, `is_active`.

### `auth.verify_token(token)`

Verify an access token and return the corresponding `User` object (with `.id`, `.name`, `.email`, `.email_verified`, `.image`, `.is_active`), or `None`.

### Error Responses

On failure, methods return `{"error": {"message": "...", "code": "..."}}` with one of these codes:

| Code | Meaning |
|---|---|
| `VALIDATION_ERROR` | Missing or invalid email/password |
| `USER_EXISTS` | Email already registered |
| `INVALID_CREDENTIALS` | Wrong email or password |
| `INVALID_TOKEN` | Token is malformed or expired |
| `UNAUTHORIZED` | No valid token provided |
| `USER_NOT_FOUND` | Token valid but user no longer exists |
| `SESSION_NOT_FOUND` | Refresh token revoked or session expired |

## Framework Integrations

### FastAPI

```bash
pip install auth101[fastapi] sqlalchemy
```

```python
from fastapi import Depends, FastAPI
from auth101 import Auth101
from auth101.adapters import SQLAlchemyAdapter

adapter = SQLAlchemyAdapter("sqlite:///auth.db")
auth = Auth101(secret="change-me-in-production", database=adapter)

app = FastAPI()

# Mount auth endpoints: POST /auth/sign-up/email, /auth/sign-in/email,
#                       POST /auth/refresh, POST /auth/revoke,
#                       POST /auth/sign-out, GET /auth/session
app.include_router(auth.fastapi_router(), prefix="/auth", tags=["auth"])

# Dependency that resolves to the authenticated User or raises 401
CurrentUser = auth.fastapi_current_user()

@app.get("/me")
async def me(user=Depends(CurrentUser)):
    return {"id": user.id, "name": user.name, "email": user.email}
```

### Flask

```bash
pip install auth101[flask] sqlalchemy
```

```python
from flask import Flask, g, jsonify
from auth101 import Auth101
from auth101.adapters import SQLAlchemyAdapter

adapter = SQLAlchemyAdapter("sqlite:///auth.db")
auth = Auth101(secret="change-me-in-production", database=adapter)

app = Flask(__name__)

# Mount auth endpoints under /auth
app.register_blueprint(auth.flask_blueprint(), url_prefix="/auth")

# Decorator that sets g.auth_user or returns 401
login_required = auth.flask_login_required()

@app.get("/me")
@login_required
def me():
    return jsonify({"id": g.auth_user.id, "name": g.auth_user.name, "email": g.auth_user.email})
```

### Django

```bash
pip install auth101[django]
```

**Option A – Zero-config (use auth101's default tables)**

**1. Add the auth101 Django app** (`settings.py`):

```python
INSTALLED_APPS = [
    ...
    "auth101.contrib.django",
]
```

**2. Run migrations:** `python manage.py makemigrations && python manage.py migrate`

**3. Configure auth101** (`myapp/auth.py`):

```python
from django.conf import settings
from auth101 import Auth101
from auth101.adapters import DjangoAdapter
from auth101.contrib.django.models import Auth101User

auth = Auth101(
    secret=settings.SECRET_KEY,
    database=DjangoAdapter(Auth101User),  # session, account, verification use defaults
)
Auth101Middleware = auth.get_django_middleware()
login_required = auth.django_login_required()
```

**Option B – Custom user model only**

If you have your own User model (with fields: id, name, email, email_verified, image, is_active, created_at, updated_at), pass it and omit session/account/verification; auth101 uses its default tables for those:

```python
from myapp.models import MyUser
auth = Auth101(secret=..., database=DjangoAdapter(MyUser))
```

**Option C – Custom models for all tables**

Pass `session_model`, `account_model`, and/or `verification_model` to `DjangoAdapter(...)` to use your own tables. Alternatively, pass `session`, `account`, or `verification` to `Auth101(...)` with `{"model": YourModel}`.

**4. Add middleware** (`settings.py`):

```python
MIDDLEWARE = [
    "myapp.auth.Auth101Middleware",   # sets request.auth_user on every request
    ...
]
```

**5. Mount URLs** (`urls.py`):

```python
from django.urls import include, path
from myapp.auth import auth

urlpatterns = [
    path("auth/", include(auth.django_urls())),
    ...
]
```

**6. Protect views** (`myapp/views.py`):

```python
from django.http import JsonResponse
from myapp.auth import login_required

@login_required
def profile(request):
    return JsonResponse({"id": request.auth_user.id, "name": request.auth_user.name, "email": request.auth_user.email})
```

## Auth Endpoints

All framework integrations expose the same endpoints (relative to the mount prefix):

| Method | Path | Body / Header | Response |
|---|---|---|---|
| POST | `/sign-up/email` | `{"email": "...", "password": "..."}` | `{"user": {...}, "access_token": "...", "refresh_token": "...", "expires_in": N}` |
| POST | `/sign-in/email` | `{"email": "...", "password": "..."}` | `{"user": {...}, "access_token": "...", "refresh_token": "...", "expires_in": N}` |
| POST | `/refresh` | `{"refresh_token": "..."}` | `{"user": {...}, "access_token": "...", "refresh_token": "...", "expires_in": N}` |
| POST | `/revoke` | `{"refresh_token": "..."}` | `{"success": true}` |
| POST | `/sign-out` | `Authorization: Bearer <access_token>` | `{"success": true}` |
| GET | `/session` | `Authorization: Bearer <access_token>` | `{"user": {...}}` |

In all responses, `user` has: `id`, `name`, `email`, `email_verified`, `image`, `is_active`.

## Database schema

auth101 uses four tables (default names: `users`, `account`, `session`, `verification`). Column names can be customized via entity config `fields` mapping.

- **user** – id, name, email, email_verified, image, is_active, created_at, updated_at. No password; credentials live in the account table.
- **account** – id, account_id, provider_id, user_id, password (for credential provider), optional OAuth fields, created_at, updated_at. One row per (user, provider); email/password uses `provider_id="credential"`.
- **session** – id, token (stores refresh-token `jti`), expires_at, user_id, ip_address, user_agent, created_at, updated_at. Used for refresh-token revocation and rotation.
- **verification** – id, identifier, value, expires_at, created_at, updated_at (for email verification / password reset; table is ready, flows are optional).

With **SQLAlchemy** you pass one adapter; `auth101 migrate` creates tables for any entity not customized in config. With **Django** you can use auth101’s default models (`auth101.contrib.django`) or pass your own to `DjangoAdapter` or via Auth101's entity config.

## Database Persistence

### SQLAlchemy (one adapter, four tables)

Pass a `SQLAlchemyAdapter` with a URL or engine. Run `auth101 migrate --config myapp.auth_config` to create the user, account, session, and verification tables (for any entity not customized in your Auth101 config). Table and column names are configured on Auth101, not on the adapter.

```python
from auth101.adapters import SQLAlchemyAdapter

auth = Auth101(
    secret="...",
    database=SQLAlchemyAdapter("postgresql://user:pass@localhost/mydb"),
)
```

Works with any SQLAlchemy-supported database: PostgreSQL, MySQL, SQLite, etc.

### Django ORM

Pass a `DjangoAdapter` with your user model. Session/account/verification are optional (auth101 uses default tables from `auth101.contrib.django` if omitted):

```python
from auth101.adapters import DjangoAdapter
from auth101.contrib.django.models import Auth101User

auth = Auth101(
    secret=settings.SECRET_KEY,
    database=DjangoAdapter(Auth101User),
)
```

## Testing

Use SQLite in-memory so each run is isolated; tables are created on first use, no migrate needed:

```python
from auth101 import Auth101
from auth101.adapters import SQLAlchemyAdapter

auth = Auth101(
    secret="test-secret",
    database=SQLAlchemyAdapter("sqlite:///:memory:"),
)
# sign_up, sign_in, get_session, refresh, etc. work as in production
```

Data is lost when the process exits, which is ideal for unit tests.

## License

MIT
