Skip to main content

Drop-in device tracking for FastAPI - persistent IDs without user accounts

Project description

FastAPI Device ID

PyPI version Python versions Tests License

Track devices across sessions without user registration. Perfect for analytics, rate limiting, and A/B testing.

Drop-in device tracking for FastAPI apps. Get persistent device IDs without user accounts, databases, or privacy concerns.

Why FastAPI Device ID?

The Problem

Modern web applications often need to:

  • Track user behavior across sessions without requiring login
  • Implement rate limiting per device to prevent abuse
  • Provide consistent A/B testing experiences
  • Analyze usage patterns and user journeys
  • Differentiate between devices for security or UX purposes

Traditional solutions are either too complex (full user accounts), privacy-invasive (fingerprinting), or unreliable (IP addresses change, shared networks).

The Solution

FastAPI Device ID provides a privacy-friendly middle ground:

No personal data required - Just a unique identifier per browser
Persistent across sessions - Survives browser restarts
Secure by default - HTTP-only cookies with CSRF protection
Scalable - Works across multiple server instances
Compliant - Respects user privacy, no tracking across domains

When to Use

Perfect for applications that need:

  • Anonymous analytics without user accounts
  • Rate limiting by device rather than IP
  • Session continuity across page reloads
  • A/B testing with consistent user experiences
  • Fraud prevention through device recognition
  • Usage quotas for anonymous API access

When NOT to Use

Consider alternatives if you need:

  • Cross-domain tracking (this is single-domain only)
  • User authentication (use proper auth systems instead)
  • Long-term user identification (users can clear cookies)
  • Foolproof uniqueness (sophisticated users can bypass)

Features

  • 🔒 Secure by default: HTTP-only, secure, and SameSite cookies
  • 🆔 Modern UUID: Uses UUID7 (time-ordered) with UUID4 fallback for older Python versions
  • 📦 Zero dependencies: Only requires FastAPI/Starlette (which you already have)
  • ⚙️ Highly configurable: Customize cookie names, expiration, and security settings
  • 🪶 Lightweight: Minimal overhead with pluggable architecture
  • 🔧 Type safe: Full type hints with dependency injection support
  • 🔌 Pluggable: Custom ID generators and security strategies
  • 🛡️ Multiple security strategies: Plaintext, JWT, and AES encryption support
  • Performance optimized: Constant-time comparisons and efficient encoding
  • 🌐 Production ready: Distributed deployment support with consistent encryption

🚀 Quick Start

Installation

pip install fastapi-device-id

In production in 2 minutes:

  1. Add the middleware: app.add_middleware(DeviceMiddleware)
  2. Use the dependency: async def handler(device_id: DeviceId):
  3. Start tracking: analytics.track(device_id, event)

Basic Usage

from fastapi import FastAPI
from fastapi_device_id import DeviceMiddleware, DeviceId

app = FastAPI()

# Add the middleware
app.add_middleware(DeviceMiddleware)

@app.get("/")
async def read_root(device_id: DeviceId):
    # This device_id is automatically persistent across browser sessions
    # No database setup required!
    return {"message": f"Welcome back, device {device_id}"}

@app.get("/analytics")
async def track_visit(device_id: DeviceId):
    # Log analytics, track user behavior, etc.
    print(f"Device {device_id} visited /analytics")
    return {"status": "visit tracked"}

Custom Configuration

from fastapi import FastAPI
from fastapi_device_id import DeviceMiddleware

app = FastAPI()

# Customize the middleware
app.add_middleware(
    DeviceMiddleware,
    cookie_name="my_device_id",              # Custom cookie name
    cookie_max_age=30 * 24 * 60 * 60,        # 30 days instead of 1 year
    cookie_secure=False,                      # Allow HTTP in development
    cookie_samesite="strict",                 # Stricter cookie policy
)

Custom ID Generator

import secrets
from fastapi import FastAPI
from fastapi_device_id import DeviceMiddleware

def custom_id_generator() -> str:
    """Generate a custom device ID."""
    return f"device_{secrets.token_hex(16)}"

app = FastAPI()
app.add_middleware(
    DeviceMiddleware,
    id_generator=custom_id_generator,
)

Security Strategies

FastAPI Device ID supports multiple security strategies for device ID storage:

Plaintext Strategy (Default)

from fastapi_device_id import DeviceMiddleware, PlaintextStrategy

app.add_middleware(
    DeviceMiddleware,
    security_strategy=PlaintextStrategy(),
)

JWT Strategy

Provides tamper detection with cryptographic signatures:

from fastapi_device_id import DeviceMiddleware, JWTStrategy

# JWT with signature verification
app.add_middleware(
    DeviceMiddleware,
    security_strategy=JWTStrategy(
        secret="your-secret-key",
        algorithm="HS256",
        expiration_hours=24  # Optional expiration
    ),
)

Encrypted Strategy

Provides confidentiality - device ID is hidden from client:

from fastapi_device_id import DeviceMiddleware, EncryptedStrategy

