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.
⚠️ 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 emailPOST /auth/verify- Verify code and get JWT tokenGET /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
- Email: bronze_58_radar@icloud.com
- Issues: GitHub Issues
Made with ❤️ by Mukiev Mukhammad
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
72ba4ecc1899b2dd4b40b3a7f05686ab31ace0b8e104c12c51e462d9beaf2a85
|
|
| MD5 |
0d60c2cc129fb62d95cd4e33b8173378
|
|
| BLAKE2b-256 |
5b4f86797fb57db312e4be681650b0df98cb3d0e397b3c0f92e8b1cb53c07dbd
|
File details
Details for the file fastapi_email_auth-0.1.0a1-py3-none-any.whl.
File metadata
- Download URL: fastapi_email_auth-0.1.0a1-py3-none-any.whl
- Upload date:
- Size: 22.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
81c977a114da622046e771f5df1271aee7884ac74213fa7bcacfc44bcd353927
|
|
| MD5 |
632d847c63fa3dc6f5423a89d619c024
|
|
| BLAKE2b-256 |
863589afa9004b8637fb3e58f9b940b2f47c08cba825b209da420cec0fe43fde
|