Skip to main content

auth2fa

A flexible and easy-to-use Python library for implementing TOTP-based two-factor authentication with support for multiple storage backends.


Features

  • ✅ TOTP-based 2FA (Time-based One-Time Password)
  • ✅ QR code generation for easy setup
  • ✅ Recovery codes (8 codes, single-use)
  • ✅ Multiple storage backends (JSON file / SQL database)
  • ✅ Storage interface for custom backends
  • ✅ sqloader integration (optional)
  • ✅ No async dependencies - pure synchronous implementation
  • ✅ Compatible with Google Authenticator, Authy, etc.

Installation

Basic Installation (JSON storage only)

pip install auth2fa

With SQL Storage Support

pip install auth2fa[sql]

Quick Start

JSON Mode (Default)

from auth2fa import TwoFactorAuth

# Initialize with JSON storage (default)
tfa = TwoFactorAuth(issuer="MyApp")

# Setup 2FA for a user
result = tfa.setup("user123", username="john@example.com")
print(result["qr_uri"])          # otpauth:// URI for QR code
print(result["qr_image"])        # Base64 encoded PNG image
print(result["recovery_codes"])  # ['A3F8K2M1', 'B7D2N4P9', ...]

# Activate 2FA (user enters code from authenticator app)
if tfa.activate("user123", "482901"):
    print("2FA activated!")

# Verify during login
if tfa.verify("user123", "173829"):
    print("Login successful!")

SQL Mode (with sqloader)