# AES encryption with Fernet (recommended)
app.add_middleware(
    DeviceMiddleware,
    security_strategy=EncryptedStrategy(
        key="your-encryption-key",
        algorithm="fernet"  # or "aes-256-gcm", "aes-128-gcm"
    ),
)

# Production example with environment variable
import os
app.add_middleware(
    DeviceMiddleware,
    security_strategy=EncryptedStrategy(
        key=os.environ["DEVICE_ID_ENCRYPTION_KEY"],
        algorithm="fernet"
    ),
)

Security Strategy Comparison

Strategy Confidentiality Integrity Performance Use Case
Plaintext ⚡ Fastest Development, non-sensitive
JWT 🟨 Medium Tamper detection needed
Encrypted 🟨 Medium Production, sensitive data

API Reference

DeviceMiddleware

The main middleware class that handles device identification.

Parameters

  • cookie_name (str, default: "device_id"): Name of the cookie to store the device ID
  • cookie_max_age (int, default: 365 * 24 * 60 * 60): Cookie expiration time in seconds (1 year)
  • cookie_expires (str | None, default: None): Cookie expiration date string
  • cookie_path (str, default: "/"): Cookie path
  • cookie_domain (str | None, default: None): Cookie domain
  • cookie_secure (bool, default: True): Whether the cookie should only be sent over HTTPS
  • cookie_httponly (bool, default: True): Whether the cookie should be inaccessible to JavaScript
  • cookie_samesite (str, default: "lax"): SameSite policy ("strict", "lax", or "none")
  • id_generator (Callable[[], str], default: default_id_generator): Function to generate device IDs
  • security_strategy (DeviceIDSecurityStrategy, default: PlaintextStrategy()): Security strategy for encoding device IDs

DeviceId Type

A FastAPI dependency that extracts the device ID from the request:

from fastapi_device_id import DeviceId

@app.get("/my-endpoint")
async def my_handler(device_id: DeviceId):
    # device_id is automatically extracted and typed as str
    return {"device_id": device_id}

get_device_id Function

Direct function to extract device ID from a request:

from fastapi import Request
from fastapi_device_id import get_device_id

@app.get("/manual")
async def manual_extraction(request: Request):
    device_id = get_device_id(request)
    return {"device_id": device_id}

compare_device_ids Function

Secure constant-time comparison of device IDs to prevent timing attacks:

from fastapi_device_id import compare_device_ids

@app.get("/secure-compare")
async def secure_comparison(device_id: DeviceId, stored_id: str):
    # ❌ VULNERABLE to timing attacks
    # if device_id == stored_id:
    
    # ✅ SECURE constant-time comparison
    if compare_device_ids(device_id, stored_id):
        return {"access": "granted"}
    return {"access": "denied"}

Performance

  • Zero dependencies beyond FastAPI/Starlette
  • <1ms overhead per request
  • Memory efficient: No server-side storage required
  • Scales horizontally: Stateless design works across instances
  • Cookie-based: Works with CDNs and load balancers

Use Cases

Analytics and Tracking

@app.get("/track-page-view")
async def track_page_view(device_id: DeviceId, page: str):
    # Store page view with device ID
    analytics.track_page_view(device_id, page)
    return {"status": "tracked"}

A/B Testing

@app.get("/feature")
async def get_feature_flag(device_id: DeviceId):
    # Consistent A/B testing based on device ID
    variant = "A" if int(device_id.replace("-", ""), 16) % 2 else "B"
    return {"variant": variant}

Rate Limiting

from collections import defaultdict
from time import time

rate_limits = defaultdict(list)

@app.get("/api/data")
async def get_data(device_id: DeviceId):
    now = time()
    device_requests = rate_limits[device_id]
    
    # Clean old requests (1 hour window)
    device_requests[:] = [req_time for req_time in device_requests if now - req_time < 3600]
    
    if len(device_requests) >= 100:  # 100 requests per hour
        return {"error": "Rate limit exceeded"}
    
    device_requests.append(now)
    return {"data": "your data here"}

Common Patterns

Anonymous User Analytics

from fastapi_device_id import DeviceId
import logging

analytics_logger = logging.getLogger("analytics")

@app.get("/page/{page_name}")
async def track_page_view(page_name: str, device_id: DeviceId):
    analytics_logger.info(f"Device {device_id} viewed {page_name}")
    return {"content": f"Welcome to {page_name}"}

@app.post("/event")
async def track_custom_event(event: dict, device_id: DeviceId):
    event["device_id"] = device_id
    analytics_logger.info(f"Custom event: {event}")
    return {"status": "tracked"}

Shopping Cart Persistence

from typing import Dict, List
carts: Dict[str, List[dict]] = {}

@app.post("/cart/add")
async def add_to_cart(item: dict, device_id: DeviceId):
    if device_id not in carts:
        carts[device_id] = []
    carts[device_id].append(item)
    return {"cart_size": len(carts[device_id])}

@app.get("/cart")
async def get_cart(device_id: DeviceId):
    return {"items": carts.get(device_id, [])}

Feature Flag Management

import hashlib

