Starlette's SessionMiddleware is a middleware component that enables the request.session attribute in FastAPI applications. The middleware must be registered before request.session becomes available; they are not alternatives but rather prerequisite and interface.
Relationship:
- SessionMiddleware: The middleware that provides session functionality
- request.session: The API exposed by SessionMiddleware for accessing session data
- Requirement: SessionMiddleware must be added before routes can use
request.session
Setting Up SessionMiddleware:
from fastapi import FastAPI, Request
from starlette.middleware.sessions import SessionMiddleware
import secrets
app = FastAPI()
# Required: Add SessionMiddleware BEFORE defining routes
app.add_middleware(
SessionMiddleware,
secret_key=secrets.token_hex(32), # 64-char hex string (32 bytes)
session_cookie="session", # Cookie name (default: "session")
max_age=1800, # Session duration in seconds (30 min)
same_site="lax", # "strict", "lax", or "none"
https_only=True, # Production: True, Dev: False
path="/" # Cookie path scope
)
OAuth Flow with SessionMiddleware:
from authlib.integrations.starlette_client import OAuth
from fastapi import FastAPI, Request
from fastapi.responses import RedirectResponse
from starlette.middleware.sessions import SessionMiddleware
import secrets
app = FastAPI()
# Step 1: Add SessionMiddleware (required for OAuth state storage)
app.add_middleware(
SessionMiddleware,
secret_key=secrets.token_hex(32)
)
# Step 2: Configure OAuth
oauth = OAuth()
oauth.register(
name='google',
client_id='YOUR_CLIENT_ID',
client_secret='YOUR_CLIENT_SECRET',
server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
client_kwargs={'scope': 'openid email profile'}
)
# Step 3: Login endpoint - stores OAuth state in session
@app.get("/login")
async def login(request: Request):
redirect_uri = request.url_for('auth_callback')
# Authlib automatically stores OAuth state in request.session
# State includes CSRF token for security
return await oauth.google.authorize_redirect(request, redirect_uri)
# Behind the scenes: request.session['_google_authlib_state_'] = state
# Step 4: Callback endpoint - retrieves OAuth state from session
@app.get("/auth/callback")
async def auth_callback(request: Request):
try:
# Authlib verifies state from request.session
# Raises error if state mismatch (CSRF protection)
token = await oauth.google.authorize_access_token(request)
user = token.get('userinfo')
# Store user in session for subsequent requests
request.session['user_id'] = user['sub']
request.session['email'] = user['email']
request.session['name'] = user['name']
return RedirectResponse(url='/dashboard')
except Exception as e:
# State mismatch, token exchange failure, etc.
return RedirectResponse(url='/login?error=auth_failed')
# Step 5: Protected endpoint - reads user from session
@app.get("/dashboard")
async def dashboard(request: Request):
user_id = request.session.get('user_id')
if not user_id:
return RedirectResponse(url='/login')
return {
"user_id": user_id,
"email": request.session.get('email'),
"name": request.session.get('name')
}
# Step 6: Logout - clears session
@app.get("/logout")
async def logout(request: Request):
request.session.clear() # Remove all session data
return RedirectResponse(url='/')
Session Backend Options:
1. Cookie-Based (Default):
from starlette.middleware.sessions import SessionMiddleware
# Session data stored in encrypted cookie
app.add_middleware(
SessionMiddleware,
secret_key=secrets.token_hex(32)
)
# Pros: No server-side storage needed
# Cons: 4KB size limit, sent with every request
2. Server-Side with Redis:
from starlette_session import SessionMiddleware
from starlette_session.backends import BackendType
app.add_middleware(
SessionMiddleware,
secret_key=secrets.token_hex(32),
backend_type=BackendType.redis,
backend_client=redis.Redis(host='localhost', port=6379)
)
# Pros: No size limit, faster (no cookie decryption), can invalidate server-side
# Cons: Requires Redis infrastructure
How request.session Works:
# request.session is a dict-like interface
# Set values
request.session['key'] = 'value'
request.session['user_id'] = 123
request.session['cart'] = ['item1', 'item2']
# Get values
value = request.session.get('key') # Returns None if missing
user_id = request.session.get('user_id', default=0) # With default
# Check existence
if 'user_id' in request.session:
print("User logged in")
# Delete keys
del request.session['key']
# Clear all
request.session.clear()
# Update multiple
request.session.update({'key1': 'val1', 'key2': 'val2'})
OAuth State Storage (Internal):
Authlib uses session to store OAuth state automatically:
# When authorize_redirect() is called:
request.session['_google_authlib_state_'] = {
'state': 'random-csrf-token',
'redirect_uri': 'https://yourapp.com/auth/callback'
}
# When authorize_access_token() is called:
stored_state = request.session.get('_google_authlib_state_')
request_state = request.query_params.get('state')
if stored_state['state'] != request_state:
raise OAuthError("State mismatch - possible CSRF attack")
# Clean up after successful auth
del request.session['_google_authlib_state_']
Security Considerations:
# 1. Use strong secret key (minimum 32 bytes)
import secrets
secret_key = secrets.token_hex(32) # DO NOT hardcode in production
# 2. Enable HTTPS-only in production
app.add_middleware(
SessionMiddleware,
secret_key=secret_key,
https_only=True, # Prevents cookie theft over HTTP
same_site="lax" # CSRF protection
)
# 3. Set appropriate session timeout
app.add_middleware(
SessionMiddleware,
secret_key=secret_key,
max_age=1800 # 30 minutes (not days/weeks)
)
# 4. Regenerate session ID after login
@app.post("/login")
async def login(request: Request):
# Clear old session
old_data = dict(request.session)
request.session.clear()
# Authenticate user...
# Create new session with new ID
request.session['user_id'] = user_id
# Don't copy old session data blindly
Error Without SessionMiddleware:
app = FastAPI()
# ❌ SessionMiddleware NOT added
@app.get("/login")
async def login(request: Request):
request.session['user'] = 'data' # AttributeError!
# Error: 'Request' object has no attribute 'session'
Version Note: SessionMiddleware available since Starlette 0.12.0, FastAPI 0.38.0+. OAuth state storage pattern standard in Authlib 0.15.0+