FastAPI middleware for Stytch B2B authentication with Redis caching
Project description
ayz-auth
FastAPI middleware for Stytch B2B authentication with Redis caching.
Overview
ayz-auth is a lightweight, production-ready authentication middleware for FastAPI applications using Stytch B2B authentication services. It provides session token verification with Redis caching for optimal performance and includes comprehensive error handling and logging.
Features
- 🔐 Stytch B2B Integration: Seamless integration with Stytch B2B authentication
- ⚡ Redis Caching: Intelligent caching to reduce API calls and improve performance
- 🚀 FastAPI Native: Built specifically for FastAPI with proper dependency injection
- 📝 Type Safe: Full Pydantic models with type hints throughout
- 🛡️ Security First: Secure token handling with configurable logging levels
- 🔧 Configurable: Environment-based configuration with sensible defaults
- 📊 Comprehensive Logging: Structured logging with sensitive data protection
- 🧪 Well Tested: Comprehensive test suite with mocking support
Installation
pip install ayz-auth
Or with UV:
uv add ayz-auth
Quick Start
1. Environment Configuration
Create a .env file or set environment variables:
STYTCH_PROJECT_ID=your_project_id
STYTCH_SECRET=your_secret_key
STYTCH_ENVIRONMENT=test # or "live" for production
REDIS_URL=redis://localhost:6379
2. Basic Usage
from fastapi import FastAPI, Depends
from ayz_auth import verify_auth, StytchContext
app = FastAPI()
@app.get("/protected")
async def protected_route(user: StytchContext = Depends(verify_auth)):
return {
"message": f"Hello {user.member_email}",
"member_id": user.member_id,
"organization_id": user.organization_id
}
@app.get("/user-info")
async def get_user_info(user: StytchContext = Depends(verify_auth)):
return {
"member_id": user.member_id,
"email": user.member_email,
"name": user.member_name,
"organization_id": user.organization_id,
"session_expires_at": user.session_expires_at,
"authentication_factors": user.authentication_factors
}
3. Optional Authentication
For endpoints that work with or without authentication:
from typing import Optional
from ayz_auth import verify_auth_optional
@app.get("/optional-auth")
async def optional_route(user: Optional[StytchContext] = Depends(verify_auth_optional)):
if user:
return {"message": f"Hello {user.member_email}"}
else:
return {"message": "Hello anonymous user"}
4. Custom Authentication Requirements
Create custom dependencies with additional requirements:
from ayz_auth import create_auth_dependency
# Require specific custom claims
admin_auth = create_auth_dependency(required_claims=["admin"])
moderator_auth = create_auth_dependency(required_claims=["moderator", "verified"])
# Require specific authentication factors
mfa_auth = create_auth_dependency(required_factors=["mfa"])
@app.get("/admin")
async def admin_route(user: StytchContext = Depends(admin_auth)):
return {"message": "Admin access granted"}
@app.get("/sensitive")
async def sensitive_route(user: StytchContext = Depends(mfa_auth)):
return {"message": "MFA verified access"}
Configuration
All configuration is handled through environment variables with the STYTCH_ prefix:
| Variable | Default | Description |
|---|---|---|
STYTCH_PROJECT_ID |
required | Your Stytch project ID |
STYTCH_SECRET |
required | Your Stytch secret key |
STYTCH_ENVIRONMENT |
test |
Stytch environment (test or live) |
STYTCH_REDIS_URL |
redis://localhost:6379 |
Redis connection URL |
STYTCH_REDIS_PASSWORD |
None |
Redis password (if required) |
STYTCH_REDIS_DB |
0 |
Redis database number |
STYTCH_CACHE_TTL |
300 |
Cache TTL in seconds (5 minutes) |
STYTCH_CACHE_PREFIX |
ayz_auth |
Redis key prefix |
STYTCH_LOG_LEVEL |
INFO |
Logging level |
STYTCH_LOG_SENSITIVE_DATA |
False |
Log sensitive data (never in production) |
STYTCH_REQUEST_TIMEOUT |
10 |
Request timeout in seconds |
STYTCH_MAX_RETRIES |
3 |
Maximum retry attempts |
StytchContext Model
The StytchContext model contains all the essential session data from Stytch:
class StytchContext(BaseModel):
# Core identifiers
member_id: str
session_id: str
organization_id: str
# Session timing
session_started_at: datetime
session_expires_at: datetime
session_last_accessed_at: datetime
authenticated_at: datetime
# Member information
member_email: Optional[str]
member_name: Optional[str]
# Session metadata
session_custom_claims: Dict[str, Any]
authentication_factors: List[str]
raw_session_data: Dict[str, Any]
# Utility properties
@property
def is_expired(self) -> bool: ...
@property
def time_until_expiry(self) -> Optional[float]: ...
Error Handling
The middleware provides structured error responses:
# 401 Unauthorized - Missing or invalid token
{
"error": "authentication_failed",
"message": "Authorization header is required",
"type": "token_extraction"
}
# 401 Unauthorized - Token verification failed
{
"error": "authentication_failed",
"message": "Invalid or expired session token",
"type": "token_verification"
}
# 503 Service Unavailable - Stytch API issues
{
"error": "service_unavailable",
"message": "Authentication service temporarily unavailable",
"type": "stytch_api"
}
# 403 Forbidden - Insufficient permissions (custom auth)
{
"error": "insufficient_permissions",
"message": "Missing required claims: ['admin']",
"type": "authorization"
}
# 403 Forbidden - Insufficient authentication factors (custom auth)
{
"error": "insufficient_authentication",
"message": "Missing required authentication factors: ['mfa']",
"type": "authorization"
}
Caching Strategy
The middleware implements a two-tier verification system:
- Redis Cache Check: Fast lookup of previously verified tokens
- Stytch API Fallback: Fresh verification when cache misses
Cache entries automatically expire based on the session expiration time, ensuring security while maximizing performance.
Integration with Your User System
Since the middleware only returns Stytch session data, you can easily integrate it with your existing user system:
from your_app.models import User
from your_app.database import get_user_by_stytch_member_id
@app.get("/profile")
async def get_profile(stytch: StytchContext = Depends(verify_auth)):
# Use the member_id to fetch your user data
user = await get_user_by_stytch_member_id(stytch.member_id)
if not user:
raise HTTPException(404, "User not found")
# Check permissions using your user model
if "read_profile" not in user.permissions:
raise HTTPException(403, "Insufficient permissions")
return {
"stytch_data": stytch.to_dict(),
"user_data": user.to_dict()
}
Development
Running Tests
# Install development dependencies
uv sync --dev
# Run tests
pytest
# Run tests with coverage
pytest --cov=ayz_auth
Code Quality
# Format code
black src/ tests/
isort src/ tests/
# Lint code
ruff check src/ tests/
# Type checking
mypy src/
Contributing
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
License
MIT License - see LICENSE file for details.
Support
For issues and questions:
- GitHub Issues: https://github.com/brandsoulmates/ayz-auth/issues
- Documentation: https://github.com/brandsoulmates/ayz-auth
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 ayz_auth-0.2.5.tar.gz.
File metadata
- Download URL: ayz_auth-0.2.5.tar.gz
- Upload date:
- Size: 30.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3ffe439dfb21c7b4da1199433e3e87d9e901f07d91e6ed499ebf7c0a0852b0ce
|
|
| MD5 |
7f415ee63bf2c1871677666f1092f4e4
|
|
| BLAKE2b-256 |
c9ec0b355d46d8e6d03ad4c9f91787ecb2d2e0c78600d234f589c9a5ae522c7f
|
Provenance
The following attestation bundles were made for ayz_auth-0.2.5.tar.gz:
Publisher:
ci-cd.yml on brandsoulmates/ayz-auth
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ayz_auth-0.2.5.tar.gz -
Subject digest:
3ffe439dfb21c7b4da1199433e3e87d9e901f07d91e6ed499ebf7c0a0852b0ce - Sigstore transparency entry: 251758083
- Sigstore integration time:
-
Permalink:
brandsoulmates/ayz-auth@e8dd4610ce88dc4207fb026c8e3f0ed3edba46fc -
Branch / Tag:
refs/heads/main - Owner: https://github.com/brandsoulmates
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci-cd.yml@e8dd4610ce88dc4207fb026c8e3f0ed3edba46fc -
Trigger Event:
push
-
Statement type:
File details
Details for the file ayz_auth-0.2.5-py3-none-any.whl.
File metadata
- Download URL: ayz_auth-0.2.5-py3-none-any.whl
- Upload date:
- Size: 19.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
496065f849f15dabf0ea60889d44c27e31e333d28e309b84ea747e930c0d3e25
|
|
| MD5 |
4ddfd02cefee61a3901c9d4d81d5d74e
|
|
| BLAKE2b-256 |
d80a6b869c6eb7780e0cd8bc7bf697f629ba78b52b33f174c1ff90d843f34705
|
Provenance
The following attestation bundles were made for ayz_auth-0.2.5-py3-none-any.whl:
Publisher:
ci-cd.yml on brandsoulmates/ayz-auth
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ayz_auth-0.2.5-py3-none-any.whl -
Subject digest:
496065f849f15dabf0ea60889d44c27e31e333d28e309b84ea747e930c0d3e25 - Sigstore transparency entry: 251758089
- Sigstore integration time:
-
Permalink:
brandsoulmates/ayz-auth@e8dd4610ce88dc4207fb026c8e3f0ed3edba46fc -
Branch / Tag:
refs/heads/main - Owner: https://github.com/brandsoulmates
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci-cd.yml@e8dd4610ce88dc4207fb026c8e3f0ed3edba46fc -
Trigger Event:
push
-
Statement type: