Metadata-Version: 2.4
Name: taleemabad-auth
Version: 0.1.0
Summary: Zero-auth FastAPI middleware for Taleemabad authentication service
Author-email: Taleemabad Team <dev@taleemabad.com>
License: MIT
Project-URL: Homepage, https://github.com/taleemabad/auth-service
Project-URL: Documentation, https://github.com/taleemabad/auth-service/tree/main/docs
Project-URL: Repository, https://github.com/taleemabad/auth-service
Project-URL: Bug Tracker, https://github.com/taleemabad/auth-service/issues
Keywords: auth,fastapi,jwt,middleware,zero-auth
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
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: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: httpx>=0.24.0
Requires-Dist: pydantic>=2.0
Requires-Dist: python-jose>=3.3.0
Requires-Dist: starlette>=0.27.0
Provides-Extra: dev
Requires-Dist: pytest>=7.4.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
Requires-Dist: fastapi>=0.100.0; extra == "dev"
Requires-Dist: uvicorn>=0.23.0; extra == "dev"
Requires-Dist: black>=23.0.0; extra == "dev"
Requires-Dist: ruff>=0.0.280; extra == "dev"
Requires-Dist: mypy>=1.4.0; extra == "dev"

# Taleemabad Auth — FastAPI Middleware

Zero-auth FastAPI middleware for Taleemabad authentication service. Clients add 1 line of setup, define 1 enrichment hook, and get full authentication with zero auth code.

## Features

✅ **Zero Auth Code** — No JWT verification, token refresh, or auth logic in your app  
✅ **Backend-to-Backend Verification** — Private key never leaves Taleemabad  
✅ **Automatic Token Refresh** — Expired tokens automatically refreshed  
✅ **Per-Request Enrichment** — Custom fields fetched fresh on every request  
✅ **Multi-Tenant Ready** — Built for organizations  
✅ **Configurable Token Source** — Header (default), session, or cookie  

## Installation (Phase 3: Integration Testing)

During initial integration testing, copy the middleware directory into your FastAPI project:

```bash
# Copy the taleemabad_auth/ directory to your project
cp -r /path/to/taleemabad-auth/middleware/python/taleemabad_auth ./
```

Your project structure should look like:
```
your-project/
├── main.py
├── taleemabad_auth/          ← Copy this entire directory
│   ├── __init__.py
│   ├── models.py
│   ├── exceptions.py
│   ├── client.py
│   ├── hooks.py
│   ├── token.py
│   └── middleware.py
└── ...
```

Then import from your local directory:
```python
from taleemabad_auth import TaleebabadAuth, require_user
```

**Future (Phase 4):** After successful integration testing, this will be available on PyPI:
```bash
pip install taleemabad-auth
```

## Quick Start

### 1. Get Credentials

Contact the Taleemabad team with your organization name. You'll receive:
- `TALEEMABAD_CLIENT_ID` — your organization UUID
- `TALEEMABAD_CLIENT_SECRET` — your organization secret
- `TALEEMABAD_AUTH_SERVICE_URL` — the auth service URL

Store in `.env`:
```bash
TALEEMABAD_CLIENT_ID=your-org-uuid
TALEEMABAD_CLIENT_SECRET=your-secret
TALEEMABAD_AUTH_SERVICE_URL=https://auth.taleemabad.com
```

### 2. Initialize in Your App

```python
from fastapi import FastAPI, Depends
from taleemabad_auth import TaleebabadAuth, require_user
import os

app = FastAPI()

# Initialize Taleemabad auth
auth = TaleebabadAuth(
    auth_service_url=os.getenv("TALEEMABAD_AUTH_SERVICE_URL"),
    client_id=os.getenv("TALEEMABAD_CLIENT_ID"),
    client_secret=os.getenv("TALEEMABAD_CLIENT_SECRET"),
    token_source="header",  # or "session" or "cookie"
)

# Add to app
app.add_middleware(auth.middleware)
```

### 3. Define Enrichment Hook

This is where you fetch custom fields from YOUR database:

```python
@auth.register_hook("enrich_user_claims")
async def enrich_user(user_id: str) -> dict:
    """Fetch custom fields from your database."""
    # Example: fetch user role and permissions from your DB
    user = await db.get_user(user_id)
    return {
        "role": user.role,
        "permissions": {
            "create_class": True,
            "grade_students": user.permissions.get("grade_students", False),
        },
        "organization_id": user.org_id,
    }
```

### 4. Protect Routes

Use `require_user` dependency:

```python
@app.get("/protected")
async def protected_route(user=Depends(require_user)):
    return {"username": user.username, "role": user.role}

# Or access user from request.state
@app.get("/optional")
async def optional_auth(request):
    if hasattr(request.state, "user"):
        return {"username": request.state.user.username}
    else:
        return {"username": "anonymous"}
```

### 5. Logout

Add logout endpoint:

```python
@app.post("/api/logout")
async def logout(request):
    await auth.logout(request)
    return {"message": "logged out"}
```

That's it! ✨

## Request Flow

