Search code examples
pythonoauth-2.0fastapi

Fastapi OAuth2 token handeling. Missing Authorization header


I'm using FastAPI with OAuth2PasswordBearer and RequestForm to implement a user login. The Login and retreiving the token works, but working with the token is not working for me.

I got this OAuth2PasswordBearer setup and /token function:


authmanager = OAuth2PasswordBearer(tokenUrl='dauPP/token')

@router.post("/token", response_model=Token)
async def login_for_access_token(db: AsyncIOMotorDatabase = Depends(get_database),
                                 form_data: OAuth2PasswordRequestForm = Depends()):
    user = await authenticate_user(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"}

This function checks the given username and password provided in a form against my MongoDB and this works great. It returns an access_token and token_type: enter image description here

But if I try to access a managed router f.e. /home

@router.get("/home", response_class=HTMLResponse)
async def get_home(request: Request, current_user: User = Depends(get_current_active_user)):
    return templates.TemplateResponse("home.html", {
        "request": request, "title": "[D]ocument [A]dvanced [U]tility", "subtitle": current_user.username
    })

I receive a

"Not authenticated"

response. Which would be good...if I didn't get the token beforehand.

I checked what the PasswordBearer is processing:

    async def __call__(self, request: Request) -> Optional[str]:
        print(request.headers)
        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

So I took a look at the contents of the request.headers and this is what I found (the "Cheking for...." is a print I added):

INFO:     127.0.0.1:59213 - "POST /dauAPP/token HTTP/1.1" 200 OK
Headers({'host': '127.0.0.1:8000', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:98.0) Gecko/20100101 Firefox/98.0', 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', 'accept-language': 'de,en-US;q=0.7,en;q=0.3', 'accept-encoding': 'gzip, deflate', 'connection': 'keep-alive', 'upgrade-insecure-requests': '1', 'sec-fetch-dest': 'document', 'sec-fetch-mode': 'navigate', 'sec-fetch-site': 'none', 'sec-fetch-user': '?1'})
Checking for authorization in header...result:None
INFO:     127.0.0.1:59213 - "GET /dauAPP/home HTTP/1.1" 401 Unauthorized

I am quite new to web-dev so I might have fundamental flaws in my thinking, but I would assume, that returning an access token would result in the client/browser storing it and automatically adding it to its request headers in "Authorization". Do I have to do something with the token on client end? Might there be something wrong with the configuration, so that the Auth-Header gets cut off?

How I think it works:

get_home is called 
async def get_home(request: Request, current_user: User = Depends(get_current_active_user)):
--> dependency calling get_current_active_user

async def get_current_active_user(current_user: User = Depends(get_current_user)):
--> dependency calling get_current_user

async def get_current_user(db: AsyncIOMotorDatabase = Depends(get_database), token: str = Depends(authmanager)):
--> dependency calls authmanager (OAuth2PasswordBearer)

which calls the def __call__ function mentioned earlier and here the header is missing.

Solution

  • but I would assume, that returning an access token would result in the client/browser storing it and automatically adding it to its request headers in "Authorization"

    This assumption is where you're wrong. It's your apps responsibility to store the token and transmit it for each request against an endpoint. If you're planning on using regular HTML and navigating through it as a user clicking links instead of as an API endpoint, you might want to look at using cookies instead of an HTTP header (which are sent automagically by your browser).

    The swagger-ui can determine from the API signature that an Authorization header is required (which is what OAuth2PasswordBearer does, among other things), it knows that it can ask for and is expected to present that header. Since swagger-ui is not a common web standard as a part of HTTP, that's not something browsers should, or are able to, do.

    Cookies do serve that purpose, however - so you could use cookies if you want to do that. But API requests does not include cookies, and its far more common to use the Authorization header for those requests.