OpenAPI UI authorize button doesnt take all of the form parameters when trying to authenticate with a custom flow (i.e not the usual OAuth2PasswordBearer)
from typing import Optional
from fastapi import Depends, FastAPI
from fastapi import HTTPException
from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel
from fastapi.params import Form
from fastapi.security.oauth2 import OAuth2
from fastapi.security.utils import get_authorization_scheme_param
from starlette.requests import Request
from starlette.status import HTTP_401_UNAUTHORIZED
fake_users_db = {
"client_id": "x",
"client_secret": "y",
"disabled": False,
}
app = FastAPI()
class Oauth2ClientCredentials(OAuth2):
def __init__(
self,
tokenUrl: str,
scheme_name: str = None,
scopes: dict = None,
auto_error: bool = True,
):
if not scopes:
scopes = {}
flows = OAuthFlowsModel(
clientCredentials={"tokenUrl": tokenUrl, "scopes": scopes}
)
super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)
async def __call__(self, request: Request) -> Optional[str]:
authorization: str = request.headers.get("Authorization")
scheme, param = get_authorization_scheme_param(authorization)
if not authorization or scheme.lower() != "bearer":
if self.auto_error:
raise HTTPException(
status_code=HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
else:
return None
return param
oauth2_scheme = Oauth2ClientCredentials(
tokenUrl="/token", scopes={"write": "write permission"}
)
async def get_current_user(token: str = Depends(oauth2_scheme)):
return token
class ClientCredentials:
"""operates in the same way as fast apis 'OAuth2PasswordRequestForm'"""
def __init__(
self,
grant_type: str = Form(None, regex="client_credentials"),
client_id: str = Form(...),
client_secret: str = Form(...),
scope: str = Form(""),
):
self.grant_type = grant_type
self.client_id = client_id
self.client_secret = client_secret
self.scope = scope.split()
@app.post("/token")
async def login(form_data: ClientCredentials = Depends()):
user_dict = form_data.client_id in fake_users_db.values()
return {"made_up_bool_token": user_dict}
@app.get("/users/me")
async def read_users_me(current_user=Depends(get_current_user)):
return current_user
uvicorn FILENAME:app --reloadgrant_type=client_credentials&scope=write and are missing the client_id and client_secret parameters. Is this a bug or am I doing something silly?update: I have tried reverting to using oauth2_scheme = OAuth2PasswordBearer instead of oauth2_scheme = Oauth2ClientCredentials and if I ignore the username and password fields but fill out the client_id and client_secrets (and enable Client credentials location: request body) then I can get the correct form params to be picked up except for the grant_type which is password. This needs to be client_credentials in my case so it seems theres some issue between openapi and fastapi in creating the swagger schema when using a customized authorization class
looks like this might be related to my issue and might be more of an issue with implementation rather than a fastapi issue: https://github.com/swagger-api/swagger-ui/issues/4533
This ended up being a misunderstanding in how the swagger UI sends the required fields. It puts the client_id and secret in a header so I modified my endpoint to have the following params:
grant_type: str = Form(...),
scope: List = Form(None),
authorization: str = Header(...)
and then did some manipulation from there. In the passwordbearer example you have the option of sending the params in the request body but when you change to client_credentials this is no longer the case. This allowed me to keep my customized Oauth2ClientCredentials class to maintain a clear UI for users. Hope this helps any others that run into this issue :)
Thanks for reporting back and closing the issue :+1:
Most helpful comment
This ended up being a misunderstanding in how the swagger UI sends the required fields. It puts the client_id and secret in a header so I modified my endpoint to have the following params:
and then did some manipulation from there. In the passwordbearer example you have the option of sending the params in the request body but when you change to client_credentials this is no longer the case. This allowed me to keep my customized
Oauth2ClientCredentialsclass to maintain a clear UI for users. Hope this helps any others that run into this issue :)