Subclass fastapi.params.Security to extend JWT payload checks for route authorization(?)
See original GitHub issueFirst check
- I added a very descriptive title to this issue.
- I used the GitHub search to find a similar issue and didn’t find it.
- I searched the FastAPI documentation, with the integrated search.
- I already searched in Google “How to X in FastAPI” and didn’t find any information.
- I already read and followed all the tutorial in the docs and didn’t find an answer.
- I already checked if it is not related to FastAPI but to Pydantic.
- I already checked if it is not related to FastAPI but to Swagger UI.
- I already checked if it is not related to FastAPI but to ReDoc.
- After submitting this, I commit to one of:
- Read open issues with questions until I find 2 issues where I can help someone and add a comment to help there.
- I already hit the “watch” button in this repository to receive notifications and I commit to help at least 2 people that ask questions in the future.
- Implement a Pull Request for a confirmed bug.
Example
Here’s a self-contained, minimal, reproducible, example with my use case:
import logging
from dataclasses import dataclass, field
from enum import Enum, unique
from typing import (
Any,
Callable,
List,
Mapping,
Optional,
Sequence,
Type,
)
import requests
from fastapi import (
APIRouter,
Depends,
FastAPI,
HTTPException,
status,
)
from fastapi.params import Security
from fastapi.security.oauth2 import OAuth2AuthorizationCodeBearer
from jose import jwt
from jose.constants import ALGORITHMS
from pydantic import BaseModel, BaseSettings
logger = logging.getLogger(__name__)
class Config(BaseSettings):
AUTH0_DOMAIN: str
AUTH0_AUDIENCE: str
class Config:
env_file = ".env"
case_sensitive = True
validate_all = True
allow_mutation = False
config = Config()
@dataclass
class JwtPayload:
iss: str
sub: str
aud: List[str]
iat: int
exp: int
azp: str
scope: str
permissions: List[str] = field(default_factory=list)
@unique
class Permission(Enum):
"""Enum values match permissions configured in Auth0"""
THING_READ = "read:thing"
THING_WRITE = "write:thing"
@dataclass
class Auth0User:
token: str
payload: JwtPayload
permissions: List[Permission]
@property
def id(self) -> str:
return self.payload.sub
def has_permissions(self, permissions: List[Permission]) -> bool:
return bool(permissions) and all(p in self.permissions for p in permissions)
def get_permissions(payload: JwtPayload, all_permissions: Type[Permission]) -> List[Permission]:
"""Parse permission strings into corresponding Enums"""
permissions = []
available_permissions_map = {r.value: r for r in all_permissions}
for p_value in payload.permissions:
if p := available_permissions_map.get(p_value):
permissions.append(p)
else:
logger.warning(f"unrecognized: {p_value}")
return permissions
def get_jwk(domain: str, token: str) -> Optional[Mapping[str, Any]]:
"""Get pubkey associated with Auth0 account that matches token's KID"""
header = jwt.get_unverified_header(token)
jwks = requests.get(f"https://{domain}/.well-known/jwks.json").json()
return next((key for key in jwks["keys"] if key.get("kid") == header["kid"]), None)
oauth2_scheme = OAuth2AuthorizationCodeBearer(
authorizationUrl=config.AUTH0_DOMAIN, tokenUrl=config.AUTH0_AUDIENCE,
)
# Question: does the value for `permissions` come from the route dependency? ex: Auth0Security(current_user, permissions=[Permission.THING_READ])
async def current_user(
permissions: Optional[Sequence[Permission]], token: str = Depends(oauth2_scheme)
) -> Auth0User:
rsa_key = get_jwk(config.AUTH0_DOMAIN, token)
try:
payload = jwt.decode(
token,
rsa_key,
algorithms=[ALGORITHMS.RS256],
audience=config.AUTH0_AUDIENCE,
issuer=f"https://{config.AUTH0_DOMAIN}",
)
except Exception:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="unauthorized")
jwt_payload = JwtPayload(**payload)
user = Auth0User(
token=token, payload=jwt_payload, permissions=get_permissions(jwt_payload, Permission)
)
if permissions and not user.has_permissions(permissions):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="insufficient permissions",
)
return user
class Auth0SecurityParam(Security):
"""Following pattern like fastapi.params.Security"""
def __init__(
self,
dependency: Optional[Callable[..., Any]] = None,
*,
scopes: Optional[Sequence[str]] = None,
permissions: Optional[Sequence[Permission]] = None,
use_cache: bool = True,
):
super().__init__(dependency=dependency, scopes=scopes, use_cache=use_cache)
self.permissions = list(permissions or [])
def Auth0Security(
dependency: Optional[Callable[..., Any]] = None,
*,
scopes: Optional[Sequence[str]] = None,
permissions: Optional[Sequence[Permission]] = None,
use_cache: bool = True,
) -> Any:
"""Following pattern like fastapi.param_functions.Security"""
return Auth0SecurityParam(
dependency=dependency, scopes=scopes, permissions=permissions, use_cache=use_cache
)
hello = APIRouter()
class OutModel(BaseModel):
hello: str
@hello.get("/", response_model=OutModel)
async def hello_world(
*, user: Auth0User = Auth0Security(current_user, permissions=[Permission.THING_READ]),
):
return {"hello": "world"}
router = APIRouter()
router.include_router(hello, prefix="/hello", tags=["hello"])
app = FastAPI()
app.include_router(router, prefix="/v1")
Description
Auth0’s core RBAC can include permissions
in the JWT payload along with scopes
. Given the documented ability to use Security(factory, scopes=["me"])
as deps for a route, I thought it may be possible to subclass Security
to extend this functionality to verify the permissions
field of a JWT’s payload. This doesn’t appear to work as expected.
Should this be possible? Or is there a different way that FastAPI recommends achieving this?
As commented in the minimal example: does the value for permissions
come from the route dependency? ex: Auth0Security(current_user, permissions=[Permission.THING_READ])
Steps to reproduce
- Open the browser and call the endpoint
/v1/hello
with a validAuthorization: Bearer <jwt>
header that includes"permissions":["read:thing"]
in the token’s payload - It returns 422:
{"detail":[{"loc":["body"],"msg":"field required","type":"value_error.missing"}]}
- But I expected it to return
{"hello": "world"}
.
Environment
- OS: Linux
- FastAPI Version: 0.61.1
- Python version: 3.8.5
Additional context
- I have been using Flask in production for years. I am trying-out FastAPI for the first time.
Issue Analytics
- State:
- Created 3 years ago
- Comments:5 (1 by maintainers)
Top Results From Across the Web
OAuth2 scopes - FastAPI
OAuth2 with scopes is the mechanism used by many big authentication providers, ... try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) ...
Read more >Securing FastAPI with JWT Token-based Authentication
In this tutorial, you'll learn how to secure a FastAPI app by enabling authentication using JSON Web Tokens (JWTs). We'll be using PyJWT...
Read more >Securing a FastAPI route using JWT token (step-by-step)
By the end of this post, we'll build a small FastAPI server, send it a request and receive a response, and add authentication/authorization...
Read more >tiangolo/fastapi - Gitter
FastAPI documentation in security uses parameter approach with Depends . ... unlike get_user """ return token.jwt_user() def get_user(db: Session ...
Read more >FastAPI – Vector Tiles with Autorization - GIS • OPS
Learn how to build a vector tile server with built-in authorization using FastAPI and PostGIS.
Read more >
Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free
Top Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found
I based my initial implementation on the Oauth2 Scopes example from the Advanced User Guide (referenced at the bottom of the JWT example). The documentation and code for
fastapi.params.Security
led me to believe thatSecurity(...)
can be used in the same way asDepends(...)
(in fact,Security
is a subclass ofDepends
). For reference, here is the example:It was my hope that I can subclass
Security
to add my own JWT payload checks. Upon closer inspection, however, it seems likeSecurity
may be a single-purpose interface that cannot be extended. The fact thatfastapi.dependencies.utils.get_sub_dependent()
has conditional logic forisinstance(depends, params.Security)
leads me to believe that this may be the case.If
Security
is only meant to be used for authz based only onscopes
, then I will definitely use your recommendation. It would be great, however, if the documented ability to nestSecurity
dependencies for a dependency tree could be extended to other parts of a token’s payload. Is this possible without a change to framework code, @tiangolo?You aren’t calling
Depends()
on any function in your route, so the other code isn’t being used. Its expecting there to be data in the body of the request. And, at the moment you are sayinguser = Auth0Security(...)
which does not return a user type, if it did get called.Might be more like this:
The JWT Example is really good. You shouldn’t have to do much different from it.