Authlib's OAuth methods for Starlette/FastAPI require the Request object as the first parameter. This is necessary because Authlib uses the request to access session data (for state storage), build redirect URLs, and extract query parameters.
Core Pattern:
from fastapi import FastAPI, Request
from fastapi.responses import RedirectResponse
from authlib.integrations.starlette_client import OAuth
from starlette.middleware.sessions import SessionMiddleware
import secrets
app = FastAPI()
app.add_middleware(
SessionMiddleware,
secret_key=secrets.token_hex(32)
)
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'}
)
# Pattern 1: authorize_redirect() - Requires Request
@app.get("/login")
async def login(request: Request): # ← Request parameter required
redirect_uri = request.url_for('auth_callback')
# ✅ CORRECT: Pass request as first argument
return await oauth.google.authorize_redirect(request, redirect_uri)
# ❌ WRONG: Missing request parameter
# return await oauth.google.authorize_redirect(redirect_uri)
# Error: authorize_redirect() missing required positional argument: 'request'
# Pattern 2: authorize_access_token() - Requires Request
@app.get("/auth/callback")
async def auth_callback(request: Request): # ← Request parameter required
# ✅ CORRECT: Pass request as first (and only) argument
token = await oauth.google.authorize_access_token(request)
# ❌ WRONG: No request parameter
# token = await oauth.google.authorize_access_token()
# Error: authorize_access_token() missing required positional argument: 'request'
user = token.get('userinfo')
request.session['user'] = user
return RedirectResponse(url='/dashboard')
Why Request is Required:
- Session Access: Authlib stores/retrieves OAuth state in
request.session - URL Building: Needs
request.url_for()andrequest.base_urlfor redirects - Query Parameters: Extracts
code,state,errorfromrequest.query_params - Headers: May access
request.headersfor additional context
What Authlib Does with Request:
# Inside authorize_redirect():
async def authorize_redirect(self, request, redirect_uri, **kwargs):
# 1. Generate and store state in session
state = generate_token()
request.session[f'_{self.name}_authlib_state_'] = state
# 2. Build authorization URL with state
url = self.create_authorization_url(
redirect_uri=redirect_uri,
state=state,
**kwargs
)
# 3. Return redirect response
return RedirectResponse(url=url)
# Inside authorize_access_token():
async def authorize_access_token(self, request, **kwargs):
# 1. Retrieve state from session
stored_state = request.session.get(f'_{self.name}_authlib_state_')
# 2. Get state and code from query parameters
params = dict(request.query_params)
request_state = params.get('state')
auth_code = params.get('code')
# 3. Verify state matches (CSRF protection)
if stored_state != request_state:
raise OAuthError('State mismatch')
# 4. Exchange code for token
token = await self.fetch_access_token(
code=auth_code,
redirect_uri=str(request.base_url) + 'auth/callback',
**kwargs
)
# 5. Clean up session
del request.session[f'_{self.name}_authlib_state_']
return token
Complete OAuth Flow with Request Handling:
from fastapi import FastAPI, Request, Depends
from fastapi.responses import RedirectResponse, HTMLResponse
from authlib.integrations.starlette_client import OAuth
from starlette.middleware.sessions import SessionMiddleware
import secrets
app = FastAPI()
app.add_middleware(
SessionMiddleware,
secret_key=secrets.token_hex(32)
)
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 1: Login page
@app.get("/login")
async def login_page():
return HTMLResponse("""
<html>
<body>
<h1>Login</h1>
<a href="/login/google">Login with Google</a>
</body>
</html>
""")
# Step 2: Initiate OAuth flow (requires Request)
@app.get("/login/google")
async def login_google(request: Request):
# Build callback URL using request.url_for()
redirect_uri = request.url_for('auth_google_callback')
# authorize_redirect() requires request for session access
return await oauth.google.authorize_redirect(request, redirect_uri)
# Step 3: OAuth callback (requires Request)
@app.get("/auth/google/callback")
async def auth_google_callback(request: Request):
# authorize_access_token() requires request to:
# - Get query params (code, state)
# - Validate state from session
# - Exchange code for token
token = await oauth.google.authorize_access_token(request)
user = token.get('userinfo')
# Store user in session (request.session)
request.session['user'] = {
'id': user['sub'],
'email': user['email'],
'name': user['name']
}
return RedirectResponse(url='/dashboard')
# Step 4: Protected route using session
@app.get("/dashboard")
async def dashboard(request: Request):
user = request.session.get('user')
if not user:
return RedirectResponse(url='/login')
return {
"message": f"Welcome {user['name']}",
"user": user
}
# Step 5: Logout
@app.get("/logout")
async def logout(request: Request):
request.session.clear()
return RedirectResponse(url='/')
Using Dependencies (Still Requires Request):
from fastapi import Depends
# Dependency to get current user from session
async def get_current_user(request: Request):
user = request.session.get('user')
if not user:
raise HTTPException(status_code=401, detail="Not authenticated")
return user
# Using dependency in route (Request still needed)
@app.get("/profile")
async def profile(
request: Request, # ← Still need Request for session access
user: dict = Depends(get_current_user)
):
return {
"user": user,
"session_id": request.session.get('_id', 'unknown')
}
API Methods Requiring Request:
# All these methods require Request as first parameter:
# 1. Start OAuth flow
await oauth.google.authorize_redirect(request, redirect_uri)
# 2. Complete OAuth flow
await oauth.google.authorize_access_token(request)
# 3. Parse ID token (if not auto-included)
await oauth.google.parse_id_token(request, token)
# 4. Fetch userinfo (if not in token)
await oauth.google.userinfo(request=request, token=token)
# Note: Some methods can work without request if you pass token explicitly:
await oauth.google.get('https://api.example.com/data', token=token_dict)
Common Mistakes:
# ❌ WRONG: Trying to use OAuth without Request
@app.get("/login")
async def login(): # Missing Request parameter
return await oauth.google.authorize_redirect("/auth/callback")
# Error: authorize_redirect() missing required argument: 'request'
# ❌ WRONG: Passing redirect_uri as first parameter
@app.get("/login")
async def login(request: Request):
return await oauth.google.authorize_redirect(
"/auth/callback", # ← WRONG position
request # ← WRONG position
)
# Error: 'str' object has no attribute 'session'
# ✅ CORRECT: Request first, then redirect_uri
@app.get("/login")
async def login(request: Request):
return await oauth.google.authorize_redirect(
request, # ← First parameter
"/auth/callback" # ← Second parameter
)
Type Hints for Clarity:
from starlette.requests import Request # More specific import
from authlib.integrations.starlette_client import StarletteOAuth2App
@app.get("/login")
async def login(request: Request) -> RedirectResponse:
redirect_uri: str = request.url_for('auth_callback')
return await oauth.google.authorize_redirect(request, redirect_uri)
@app.get("/auth/callback")
async def auth_callback(request: Request) -> RedirectResponse:
token: dict = await oauth.google.authorize_access_token(request)
user: dict = token.get('userinfo', {})
request.session['user'] = user
return RedirectResponse(url='/dashboard')
Version Note: Request parameter requirement consistent since Authlib 0.15.0 (Starlette integration)