Here's a self-contained with my use case:
From official docs website
from datetime import datetime, timedelta
import jwt
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jwt import PyJWTError
from passlib.context import CryptContext
from pydantic import BaseModel
# to get a string like this run:
# openssl rand -hex 32
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "[email protected]",
"hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
"disabled": False,
}
}
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: str = None
class User(BaseModel):
username: str
email: str = None
full_name: str = None
disabled: bool = None
class UserInDB(User):
hashed_password: str
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
app = FastAPI()
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password):
return pwd_context.hash(password)
def get_user(db, username: str):
if username in db:
user_dict = db[username]
return UserInDB(**user_dict)
def authenticate_user(fake_db, username: str, password: str):
user = get_user(fake_db, username)
if not user:
return False
if not verify_password(password, user.hashed_password):
return False
return user
def create_access_token(data: dict, expires_delta: timedelta = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except PyJWTError:
raise credentials_exception
user = get_user(fake_users_db, username=token_data.username)
if user is None:
raise credentials_exception
return user
async def get_current_active_user(current_user: User = Depends(get_current_user)):
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(fake_users_db, form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
@app.get("/users/me/", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
return current_user
@app.get("/users/me/items/")
async def read_own_items(current_user: User = Depends(get_current_active_user)):
return [{"item_id": "Foo", "owner": current_user.username}]```
/token with tutorial user and pass, johndoe / secretBut I expected it to return {"access_token": access_token, "token_type": "bearer"}
Do the same out of Swagger. Same error
I have testes out of swagger to test if problem is on swagger UI.
I have used RESTClient from Firefox. I have POSTed with JSON and form .
All times got {"detail":"There was an error parsing the body"}
---maybe about swagger-UI---
The field client_id in Firefox 77.0.1 (64 bits) is marked as required, in red. But you can POST with no problem ( see gif)
The same field with Chrome 83.0
FastAPI Version [e.g. 0.3.0]: 0.58.0
Python version: Python 3.8.2
Authorize form fail

Chrome field client_id

Firefox field client_id with required

Console output:
(venv) gil@gil-NUC10i7FNH:~/PycharmProjects/fastapi$ uvicorn test:app --reload --port 5050
INFO: Uvicorn running on http://127.0.0.1:5050 (Press CTRL+C to quit)
INFO: Started reloader process [293259] using statreload
INFO: Started server process [293261]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: 127.0.0.1:60596 - "GET /docs HTTP/1.1" 200 OK
INFO: 127.0.0.1:60596 - "GET /openapi.json HTTP/1.1" 200 OK
INFO: 127.0.0.1:60616 - "GET /docs HTTP/1.1" 200 OK
INFO: 127.0.0.1:60616 - "GET /openapi.json HTTP/1.1" 200 OK
INFO: 127.0.0.1:60622 - "POST /token HTTP/1.1" 400 Bad Request
I have enabled debug on app and trace log-level
app = FastAPI(debug=True)
And I can see no query_string :
TRACE: 127.0.0.1:35304 - ASGI [4] Started scope={'type': 'http', 'http_version': '1.1', 'server': ('127.0.0.1', 5050), 'client': ('127.0.0.1', 35304), 'scheme': 'http', 'method': 'POST', 'root_path': '', 'path': '/token', 'raw_path': b'/token', 'query_string': b'', 'headers': '<...>'}
From this request (Firefox)
curl 'http://127.0.0.1:5050/token' -H 'User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:77.0) Gecko/20100101 Firefox/77.0' -H 'Accept: application/json, text/plain, */*' -H 'Accept-Language: ca,es-ES;q=0.7,en;q=0.3' --compressed -H 'Referer: http://127.0.0.1:5050/docs' -H 'Content-Type: application/x-www-form-urlencoded' -H 'X-Requested-With: XMLHttpRequest' -H 'Authorization: Basic Og==' -H 'Origin: http://127.0.0.1:5050' -H 'Connection: keep-alive' -H 'Pragma: no-cache' -H 'Cache-Control: no-cache' --data-raw 'grant_type=password&username=johndoe&password=secret'
And the same for CLI:
curl -X POST "http://127.0.0.1:5050/token" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "username=johndoe&password=secret"
Continuing investigation
Solved!
There are existents info notes that I didn't read.
https://fastapi.tiangolo.com/tutorial/security/first-steps/
https://fastapi.tiangolo.com/tutorial/request-forms/
pip install python-multipart
Thanks for reporting back and closing the issue! :+1: :tada:
As this error was easy to have and difficult to debug, in recent versions there's an extra check that detects it early and exits with an error, warning about python-multipart not being installed. :rocket:
Most helpful comment
Solved!
There are existents info notes that I didn't read.
https://fastapi.tiangolo.com/tutorial/security/first-steps/
https://fastapi.tiangolo.com/tutorial/request-forms/