Skip to main content

A flexible, type-safe library for adding OTP-based authentication to FastAPI applications

Project description

FastAPI OTP Authentication

A flexible, type-safe library for adding OTP (One-Time Password) based authentication to FastAPI applications with minimal configuration.

Python FastAPI Type Checked Code Style

Features

  • 🔐 Cryptographically Secure: Uses secrets module for OTP generation
  • 🍪 HTTP-Only Cookies: Refresh tokens stored securely (web)
  • 📱 App-Friendly: Refresh tokens can also be returned in response body (mobile/native)
  • 🔀 Dual Mode: Same backend can serve web (cookie) and app (body) clients simultaneously
  • 🚫 Token Blacklisting: Revoke tokens on logout
  • 💯 100% Type Safe: Full mypy strict mode compliance
  • 🎯 Flexible: Extend abstract classes to customize behavior
  • 🧑‍💻 Developer Mode: Testing mode with predictable OTP codes
  • 🔧 Multiple Database Backends: SQLAlchemy (SQL) or MongoDB (NoSQL)
  • 📝 Custom Claims: Add your own JWT claims
  • Production Ready: Secret validation and security best practices

Database Support

This library supports both SQL and NoSQL databases:

Backend Adapter Models Use Case
SQLAlchemy SQLAlchemyAdapter BaseOTPUserTable (ORM) PostgreSQL, MySQL, SQLite, etc.
MongoDB MongoDBAdapter BaseOTPUserDocument (Pydantic) MongoDB Atlas, local MongoDB

Choose the backend that fits your stack. Both implementations provide the same OTP authentication features.

Installation

# Install base package
uv add fastapi-otp-authentication # or pip install if using pip

# Install with SQLAlchemy support
uv add "fastapi-otp-authentication[sqlalchemy] @ git+https://github.com/gronnmann/fastapi-otp-authentication.git"

# Install with MongoDB support
uv add "fastapi-otp-authentication[mongodb] @ git+https://github.com/gronnmann/fastapi-otp-authentication.git"

# Install both (if needed)
uv add "fastapi-otp-authentication[all] @ git+https://github.com/gronnmann/fastapi-otp-authentication.git"

# Then install your database driver
uv add asyncpg      # PostgreSQL with SQLAlchemy
uv add aiomysql     # MySQL with SQLAlchemy
uv add aiosqlite    # SQLite with SQLAlchemy
# MongoDB driver (motor) is included with [mongodb] extra

Quick Start (SQLAlchemy)

1. Create Your User Model and Register Blacklist table

from sqlalchemy import Integer, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from fastapi_otp_authentication import BaseOTPUserTable
from fastapi_otp_authentication import TokenBlacklist


class Base(DeclarativeBase):
    pass

class User(BaseOTPUserTable[int], Base):
    __tablename__ = "users"
    
    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    username: Mapped[str] = mapped_column(String(50), unique=True)

class Blacklist(TokenBlacklist, Base):
    pass

2. Configure OTP Authentication

You configure the library by extending the OTPAuthConfig abstract class:

from datetime import timedelta
from fastapi_otp_authentication import OTPAuthConfig

class MyOTPConfig(OTPAuthConfig):
    # Security - REQUIRED
    secret_key = "your-secret-key-generate-with-openssl-rand-hex-32"
    
    # Optional: Developer mode for testing (default: False)
    developer_mode = False
    
    cookie_secure = True  # Use secure cookies (https) in production
    
    # Token lifetimes
    access_token_lifetime = timedelta(hours=1)
    refresh_token_lifetime = timedelta(days=7)
    
    # OTP settings
    otp_length = 6
    otp_expiry = timedelta(minutes=10)
    max_otp_attempts = 5
    
    async def send_otp(self, email: str, code: str) -> None:
        """Implement your OTP delivery method."""
        # Send via email, SMS, etc.
        print(f"OTP for {email}: {code}")
    
    def get_additional_claims(self, user: User) -> dict[str, Any]:
        """Add custom claims to JWT tokens."""
        return {"username": user.username}

3. Set Up Database and Dependencies

from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from fastapi_otp_authentication import SQLAlchemyAdapter

# Database setup
engine = create_async_engine("sqlite+aiosqlite:///./app.db")
async_session_maker = async_sessionmaker(engine, expire_on_commit=False)

async def get_async_session():
    async with async_session_maker() as session:
        yield session

async def get_otp_db(session: AsyncSession = Depends(get_async_session)):
    yield SQLAlchemyAdapter(session, User, Blacklist)

4. Register Authentication Router

from fastapi import FastAPI
from fastapi_otp_authentication import get_auth_router

