I want to add an auth_required decorator to my endpoints.
(Please consider that this question is about decorators, not middleware)
So a simple decorator looks like this:
def auth_required(func):
def wrapper(*args, **kwargs):
if user_ctx.get() is None:
raise HTTPException(...)
return func(*args, **kwargs)
return wrapper
So there are 2 usages:
@auth_required
@router.post(...)
or
@router.post(...)
@auth_required
The first way doesn't work because router.post creates a router that saved into self.routes of APIRouter object. The second way doesn't work because it fails to verify pydantic object. For any request model, it says missing args, missing kwargs.
So my question is - how can I add any decorators to FastAPI endpoints? Should I get into router.routes and modify the existing endpoint? Or use some functools.wraps like functions?
6 Answers 6
How can I add any decorators to FastAPI endpoints?
As you said, you need to use @functools.wraps(...)--(PyDoc) decorator as,
from functools import wraps
from fastapi import FastAPI
from pydantic import BaseModel
class SampleModel(BaseModel):
name: str
age: int
app = FastAPI()
def auth_required(func):
@wraps(func)
async def wrapper(*args, **kwargs):
return await func(*args, **kwargs)
return wrapper
@app.post("/")
@auth_required # Custom decorator
async def root(payload: SampleModel):
return {"message": "Hello World", "payload": payload}
The main caveat of this method is that you can't access the request object in the wrapper and I assume it is your primary intention.
If you need to access the request, you must add the argument to the router function as,
from fastapi import Request
@app.post("/")
@auth_required # Custom decorator
async def root(request: Request, payload: SampleModel):
return {"message": "Hello World", "payload": payload}
I am not sure what's wrong with the FastAPI middleware, after all, the @app.middleware(...) is also a decorator.
7 Comments
@app.middleware(...) you mean that can work as decorators also? any example or tutorial of this?@auth_required is independent of the request method.async def. Above @auth_required decorator will only work with functions declared with async def. If you don't use async/await in endpoint functions, just drop it from def wrapper(*args, **kwargs) definition.Simply use the dependencies inside of the path operation decorator:
from fastapi import Depends, FastAPI, Header, HTTPException
app = FastAPI()
async def verify_token(x_token: str = Header()):
if x_token != "fake-super-secret-token":
raise HTTPException(status_code=400, detail="X-Token header invalid")
async def verify_key(x_key: str = Header()):
if x_key != "fake-super-secret-key":
raise HTTPException(status_code=400, detail="X-Key header invalid")
return x_key
@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)])
async def read_items():
return [{"item": "Foo"}, {"item": "Bar"}]
1 Comment
Here is how you can use a decorator that adds extra parameters to the route handler:
from fastapi import FastAPI, Request
from pydantic import BaseModel
class SampleModel(BaseModel):
name: str
age: int
app = FastAPI()
def do_something_with_request_object(request: Request):
print(request)
def auth_required(handler):
async def wrapper(request: Request, *args, **kwargs):
do_something_with_request_object(request)
return await handler(*args, **kwargs)
# Fix signature of wrapper
import inspect
wrapper.__signature__ = inspect.Signature(
parameters = [
# Use all parameters from handler
*inspect.signature(handler).parameters.values(),
# Skip *args and **kwargs from wrapper parameters:
*filter(
lambda p: p.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD),
inspect.signature(wrapper).parameters.values()
)
],
return_annotation = inspect.signature(handler).return_annotation,
)
return wrapper
@app.post("/")
@auth_required # Custom decorator
async def root(payload: SampleModel):
return {"message": f"Hello {payload.name}, {payload.age} years old!"}
9 Comments
TypeError: wrapper() missing 1 required positional argument: 'request'TypeError: wrapper() missing 1 required positional argument: 'request'In addtion to JPG's answer, you can access the Request object inside your decorator with kwargs.get('request'). A full decorator would look something like:
def render_template(template):
"""decorator to render a template with a context"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# access request object
request = kwargs.get('request')
context = func(*args, **kwargs)
if context is None:
context = {}
return templates.TemplateResponse(template, {**context, 'request': request})
return wrapper
return decorator
The decorated function will need to take the Request as a parameter, however.
Comments
The accepted answer won't work if your decorator needs to access FastAPI dependencies (e.g. request info, auth schemes, database sessions, ...).
If you need your endpoint decorators to use FastAPI dependencies, you can use the @depends() decorator from fastapi-decorators to achieve it:
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def authorize(scope: str):
@depends
def decorator(token: str = Depends(oauth2_scheme)):
jwt = jwt_decode(token)
if scope in not jwt.scopes:
raise HTTPException(status_code=403, detail="Unauthorized")
return decorator
@app.put("/users/{user_id}")
@authorize("users:write")
def update_user(*, user_id: int, user_update: UserUpdate):
...
Comments
The proposed solution can depending on the situation lead to problems. In my case, the decorator was not an "auth_required" but a "use_cached" answer one and I was using a nonlocal var. As a result I was getting:
RuntimeError: cannot reuse already awaited coroutine
This is happening because as the code is proposed, it uses async / await in two locations / functions. If you get this error, to avoid it, you need to get rid of the extra async / await.
This has the disadvantage of breaking the aesthetics and that adding your decorator slightly changes the following function (you drop async). It has the advantage that the code produces no errors and is correct however...
TL;DR:
def auth_required(func):
@wraps(func)
async def wrapper(*args, **kwargs):
return func(*args, **kwargs) #DO NOT WAIT
return wrapper
@app.post("/")
@auth_required # Custom decorator
def root(payload: SampleModel): #NOT ASYNC
return {"message": "Hello World", "payload": payload}
Comments
Explore related questions
See similar questions with these tags.
user: User = Depends(auth_function)to the path or function. That gets called before your endpoint function, similar to how a decorator wraps it. It should also have access to the req-resp context.