How to inspect every request (including request body) with fastapi? - python

I would like to create a middleware that authorizes every request based on its url, headers and body (OPA). To do that I created a custom request and custom route class. It is the advised way if one wants to manipulate the request body. Firstly, I tried to override the .json() method and do the authorization there, however, not every request handler calls the .json() method, so that is not a possible solution. Then I decided to create a custom method on the request that will do the authorization and call it from a custom middleware. The issue is that the middleware receives a plain request, which does not have the .is_authorized method, rather than the subclassed request.
Minimal example:
import json
from typing import Callable, Any
from fastapi import FastAPI, Request, Response
from fastapi.routing import APIRoute
from starlette.middleware import Middleware
from starlette.middleware.base import BaseHTTPMiddleware
from fastapi.responses import JSONResponse
from pydantic import BaseModel
def some_authz_func(request: Request, body: Any) -> bool:
print(vars(request), body)
return True
class MessageModel(BaseModel):
message: str
class AuthzMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
await request.is_authorized()
response = await call_next(request)
return response
class AuthzRequest(Request):
async def is_authorized(self) -> bool:
try:
json_ = await super().json()
except json.decoder.JSONDecodeError:
json_ = None
return some_authz_func(self, json_)
class AuthzRoute(APIRoute):
def get_route_handler(self) -> Callable:
original_route_handler = super().get_route_handler()
async def custom_route_handler(request: Request) -> Response:
request = AuthzRequest(request.scope, request.receive)
return await original_route_handler(request)
return custom_route_handler
app = FastAPI(route_class=AuthzRoute, middleware=[Middleware(AuthzMiddleware)])
#app.get('/')
async def no_body():
return JSONResponse(content={'no-body': True})
#app.post('/')
async def some_body(body: MessageModel):
print(body)
return JSONResponse(content={'body': True})
Raised exception
File ".virtualenvs/tempenv-416e126962124/lib/python3.8/site-packages/starlette/middleware/errors.py", line 162, in __call__
await self.app(scope, receive, _send)
File ".virtualenvs/tempenv-416e126962124/lib/python3.8/site-packages/starlette/middleware/base.py", line 68, in __call__
response = await self.dispatch_func(request, call_next)
File "main.py", line 23, in dispatch
await request.is_authorized()
AttributeError: 'Request' object has no attribute 'is_authorized'
Dependencies
$ pip freeze
anyio==3.6.1
click==8.1.3
fastapi==0.79.0
h11==0.13.0
idna==3.3
pydantic==1.9.1
sniffio==1.2.0
starlette==0.19.1
typing_extensions==4.3.0
uvicorn==0.18.2

