from typing import List, Optional from fastapi.openapi.models import OAuth2 as OAuth2Model, OAuthFlows as OAuthFlowsModel from fastapi.security.base import SecurityBase from pydantic import BaseModel, Schema from starlette.exceptions import HTTPException from starlette.requests import Request from starlette.status import HTTP_403_FORBIDDEN class OAuth2PasswordRequestData(BaseModel): grant_type: str = "password" username: str password: str scope: Optional[List[str]] = None # Client ID and secret might come from headers client_id: Optional[str] = None client_secret: Optional[str] = None class OAuth2PasswordRequestForm(BaseModel): """ This is not a "Security" model. Use it as request Body. As in: @app.post("/login") def login(form_data: Oauth2PasswordRequestForm): data = form_data.parse() print(data.username) print(data.password) for scope in data.scope: print(scope) if data.client_id: print(data.client_id) if data.client_secret: print(data.client_secret) return data It creates the following Form request parameters in your endpoint: grant_type: the OAuth2 spec says it is required and MUST be the fixed string "password". Nevertheless, this model is permissive and allows not passing it. If you want to enforce it, use instead the OAuth2PasswordRequestFormStrict model. username: username string. The OAuth2 spec requires the exact field name "username". password: password string. The OAuth2 spec requires the exact field name "password". scope: Optional string. Several scopes (each one a string) separated by spaces. E.g. "items:read items:write users:read profile openid" client_id: optional string. OAuth2 recommends sending the client_id and client_secret (if any) using HTTP Basic auth, as: client_id:client_secret client_secret: optional string. OAuth2 recommends sending the client_id and client_secret (if any) using HTTP Basic auth, as: client_id:client_secret It has the method parse() that returns a model with all the same data and the scopes extracted as a list of strings. """ grant_type: str = Schema(None, regex="password") username: str password: str scope: str = "" # Client ID and secret might come from headers client_id: Optional[str] = None client_secret: Optional[str] = None def parse(self) -> OAuth2PasswordRequestData: return OAuth2PasswordRequestData( grant_type=self.grant_type, username=self.username, password=self.password, scope=self.scope.split(), client_id=self.client_id, client_secret=self.client_secret, ) class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm): # The OAuth2 spec says it MUST have the value "password" grant_type: str = Schema(..., regex="password") class OAuth2(SecurityBase): def __init__( self, *, flows: OAuthFlowsModel = OAuthFlowsModel(), scheme_name: str = None ): self.model = OAuth2Model(flows=flows) self.scheme_name = scheme_name or self.__class__.__name__ async def __call__(self, request: Request) -> str: return request.headers.get("Authorization") class OAuth2PasswordBearer(OAuth2): def __init__(self, tokenUrl: str, scheme_name: str = None, scopes: dict = None): if not scopes: scopes = {} flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes}) super().__init__(flows=flows, scheme_name=scheme_name) async def __call__(self, request: Request) -> str: authorization: str = request.headers.get("Authorization") if not authorization or "Bearer " not in authorization: raise HTTPException( status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" ) token = authorization.replace("Bearer ", "") return token