@app.get("/feature/{feature_name}")
async def get_feature_flag(feature_name: str, device_id: DeviceId):
    # Consistent feature assignment based on device ID
    hash_input = f"{feature_name}:{device_id}"
    hash_value = int(hashlib.md5(hash_input.encode()).hexdigest()[:8], 16)
    
    enabled = hash_value % 100 < 50  # 50% rollout
    return {"feature": feature_name, "enabled": enabled}

Configuration Examples

Development Setup

# Relaxed settings for local development
app.add_middleware(
    DeviceMiddleware,
    cookie_secure=False,      # Allow HTTP
    cookie_samesite="lax",    # Relaxed policy
    security_strategy=PlaintextStrategy(),  # No encryption overhead
)

Production Setup

import os
from fastapi_device_id import EncryptedStrategy

# Strict security for production
app.add_middleware(
    DeviceMiddleware,
    cookie_secure=True,       # HTTPS only
    cookie_httponly=True,     # No JavaScript access
    cookie_samesite="strict", # Strict policy
    security_strategy=EncryptedStrategy(
        key=os.environ["DEVICE_ID_ENCRYPTION_KEY"],
        algorithm="fernet"
    ),
)

Short-lived Sessions with JWT

from fastapi_device_id import JWTStrategy

# JWT tokens that expire after 24 hours
app.add_middleware(
    DeviceMiddleware,
    security_strategy=JWTStrategy(
        secret="your-jwt-secret",
        expiration_hours=24
    ),
)

High-Security Setup

import os
from fastapi_device_id import EncryptedStrategy

# Maximum security configuration
app.add_middleware(
    DeviceMiddleware,
    cookie_secure=True,
    cookie_httponly=True,
    cookie_samesite="strict",
    cookie_max_age=7 * 24 * 60 * 60,  # 1 week
    security_strategy=EncryptedStrategy(
        key=os.environ["DEVICE_ID_ENCRYPTION_KEY"],
        algorithm="aes-256-gcm",  # Strong encryption
        iterations=200000,  # Higher security
    ),
)

Requirements

  • Python 3.8+
  • FastAPI 0.68.0+
  • Starlette 0.14.0+

Optional Dependencies

For JWT strategy support:

pip install "fastapi-device-id[jwt]"

For encryption strategy support:

pip install "fastapi-device-id[crypto]"

For development:

pip install "fastapi-device-id[dev]"

For all features:

pip install "fastapi-device-id[jwt,crypto,dev]"

Production Checklist

Before deploying to production, ensure:

  • Security Strategy: Use EncryptedStrategy or JWTStrategy instead of PlaintextStrategy
  • Environment Variables: Store encryption keys in os.environ, never in code
  • Cookie Security: Set cookie_secure=True for HTTPS-only cookies
  • SameSite Policy: Use cookie_samesite="strict" for maximum security
  • Key Rotation: Plan for periodic encryption key rotation
  • Monitoring: Log device ID operations for debugging
  • Privacy Policy: Update your privacy policy to mention device tracking
  • GDPR Compliance: Implement cookie consent if required in your jurisdiction

Example Production Configuration

import os
from fastapi_device_id import DeviceMiddleware, EncryptedStrategy

app.add_middleware(
    DeviceMiddleware,
    cookie_secure=True,
    cookie_httponly=True,
    cookie_samesite="strict",
    security_strategy=EncryptedStrategy(
        key=os.environ["DEVICE_ID_ENCRYPTION_KEY"],
        algorithm="fernet"
    ),
)

Contributing

We welcome contributions! Please see our Contributing Guide for details.

Development Setup

# Clone the repository
git clone https://github.com/ideatives/fastapi-device-id.git
cd fastapi-device-id

# Install development dependencies
pip install -e ".[dev]"

# Run tests
pytest

# Format code
black src tests
ruff --fix src tests

# Type checking
mypy src

License

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

Changelog

See CHANGELOG.md for version history.

Security

For security issues, please see our Security Policy.

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_device_id-0.1.0.tar.gz (28.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_device_id-0.1.0-py3-none-any.whl (13.5 kB view details)

Uploaded Python 3

File details

Details for the file fastapi_device_id-0.1.0.tar.gz.

File metadata

  • Download URL: fastapi_device_id-0.1.0.tar.gz
  • Upload date:
  • Size: 28.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.1

File hashes

Hashes for fastapi_device_id-0.1.0.tar.gz
Algorithm Hash digest
SHA256 cbf294b1071fab2dec8d19630ec6b2dacf01ff63ad397583fb549da6ea5beed2
MD5 d97602b1f9454798b396d874929b87ff
BLAKE2b-256 a3279618ad9ce64032beceb05c0e34bbc3a59092c629511b18e32a41097d8082

See more details on using hashes here.

File details

Details for the file fastapi_device_id-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for fastapi_device_id-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0b0ab0a439e9bcfd5af94fa09e2a0117487fa06865a533f4e6e45e5466180b58
MD5 b76e7c71094ff2e58160cfdbffc5b199
BLAKE2b-256 b0824a07db6314203d4db56e41722b87c3718cb0e40004f4c49e3e0d9d29e0d8

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