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 ofSecurityBase._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:
own_oauth_scopes: Scopes explicitly defined on the current dependency via theSecurityparameter.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_dependencychecks if a parameter's type is a subclass ofSecurityScopes.- 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.