Option 1: Using sqloader directly (if sqloader supports auth2fa's execute interface)

from auth2fa import TwoFactorAuth
from sqloader import SQLite3  # or MySQL

# Initialize database
sq = SQLite3("database.db", sql_path="./auth2fa/sql")

# Initialize with SQL storage
tfa = TwoFactorAuth(sq=sq, issuer="FileForge")

# Rest of the API is the same
result = tfa.setup("user123", username="shin")
tfa.activate("user123", "482901")
tfa.verify("user123", "173829")

Option 2: Using Auth2FAAdapter (for sqloader's raw database instances)

from auth2fa import TwoFactorAuth, Auth2FAAdapter
from sqloader import SQLiteWrapper  # or MySQLWrapper

# Initialize sqloader database instance
db = SQLiteWrapper('database.db')

# Wrap with adapter
adapter = Auth2FAAdapter(db)

# Initialize with SQL storage
tfa = TwoFactorAuth(sq=adapter, issuer="MyApp")

# Use normally
result = tfa.setup("user123", username="john@example.com")
tfa.activate("user123", "482901")

Custom Storage Path

# Use custom JSON file path
tfa = TwoFactorAuth(storage_path="./data/2fa.json", issuer="MyApp")

API Reference

TwoFactorAuth

__init__(issuer="MyApp", storage_path=None, sq=None)

Initialize TwoFactorAuth instance.

Parameters:

  • issuer (str): Application name displayed in authenticator apps
  • storage_path (str, optional): Path to JSON storage file
  • sq (optional): sqloader instance for SQL storage

Storage Priority:

  1. If sq is provided → SQL storage
  2. If storage_path is provided → JSON storage with custom path
  3. Otherwise → JSON storage at ./totp_data.json

setup(user_id, username="")

Set up TOTP for a user.

Returns: dict

{
    "secret": "BASE32SECRET",
    "qr_uri": "otpauth://totp/...",
    "qr_image": "base64encodedPNG...",
    "recovery_codes": ["A3F8K2M1", ...]
}

activate(user_id, code)

Activate TOTP after user verifies the code from their authenticator app.

Returns: bool (True if successful)

verify(user_id, code)

Verify TOTP code during login.

Behavior:

  • Unconfigured users → returns True (pass through)
  • Verifies TOTP code first
  • If TOTP fails, automatically tries recovery code
  • Recovery codes are single-use (automatically removed)

Returns: bool

disable(user_id)

Completely remove TOTP configuration for a user.

is_enabled(user_id)

Check if TOTP is enabled for a user.

Returns: bool

regenerate_recovery_codes(user_id)

Generate new recovery codes (invalidates old ones).

Returns: list

Integration Example

Login Flow with 2FA

from auth2fa import TwoFactorAuth

tfa = TwoFactorAuth(issuer="MyApp")

@router.post("/login")
async def login(credentials):
    user = authenticate(credentials)

    if tfa.is_enabled(str(user.id)):
        return {"status": "totp_required", "temp_token": create_temp_token(user)}

    return {"access_token": create_jwt(user)}

@router.post("/login/verify-totp")
async def verify_totp(temp_token, code):
    user = validate_temp_token(temp_token)

    if tfa.verify(str(user.id), code):
        return {"access_token": create_jwt(user)}

    return {"error": "invalid_code"}

@router.post("/user/setup-2fa")
async def setup_2fa(user):
    result = tfa.setup(str(user.id), username=user.email)

    return {
        "qr_image": result["qr_image"],  # Display as <img src="data:image/png;base64,..." />
        "recovery_codes": result["recovery_codes"]  # Show to user (print/save)
    }

@router.post("/user/activate-2fa")
async def activate_2fa(user, code):
    if tfa.activate(str(user.id), code):
        return {"success": True}
    return {"error": "invalid_code"}

@router.post("/user/disable-2fa")
async def disable_2fa(user):
    tfa.disable(str(user.id))
    return {"success": True}

Storage Backends

JSON Storage (Default)

  • File-based storage with file locking
  • Default path: ./totp_data.json
  • Thread-safe with lock file mechanism
  • Auto-converts user_id to string

SQL Storage (with sqloader)

Requires auth2fa[sql] installation.

Supported Databases:

  • SQLite3
  • MySQL
  • PostgreSQL (via sqloader)

Table Schema:

CREATE TABLE totp_auth (
    user_id VARCHAR(255) PRIMARY KEY,
    secret VARCHAR(255) NOT NULL,
    enabled BOOLEAN DEFAULT FALSE,
    recovery_codes TEXT,
    created_at TIMESTAMP NOT NULL,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Indexes
CREATE INDEX idx_totp_auth_enabled ON totp_auth(enabled);

Database Compatibility:

  • ✅ SQLite3
  • ✅ MySQL / MariaDB
  • ✅ PostgreSQL

Auth2FAAdapter

The Auth2FAAdapter is an optional adapter that bridges auth2fa with sqloader's database instances. It's automatically available when sqloader is installed.

Features:

  • 🔄 Automatic SQL file loading from auth2fa/sql directory
  • 🔀 Parameter binding conversion (SQLite :param vs MySQL %s)
  • 🗃️ SQL dialect conversion (PostgreSQL ON CONFLICT vs MySQL ON DUPLICATE KEY UPDATE)
  • 📊 Support for both SELECT and DML statements

When to use:

  • You're using sqloader's SQLiteWrapper or MySQLWrapper directly
  • You need automatic database dialect conversion
  • You want to avoid sqloader dependency in auth2fa core

Example:

from auth2fa import Auth2FAAdapter
from sqloader import SQLiteWrapper

# Your existing sqloader setup
db = SQLiteWrapper('app.db')

# Wrap it for auth2fa
adapter = Auth2FAAdapter(db)

# Use with TwoFactorAuth
from auth2fa import TwoFactorAuth
tfa = TwoFactorAuth(sq=adapter, issuer="MyApp")

Without sqloader: If sqloader is not installed, Auth2FAAdapter won't be available, but auth2fa will still work with JSON storage:

from auth2fa import TwoFactorAuth

# Falls back to JSON storage
tfa = TwoFactorAuth(issuer="MyApp")  # Uses ./totp_data.json

Custom Storage Backend

Implement BaseStorage interface:

from auth2fa.storage.base import BaseStorage

class CustomStorage(BaseStorage):
    def save(self, user_id: str, data: dict) -> None:
        # Your implementation
        pass

    def get(self, user_id: str) -> dict | None:
        # Your implementation
        pass

    def delete(self, user_id: str) -> None:
        # Your implementation
        pass

    def exists(self, user_id: str) -> bool:
        # Your implementation
        pass

# Use custom storage
from auth2fa import TwoFactorAuth
tfa = TwoFactorAuth.__new__(TwoFactorAuth)
tfa.issuer = "MyApp"
tfa.storage = CustomStorage()

Data Structure

TOTP data stored for each user:

{
    "secret": "BASE32SECRET",           # TOTP secret key
    "enabled": False,                   # Activation status
    "recovery_codes": [                 # Single-use backup codes
        "A3F8K2M1",
        "B7D2N4P9",
        # ... 8 codes total
    ],
    "created_at": "2026-02-11T18:00:00"  # ISO format timestamp
}

Security Notes

  • Recovery codes are alphanumeric (excluding 0, O, I, 1 for clarity)
  • TOTP uses 30-second time window with ±1 window tolerance
  • Recovery codes are case-insensitive
  • Each recovery code can only be used once
  • File locking prevents race conditions in JSON storage

Requirements

  • Python 3.10+
  • pyotp
  • qrcode
  • Pillow

Optional:

  • sqloader (for SQL storage)

License

MIT License

Links

Contributing

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

Metadata

Release files for auth2fa 0.1.4

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for auth2fa 0.1.4
File Size Uploaded Provenance
auth2fa-0.1.4.tar.gz 15.2 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for auth2fa 0.1.4
File Interpreter ABI Platform
auth2fa-0.1.4-py3-none-any.whl Python 3 none any Details

Total release size: 31.1 kB

Release files / auth2fa-0.1.4.tar.gz

Download URL auth2fa-0.1.4.tar.gz
Size 15.2 kB
Tags Source
SHA-256 checksum
How to use checksums
df740bf713ec564b4afc6d989c087839db60bf69a7155a6b4df4df1f3b2c2883
BLAKE2b-256 checksum
How to use checksums
a8f152925684d362eca1edd9580e31733ac55fcfbf3d66b9e0379092fb68fae5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.2

Release files / auth2fa-0.1.4-py3-none-any.whl

Download URL auth2fa-0.1.4-py3-none-any.whl
Size 15.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
114b67e8001e55f597934f7e66d873b5ed4392296257e1c624a3b8fbcfe6bf66
BLAKE2b-256 checksum
How to use checksums
b47fb17322bc421de94af37f713c7c9a5dc9a89ac0617a90014da3219207a5ac
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.2

Security for auth2fa 0.1.4

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page