Chapter 15 · Security Advanced: Scopes and API Keys
Chapter goals: Use OAuth2 scopes to implement fine-grained authorization ("logged in AND has the right permission"), and master three API Key schemes commonly used for service-to-service calls.
15.1 OAuth2 Scopes: The Minimal Unit of Permission
Chapter 14 solved "who are you"; this chapter solves "what can you do." OAuth2 defines permissions as space-separated string lists (scopes), e.g. me, items, users:read. GitHub, Google, and other third-party logins use exactly this mechanism. FastAPI integrates it seamlessly into OpenAPI docs: you can check the scopes to grant during login.
Declaring available scopes requires only adding scopes to OAuth2PasswordBearer:
from fastapi.security import OAuth2PasswordBearer
# key is scope name, value is description (shown in docs)
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="token",
scopes={"me": "Read current user info", "items": "Read item list"},
)15.2 Security() and SecurityScopes: Validating Permissions in the Dependency Tree
Security works identically to Depends, with the addition of a scopes parameter to declare "which permissions this dependency chain requires":
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, Security
from fastapi.security import OAuth2PasswordBearer, SecurityScopes
import jwt
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="token",
scopes={"me": "Read current user info", "items": "Read items"},
)
app = FastAPI()
async def get_current_user(
security_scopes: SecurityScopes, # Auto-collects all scopes declared by Security in the dependency tree
token: Annotated[str, Security(oauth2_scheme)],
):
if security_scopes.scopes:
authenticate_value = f'Bearer scope="{security_scopes.scope_str}"'
else:
authenticate_value = "Bearer"
credentials_exception = HTTPException(
status_code=401,
detail="Could not validate credentials",
headers={"WWW-Authenticate": authenticate_value},
)
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
token_scopes = payload.get("scopes", [])
user = get_user_from_db(payload.get("sub"))
if user is None:
raise credentials_exception
# Check each required scope against the token's scopes
for scope in security_scopes.scopes:
if scope not in token_scopes:
raise HTTPException(
status_code=403, # Note: insufficient permission is 403, not 401
detail="Not enough permissions",
headers={"WWW-Authenticate": authenticate_value},
)
return user
async def get_current_active_user(
current_user: Annotated[dict, Security(get_current_user, scopes=["me"])],
):
return current_user
@app.get("/users/me/items/")
async def read_own_items(
# Dependency tree accumulates requirements: me (from parent) + items (declared here)
current_user: Annotated[dict, Security(get_current_active_user, scopes=["items"])],
):
return {"item_id": "Foo", "owner": current_user["username"]}Three key points:
SecurityScopesparameter auto-aggregates all scopes declared bySecurity(..., scopes=[...])across the entire dependency tree — no manual passing needed;- Scope validation failure returns 403 (authenticated but unauthorized), distinguished from 401 (invalid token);
- When issuing the token, write the user's actual scopes into the payload:
create_access_token(data={"sub": username, "scopes": form_data.scopes}). In production, validate that the requested scopes are within the user's allowed range — do not blindly accept everything.
The login form OAuth2PasswordRequestForm has a built-in scopes attribute (the checkboxes from the docs Authorize popup).
15.3 API Key: Three Placement Options
Service-to-service calls (no "user login" concept) typically use API Keys. fastapi.security provides three classes that extract keys from different locations, all written into OpenAPI docs:
from fastapi import Security
from fastapi.security import APIKeyCookie, APIKeyHeader, APIKeyQuery
# Extract from query parameter ?api_key=xxx
api_key_query = APIKeyQuery(name="api_key", auto_error=False)
# Extract from header X-API-Key: xxx
api_key_header = APIKeyHeader(name="X-API-Key")
# Extract from cookie api_key=xxx
api_key_cookie = APIKeyCookie(name="api_key")15.4 Chapter Summary
- Scopes are space-separated permission strings; declare them in
OAuth2PasswordBearer(scopes={...}); Security(..., scopes=[...])validates permissions in the dependency tree; 403 for insufficient permission, 401 for invalid token;SecurityScopesauto-aggregates all required scopes across the dependency chain;- API Keys:
APIKeyHeader,APIKeyQuery,APIKeyCookie— choose the placement that fits your architecture.
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. What HTTP status code is returned when a user is authenticated but lacks the required scope?
2. What does SecurityScopes.auto-collect mean?
3. When issuing a JWT token, what should be included to support scope-based authorization?
4. Which API Key extraction method is most suitable for service-to-service calls?
🛠️ Hands-On Exercises
- Add scopes to the Chapter 14 login flow: issue a token with
scopes=["me", "items"]; create two endpoints — one requiringme, one requiringitems— and verify 403 when the scope is missing. - Implement an API Key header dependency (
X-API-Key); protect a/adminendpoint with it. - Combine scope validation and API key auth: a token must have both a valid scope AND a matching API key to access a sensitive endpoint.
Proceed to Chapter 16: Async Concurrency and Background Tasks.