It can be solved by using dependency injection and applying it to the app object (Thanks #MatsLindh). The dependency function can take a Request object and get the ulr, headers and body from it.
from fastapi import FastAPI, Request, Depends
async def some_authz_func(request: Request):
try:
json_ = await request.json()
except json.decoder.JSONDecodeError:
json_ = None
print(vars(request), json_)
app = FastAPI(dependencies=[Depends(some_authz_func)])
Whole working example
import json
from fastapi import FastAPI, Request, Depends
from fastapi.responses import JSONResponse
from pydantic import BaseModel
async def some_authz_func(request: Request):
try:
json_ = await request.json()
except json.decoder.JSONDecodeError:
json_ = None
print(vars(request), json_)
app = FastAPI(dependencies=[Depends(some_authz_func)])
class MessageModel(BaseModel):
message: str
#app.get('/')
async def no_body():
return JSONResponse(content={'no-body': True})
#app.post('/')
async def some_body(body: MessageModel):
print(body)
return JSONResponse(content={'body': True})

Related

How to get the cookies from an HTTP request using FastAPI?

Is it possible to get the cookies when someone hits the API? I need to read the cookies for each request.
#app.get("/")
async def root(text: str, sessionKey: str = Header(None)):
print(sessionKey)
return {"message": text+" returned"}
if __name__ == "__main__":
uvicorn.run("main:app", host="0.0.0.0", port=5001 ,reload=True)
You can do it in the same way you are accessing the headers in your example (see docs):
from fastapi import Cookie
#app.get("/")
async def root(text: str, sessionKey: str = Header(None), cookie_param: int | None = Cookie(None)):
print(cookie_param)
return {"message": f"{text} returned"}
Option 1
Use the Request object to get the cookie you wish, as described in Starlette documentation.
from fastapi import Request
#app.get('/')
async def root(request: Request):
print(request.cookies.get('sessionKey'))
return 'OK'
Option 2
Use the Cookie parameter, as described in FastAPI documentation.
from fastapi import Cookie
#app.get('/')
async def root(sessionKey: str = Cookie(None)):
print(sessionKey)
return 'OK'

Fast API with pytest using AsyncClient gives 422 on post?

I'm trying to send a request to an api using pytest through httpx.AsynClient
#pytest.mark.anyio
async def test_device_create_with_data(self, client, random_uuid):
device_create = DeviceCreateFactory.build(subId=random_uuid)
json = device_create.json(by_alias=True)
response = await client.post("/device", json=json)
assert response.status_code == 200
Client fixture:
from httpx import AsyncClient
#pytest.fixture(scope="session")
async def client():
async with AsyncClient(
app=app,
base_url="http://test/api/pc",
headers={"Content-Type": "application/json"}
) as client:
yield client
API endpoint:
#device_router.post("/device", response_model=CommonResponse)
async def create_device(device: DeviceCreate):
_, err = await crud_device.create_device(device)
if err:
return get_common_response(400, err)
return get_common_response(200, "ok")
Schemas:
class DeviceBase(BaseModel):
device_id: StrictStr = Field(..., alias='deviceId')
device_name: StrictStr = Field(..., alias='deviceName')
device_type: StrictStr = Field(..., alias='deviceType')
age_mode: AgeModeType = Field(..., alias='ageMode')
class Config:
allow_population_by_field_name = True
validate_all = True
validate_assignment = True
class DeviceCreate(DeviceBase):
sub_id: StrictStr = Field(..., alias='subId')
class Config:
orm_mode = True
Factory:
from pydantic_factories import ModelFactory
from app.core.schemas.device import DeviceCreate
class DeviceCreateFactory(ModelFactory):
__model__ = DeviceCreate
And i'm getting a 422 error with following response content:
"message":"bad request","details":{"deviceId":"field required","deviceName":"field required","deviceType":"field required","ageMode":"field required","subId":"field required"}
Then i examined the data of the request being sent and got:
b'"{\\"deviceId\\": \\"de\\", \\"deviceName\\": \\"\\", \\"deviceType\\": \\"\\", \\"ageMode\\": \\"child\\", \\"subId\\": \\"11aded61-9966-4be1-a781-387f75346811\\"}"'
Seems like everything is okay, but where is the trouble then?
I've tried to examine the request data in exception handler of 422
I've made:
#app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
print(await request.json())
response = validation_error_response(exc)
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content=jsonable_encoder(response.dict())
)
But code after print is unreachable, because await request.json() never ends and runs forever trying to print a request json
Is there a way to manage this problem?
Thanks for any suggest!
P.S.
python version: 3.8.9
fastapi version: 0.68.1
httpx version: 0.21.1
You're double encoding your content as JSON - you're both asking for it to be returned as a JSON string, and then telling your request method to encode it as JSON a second time. json= as an argument to the method on the client converts the given data to JSON - it does not expect already serialized JSON.
You can see this in your request string because it starts with " and not with { as you'd expect:
b'"{\
^
Instead build your model around a dictionary - or as I'd prefer in a test - build the request by hand, so that you're testing how you imagine an actual request should look like.
You can use dict in the same was as you'd use json for a Pydantic model:
device_create = DeviceCreateFactory.build(subId=random_uuid)
response = await client.post("/device", json=device_create.dict(by_alias=True))
assert response.status_code == 200

