Skip to main content

Security Schemes and Scope Propagation

Security in this codebase is integrated directly into the dependency injection system. The core mechanism for managing security requirements and OAuth2 scopes is the Dependant model, which tracks how security schemes are applied and how scopes propagate through the dependency tree.

Identifying Security Schemes

The Dependant class (found in fastapi/dependencies/models.py) distinguishes between regular dependencies and security schemes. A dependency is identified as a security scheme if its underlying callable is an instance of SecurityBase.

The Dependant model uses several cached properties to manage this:

  • _is_security_scheme: Uses _unwrapped_call(self.call) to check if the dependency is an instance of SecurityBase.
  • _security_dependencies: A filtered list of sub-dependencies that are themselves security schemes.

This identification allows the system to treat security dependencies specially, particularly regarding how they contribute to the OpenAPI security requirements.

Scope Propagation and Merging

OAuth2 scopes are managed through a hierarchical propagation system. When a dependency is declared using Security(dependency, scopes=["..."]), those scopes are tracked within the Dependant object.

Own vs. Parent Scopes

The Dependant class maintains two distinct lists of scopes:

  1. own_oauth_scopes: Scopes explicitly defined on the current dependency via the Security parameter.
  2. parent_oauth_scopes: Scopes inherited from the parent dependency that required this one.

The effective scopes for a dependency are calculated by the oauth_scopes property, which merges these two lists while preserving order:

@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

The Propagation Process

The propagation happens during the construction of the dependency tree in get_dependant (in fastapi/dependencies/utils.py). When get_dependant encounters a sub-dependency, it calculates the current_scopes (parent + own) and passes them down to the child:

# In fastapi/dependencies/utils.py:get_dependant
current_scopes = (parent_oauth_scopes or []) + (own_oauth_scopes or [])
# ...
sub_dependant = get_dependant(
path=path,
call=param_details.depends.dependency,
name=param_name,
own_oauth_scopes=sub_own_oauth_scopes,
parent_oauth_scopes=current_scopes,
# ...
)

This ensures that a sub-dependency is aware of the entire chain of scopes required by its ancestors.

Accessing Scopes with SecurityScopes

Dependencies can access the accumulated list of required scopes by including a parameter of type SecurityScopes (from fastapi/security/oauth2.py).

The Dependant model identifies this parameter during analysis:

  • add_non_field_param_to_dependency checks if a parameter's type is a subclass of SecurityScopes.
  • If found, it sets dependant.security_scopes_param_name.

At runtime, FastAPI injects an instance of SecurityScopes containing the merged oauth_scopes. This is useful for dependencies that need to perform fine-grained authorization based on the scopes required by the specific route or parent dependency.

Example: Nested Scope Requirements

In tests/test_security_scopes_sub_dependency.py, the interaction between nested dependencies and scopes is demonstrated:

def get_current_user(
security_scopes: SecurityScopes,
db_session: Annotated[str, Depends(get_db_session)],
):
# security_scopes.scopes will contain ['me'] when called via get_user_me
# and ['items', 'me'] when called via a route requiring both
return {
"user": "user_1",
"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 read_items(
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}

Dependency Caching and Scopes

The dependency injection system caches results to avoid redundant executions. However, because a dependency's behavior might change based on the scopes it receives (via SecurityScopes), the cache key must be scope-aware.

The Dependant.cache_key property includes a sorted tuple of the oauth_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 "",
)

If the same dependency function is used multiple times in a single request but with different required scopes, it will be executed once for each unique set of scopes.

Flattening for OpenAPI

For OpenAPI generation, the hierarchical dependency tree is flattened using get_flat_dependant in fastapi/dependencies/utils.py. This utility recursively merges sub-dependencies and their scopes into a single Dependant structure. This flattened view allows the OpenAPI generator to correctly identify all security schemes and the union of all required scopes for a specific operation.