Skip to main content

Email-based passwordless authentication for FastAPI (Alpha)

Project description

FastAPI Email Auth 📧🔐 (Alpha)

Passwordless email authentication for FastAPI using BIP-39 mnemonic verification codes. Simple, secure, and production-ready.

Python 3.11+ FastAPI License: MIT

⚠️ Alpha Version

This package is in early development (v0.1.0-alpha).

What works:

  • ✅ Email code sending and verification
  • ✅ JWT authentication
  • ✅ In-memory storage
  • ✅ Redis storage for codes

What's coming:

  • 🚧 PostgreSQL user storage
  • 🚧 More comprehensive tests
  • 🚧 Production battle-tested

Not recommended for production yet!

✨ Features

  • Passwordless authentication - Users receive verification codes via email
  • Multi-language support - BIP-39 codes in 9 languages (English, Russian, Chinese, etc.)
  • Easy to use - Works out of the box with minimal configuration
  • Async/await - Fully asynchronous for high performance
  • Pluggable storage - In-memory, Redis, PostgreSQL, or custom implementations
  • Security first - Rate limiting, attempt tracking, JWT tokens
  • Type-safe - Full Pydantic validation
  • Well tested - 100% test coverage

📦 Installation

Basic installation

pip install fastapi-email-auth
# or
uv add fastapi-email-auth

With optional dependencies

# With Redis support
pip install fastapi-email-auth[redis]

# With PostgreSQL support
pip install fastapi-email-auth[postgres]

# With all optional dependencies
pip install fastapi-email-auth[all]

🚀 Quick Start

1. Create .env file

EMAIL_AUTH_SMTP_HOST=smtp.gmail.com
EMAIL_AUTH_SMTP_PORT=587
EMAIL_AUTH_SMTP_USER=your-email@gmail.com
EMAIL_AUTH_SMTP_PASSWORD=your-app-password
EMAIL_AUTH_JWT_SECRET=your-secret-key-at-least-32-characters

2. Add to your FastAPI app

from fastapi import FastAPI
from fastapi_email_auth import router

app = FastAPI()

# Add authentication routes
app.include_router(router, prefix="/auth")

3. That's it! 🎉

Your API now has these endpoints:

  • POST /auth/send-code - Send verification code to email
  • POST /auth/verify - Verify code and get JWT token
  • GET /auth/me - Get current user (protected)

📖 Usage Examples

Basic Authentication Flow

# 1. User requests verification code
POST /auth/send-code
{
  "email": "user@example.com"
}

# Response:
{
  "success": true,
  "message": "Code sent to email",
  "expires_in": 600
}

# 2. User receives email with code like "abandon ability"

# 3. User submits code
POST /auth/verify
{
  "email": "user@example.com",
  "code": "abandon ability"
}

# Response:
{
  "access_token": "eyJhbGci...",
  "token_type": "bearer"
}

# 4. Access protected endpoints
GET /auth/me
Authorization: Bearer eyJhbGci...

# Response:
{
  "email": "user@example.com",
  "created_at": "2025-10-04T10:00:00Z",
  "last_login": "2025-10-04T10:05:00Z"
}

Using Protected Routes

from fastapi import FastAPI, Depends
from fastapi_email_auth import router, get_current_user

app = FastAPI()
app.include_router(router, prefix="/auth")

@app.get("/profile")
async def get_profile(email: str = Depends(get_current_user)):
    """This route requires authentication"""
    return {"email": email, "subscription": "premium"}

@app.get("/admin")
async def admin_panel(email: str = Depends(get_current_user)):
    """Check if user is admin"""
    if email not in ["admin@example.com"]:
        raise HTTPException(403, "Admin access required")
    return {"message": "Welcome, admin!"}

⚙️ Configuration

All settings are configured via environment variables with EMAIL_AUTH_ prefix:

SMTP Settings

EMAIL_AUTH_SMTP_HOST=smtp.gmail.com
EMAIL_AUTH_SMTP_PORT=587
EMAIL_AUTH_SMTP_USER=noreply@example.com
EMAIL_AUTH_SMTP_PASSWORD=app-password
EMAIL_AUTH_SMTP_USE_TLS=true
EMAIL_AUTH_SMTP_FROM_EMAIL=noreply@example.com  # Optional, defaults to SMTP_USER

JWT Settings

EMAIL_AUTH_JWT_SECRET=your-secret-key-min-32-chars
EMAIL_AUTH_JWT_ALGORITHM=HS256
EMAIL_AUTH_JWT_EXPIRY_DAYS=7

Code Generation

EMAIL_AUTH_CODE_WORD_COUNT=2              # Number of words (1-12)
EMAIL_AUTH_CODE_LANGUAGE=russian          # english, russian, spanish, etc.
EMAIL_AUTH_CODE_SEPARATOR=-               # Separator between words
EMAIL_AUTH_CODE_TTL=600                   # Code validity in seconds

