Skip to main content

OAuth2 Scopes and Permissions

OAuth2 scopes allow for fine-grained access control by defining specific permissions that a client must have to access certain resources. In this codebase, scopes are managed through the Security function and the SecurityScopes class, which together enable complex permission trees and automatic documentation in OpenAPI.

Declaring Required Scopes

To enforce scopes on a path operation or a dependency, use the Security function from fastapi/param_functions.py. Security works similarly to Depends, but it accepts an additional scopes parameter—a list of strings representing the required permissions.

In tests/test_security_scopes.py, scopes are declared directly in the path operation:

from typing import Annotated
from fastapi import FastAPI, Security

app = FastAPI()

def get_user(db: Annotated[str, Depends(get_db)]):
return "user"

@app.get("/")
def endpoint(
user: Annotated[str, Security(get_user, scopes=["read"])],
):
return {"user": user}

When a dependency is declared using Security(dependency, scopes=["..."]), FastAPI tracks these scopes throughout the dependency resolution process.

Inspecting Scopes with SecurityScopes

The SecurityScopes class (defined in fastapi/security/oauth2.py) is a special dependency type that allows a security dependency to "look up" which scopes are currently required by the path operation or other dependencies in the chain.

It provides two main attributes:

  • scopes: A list[str] containing all required scopes.
  • scope_str: A space-separated str of all scopes, following the OAuth2 specification.

A common pattern is to use SecurityScopes in a central authentication dependency to verify the token's permissions against the required scopes:

from fastapi.security import SecurityScopes

def get_current_user(
security_scopes: SecurityScopes,
token: Annotated[str, Depends(oauth2_scheme)],
):
# security_scopes.scopes contains the list of required scopes
for scope in security_scopes.scopes:
if scope not in token.scopes:
raise HTTPException(status_code=401, detail="Not enough permissions")
return user

Scope Aggregation and Dependency Trees

One of the most powerful features of SecurityScopes is its ability to aggregate scopes from "dependants"—the functions that depend on the current dependency.

If a path operation depends on Dependency A (requiring scope items) and Dependency A depends on Dependency B (requiring scope me), Dependency B will receive both ["items", "me"] when it requests SecurityScopes.

This behavior is demonstrated in tests/test_security_scopes_sub_dependency.py:

def get_current_user(security_scopes: SecurityScopes):
return {"scopes": security_scopes.scopes}

def get_user_me(
current_user: Annotated[dict, Security(get_current_user, scopes=["me"])],
):
return current_user

@app.get("/")
def path_operation(
user_me: Annotated[dict, Depends(get_user_me)],
user_items: Annotated[dict, Security(get_user_items, scopes=["items"])],
):
return {"user_me": user_me, "user_items": user_items}

In this tree:

  1. path_operation requires items.
  2. get_user_me requires me.
  3. Both eventually call get_current_user.

When get_current_user is called via get_user_me, its SecurityScopes.scopes will be ["me"]. When it is called via user_items (which depends on get_user_me), the aggregated scopes will be ["items", "me"].

Dependency Caching and Scopes

FastAPI normally caches dependency results within a single request. However, when using Security with different scopes, the cache key includes the required scopes.

As seen in tests/test_security_scopes_sub_dependency.py, if the same dependency is reached multiple times with different scope requirements, FastAPI will re-run the dependency for each unique set of scopes. This ensures that security checks are performed correctly for every permission level required by the request.

OpenAPI Integration

Scopes declared via Security or within security schemes like OAuth2PasswordBearer are automatically included in the generated OpenAPI schema.

The OAuth2PasswordBearer class in fastapi/security/oauth2.py accepts a scopes dictionary during initialization to define the available scopes for the entire security scheme:

oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="token",
scopes={"read": "Read items", "write": "Write items"}
)

These definitions allow Swagger UI to display checkboxes for scopes during the authentication flow, providing a standard interface for testing permission-restricted endpoints.