Is it possible to have an api call another api, having them both in same application?

I have a python application running on my localhost:3978. Is it possible to make an api call to http://localhost:3978/api/users from http://localhost:3978/api/accounts?
#routes.get("/api/accounts")
async def accounts(request):
api_url = "http://127.0.0.1:3978/api/users"
response = requests.get(api_url)
return json_response(data=respone.json())
#routes.get("/api/users")
async def users(request):
pass
You can use url_path_for() to feed in RedirectResponse of starlette.responses as stated here. For example:
from fastapi import FastAPI
from starlette.responses import RedirectResponse
app = FastAPI()
#app.get("/a")
async def a():
return {"message": "Hello World"}
#app.get('/b')
async def b():
url = app.url_path_for("a")
response = RedirectResponse(url=url)
return response

FastAPI - Supporting multiple authentication dependencies

Problem
I currently have JWT dependency named jwt which makes sure it passes JWT authentication stage before hitting the endpoint like this:
sample_endpoint.py:
from fastapi import APIRouter, Depends, Request
from JWTBearer import JWTBearer
from jwt import jwks
router = APIRouter()
jwt = JWTBearer(jwks)
#router.get("/test_jwt", dependencies=[Depends(jwt)])
async def test_endpoint(request: Request):
return True
Below is the JWT dependency which authenticate users using JWT (source: https://medium.com/datadriveninvestor/jwt-authentication-with-fastapi-and-aws-cognito-1333f7f2729e):
JWTBearer.py
from typing import Dict, Optional, List
from fastapi import HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import jwt, jwk, JWTError
from jose.utils import base64url_decode
from pydantic import BaseModel
from starlette.requests import Request
from starlette.status import HTTP_403_FORBIDDEN
JWK = Dict[str, str]
class JWKS(BaseModel):
keys: List[JWK]
class JWTAuthorizationCredentials(BaseModel):
jwt_token: str
header: Dict[str, str]
claims: Dict[str, str]
signature: str
message: str
class JWTBearer(HTTPBearer):
def __init__(self, jwks: JWKS, auto_error: bool = True):
super().__init__(auto_error=auto_error)
self.kid_to_jwk = {jwk["kid"]: jwk for jwk in jwks.keys}
def verify_jwk_token(self, jwt_credentials: JWTAuthorizationCredentials) -> bool:
try:
public_key = self.kid_to_jwk[jwt_credentials.header["kid"]]
except KeyError:
raise HTTPException(
status_code=HTTP_403_FORBIDDEN, detail="JWK public key not found"
)
key = jwk.construct(public_key)
decoded_signature = base64url_decode(jwt_credentials.signature.encode())
return key.verify(jwt_credentials.message.encode(), decoded_signature)
async def __call__(self, request: Request) -> Optional[JWTAuthorizationCredentials]:
credentials: HTTPAuthorizationCredentials = await super().__call__(request)
if credentials:
if not credentials.scheme == "Bearer":
raise HTTPException(
status_code=HTTP_403_FORBIDDEN, detail="Wrong authentication method"
)
jwt_token = credentials.credentials
message, signature = jwt_token.rsplit(".", 1)
try:
jwt_credentials = JWTAuthorizationCredentials(
jwt_token=jwt_token,
header=jwt.get_unverified_header(jwt_token),
claims=jwt.get_unverified_claims(jwt_token),
signature=signature,
message=message,
)
except JWTError:
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="JWK invalid")
if not self.verify_jwk_token(jwt_credentials):
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="JWK invalid")
return jwt_credentials
jwt.py:
import os
import requests
from dotenv import load_dotenv
from fastapi import Depends, HTTPException
from starlette.status import HTTP_403_FORBIDDEN
from app.JWTBearer import JWKS, JWTBearer, JWTAuthorizationCredentials
load_dotenv() # Automatically load environment variables from a '.env' file.
jwks = JWKS.parse_obj(
requests.get(
f"https://cognito-idp.{os.environ.get('COGNITO_REGION')}.amazonaws.com/"
f"{os.environ.get('COGNITO_POOL_ID')}/.well-known/jwks.json"
).json()
)
jwt = JWTBearer(jwks)
async def get_current_user(
credentials: JWTAuthorizationCredentials = Depends(auth)
) -> str:
try:
return credentials.claims["username"]
except KeyError:
HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Username missing")
api_key_dependency.py (very simplified right now, it will be changed):
from fastapi import Security, FastAPI, HTTPException
from fastapi.security.api_key import APIKeyHeader
from starlette.status import HTTP_403_FORBIDDEN
async def get_api_key(
api_key_header: str = Security(api_key_header)
):
API_KEY = ... getting API KEY logic ...
if api_key_header == API_KEY:
return True
else:
raise HTTPException(
status_code=HTTP_403_FORBIDDEN, detail="Could not validate credentials"
)
Question
Depending on the situation, I would like to first check if it has API Key in the header, and if its present, use that to authenticate. Otherwise, I would like to use jwt dependency for authentication. I want to make sure that if either api-key authentication or jwt authentication passes, the user is authenticated. Would this be possible in FastAPI (i.e. having multiple dependencies and if one of them passes, authentication passed). Thank you!
Sorry, got lost with things to do
The endpoint has a unique dependency, call it check from the file check_auth
ENDPOINT
from fastapi import APIRouter, Depends, Request
from check_auth import check
from JWTBearer import JWTBearer
from jwt import jwks
router = APIRouter()
jwt = JWTBearer(jwks)
#router.get("/test_jwt", dependencies=[Depends(check)])
async def test_endpoint(request: Request):
return True
The function check will depend on two separate dependencies, one for api-key and one for JWT. If both or one of these passes, the authentication passes. Otherwise, we raise exception as shown below.
DEPENDENCY
def key_auth(api_key=Header(None)):
if not api_key:
return None
... verification logic goes here ...
def jwt(authorization=Header(None)):
if not authorization:
return None
... verification logic goes here ...
async def check(key_result=Depends(jwt_auth), jwt_result=Depends(key_auth)):
if not (key_result or jwt_result):
raise Exception
This worked for me (JWT or APIkey Auth). If both or one of the authentication method passes, the authentication passes.
def jwt_auth(auth: HTTPAuthorizationCredentials = Depends(HTTPBearer(auto_error=False))):
if not auth:
return None
## validation logic
return True
def key_auth(apikey_header=Depends(APIKeyHeader(name='X-API-Key', auto_error=False))):
if not apikey_header:
return None
## validation logic
return True
async def jwt_or_key_auth(jwt_result=Depends(jwt_auth), key_result=Depends(key_auth)):
if not (key_result or jwt_result):
raise HTTPException(status_code=401, detail="Not authenticated")
#app.get("/", dependencies=[Depends(jwt_or_key_auth)])
async def root():
return {"message": "Hello World"}

Aiohttp authentication middleware

I want to attach a middleware to specific handler and if client is not authorized then want to return an error response. However with the following code :
async def middleware_factory(app, handler):
async def auth_handler(request):
if request.headers.get('Authorization') == 'Basic test1234':
return await handler(request)
return web.Response(text='Forbidden', status='403')
return auth_handler
I am getting an exception that :
AssertionError: Handler <function AbstractRoute.__init__.
<locals>.handler_wrapper at 0x10da56bf8> should return response
instance, got <class 'NoneType'> [middlewares [<function
middleware_factory at 0x1104cb268>]]
Documentation states that I should return a response object which I am doing. Still this error. Where am I going wrong?
You can look to an example from official documentation.
But the main concern that if you want to have Middleware Factory - needs to be a function not a coroutine. Also, recommend to use #web.middleware decorator for that.
from aiohttp import web
def middleware_factory(text):
#web.middleware
async def sample_middleware(request, handler):
resp = await handler(request)
resp.text = resp.text + text
return resp
return sample_middleware

Categories

Resources