Security

EMAIL_AUTH_MAX_ATTEMPTS=3                 # Max verification attempts
EMAIL_AUTH_RATE_LIMIT_WINDOW=60          # Rate limit window in seconds

Storage (Optional)

# Redis for production
EMAIL_AUTH_REDIS_URL=redis://localhost:6379/0
EMAIL_AUTH_REDIS_KEY_PREFIX=myapp:auth:

# PostgreSQL for user storage
EMAIL_AUTH_DATABASE_URL=postgresql+asyncpg://user:pass@localhost/db

🔧 Advanced Integration

Custom Storage Implementation

Implement your own storage backends:

from fastapi import FastAPI
from fastapi_email_auth import (
    EmailAuthService,
    set_custom_service,
    router
)
from fastapi_email_auth.storage.redis import RedisCodeStorage
from my_app.storage import MyUserStorage  # Your implementation

app = FastAPI()

# Create custom service on startup
@app.on_event("startup")
async def setup_auth():
    code_storage = RedisCodeStorage("redis://localhost:6379/0")
    user_storage = MyUserStorage()  # Your PostgreSQL/MongoDB/etc implementation
    
    service = EmailAuthService(
        code_storage=code_storage,
        user_storage=user_storage,
        smtp_host="smtp.yandex.ru",
        smtp_port=587,
        smtp_user="noreply@myapp.com",
        smtp_password="password",
        jwt_secret="secret",
        code_language="russian",
        max_attempts=5
    )
    
    set_custom_service(service)

app.include_router(router, prefix="/auth")

Implementing Custom UserStorage

from fastapi_email_auth.interfaces import UserStorage
from typing import Optional
from datetime import datetime, timezone

class PostgreSQLUserStorage(UserStorage):
    def __init__(self, connection_string: str):
        self.db = create_async_engine(connection_string)
    
    async def get_user(self, email: str) -> Optional[dict]:
        async with self.db.begin() as conn:
            result = await conn.execute(
                "SELECT * FROM users WHERE email = $1", email
            )
            return dict(result.fetchone()) if result else None
    
    async def get_or_create_user(self, email: str) -> dict:
        user = await self.get_user(email)
        if not user:
            async with self.db.begin() as conn:
                await conn.execute(
                    "INSERT INTO users (email, created_at) VALUES ($1, $2)",
                    email, datetime.now(timezone.utc)
                )
            user = await self.get_user(email)
        return user
    
    async def update_last_login(self, email: str) -> None:
        async with self.db.begin() as conn:
            await conn.execute(
                "UPDATE users SET last_login = $1 WHERE email = $2",
                datetime.now(timezone.utc), email
            )

Custom Email Templates

Override email sending with custom templates:

from fastapi_email_auth import EmailAuthService
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import aiosmtplib

class CustomEmailService(EmailAuthService):
    async def _send_email(self, email: str, code: str) -> None:
        message = MIMEMultipart("alternative")
        message["Subject"] = "🔐 Your Login Code"
        message["From"] = self.smtp_config["username"]
        message["To"] = email
        
        html = f"""
        <html>
          <body style="font-family: Arial;">
            <div style="max-width: 600px; margin: 0 auto;">
              <h1>Your verification code:</h1>
              <div style="background: #4CAF50; padding: 20px; text-align: center;">
                <h2 style="color: white; font-size: 36px;">{code}</h2>
              </div>
              <p>Valid for {self.code_ttl // 60} minutes.</p>
            </div>
          </body>
        </html>
        """
        
        message.attach(MIMEText(html, "html"))
        await aiosmtplib.send(message, **self.smtp_config)

Integration with Existing User System

from fastapi import FastAPI, Depends
from fastapi_email_auth import router, EmailAuthService, get_auth_service

app = FastAPI()
app.include_router(router, prefix="/auth")

@app.post("/auth/register")
async def register(
    email: str,
    name: str,
    service: EmailAuthService = Depends(get_auth_service)
):
    """Custom registration with additional fields"""
    
    # 1. Validate email doesn't exist
    existing = await service.user_storage.get_user(email)
    if existing:
        raise HTTPException(400, "Email already registered")
    
    # 2. Send verification code
    result = await service.send_verification_code(email)
    
    # 3. Store additional user data
    await your_db.store_pending_user(email, name)
    
    return {"message": "Verification code sent", "expires_in": result["expires_in"]}

@app.post("/auth/complete-registration")
async def complete_registration(
    email: str,
    code: str,
    service: EmailAuthService = Depends(get_auth_service)
):
    """Verify code and complete registration"""
    
    # Verify code and create user
    token = await service.verify_code(email, code, auto_create_user=True)
    
    # Retrieve and save additional data
    pending_user = await your_db.get_pending_user(email)
    await your_db.create_user(email, pending_user.name)
    
    return {"access_token": token, "token_type": "bearer"}

