Skip to content

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:

python
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":

python
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:

  • SecurityScopes parameter auto-aggregates all scopes declared by Security(..., 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:

python
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;
  • SecurityScopes auto-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 ​

  1. Add scopes to the Chapter 14 login flow: issue a token with scopes=["me", "items"]; create two endpoints — one requiring me, one requiring items — and verify 403 when the scope is missing.
  2. Implement an API Key header dependency (X-API-Key); protect a /admin endpoint with it.
  3. 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.