MushroomMaula / fastapi_login

FastAPI-Login tries to provide similar functionality as Flask-Login does.
https://pypi.org/project/fastapi-login
MIT License
639 stars 58 forks source link

Exception handling #78

Closed agent666 closed 2 years ago

agent666 commented 2 years ago

When I use the example code from https://fastapi-login.readthedocs.io/advanced_usage/ and there is no, or an invalid token present in the request I get an error:

INFO:     127.0.0.1:5898 - "GET /proteced HTTP/1.1" 500 Internal Server Error
ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\starlette\middleware\base.py", line 41, in call_next
    message = await recv_stream.receive()
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\anyio\streams\memory.py", line 81, in receive
    return self.receive_nowait()
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\anyio\streams\memory.py", line 74, in receive_nowait
    raise EndOfStream
anyio.EndOfStream

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\uvicorn\protocols\http\h11_impl.py", line 373, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\uvicorn\middleware\proxy_headers.py", line 75, in __call__
    return await self.app(scope, receive, send)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\fastapi\applications.py", line 261, in __call__
    await super().__call__(scope, receive, send)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\starlette\applications.py", line 112, in __call__
    await self.middleware_stack(scope, receive, send)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\starlette\middleware\errors.py", line 181, in __call__
    raise exc
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\starlette\middleware\errors.py", line 159, in __call__
    await self.app(scope, receive, _send)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\starlette\middleware\base.py", line 63, in __call__
    response = await self.dispatch_func(request, call_next)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\fastapi_login\fastapi_login.py", line 439, in user_middleware
    return await call_next(request)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\starlette\middleware\base.py", line 44, in call_next
    raise app_exc
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\starlette\middleware\base.py", line 34, in coro
    await self.app(scope, request.receive, send_stream.send)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\starlette\exceptions.py", line 82, in __call__
    raise exc
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\starlette\exceptions.py", line 71, in __call__
    await self.app(scope, receive, sender)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\fastapi\middleware\asyncexitstack.py", line 21, in __call__
    raise e
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\fastapi\middleware\asyncexitstack.py", line 18, in __call__
    await self.app(scope, receive, send)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\starlette\routing.py", line 656, in __call__
    await route.handle(scope, receive, send)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\starlette\routing.py", line 259, in handle
    await self.app(scope, receive, send)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\starlette\routing.py", line 61, in app
    response = await func(request)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\fastapi\routing.py", line 217, in app
    solved_result = await solve_dependencies(
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\fastapi\dependencies\utils.py", line 527, in solve_dependencies
    solved = await call(**sub_values)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\fastapi_login\fastapi_login.py", line 405, in __call__
    token = await self._get_token(request)
  File "C:\Users\arne\AppData\Local\Programs\Python\Python39\lib\site-packages\fastapi_login\fastapi_login.py", line 349, in _get_token
    except type(self.not_authenticated_exception):
TypeError: catching classes that do not inherit from BaseException is not allowed

I use:

class NotAuthenticatedException(Exception):
    pass

secret = SECRET
manager = LoginManager(SECRET, '/login', use_header=False, use_cookie=True, custom_exception=NotAuthenticatedException)
@app.exception_handler(NotAuthenticatedException)
def auth_exception_handler(request: Request, exc: NotAuthenticatedException):
    """
    Redirect the user to the login page if not logged in
    """
    return RedirectResponse(url='/login')

What do I do wrong?

agent666 commented 2 years ago

But if I am not logged in yet in the /login and give incorrect login details I am redirected to /login again as planned. But if I'm not logged in and visit /protected I get the above error

@app.post('/login' , response_class=RedirectResponse, status_code=302)
async def login(response: Response, data: OAuth2PasswordRequestForm = Depends()):
    username = data.username
    password = data.password
    user_data = await query_user(username)
    if not user_data:
        raise NotAuthenticatedException
    elif not pwdhasher.verify(password, user_data[1]):
        raise NotAuthenticatedException
    access_token = manager.create_access_token(
        data={'sub': user_data[0]}, expires=timedelta(days=365)
    )

@manager.user_loader()
@app.get('/protected', response_class=HTMLResponse)
def protected_route(user=Depends(manager)):
    return user
MushroomMaula commented 2 years ago

We already had a similar issue about catching FastAPI's HTTPExceptions, this was resolved by changing except self.not_authenticated_exception to except type(self.not_authenticated_exception). However it seems that this breaks catching of standard exception as type(Exception) returns <class 'type'>, it also looks like none of my test cases actually checks this.

If I have enough time I will look into this over the weekend.

You can use the following snippet as a "hacky" workaround. What we basically do is subclass HTTPException instead of the normal exception class from python, as the error handeling for the former should work correctly. Then we modify our exception handler such that it catches all HTTPExceptions raised anywhere and checks if the exception is a actually a instance of our subclass.

from fastapi import HTTPException
# This is the default http_exception_handler used out of the box
from fastapi.exception_handlers import http_exception_handler
from starlette.exceptions import HTTPException as StarletteHTTPException

# We have to create a new class so that we can check if the HTTPException in our handler belongs
# to our custom exception
class NotAuthenticatedException(HTTPException):
    def __init__(self) -> None:
        super().__init__(400)   # Error code can basically be any valid http status code as we don't return it

# From the fastapi docs we have to use the StarletteHTTPException here
@app.exception_handler(StarletteHTTPException)
async def auth_exception_handler(request: Request, exc: NotAuthenticatedException):
    """
    Redirect the user to the login page if not logged in
    """
    if isinstance(exc, NotAuthenticatedException):
        return RedirectResponse(url='/login')

    else:
        # This should be called for any other http exception that does not belong to our "custom" exception class
        return await http_exception_handler(request, exc)

# NOTE how we pass a instance of our class to the LoginManager
manager = LoginManager(..., custom_exception=NotAuthenticatedException())
MushroomMaula commented 2 years ago

Should be fixed in the newest version.