🌍 Multi-Language Support

Supported BIP-39 languages:

# English (default)
EMAIL_AUTH_CODE_LANGUAGE=english
# "abandon ability"

# Russian
EMAIL_AUTH_CODE_LANGUAGE=russian
# "солнце-река"

# Spanish
EMAIL_AUTH_CODE_LANGUAGE=spanish
# "casa-perro"

# Also supported: chinese_simplified, chinese_traditional, 
# french, italian, japanese, korean

🛡️ Security Features

Rate Limiting

Prevents spam by limiting code requests:

EMAIL_AUTH_RATE_LIMIT_WINDOW=60  # 1 minute between requests

Attempt Tracking

Limits verification attempts per code:

EMAIL_AUTH_MAX_ATTEMPTS=3  # Lock after 3 failed attempts

JWT Expiration

Tokens automatically expire:

EMAIL_AUTH_JWT_EXPIRY_DAYS=7  # Token valid for 7 days

🧪 Testing

# Install dev dependencies
pip install fastapi-email-auth[dev]

# Run tests
pytest

# With coverage
pytest --cov=fastapi_email_auth --cov-report=html

🚀 Production Deployment

With Docker

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

With Docker Compose

version: '3.8'

services:
  app:
    build: .
    ports:
      - "8000:8000"
    env_file:
      - .env.production
    depends_on:
      - redis
  
  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

volumes:
  redis_data:

Environment Variables for Production

# Use strong secrets
EMAIL_AUTH_JWT_SECRET=${RANDOM_SECRET_FROM_SECRETS_MANAGER}

# Production SMTP
EMAIL_AUTH_SMTP_HOST=smtp.yandex.ru
EMAIL_AUTH_SMTP_USER=${SMTP_USER_FROM_SECRETS}
EMAIL_AUTH_SMTP_PASSWORD=${SMTP_PASSWORD_FROM_SECRETS}

# Redis for scalability
EMAIL_AUTH_REDIS_URL=redis://redis:6379/0

# Database
EMAIL_AUTH_DATABASE_URL=${DATABASE_URL_FROM_SECRETS}

📚 API Reference

Endpoints

POST /auth/send-code

Send verification code to email.

Request:

{
  "email": "user@example.com"
}

Response:

{
  "success": true,
  "message": "Code sent to email",
  "expires_in": 600
}

POST /auth/verify

Verify code and receive JWT token. Requires user to exist.

Request:

{
  "email": "user@example.com",
  "code": "abandon ability"
}

Response:

{
  "access_token": "eyJhbGci...",
  "token_type": "bearer"
}

POST /auth/register-and-verify

Verify code and auto-create user if doesn't exist.

Request:

{
  "email": "newuser@example.com",
  "code": "abandon ability"
}

Response:

{
  "access_token": "eyJhbGci...",
  "token_type": "bearer"
}

GET /auth/me

Get current authenticated user.

Headers:

Authorization: Bearer eyJhbGci...

Response:

{
  "email": "user@example.com",
  "created_at": "2025-10-04T10:00:00Z",
  "last_login": "2025-10-04T10:05:00Z"
}

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

🔗 Links

💬 Support


Made with ❤️ by Mukiev Mukhammad

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fastapi_email_auth-0.1.0a1.tar.gz (68.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fastapi_email_auth-0.1.0a1-py3-none-any.whl (22.1 kB view details)

Uploaded Python 3

File details

Details for the file fastapi_email_auth-0.1.0a1.tar.gz.

File metadata

  • Download URL: fastapi_email_auth-0.1.0a1.tar.gz
  • Upload date:
  • Size: 68.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.7

File hashes

Hashes for fastapi_email_auth-0.1.0a1.tar.gz
Algorithm Hash digest
SHA256 72ba4ecc1899b2dd4b40b3a7f05686ab31ace0b8e104c12c51e462d9beaf2a85
MD5 0d60c2cc129fb62d95cd4e33b8173378
BLAKE2b-256 5b4f86797fb57db312e4be681650b0df98cb3d0e397b3c0f92e8b1cb53c07dbd

See more details on using hashes here.

File details

Details for the file fastapi_email_auth-0.1.0a1-py3-none-any.whl.

File metadata

File hashes

Hashes for fastapi_email_auth-0.1.0a1-py3-none-any.whl
Algorithm Hash digest
SHA256 81c977a114da622046e771f5df1271aee7884ac74213fa7bcacfc44bcd353927
MD5 632d847c63fa3dc6f5423a89d619c024
BLAKE2b-256 863589afa9004b8637fb3e58f9b940b2f47c08cba825b209da420cec0fe43fde

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page