```
Client Frontend
    │
    ├─→ POST /api/login
    │   └─→ Middleware intercepts response
    │       └─→ Stores tokens (access in localStorage, refresh in cookie)
    │
    ├─→ GET /api/classes (with Authorization: Bearer <access_token>)
    │   └─→ Middleware intercepts request
    │       ├─ Extract token from Authorization header
    │       ├─ POST /api/verify-token (backend-to-backend)
    │       ├─ If expired: POST /api/refresh (auto-refresh)
    │       ├─ Call enrichUserClaims hook
    │       ├─ POST /api/sync-user-profile (store enriched data)
    │       ├─ Attach user + claims to request.state
    │       └─ Continue to route handler
    │
    └─→ POST /api/logout
        └─→ Middleware revokes refresh token
```

## Configuration

### Token Source

Control where the middleware extracts the access token from:

```python
auth = TaleebabadAuth(
    token_source="header",      # Authorization: Bearer <token>
    # OR
    token_source="session",     # request.session['taleemabad_token']
    # OR
    token_source="cookie",      # request.cookies['access_token']
)
```

### Timeout

Adjust HTTP request timeout (default 10 seconds):

```python
auth = TaleebabadAuth(
    timeout=5,  # 5 second timeout for all HTTP calls
)
```

## Hooks

### enrich_user_claims

Called on every request to fetch custom fields from your database.

```python
@auth.register_hook("enrich_user_claims")
async def enrich(user_id: str) -> dict:
    # Fetch from your database
    user = await db.get_user(user_id)
    return {
        "role": user.role,
        "permissions": user.permissions,
        "organization_id": user.org_id,
    }
```

### on_user_verified (Optional)

Called after user is verified. Use for logging, analytics, etc.

```python
@auth.register_hook("on_user_verified")
async def on_verified(user, claims):
    # Log user verification
    print(f"User {user.username} verified")
    # Or send to analytics service
    await analytics.track_login(user.id)
```

## Error Handling

The middleware is non-fatal:
- If token verification fails → request continues without user
- If enrichment hook fails → request continues (empty custom fields)
- If sync fails → request continues (non-blocking)
- If token refresh fails → request continues without user

Use `require_user` dependency to enforce authentication on protected routes:

```python
@app.get("/protected")
async def protected(user=Depends(require_user)):
    # If user is not authenticated, returns 401
    return {"username": user.username}
```

## Testing

Run tests:

```bash
# Install dev dependencies
pip install -e ".[dev]"

# Run tests
pytest -v

# With coverage
pytest --cov=taleemabad_auth tests/
```

Test files:
- `tests/test_client.py` — HTTP client tests
- `tests/test_hooks.py` — Hook registry tests
- `tests/test_token.py` — Token extraction and expiry checks
- `tests/test_middleware.py` — Full middleware flow tests

## Development

```bash
# Install with dev dependencies
pip install -e ".[dev]"

# Format code
black taleemabad_auth/

# Lint
ruff check taleemabad_auth/

# Type check
mypy taleemabad_auth/
```

## Under the Hood

### What Middleware Does

1. **Extract Token** — From header/session/cookie (configurable)
2. **Verify Backend-to-Backend** — Calls `/api/verify-token` with client credentials
3. **Handle Expiry** — If token expired, auto-refreshes via `/api/refresh`
4. **Enrich with Hook** — Calls your `enrich_user_claims` function
5. **Sync Data** — Stores enriched data in Taleemabad via `/api/sync-user-profile`
6. **Attach to Request** — Adds `request.state.user` and `request.state.claims`

### What Your App Doesn't Need

✅ JWT verification  
✅ Token refresh logic  
✅ Password hashing  
✅ Role checking  
✅ Permission validation  
✅ Session management  
✅ Cookie handling  

All handled by the middleware.

## API Reference

### TaleebabadAuth

Main class for initialization.

```python
auth = TaleebabadAuth(
    auth_service_url: str,      # Taleemabad service URL
    client_id: str,             # Organization UUID
    client_secret: str,         # Organization secret
    token_source: str = "header",  # "header", "session", or "cookie"
    schema: dict = None,        # Optional schema definition
    timeout: int = 10,          # HTTP timeout in seconds
)

# Methods
auth.register_hook(name)        # @auth.register_hook("hook_name")
await auth.logout(request)      # Call from logout endpoint
```

### require_user

FastAPI dependency for protecting routes.

```python
from taleemabad_auth import require_user
from fastapi import Depends

@app.get("/protected")
async def protected(user=Depends(require_user)):
    # user is AuthUser (guaranteed non-None)
    return {"username": user.username}
```

### Models

```python
from taleemabad_auth import AuthUser, TokenClaims

# In your route handler:
@app.get("/me")
async def get_user(request):
    user: AuthUser = request.state.user  # id, username, email, phone
    claims: TokenClaims = request.state.claims  # user + custom_fields
    return {"user": user, "claims": claims}
```

## Support

- Documentation: [Taleemabad Auth Service](https://github.com/taleemabad/auth-service)
- Issues: [GitHub Issues](https://github.com/taleemabad/auth-service/issues)
- Email: dev@taleemabad.com

## License

MIT
