Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Web-how to : FastAPI OAuth2 with Password returns 400 Bad Request on authorize #1639

Closed
9 tasks done
gil-obradors opened this issue Jun 28, 2020 · 3 comments
Closed
9 tasks done
Labels
question Question or problem question-migrate

Comments

@gil-obradors
Copy link

gil-obradors commented Jun 28, 2020

First 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 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": "johndoe@example.com",
        "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}]```

Description

  • Open the browser and call the Authorize form /token with tutorial user and pass, johndoe / secret

  • Bad Request 400

  • But 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

Environment

  • OS: [e.g. Linux / Windows / macOS]: Ubunu 20.04 LTS

  • FastAPI Version [e.g. 0.3.0]: 0.58.0

  • Python version: Python 3.8.2

Additional context

Authorize form fail
fastapi-badreq400

Chrome field client_id
Captura de pantalla de 2020-06-28 08-40-41
Firefox field client_id with required
Captura de pantalla de 2020-06-28 08-42-12

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
@gil-obradors gil-obradors added the question Question or problem label Jun 28, 2020
@gil-obradors gil-obradors changed the title Web-howto: FastAPI OAuth2 with Password returns 400 Bad Request on authorize Web-how to : FastAPI OAuth2 with Password returns 400 Bad Request on authorize Jun 28, 2020
@gil-obradors
Copy link
Author

gil-obradors commented Jun 28, 2020

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

@gil-obradors
Copy link
Author

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

@tiangolo
Copy link
Owner

Thanks for reporting back and closing the issue! 👍 🎉

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. 🚀

@tiangolo tiangolo reopened this Feb 28, 2023
Repository owner locked and limited conversation to collaborators Feb 28, 2023
@tiangolo tiangolo converted this issue into discussion #7509 Feb 28, 2023

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
question Question or problem question-migrate
Projects
None yet
Development

No branches or pull requests

2 participants