app = FastAPI()
config = MyOTPConfig()

auth_router = get_auth_router(get_otp_db, config)
app.include_router(auth_router, prefix="/auth", tags=["auth"])

5. Protect Your Routes

from fastapi import Depends
from fastapi_otp_authentication import (
    get_current_user_dependency,
    get_verified_user_dependency,
    get_custom_claims_dependency,
)

# Create dependencies
current_user = Depends(get_current_user_dependency(get_otp_db, config))
verified_user = Depends(get_verified_user_dependency(get_otp_db, config))
custom_claims = Depends(get_custom_claims_dependency(config))

@app.get("/protected")
async def protected_route(user: User = current_user):
    return {"user_id": user.id, "email": user.email}

@app.get("/verified-only")
async def verified_only(user: User = verified_user):
    return {"message": "Access granted to verified user"}

@app.get("/check-claims")
async def check_claims(claims: dict = custom_claims):
    return {"custom_claims": claims}

Quick Start (MongoDB)

1. Create Your User Model

from pydantic import Field
from fastapi_otp_authentication import BaseOTPUserDocument, TokenBlacklistDocument

class User(BaseOTPUserDocument):
    """User model with custom fields."""
    username: str = Field(..., max_length=50)
    full_name: str | None = Field(None, max_length=100)

# Blacklist uses default TokenBlacklistDocument
class Blacklist(TokenBlacklistDocument):
    pass

2. Configure OTP Authentication

Same as SQLAlchemy - extend OTPAuthConfig:

from datetime import timedelta
from fastapi_otp_authentication import OTPAuthConfig

class MyOTPConfig(OTPAuthConfig):
    secret_key = "your-secret-key-generate-with-openssl-rand-hex-32"
    developer_mode = False
    
    access_token_lifetime = timedelta(hours=1)
    refresh_token_lifetime = timedelta(days=7)
    
    otp_length = 6
    otp_expiry = timedelta(minutes=10)
    max_otp_attempts = 5
    
    async def send_otp(self, email: str, code: str) -> None:
        print(f"OTP for {email}: {code}")
    
    def get_additional_claims(self, user: User) -> dict[str, Any]:
        return {"username": user.username}

3. Set Up MongoDB and Dependencies

from motor.motor_asyncio import AsyncIOMotorClient
from fastapi_otp_authentication import MongoDBAdapter

# MongoDB setup
client = AsyncIOMotorClient("mongodb://localhost:27017")
database = client["myapp"]

async def get_otp_db():
    return MongoDBAdapter(
        database=database,
        user_collection_name="users",
        blacklist_collection_name="token_blacklist",
        user_model_class=User,
    )

4. Create Indexes (Important!)

MongoDB requires indexes for optimal performance:

@app.on_event("startup")
async def startup():
    # Unique index on email
    await database["users"].create_index("email", unique=True)
    
    # Index on jti for blacklist lookups
    await database["token_blacklist"].create_index("jti", unique=True)
    
    # TTL index to auto-delete expired tokens
    await database["token_blacklist"].create_index(
        "expires_at", expireAfterSeconds=0
    )

5. Register Router & Protect Routes

Same as SQLAlchemy:

from fastapi_otp_authentication import get_auth_router, get_current_user_dependency

config = MyOTPConfig()
auth_router = get_auth_router(get_otp_db, config)
app.include_router(auth_router, prefix="/auth", tags=["auth"])

current_user = Depends(get_current_user_dependency(get_otp_db, config))

@app.get("/protected")
async def protected_route(user: User = current_user):
    return {"user_id": str(user.id), "email": user.email}

API Endpoints

The auth router provides these endpoints:

POST /auth/request-otp

Request OTP code to be sent to user's email.

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

POST /auth/verify-otp

Verify OTP code and receive authentication tokens.

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

Response:

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

The refresh token is set in an HTTP-only cookie.

POST /auth/refresh

Refresh access token using refresh token from cookie.

POST /auth/logout

Blacklist tokens and clear refresh cookie.

Security Best Practices

Generate Secure Secret Key

openssl rand -hex 32

The library validates that your secret key is at least 32 characters long (unless in developer mode).

Developer Mode

For testing, enable developer mode:

class MyOTPConfig(OTPAuthConfig):
    developer_mode = True
    secret_key = "any-key-allowed-in-dev-mode"

In developer mode:

  • OTP codes are always 000000 (or length of zeros)
  • Secret key validation is relaxed

⚠️ Never use developer mode in production!

Advanced Usage

Custom Claims

Add custom data to JWT tokens:

