Skip to main content

Security and OAuth2 Scopes

When you build an API with OAuth2, you often need to restrict access to specific endpoints based on "scopes" (permissions). For example, a user might have a token that allows them to read items but not delete them. FastAPI provides the Security class and the SecurityScopes utility to manage these fine-grained requirements throughout your dependency tree.

Declaring Security Requirements

To enforce specific scopes on a path operation or a dependency, use the Security class from fastapi.params. It is a subclass of Depends that adds a scopes parameter.

from typing import Annotated
from fastapi import FastAPI, Security
from fastapi.security import OAuth2PasswordBearer

app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

@app.get("/items/")
async def read_items(token: Annotated[str, Security(oauth2_scheme, scopes=["items:read"])]):
return {"token": token}

In this example, the read_items operation requires the items:read scope. FastAPI uses this information to:

  1. Populate the OpenAPI documentation with the required scopes for this endpoint.
  2. Make the required scopes available to the dependency itself for validation.

Accessing Required Scopes with SecurityScopes

While Security declares what is required, the SecurityScopes class (from fastapi.security) allows your dependency functions to see exactly which scopes were requested in the current execution context.

You can inject SecurityScopes as a parameter in your dependency. FastAPI will automatically provide an instance containing the aggregated list of scopes required by the current dependency and any "parent" dependencies that called it.

from fastapi import HTTPException, Security, status
from fastapi.security import SecurityScopes

async def get_current_user(security_scopes: SecurityScopes, token: str = Security(oauth2_scheme)):
# security_scopes.scopes contains the list of required scopes
# e.g., ["items:read"]
if security_scopes.scopes:
authenticate_value = f'Bearer scope="{security_scopes.scope_str}"'
else:
authenticate_value = "Bearer"

# ... logic to decode token and verify scopes ...
user_scopes = ["items:read"] # This would come from your token
for scope in security_scopes.scopes:
if scope not in user_scopes:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not enough permissions",
headers={"WWW-Authenticate": authenticate_value},
)
return {"user": "johndoe"}

Internal Scope Management

FastAPI manages these requirements using the Dependant class in fastapi.dependencies.models. When you use Security(call, scopes=[...]), FastAPI creates a Dependant object where:

  • own_oauth_scopes: Stores the scopes defined directly in that Security call.
  • parent_oauth_scopes: Stores scopes inherited from the dependencies that depend on this one.
  • oauth_scopes: A cached property that merges both lists to provide the full set of required permissions.

The Dependant.oauth_scopes implementation ensures that scopes are aggregated down the tree:

@cached_property
def oauth_scopes(self) -> list[str]:
scopes = self.parent_oauth_scopes.copy() if self.parent_oauth_scopes else []
for scope in self.own_oauth_scopes or []:
if scope not in scopes:
scopes.append(scope)
return scopes

Dependency Caching and Scopes

FastAPI normally caches dependency results within a single request to avoid redundant database lookups or computations. However, when using Security with different scopes, the same dependency function might need to behave differently (e.g., checking for "read" vs "write" permissions).

To handle this, the Dependant.cache_key includes the required scopes. If you call the same dependency function twice in the same request but with different scopes, FastAPI will treat them as distinct dependencies and execute the function for each unique set of scopes.

@cached_property
def cache_key(self) -> DependencyCacheKey:
scopes_for_cache = (
tuple(sorted(set(self.oauth_scopes or []))) if self._uses_scopes else ()
)
return (
self.call,
scopes_for_cache,
self.computed_scope or "",
)

Scope Inheritance in Nested Dependencies

Scopes are additive. If a path operation requires scope A and it depends on a function that requires scope B, the inner dependency will receive both A and B in its SecurityScopes parameter.

Consider this structure from tests/test_security_scopes_sub_dependency.py:

def get_current_user(security_scopes: SecurityScopes, token: str = Depends(oauth2_scheme)):
# If called via get_user_items, security_scopes.scopes is ["items", "me"]
return {"user": "admin", "scopes": security_scopes.scopes}

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

@app.get("/items/")
def get_user_items(user_me: Annotated[dict, Security(get_user_me, scopes=["items"])]):
return user_me
  1. get_user_items requires the items scope and calls get_user_me.
  2. get_user_me requires the me scope and calls get_current_user.
  3. get_current_user receives both items and me because they are aggregated by the Dependant model as it traverses the tree.

This allows you to build deeply nested security requirements where the "leaf" dependencies (like the one actually validating the token) have full visibility into the entire chain of required permissions.