Authentication Guide
This guide covers implementing authentication in Tasteful applications using the OIDC (OpenID Connect) authentication system. You’ll learn how to configure OIDC backends, create custom user models, and integrate authentication with your flavors.
Quick Start
Get authentication working in your Tasteful application in just a few steps:
from tasteful import TastefulApp
from tasteful.authn.oidc import OIDCAuthenticationBackend
# 1. Configure OIDC backend
auth_backend = OIDCAuthenticationBackend(
name="my_app",
metadata_url="https://your-provider.com/.well-known/openid_configuration",
client_id="your-client-id",
client_secret="your-client-secret",
scopes="openid profile email"
)
# 2. Create authenticated application
app = TastefulApp(
title="Secure App",
authentication_backends=Security(auth_backend),
flavors=[YourFlavor]
)
# 3. Access authenticated users in your flavors
class YourController(BaseController):
@Get("/profile")
async def get_profile(self, request: Request):
user = request.state.user # Automatically available
return {"name": user.name}
OIDC Authentication Backend
Basic Configuration
The OIDCAuthenticationBackend handles OpenID Connect authentication with automatic token validation:
from tasteful.authn.oidc import OIDCAuthenticationBackend
# Minimal configuration
oidc_backend = OIDCAuthenticationBackend(
name="company_sso",
metadata_url="https://auth.company.com/.well-known/openid_configuration",
client_id="tasteful-client",
client_secret="your-secret",
scopes="openid profile email"
)
Advanced Configuration
For more complex scenarios, you can customize the OIDC backend:
# Advanced OIDC configuration
oidc_backend = OIDCAuthenticationBackend(
name="advanced_oidc",
metadata_url="https://auth.company.com/.well-known/openid_configuration",
client_id="advanced-client",
client_secret="your-secret",
scopes="openid profile email groups roles",
introspection_endpoint="https://auth.company.com/oauth/introspect" # Optional custom endpoint
)
Environment-Based Configuration
Use environment variables for secure configuration:
import os
from tasteful.authn.oidc import OIDCAuthenticationBackend
oidc_backend = OIDCAuthenticationBackend(
name=os.getenv("OIDC_CLIENT_NAME"),
metadata_url=os.getenv("OIDC_METADATA_URL"),
client_id=os.getenv("OIDC_CLIENT_ID"),
client_secret=os.getenv("OIDC_CLIENT_SECRET"),
scopes=os.getenv("OIDC_SCOPES", "openid profile email")
)
Create a .env file:
OIDC_CLIENT_NAME=my_tasteful_app
OIDC_METADATA_URL=https://auth.company.com/.well-known/openid_configuration
OIDC_CLIENT_ID=your-client-id
OIDC_CLIENT_SECRET=your-client-secret
OIDC_SCOPES=openid profile email groups
User Models
BaseUser
All user models in Tasteful extend the BaseUser class:
from tasteful.authn.base import BaseUser
class BaseUser(BaseModel):
name: str
OIDCUser
The OIDCUser model includes OIDC token claims:
from tasteful.authn.oidc import OIDCUser
# OIDCUser automatically created by OIDCAuthenticationBackend
class OIDCUser(BaseUser):
user_info: dict # Contains all OIDC token claims
Access OIDC claims in your controllers:
from fastapi import Request
from tasteful.authn.oidc import OIDCUser
class UserController(BaseController):
@Get("/user-details")
async def get_user_details(self, request: Request):
user: OIDCUser = request.state.user
return {
"name": user.name,
"email": user.user_info.get("email"),
"groups": user.user_info.get("groups", []),
"roles": user.user_info.get("roles", []),
"all_claims": user.user_info
}
Custom User Models
Create custom user models for application-specific needs:
from tasteful.authn.base import BaseUser
from typing import Optional, List
from pydantic import Field
class CustomUser(BaseUser):
email: str
roles: List[str] = Field(default_factory=list)
department: Optional[str] = None
is_active: bool = True
permissions: List[str] = Field(default_factory=list)
@property
def is_admin(self) -> bool:
return "admin" in self.roles
def has_permission(self, permission: str) -> bool:
return permission in self.permissions
# Use custom user model with custom authentication backend
class CustomOIDCBackend(OIDCAuthenticationBackend):
async def _validate_token(self, token: str) -> CustomUser | None:
# Get standard OIDC validation
result = await super()._validate_token(token)
if result and result.user_info.get("active", False):
# Create custom user from OIDC claims
return CustomUser(
name=result.user_info.get("name"),
email=result.user_info.get("email"),
roles=result.user_info.get("roles", []),
department=result.user_info.get("department"),
permissions=result.user_info.get("permissions", [])
)
return None
Async Authentication Backend
For high-performance applications, use the async authentication pattern:
from tasteful.authn.base import AsyncAuthenticationBackend, BaseUser
from fastapi import Request
import httpx
class CustomAsyncBackend(AsyncAuthenticationBackend):
def __init__(self, api_url: str, api_key: str):
super().__init__()
self.api_url = api_url
self.api_key = api_key
async def authenticate(self, request: Request) -> BaseUser | None:
# Extract token from request
authorization = request.headers.get("Authorization")
if not authorization or not authorization.startswith("Bearer "):
return None
token = authorization.split(" ")[1]
# Async token validation
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.api_url}/validate",
headers={"Authorization": f"Bearer {self.api_key}"},
json={"token": token}
)
if response.status_code == 200:
user_data = response.json()
return BaseUser(name=user_data["username"])
return None
# Use custom async backend
custom_backend = CustomAsyncBackend(
api_url="https://auth-service.company.com",
api_key="service-api-key"
)
app = TastefulApp(
title="Async Auth App",
authentication_backends=[custom_backend],
flavors=[YourFlavor]
)
Integration with TastefulApp
Single Authentication Backend
Most applications use a single authentication method:
from tasteful import TastefulApp
from tasteful.authn.oidc import OIDCAuthenticationBackend
# Configure authentication
auth_backend = OIDCAuthenticationBackend(
name="main_auth",
metadata_url="https://auth.company.com/.well-known/openid_configuration",
client_id="tasteful-app",
client_secret="your-secret",
scopes="openid profile email"
)
# Create application
app = TastefulApp(
title="My Secure Application",
version="1.0.0",
authentication_backends=[auth_backend],
flavors=[
UserFlavor,
AdminFlavor,
APIFlavor
]
)
Multiple Authentication Backends
For applications with different authentication needs:
from tasteful.authn.oidc import OIDCAuthenticationBackend
from tasteful.authn.base import AuthenticationBackend
# User authentication via OIDC
user_auth = OIDCAuthenticationBackend(
name="user_sso",
metadata_url="https://auth.company.com/.well-known/openid_configuration",
client_id="user-client",
client_secret="user-secret",
scopes="openid profile email"
)
# Service authentication via API keys
class ServiceAuthBackend(AuthenticationBackend):
def authenticate(self, request):
api_key = request.headers.get("X-API-Key")
if api_key and self.validate_service_key(api_key):
return BaseUser(name="service_account")
return None
def validate_service_key(self, key: str) -> bool:
# Validate service API key
return key in ["service-key-1", "service-key-2"]
service_auth = ServiceAuthBackend()
# Application with multiple auth methods
app = TastefulApp(
title="Multi-Auth Application",
authentication_backends=[user_auth, service_auth],
flavors=[UserFlavor, ServiceFlavor]
)
Using Authentication in Flavors
Basic User Access
Access authenticated users in your flavor controllers:
from fastapi import Request
from tasteful.base_flavor import BaseController, BaseService
from tasteful.authn.base import BaseUser
class UserService(BaseService):
def get_user_profile(self, user: BaseUser) -> dict:
return {
"name": user.name,
"authenticated": True
}
class UserController(BaseController):
def __init__(self, service: UserService):
super().__init__(prefix="/users", tags=["users"])
self.service = service
@Get("/profile")
async def get_profile(self, request: Request):
# User automatically available from authentication backend
user: BaseUser = request.state.user
return self.service.get_user_profile(user)
@Get("/me")
async def get_current_user(self, request: Request):
user: BaseUser = request.state.user
return {"user_id": user.name}
OIDC Claims Access
When using OIDC authentication, access token claims:
from fastapi import Request, HTTPException
from tasteful.authn.oidc import OIDCUser
class AdminController(BaseController):
def __init__(self):
super().__init__(prefix="/admin", tags=["admin"])
@Get("/dashboard")
async def admin_dashboard(self, request: Request):
user: OIDCUser = request.state.user
# Check admin role from OIDC claims
roles = user.user_info.get("roles", [])
if "admin" not in roles:
raise HTTPException(status_code=403, detail="Admin access required")
return {
"message": "Welcome to admin dashboard",
"user": user.name,
"roles": roles
}
@Get("/users")
async def list_users(self, request: Request):
user: OIDCUser = request.state.user
# Check specific permissions
permissions = user.user_info.get("permissions", [])
if "users:read" not in permissions:
raise HTTPException(status_code=403, detail="Insufficient permissions")
return {"users": ["user1", "user2", "user3"]}
Optional Authentication
For endpoints that work with or without authentication:
from fastapi import Request
from typing import Optional
class PublicController(BaseController):
def __init__(self):
super().__init__(prefix="/public", tags=["public"])
@Get("/content")
async def get_content(self, request: Request):
# Check if user is authenticated (optional)
user: Optional[BaseUser] = getattr(request.state, 'user', None)
if user:
return {
"message": f"Hello {user.name}!",
"content": "Premium content for authenticated users"
}
else:
return {
"message": "Hello anonymous user!",
"content": "Basic content for everyone"
}
Error Handling
Authentication backends automatically handle common error scenarios:
Automatic Error Responses
- 401 Unauthorized: Missing or invalid token
- 500 Internal Server Error: Backend authentication errors
# These responses are automatic - no additional code needed
# Missing Authorization header
{
"detail": "Unauthorized"
}
# Invalid token
{
"detail": "Unauthorized"
}
# Backend error (network issues, invalid configuration, etc.)
{
"detail": "Internal Server Error"
}
Custom Error Handling
Add custom error handling in your authentication backend:
from fastapi import HTTPException
from tasteful.authn.base import AsyncAuthenticationBackend
class CustomErrorBackend(AsyncAuthenticationBackend):
async def authenticate(self, request: Request) -> BaseUser | None:
try:
# Your authentication logic
return await self.validate_user(request)
except TokenExpiredError:
raise HTTPException(
status_code=401,
detail="Token has expired"
)
except InvalidTokenError:
raise HTTPException(
status_code=401,
detail="Invalid token format"
)
except ServiceUnavailableError:
raise HTTPException(
status_code=503,
detail="Authentication service unavailable"
)
Testing Authentication
Testing with Mock Users
Create mock authentication for testing:
import pytest
from fastapi.testclient import TestClient
from tasteful.authn.base import BaseUser
class MockAuthBackend:
def __init__(self, mock_user: BaseUser = None):
self.mock_user = mock_user or BaseUser(name="test_user")
async def __call__(self, request):
request.state.user = self.mock_user
@pytest.fixture
def authenticated_client():
from your_app import create_app
# Create app with mock authentication
mock_auth = MockAuthBackend(BaseUser(name="test_user"))
app = create_app(authentication_backends=[mock_auth])
return TestClient(app.app)
def test_authenticated_endpoint(authenticated_client):
response = authenticated_client.get("/users/profile")
assert response.status_code == 200
assert response.json()["user_id"] == "test_user"
Integration Testing
Test with real OIDC tokens in integration tests:
import pytest
from fastapi.testclient import TestClient
@pytest.fixture
def oidc_token():
# Get real OIDC token for testing
# This would typically come from your test OIDC provider
return "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
def test_oidc_authentication(client, oidc_token):
headers = {"Authorization": f"Bearer {oidc_token}"}
response = client.get("/users/profile", headers=headers)
assert response.status_code == 200
Common Patterns
Authorization System Status: While authentication is fully implemented, formal authorization decorators and middleware are planned for future releases. The examples below show manual authorization patterns using OIDC token claims.
Manual Role-Based Access Control
Implement role checking using OIDC claims:
from functools import wraps
from fastapi import HTTPException, Request
# Current approach: Manual role checking in each endpoint
class SecureController(BaseController):
@Get("/admin-only")
async def admin_endpoint(self, request: Request):
user: OIDCUser = request.state.user
# Manual role check using OIDC claims
roles = user.user_info.get("roles", [])
if "admin" not in roles:
raise HTTPException(status_code=403, detail="Admin role required")
return {"message": "Admin access granted"}
# Future approach with decorators (planned, not yet implemented):
# from tasteful.authn.decorators import require_role
#
# class SecureController(BaseController):
# @Get("/admin-only")
# @require_role("admin") # Future feature
# async def admin_endpoint(self, request: Request):
# return {"message": "Admin access granted"}
Manual Permission-Based Access
Implement fine-grained permission checking:
# Current approach: Manual permission checking in each endpoint
class ResourceController(BaseController):
@Get("/sensitive-data")
async def get_sensitive_data(self, request: Request):
user: OIDCUser = request.state.user
# Manual permission check using OIDC claims
permissions = user.user_info.get("permissions", [])
if "data:read" not in permissions:
raise HTTPException(status_code=403, detail="Read permission required")
return {"data": "sensitive information"}
@Post("/sensitive-data")
async def create_sensitive_data(self, request: Request):
user: OIDCUser = request.state.user
# Manual permission check using OIDC claims
permissions = user.user_info.get("permissions", [])
if "data:write" not in permissions:
raise HTTPException(status_code=403, detail="Write permission required")
return {"message": "Data created"}
# Future approach with decorators (planned, not yet implemented):
# from tasteful.authn.decorators import require_permission
#
# class ResourceController(BaseController):
# @Get("/sensitive-data")
# @require_permission("data:read") # Future feature
# async def get_sensitive_data(self, request: Request):
# return {"data": "sensitive information"}
Troubleshooting
Common Issues
Token Validation Fails
- Verify OIDC metadata URL is accessible
- Check client ID and secret configuration
- Ensure token is properly formatted (Bearer token)
User Object Not Available
- Confirm authentication backend is configured in TastefulApp
- Check that endpoints are being called with valid Authorization header
- Verify OIDC provider is returning expected claims
Performance Issues
- Use AsyncAuthenticationBackend for high-throughput applications
- Consider token caching for frequently accessed endpoints
- Monitor OIDC provider response times
Debug Mode
Enable debug logging to troubleshoot authentication issues:
import logging
# Enable debug logging
logging.basicConfig(level=logging.DEBUG)
# Your OIDC backend will now log detailed information
oidc_backend = OIDCAuthenticationBackend(
name="debug_app",
metadata_url="https://auth.company.com/.well-known/openid_configuration",
client_id="debug-client",
client_secret="debug-secret",
scopes="openid profile email"
)
Adding Authentication to Existing Applications
Updating Your TastefulApp
If you have an existing Tasteful application without authentication, adding it is straightforward:
# Before: No authentication
from tasteful import TastefulApp
app = TastefulApp(
title="My Application",
flavors=[YourFlavor],
authentication_backends=[] # No authentication
)
# After: With OIDC authentication
from tasteful import TastefulApp
from tasteful.authn.oidc import OIDCAuthenticationBackend
# Configure authentication
auth_backend = OIDCAuthenticationBackend(
name="my_app_auth",
metadata_url="https://auth.company.com/.well-known/openid_configuration",
client_id="your-client-id",
client_secret="your-client-secret",
scopes="openid profile email"
)
app = TastefulApp(
title="My Secure Application",
flavors=[YourFlavor],
authentication_backends=[auth_backend] # Authentication enabled
)
Updating Your Flavors
Once authentication is enabled, update your flavor controllers to access authenticated users:
# Before: No user access
class MyController(BaseController):
@Get("/data")
async def get_data(self):
return {"data": "public data"}
# After: With authenticated user access
class MyController(BaseController):
@Get("/data")
async def get_data(self, request: Request):
user: BaseUser = request.state.user
return {
"data": "personalized data",
"user": user.name
}
Gradual Migration
For gradual migration, you can make authentication optional on some endpoints:
from typing import Optional
class MigrationController(BaseController):
@Get("/mixed-endpoint")
async def mixed_endpoint(self, request: Request):
# Check if user is authenticated (optional)
user: Optional[BaseUser] = getattr(request.state, 'user', None)
if user:
return {"message": f"Hello {user.name}!", "authenticated": True}
else:
return {"message": "Hello anonymous user!", "authenticated": False}
Next Steps