class MyOTPConfig(OTPAuthConfig):
    def get_additional_claims(self, user: User) -> dict[str, Any]:
        return {
            "role": user.role,
            "permissions": user.permissions,
            "organization_id": user.organization_id,
        }

Access custom claims in your routes:

from fastapi_otp_authentication import get_custom_claims_dependency

custom_claims = Depends(get_custom_claims_dependency(config))

@app.get("/admin-check")
async def admin_check(claims: dict = custom_claims):
    if claims.get("role") != "admin":
        raise HTTPException(status_code=403, detail="Admin access required")
    return {"message": "Admin access granted"}

Refresh Token Delivery Mode

By default the refresh token is stored in an HTTP-only cookie (best for web). To support mobile/native apps that manage tokens manually, set refresh_token_delivery:

Value Behaviour
"cookie" (default) Refresh token set in HTTP-only cookie only
"body" Refresh token returned in JSON response body; accepted from request body
"both" Cookie and body — one backend serves both web and app clients
class MyOTPConfig(OTPAuthConfig):
    # Mobile app: token returned in verify-otp response body
    refresh_token_delivery = "body"

    # Or serve both web and app from the same backend
    # refresh_token_delivery = "both"

verify-otp response ("body" / "both" mode):

{
  "access_token": "eyJ...",
  "token_type": "bearer",
  "refresh_token": "eyJ..."
}

refresh and logout with body token:

{
  "refresh_token": "eyJ..."
}

When a request contains both a cookie and a body token, the cookie takes priority.

Rate Limiting

OTP verification attempts are tracked automatically. Configure max attempts:

class MyOTPConfig(OTPAuthConfig):
    max_otp_attempts = 3  # Stricter rate limiting

Custom OTP Length

class MyOTPConfig(OTPAuthConfig):
    otp_length = 8  # 8-digit OTP codes

Token Blacklist Cleanup

Periodically clean up expired blacklisted tokens:

SQLAlchemy:

async def cleanup_expired_tokens():
    async with async_session_maker() as session:
        db = SQLAlchemyAdapter(session, User, Blacklist)
        removed = await db.cleanup_blacklist()
        print(f"Removed {removed} expired tokens")

MongoDB:

async def cleanup_expired_tokens():
    db = MongoDBAdapter(database, "users", "token_blacklist", User)
    removed = await db.cleanup_blacklist()
    print(f"Removed {removed} expired tokens")

Note: MongoDB can automatically clean up expired tokens using TTL indexes (see MongoDB Quick Start).

Example Applications

SQLAlchemy Example

See examples/basic_usage.py for a complete SQLite example.

uv run python examples/basic_usage.py

MongoDB Example

See examples/mongodb_usage.py for a complete MongoDB example.

Prerequisites: MongoDB must be running locally or update the connection string.

# Start MongoDB (if using Docker)
docker run -d -p 27017:27017 mongo

# Run the example
uv run python examples/mongodb_usage.py

Then visit http://localhost:8000/docs to try the API.

Testing

Run the test suite:

uv run pytest tests/ -v

Contributing

Contributions are welcome! Please ensure:

  1. All tests pass: pytest tests/
  2. Code is type-safe: mypy . --strict
  3. Code is linted: ruff check .
  4. Code is formatted: black .

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_otp_authentication-0.2.0.tar.gz (124.0 kB view details)

Uploaded Source

Built Distribution

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

fastapi_otp_authentication-0.2.0-py3-none-any.whl (28.9 kB view details)

Uploaded Python 3

File details

Details for the file fastapi_otp_authentication-0.2.0.tar.gz.

File metadata

  • Download URL: fastapi_otp_authentication-0.2.0.tar.gz
  • Upload date:
  • Size: 124.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for fastapi_otp_authentication-0.2.0.tar.gz
Algorithm Hash digest
SHA256 8b5470715d3aba5987fb38bba64f6c24e899b6b827518df49ce64f4db6cb0501
MD5 50296b7221f349c03daa31110f6a8257
BLAKE2b-256 117a6acd9fc85fd0a348b669c1ea27d0d06db27d0969109272ec9382dd605c08

See more details on using hashes here.

File details

Details for the file fastapi_otp_authentication-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: fastapi_otp_authentication-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 28.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for fastapi_otp_authentication-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6ca60d9ef7fef12a6d7510cfbaad693f1e0a4e7fbbb06137e03de0c7b42b4016
MD5 36fd5e7918be62077e09d53f72fffb11
BLAKE2b-256 1122af35147a261930aa86ad0c25cbc8d70ead64fb03ff731b82a824bfe4c524

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