Metadata-Version: 2.4
Name: auth101
Version: 0.2.0
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

`auth101` is a small Python package to provide email/password authentication with a pluggable user schema and pluggable user storage backends. It uses Argon2 via passlib for password hashing by default and includes JWT token generation for authenticated sessions.

## Features

- Email/password authentication
- Secure password hashing with Argon2
- JWT token generation and verification
- Pluggable user schema and storage backends
- In-memory storage for development
- **SQLAlchemy support** for SQL database persistence
- **Django ORM support** for Django projects

## Quick usage

### Basic Authentication

```py
from auth101 import Authenticator
from auth101.models import User

# default in-memory store
auth = Authenticator()
user = auth.register("alice@example.com", "s3cr3t", full_name="Alice")
assert auth.authenticate("alice@example.com", "s3cr3t")

# provide your own factory to accept a different user schema
def my_user_factory(**kwargs):
    return User(**kwargs)

auth2 = Authenticator(user_factory=my_user_factory)
```

### JWT Token Authentication

```py
from auth101 import Authenticator

# Configure authenticator with JWT support
auth = Authenticator(
    jwt_secret_key="your-secret-key-change-in-production",
    jwt_expires_in_minutes=60,  # Token expires in 60 minutes
    jwt_algorithm="HS256"  # Optional, defaults to HS256
)

# Register a new user and get JWT token
user, token = auth.register_with_token("bob@example.com", "password123")
print(f"Token: {token}")

# Authenticate existing user and get JWT token
result = auth.authenticate_with_token("bob@example.com", "password123")
if result:
    user, token = result
    print(f"User authenticated: {user.email}")
    print(f"Token: {token}")
```

### Token Verification

```py
from auth101.core.security import verify_token, get_user_id_from_token

# Verify a token
payload = verify_token(token, "your-secret-key")
if payload:
    print(f"Token valid, user_id: {payload['sub']}")
else:
    print("Token invalid or expired")

# Extract user_id from token
user_id = get_user_id_from_token(token, "your-secret-key")
if user_id:
    print(f"User ID: {user_id}")
```

## Configuration Options

When initializing the `Authenticator`, you can configure:

**Simplified configuration:**
- `db_url`: Database connection string for SQLAlchemy (e.g., "postgresql://user:pass@localhost/db")
- `django_model`: Django model class for DjangoUserStore
- `auto_create_tables`: Automatically create database tables (for SQLAlchemy, default: False)

**Advanced configuration:**
- `store`: User storage backend (default: `MemoryUserStore`)
- `user_factory`: Factory function to create user instances
- `pwd_ctx`: Password hashing context (default: Argon2)

**JWT token configuration:**
- `jwt_secret_key`: Secret key for JWT signing (required for JWT features)
- `jwt_algorithm`: JWT algorithm (default: "HS256")
- `jwt_expires_in_minutes`: Token expiration time in minutes (default: 60)

**Note:** You can only provide one of `store`, `db_url`, or `django_model`. If none are provided, an in-memory store is used by default.

## Database Persistence

### SQLAlchemy Support

Install SQLAlchemy support:

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

**Simplified approach** - just pass a connection string:

```py
from auth101 import Authenticator

# Simply pass your database URL!
auth = Authenticator(
    db_url="postgresql://user:pass@localhost/mydb",
    auto_create_tables=True  # Automatically create tables
)

user = auth.register("alice@example.com", "password123")
```

**Advanced approach** - for custom engine configuration:

```py
from sqlalchemy import create_engine
from auth101 import Authenticator
from auth101.core.persistence import SQLAlchemyUserStore

# Create engine with custom options
engine = create_engine(
    "postgresql://user:pass@localhost/mydb",
    pool_size=10,
    max_overflow=20
)

# Create store and tables
store = SQLAlchemyUserStore(engine)
store.create_tables()

# Use with authenticator
auth = Authenticator(store=store)
user = auth.register("alice@example.com", "password123")
```

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

### Django Support

Install for Django:

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

1. Add model to your Django app:

```py
from django.db import models
import uuid

class AuthUser(models.Model):
    id = models.CharField(max_length=36, primary_key=True, 
                         default=lambda: str(uuid.uuid4()))
    email = models.EmailField(unique=True, db_index=True)
    password_hash = models.CharField(max_length=255)
    is_active = models.BooleanField(default=True)
    
    class Meta:
        db_table = 'auth_users'
```

2. Run migrations:

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

3. Use in your Django views:

**Simplified approach**:

```py
from auth101 import Authenticator
from myapp.models import AuthUser

# Just pass your Django model!
auth = Authenticator(django_model=AuthUser)

# In your view:
user = auth.register(email, password)
authenticated = auth.authenticate(email, password)
```

**Advanced approach** - if you need explicit store control:

```py
from auth101 import Authenticator
from auth101.core.persistence import DjangoUserStore
from myapp.models import AuthUser

store = DjangoUserStore(AuthUser)
auth = Authenticator(store=store)

# In your view:
user = auth.register(email, password)
authenticated = auth.authenticate(email, password)
```

## Framework Integration Examples

### FastAPI

```py
from fastapi import FastAPI, HTTPException, Depends
from auth101 import Authenticator
from auth101.core.security import get_user_id_from_token

# Initialize with database
auth = Authenticator(
    db_url="postgresql://user:pass@localhost/db",
    auto_create_tables=True,
    jwt_secret_key="your-secret-key",
    jwt_expires_in_minutes=60
)

app = FastAPI()

@app.post("/register")
async def register(email: str, password: str):
    try:
        user, token = auth.register_with_token(email, password)
        return {"user_id": user.id, "token": token}
    except ValueError as e:
        raise HTTPException(400, str(e))

@app.post("/login")
async def login(email: str, password: str):
    result = auth.authenticate_with_token(email, password)
    if not result:
        raise HTTPException(401, "Invalid credentials")
    user, token = result
    return {"token": token}

# Protected route
def get_current_user(token: str):
    user_id = get_user_id_from_token(token, auth.jwt_secret_key)
    if not user_id:
        raise HTTPException(401, "Invalid token")
    return user_id

@app.get("/protected")
async def protected(user_id: str = Depends(get_current_user)):
    return {"message": f"Hello user {user_id}"}
```

### Flask

```py
from flask import Flask, request, jsonify
from auth101 import Authenticator
from auth101.core.security import get_user_id_from_token
from functools import wraps

# Initialize with database
auth = Authenticator(
    db_url="sqlite:///auth.db",
    auto_create_tables=True,
    jwt_secret_key="your-secret-key"
)

app = Flask(__name__)

# Protected route decorator
def token_required(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        token = request.headers.get('Authorization', '').replace('Bearer ', '')
        user_id = get_user_id_from_token(token, auth.jwt_secret_key)
        if not user_id:
            return jsonify({"error": "Invalid token"}), 401
        return f(user_id, *args, **kwargs)
    return decorated

@app.route('/register', methods=['POST'])
def register():
    data = request.json
    try:
        user, token = auth.register_with_token(data['email'], data['password'])
        return jsonify({"user_id": user.id, "token": token})
    except ValueError as e:
        return jsonify({"error": str(e)}), 400

@app.route('/login', methods=['POST'])
def login():
    data = request.json
    result = auth.authenticate_with_token(data['email'], data['password'])
    if not result:
        return jsonify({"error": "Invalid credentials"}), 401
    user, token = result
    return jsonify({"token": token})

@app.route('/protected')
@token_required
def protected(user_id):
    return jsonify({"message": f"Hello user {user_id}"})
```
