Write super simple decorators to shield your FastAPI endpoints.
Source Code: https://github.com/jymchng/fastapi-shield
A powerful, intuitive, and flexible authentication and authorization library for FastAPI applications. Stack your shields to create robust and customizable layers which effectively shields your endpoints from unwanted requests.
- Decorator-based Security: Apply shields as simple decorators to protect your endpoints
- Layered Protection: Stack multiple shields for fine-grained access control
- Clean Design Pattern: Shields provide a clear and intuitive metaphor for API protection
- Fully Integrated: Works seamlessly with FastAPI's dependency injection system
- Type Safety: Full type hint support for better IDE integration and code quality
- Customizable Responses: Configure error responses when access is denied
- ShieldedDepends: Special dependency mechanism for protected resources
- Lazy-Loading of Dependencies: Dependencies are only loaded from FastAPI after the request passes through all the decorated shields
With pip:
pip install fastapi-shieldWith uv:
uv add fastapi-shieldWith poetry:
poetry add fastapi-shieldLet's create a simple @auth_shield to shield against unauthenticated requests! π‘οΈ
from fastapi import Header
from fastapi_shield import shield
# Create a simple authentication shield
@shield
def auth_shield(api_token: str = Header()):
"""
A basic shield that validates an API token.
Returns the token if valid, otherwise returns None which blocks the request.
"""
if api_token in ("admin_token", "user_token"):
return api_token
return NoneNow that you've created your first shield, you can easily apply it to any FastAPI endpoint! π
The shield acts as a decorator that protects your endpoint from unauthorized access.
When a request comes in, the shield evaluates the API token before the endpoint function is called.
If the token is invalid (returning None), the request is blocked! π«
This creates a clean separation between authentication logic and business logic, making your code more maintainable.
Just like a real shield, it stands in front of your endpoint to protect it! πͺ
Now let's see how our shield works in the wild! π When a user tries to access the protected endpoint, the shield jumps into action like a superhero! π¦ΈββοΈ
from fastapi import FastAPI
app = FastAPI()
# Protected endpoint - requires authentication
@app.get("/protected/{name}")
@auth_shield # apply `@auth_shield`
async def protected_endpoint(name: str):
return {
"message": f"Hello {name}. This endpoint is protected!",
}from fastapi.testclient import TestClient
client = TestClient(app)
def test_protected():
client = TestClient(app)
response = client.get("/protected/John", headers={"API-TOKEN": "valid_token"})
assert response.status_code == 200
assert response.json() == {"message": "Hello John. This endpoint is protected!"}
def test_protected_unauthorized():
client = TestClient(app)
response = client.get("/protected/John", headers={"API-TOKEN": "invalid_token"})
assert response.status_code == 500
assert response.json() == {'detail': 'Shield with name `unknown` blocks the request'}, response.json()From the above, we can see how the shield works in practice. The auth_shield decorator is applied to our endpoint, checking the API token in the request headers before allowing access to the protected endpoint. When a valid token is provided, the request proceeds normally and returns a friendly greeting. However, when an invalid token is sent, the shield blocks the request, returning a 500 error with a message indicating that the shield has prevented access. This demonstrates the power of shields as a clean, declarative way to implement authentication in FastAPI applications without cluttering your endpoint logic with authorization checks.
First, let's see what's the final endpoint is going to look like:
@app.get("/products")
@auth_shield
@roles_shield(["user"])
async def get_all_products(db: Dict[str, Any]=Depends(get_db), username: str=ShieldedDepends(get_username_from_payload)):
"""Only user with role `user` can get their own product"""
products = list(map(lambda name: db["products"][name], db["users"][username]["products"]))
return {
"message": f"These are your products: {products}",
}We're going to make the @roles_shield(["user"]).
But before that, there's one point to note: one of the advantages of fastapi-shield is that it enables lazy injection of FastAPI's dependencies.
In the signature of the endpoint: async def get_all_products(db: Dict[str, Any]=Depends(get_db), username: str=ShieldedDepends(get_username_from_payload)), the db: Dict[str, Any]=Depends(get_db) is only injected after @roles_shield(["user"]) becomes 'unblocked', i.e. allowing the request to reach the endpoint get_all_products.
Prior to that, if the request is blocked by any of the decoratored shields, e.g. @auth_shield and @roles_shield(["user"]), then the FastAPI's dependencies are not injected. We will discuss this in more detail later, on why this is an advantage compared to other decorators-based libraries.
Let's see how the @roles_shield(["user"]) is written.
# Create a simple roles shield
def roles_shield(roles: list[str]):
"""
A shield that validates a list of roles.
"""
@shield
def wrapper(payload = ShieldedDepends(get_payload_from_token)):
if any(role in payload["roles"] for role in roles):
return payload
return None
return wrapperThe roles_shield function is a shield factory that creates a shield for role-based access control. It takes a list of roles as input and returns a shield function.
The inner wrapper function of roles_shield uses ShieldedDepends to get the payload from the token, which contains user information including their roles.
The shield then checks if any of the user's roles match the required roles using the any() function with a generator expression.
If there's a match, it returns the payload, allowing the request to proceed. If no matching role is found, it returns None, which blocks the request.
This pattern demonstrates how shields can be composed and parameterized to create flexible authorization rules.
ShieldedDepends is a mechanism to pass information from a Shield to a dependency to retrieve it.
It is a specialized dependency injection class that extends FastAPI's Security class. ShieldedDepends is used to inject dependencies that should only be resolved if a shield allows the request to proceed. It takes a callable function as its shielded_dependency parameter.
The shielded_dependency function:
- Can be synchronous or asynchronous (coroutine)
- Should accept parameters that can be resolved by FastAPI's dependency injection system
- Will only be executed when the shield is "unblocked" (when the shield function returns a truthy value)
- Can access request information and other dependencies just like regular FastAPI dependencies
One powerful feature of ShieldedDepends is its ability to pass data between shields in a decorator chain, from top decorator to the lower ones.
When a shield returns a value (other than None), that value can be captured and used by subsequent shields, via the ShieldedDepends dependencies specified in the function signature of the shield, or by the endpoint function itself via the same mechanism.
When a shield is blocked, the ShieldedDepends instance returns itself instead of executing the dependency function, preventing unnecessary computation and database access. This lazy evaluation is one of the key advantages of the fastapi-shield library.
For example, in our get_payload_from_token function (which is an argument to be passed into ShieldedDepends function; seen on the line def wrapper(payload = ShieldedDepends(get_payload_from_token)): ...):
def get_payload_from_token(token: str):
if token == "admin_token":
return {"username": "Peter", "roles": ["admin", "user"]}
elif token == "user_token":
return {"username": "John", "roles": ["user"]}
return NoneThe token parameter will be passed by the guard function of the shield decorating it.
@shield
def auth_shield(api_token: str = Header()):
"""
A basic shield that validates an API token.
Returns the token if valid, otherwise returns None which blocks the request.
"""
if api_token in ("admin_token", "user_token"):
return api_token
return NoneThe guard function of auth_shield returns the api_token which is then passed into get_payload_from_token shielded dependency function of the roles_shield.
@app.get("/products")
@auth_shield
@roles_shield(["user"])
async def get_all_products(db: Dict[str, Any]=Depends(get_db), username: str=ShieldedDepends(get_username_from_payload)):
"""Only user with role `user` can get their own product"""
products = list(map(lambda name: db["products"][name], db["users"][username]["products"]))
return {
"message": f"These are your products: {products}",
}The ShieldedDepends ensures that the payload is only retrieved if previous shields in the chain (like auth_shield) have already passed.
Check out the complete product catalog API example in the examples/app directory, which demonstrates:
- Authentication with token-based shields
- Role-based access control
- Protecting user information
- Admin-only operations for products
- Testing protected endpoints with TestClient
# Shield for requiring specific roles
def roles_required(roles: List[str]):
"""
Role-based authorization shield that checks if the authenticated user
has any of the required roles.
"""
@shield
def role_shield(token: str = ShieldedDepends(lambda t: t)):
token_data = get_token_data(token)
user_roles = token_data.get("roles", [])
# Check if user has any of the required roles
if any(role in user_roles for role in roles):
return token_data
# No matching roles, return None to block the request
return None
return role_shield
# Shortcut shields for common role checks
admin_required = roles_required(["admin"])
user_required = roles_required(["user", "admin"])Visit our documentation for more details:
- Getting Started: Installation, basic usage, and core concepts
- Shields Guide: Understanding the Shield pattern
- Authentication: Token-based, OAuth, and custom authentication shields
- Authorization: Role-based access control and permission shields
- Advanced Usage: Complex security scenarios and custom shield creation, e.g. rate limiting shield
- Examples: Complete application examples
FastAPI Shield uses a layered decorator pattern to apply security checks:
- Define Shields: Create functions decorated with
@shieldthat blocks or passes requests after evaluating the guard function of the shield - Stack Shields: Apply multiple shields to endpoints in the desired order
- Access Protected Resources: Use
ShieldedDependsto access data from successful shields and retrieve dependencies - Handle Failures: Customize error responses when shield validation fails
Each shield acts as an independent layer of security that can:
- Allow the request to continue when it passes validation (returns a value)
- Block the request when validation fails (returns
None) - Pass state to dependent shields (via
ShieldedDepends)
- Python 3.9 or higher
- FastAPI 0.100.1 or higher
pip install uv
uv sync --devgit clone https://github.com/jimchng/fastapi-shield.git
cd fastapi-shield
pip install uv
uv sync --dev# Install `uv`
pip install uv
# Install `nox`
uv add --dev nox
# OR
uv tool install nox
# Run all tests
uv run python -m nox -s test
# OR
uv tool run nox -s test
# Run specific test suite
nox -s test -- tests/test_basics.pyRun the following commands:
nox -s release-check- this checks if the library is fit for release.nox -s release- this builds the library, update the tag and commits it.git push origin main- this pushes the repository to GitHub on branchmain.git push origin <tag-name>- this pushes the tag with<tag-name>to GitHub.nox -s publish- this publishes the library to PYPI.
We welcome contributions! Please see our Contributing Guide for details.
This project is licensed under the MIT License - see the LICENSE file for details.
Special thanks to all contributors who have helped shape this project.
